Partage
  • Partager sur Facebook
  • Partager sur Twitter

insertion au milieu de la liste chainée

Sujet résolu
    15 juin 2022 à 1:17:38

    rouIoude a écrit:
    > 1) Si l'élément n'est pas inséré, il oubli de libérer la mémoire alloué !
    C'est pourquoi on devrait être certain de pouvoir insérer avant de réserver l'espace pour le nouveau.
    > 2) Si on insert un élément identique à celui recherché --> boucle infinie (il trouve à la position suivante celui qu'il vient d’insérer, il en insert donc un autre ect...) !
    Ça dépend comment on se positionne après l'insertion.
    Il faut se placer sur le suivant de celui qu'on a trouvé, pas sur le suivant de celui qu'on a inséré.

    Le code suivant insère après chaque occurence de la valeur cherchée.

    C'est plus simple d'insérer avant pour l'avancement des pointeurs, mais plus facile après car on n'insère jamais au début.

    -

    void insert(List *list, int searched, int newValue) {
        Element *current = list->first;
        while(current) {
            Element *previous = current;
            current = current->next;
            if(previous->value == searched) {
                Element *newNode = malloc(sizeof(Element));
                if(newNode == NULL) {
                    printf("Problème de mémoire\n");
                    exit(EXIT_FAILURE);
                }
                newNode->value = newValue;
                newNode->next = current;
                previous->next = newNode;
            }

        }

    }

    -
    Edité par PierrotLeFou 15 juin 2022 à 3:19:48

    • Partager sur Facebook
    • Partager sur Twitter

    Le Tout est souvent plus grand que la somme de ses parties.

      15 juin 2022 à 9:09:41

      Digression: l'immense majorité des cours de programmation ne parle pas de la nécessité de prévoir des construire des tests automatisés en même temps (et même avant) qu'on se lance dans les trucs compliqués.

      C'est un retard de quelques décennies sur la méthodologie de la programmation, mais bon, qu'attendre de cours qui n'ont pas décollé du C89, voir pire... et où la mentalité est celle des cowboys qui ont des poils sous les bras et qui savent ce qu'ils font, et n'ont pas besoin de tester puisqu'ils ne se trompent jamais.

      ---

      Bon, imaginons que je me mette en tête d'écrire une fonction dont le rôle serait d'ajouter, dans une liste, une nouvelle valeur après la première occurrence d'une valeur indiquée.

      Exemple, dans une liste contenant (11, 22, 33),   on demanderait d'ajouter 222 après 22, on obtiendrait (11, 22, 222, 33).

      Et demander d'ajouter 444  après 44 qui n'y est pas, ça ne changerait pas la liste.

      Voila, on a une spécification pas ambiguë de ce qu'on veut.

      On peut matérialiser ces tests par une petite séquence de code

       int t[] = {11, 22, 33};
          test_construction_comparaison(3, t);  // voir plus tard
      
          int r1[] = { 11, 111, 22, 33 };
          test_ajout_apres(3, t, 11, 111, 4, r1);
          int r2[] = { 11, 22, 222, 33 };
          test_ajout_apres(3, t, 22, 222, 4, r2);
          int r3[] = { 11, 22, 33, 333 };
          test_ajout_apres(3, t, 33, 333, 4, r3);
      
          test_ajout_apres(3, t, 44, 444, 3, t); // INCHANGE
      

      qu'on fera exécuter dans un main() consacré aux tests.

      Par commodité, on met les données à tester dans des tableaux.

      ---

      le rôle de la fonction, c'est de

      • construire une liste avec le premier tableau indiqué
      • lui appliquer l'insertion demandée
      • vérifier que ça correspond au résultat attendu
      • afficher ce qui se passe et le bilan : c'est juste ou c'est faux.
      Ca afficherait des trucs comme ça
      Test ajout après
      tableau	= 11 22 33 
      ajout de  222 après 22
      attendu	= 11 22 222 33 **TODO: list_add_value_after**
      obtenu	= 11 22 33 
      Egalité = Non
      
      Test ajout après
      tableau	= 11 22 33 
      ajout de  333 après 33
      attendu	= 11 22 33 333 **TODO: list_add_value_after**
      obtenu	= 11 22 33 
      Egalité = Non
      
      Test ajout après
      tableau	= 11 22 33 
      ajout de  444 après 44
      attendu	= 11 22 33 **TODO: list_add_value_after**
      obtenu	= 11 22 33 
      Egalité = Oui
      
      
      Ah oui: je n'ai pas encore écrit la fonction à tester, qui pour l'instant est juste un "stub"
      void list_add_value_after(list_t *list, int value, int new_value)
      {
          printf("**TODO: list_add_value_after**\n");
      }
      
      je vais quand même pas faire l'exercice.
      Pour en revenir à la fonction de test, qui y fait appel
      void test_ajout_apres(int nb_donnees, int donnees[nb_donnees],
                            int valeur, int nouvelle,
                            int nb_resultats, int resultats[nb_resultats])
      {
          printf("Test ajout après\ntableau\t= ");
          for (int i = 0; i < nb_donnees; i++) {
              printf("%d ", donnees[i]);
          }
          printf("\najout de  %d après %d", nouvelle, valeur);
          printf("\nattendu\t= ");
          for (int i = 0; i < nb_resultats; i++) {
              printf("%d ", resultats[i]);
          }
      
          list_t list = list_from_array(nb_donnees, donnees);
          list_add_value_after(& list, valeur, nouvelle);
          printf("obtenu\t= ");
          print_list(list);
          printf("\nEgalité = %s\n\n",
                 same_content_as_array(list, nb_resultats, resultats)
                 ? "Oui"
                 : "Non");
          clear_list(& list);
      }
      rien de suprenant.
      On fait appel à une tripotée de fonctions auxilaires
      typedef struct cell {
          int value;
          struct cell *next;
      } cell_t;
      
      typedef struct list {
          struct cell *first;
      } list_t;
      
      // alloue une nouvelle cellule avec une valeur
      // et modifie un pointeur sur une suite de cellules
      // pour que la nouvelle vienne en premier
      
      // expl : comme le pointeur doit être modifié par
      // la fonction, celle-ci reçoit son adresse
      // (where pointe sur un pointeur !)
      
      void insert_value_at(cell_t **where, int value)
      {
          cell_t *new = malloc(sizeof (cell_t));
          new->value = value;
          new->next =  *where;
          *where =  new;
      }
      
      void drop_first(list_t *list)
      {
          cell_t *c = list->first;
          list->first = c->next;
          free(c);
      }
      
      void clear_list(list_t *list)
      {
          while (list->first != NULL) {
              drop_first(list);
          }
      }
      // retourne une liste à partir des éléments
      // d'un tableau de valeurs.
      
      list_t list_from_array(int n, int values[n])
      {
          list_t list = {.first = NULL};
          for (int i = n-1; i >= 0; i--) {
              insert_value_at(& list.first, values[i]);
          }
          return list;
      }
      
      // compare une liste avec le contenu d'un tableau
      // de valeurs
      
      bool same_content_as_array(list_t list, int n, int values[n])
      {
          cell_t *c = list.first;
          for (int i = 0; i < n; i++) {
              if (c == NULL) return false;
              if (values[i] != c->value) return false;
              c = c->next;
          }
          return c == NULL;
      }
      
      void print_list(list_t list)
      {
          for (cell_t *c = list.first; c != NULL; c = c->next) {
              printf("%d ", c->value);
          }
      }
      
      
      les fonctions auxiliaires sont réemployables dans d'autres tests, par exemple la construction d'une liste à partir d'un tableau fournit bien une liste qui a le même contenu que le tableau
         int t[] = {11, 22, 33};
          test_construction_comparaison(3, t);
      avec
      void test_construction_comparaison(int n, int valeurs[n])
      {
          printf("Test construction\ntableau\t= ");
          for (int i = 0; i < n; i++) {
              printf("%d ", valeurs[i]);
          }
          printf("\n");
          list_t list = list_from_array(n, valeurs);
          printf("liste\t= ");
          print_list(list);
          printf("\nEgalité = %s\n\n",
                 same_content_as_array(list, n, valeurs)
                 ? "Oui"
                 : "Non");
          clear_list(& list);
      }
      
      Parce que, force est de constater qu'on se trompe aussi en construisant les outils de test, donc c'est préférable d'écrire des tests pour les tester.
      Au passage, on fait attention à écrire proprement les tests (liberation par clear_list) pour pouvoir vérifier qu'il n'y a pas de fuite mémoire.
      =28755== 
      ==28755== All heap blocks were freed -- no leaks are possible
      ==28755== 
      

      ** pour conclure

      il FAUT ABSOLUMENT écrire des tests automatisés pour les trucs qui ont une certaine probabilité de ne pas marcher du premier coup.

      - parce que ça permettra de relancer automatiquement la séquence de tests à chaque exécution. Rien de pire que les programmes où il faut à chaque fois rentrer les données à la main : à la troisième fois c'est tellement chiant qu'on tape titi toto et qjksdhhdfkqshjn, et qu'on ne regarde même pas si le résultat est juste. Avec des tests automatisés, y a juste à verifier qu'il y a 0 erreurs.

      - parce que ça oblige à réfléchir dès le départ à des cas concrets d'utilisation des fonctions qu'on écrit, y compris des cas limites (ici je ne l'ai pas fait mais ca serait de bon goût d'avoir un cas avec deux occurrences de la valeur à détecter, histoire de vérifier qu'on insère uniquement après la première).

      - parce que ça permet le développement dirigé par les tests. On commence par écrire le code pour que le premier test passe, en se disant que c'est déjà ça de fait et que si on a loupé un truc, ça nous sera signalé par les tests suivants.

      En pratique ça demande un peu de travail - très facile - de construire ce "cadre de tests" à la main, mais c'est payant, ça aide à travailler sereinement, ça évite de ramer pendant des heures avec des programmes buggés, aussi c'est une manière de se mettre au boulot (comme tailler ses crayons pour un écrivain !).

      En vrai il y a des bibliothèques pour faciliter les tests automatisés. Ca vaut le coup d'apprendre à s'en servir dans un cadre professionnel, quand il s'agit juste de faire des exercices de base, c'est peut être pas le moment (on ne peut pas tout apprendre en même temps).

      A savoir: quand un test s'obstine à échouer, il esst possible qu'on se soit planté dans les données du test. Une des dures réalités de la programmation, c'est qu'on arrive à se planter dans tout ce qu'on fait, de temps en temps. Penser à vérifier. La confiance n'exclue pas le contrôle, comme disait Staline.

      -
      Edité par michelbillaud 15 juin 2022 à 9:32:17

      • Partager sur Facebook
      • Partager sur Twitter
        15 juin 2022 à 10:16:48

        PierrotLeFou a écrit:

        rouIoude a écrit:
        > 1) Si l'élément n'est pas inséré, il oubli de libérer la mémoire alloué !
        C'est pourquoi on devrait être certain de pouvoir insérer avant de réserver l'espace pour le nouveau.
        > 2) Si on insert un élément identique à celui recherché --> boucle infinie (il trouve à la position suivante celui qu'il vient d’insérer, il en insert donc un autre ect...) !

        On parle de la même chose ? Je commentais le code de Copilot !

        Je le postes, il est sur une video :

        Liste* AjoutAuMilieuCopilot(Liste* liste, int valeurExist, int nvlValeur)
        {
            Element* nouveau = malloc(sizeof(*nouveau));
            nouveau->nombre = nvlValeur;
            nouveau->suivant = NULL;
        
            if (liste->premier == NULL)
            {
                liste->premier = nouveau;
            }
            else
            {
                Element* courant = liste->premier;
        
                while (courant->suivant != NULL)
                {
                    if(courant->nombre == valeurExist)
                    {
                        nouveau->suivant = courant->suivant;
                        courant->suivant = nouveau;
                    }
                    courant=courant->suivant;
                }
        
                if(courant->nombre == valeurExist)
                {
                    nouveau->suivant = courant->suivant;
                    courant->suivant = nouveau;
                }
            }
            return liste;
        }

        -
        Edité par rouIoude 15 juin 2022 à 10:21:59

        • Partager sur Facebook
        • Partager sur Twitter
        ...
          15 juin 2022 à 11:47:20

          Juste pour commenter le code copilot …

          C'est une fonction (hey captain obvious) qui souffre d'un défaut majeur : elle ne s'insère pas dans un code plus large.

          Si on part uniquement de l'entête :

          Liste* AjoutAuMilieuCopilot(Liste* liste, int valeurExist, int nvlValeur)

          et qu'on suppose que le comportement en cas de valeur non trouvée est de faire un append, on devrait carrément avoir une fonction de ce genre :

          Liste* AjoutAuMilieuCopilot(Liste* liste, int valeurExist, int nvlValeur)
          {
              maillon *maillon = liste_chercher_maillon_avec_valeur(liste, valeurExist);
          
              if (maillon) {
                  maillon_insere_nouveau_maillon_apres(maillon, nvl_valeur);
              } else {
                  liste=liste_append(liste, nvlValeur);
              }
          
              return liste;
          }

          DRY: don't repeat yourself.

          KISS: keep it simple, stupid.

          Le but est tout de même de réutiliser ce qu'on écrit.

          • Partager sur Facebook
          • Partager sur Twitter
            15 juin 2022 à 11:47:37

            @rouloude Dans ce code, la boucle peut modifier plusieurs cellules (ayant la valeur recherchée) pour que leur champ suivant pointe sur la nouvelle.

            Et j'ai un doute sur ce qui se passe si on ajoute une valeur identique à celle recherchée.

            -
            Edité par michelbillaud 15 juin 2022 à 11:50:12

            • Partager sur Facebook
            • Partager sur Twitter
              15 juin 2022 à 11:57:30

              michelbillaud a écrit:

              Et j'ai un doute sur ce qui se passe si on ajoute une valeur identique à celle recherchée.

              Boucle sans fin !

              • Partager sur Facebook
              • Partager sur Twitter
              ...
                15 juin 2022 à 18:36:18

                > On parle de la même chose ? Je commentais le code de Copilot !
                Oui et non, je précisais ce qu'il fallait faire (ou ne pas faire).
                Est-ce que Copilot est issu de l'Intelligence Artificielle? (ou SI = Stupidité Artificielle ...)
                C'est stupide que de réserver l'espace sans être certain qu'on va pouvoir l'insérer.
                Mon code ne crée pas de boucle infinie si on ajoute des éléments identiqques à ceux cherchés.

                Ouf! On peut respirer pendant quelques décennies. L'AI ne nous remplacera pas de sitôt.

                -
                Edité par PierrotLeFou 15 juin 2022 à 18:42:07

                • Partager sur Facebook
                • Partager sur Twitter

                Le Tout est souvent plus grand que la somme de ses parties.

                  15 juin 2022 à 19:57:09

                  michelbillaud a écrit :

                  En pratique ça demande un peu de travail - très facile - de construire ce "cadre de tests" à la main, mais c'est payant, ça aide à travailler sereinement, ça évite de ramer pendant des heures avec des programmes buggés, aussi c'est une manière de se mettre au boulot (comme tailler ses crayons pour un écrivain !).

                  En vrai il y a des bibliothèques pour faciliter les tests automatisés. Ca vaut le coup d'apprendre à s'en servir dans un cadre professionnel, quand il s'agit juste de faire des exercices de base, c'est peut être pas le moment (on ne peut pas tout apprendre en même temps).

                  J'aime bien ton analogie "aussi c'est une manière de se mettre au boulot (comme tailler ses crayons pour un écrivain !)". Les tests sont un outil d'écriture, et donc de création.

                  Dans le genre simple et directement utilisable, il y a assert() dans le standard du C. On peut faire des choses très bien juste avec cela et on peut rapidement montrer à un débutant l'intérêt de programmer en créant des tests avant l'implémentation, qui permettent d'avancer progressivement dans la mise en place des spécifications du comportement de l'application, avec les contrôles d'absence de régression au fur et à mesure du développement que tu rappelles (et pleins d'autres comme la conception découplée ou la refactorisation du code "sans peur").

                  Uncle Bob a un Kata (un exercice d’entraînement et d'apprentissage) qu'il appelle le "Bowling Game" qui est orienté Java. Il s'agit de créer un programme qui calcule un score de bowling. Ce n'est pas tout à fait un programme trivial, mais cela reste dans les limites du raisonnable. Bref c'est un bon sujet d'exercice.

                  Le "Kata" est une allusion aux formes connues des arts martiaux japonais, avec des enchaînements de techniques codifiés (qui simule un combat dans ce cas). L'exercice est conçu, comme un Kata, pour permettre la mise en oeuvre complète des techniques et leurs enchaînements.

                  Avec sa permission, Olve Maudal a créé en 2007 une présentation TDD in C qui reprend le Kata avec C et juste assert(). Les slides sont là :

                  https://olvemaudal.com/2007/11/27/test-driven-development-in-c/

                  Les 122 slides sont très bien faits et montrent, en C, le raisonnement propre à ce mode de développement, avec les mêmes expérience révélatrices et "Aha moment" de l'exercice d'origine, mais appliqué au C.

                  Ce ne sont pas 122 slides de contenus indigeste, mais 122 slides montrant le cheminement de la pensée et les évolutions successives des tests, et comment les tests pilotent aussi la réflexion et la mise au point de l'algorithme, en partant de tests qui décrivent les comportements attendus avant de coder ces comportements. Olve Maudal avait conçu ces slides comme pour une présentation éclair de 10 minutes.

                  C'est assez transformateur :-)

                  Je le recommande à tous ceux qui sont intéressés par une compréhension concrète du TDD en C.

                  -
                  Edité par Dlks 15 juin 2022 à 20:06:00

                  • Partager sur Facebook
                  • Partager sur Twitter
                    16 juin 2022 à 13:58:30

                    Ah oui, c'est ingénieux la combinaison assert ( condition && "chaine" )  pour avoir une description du test quand il (pas seulement la condition).

                    Pour les tout-à-fait débutants ça va être un peu wtf de faire un && avec une chaine, mais bon, c'est jouable.

                    int main() 
                    {
                        assert (somme_entiers(0) == 0 && "cas limite");
                        assert (somme_entiers(1) == 1 && " 1 = 1 !");
                        assert (somme_entiers(3) == 6 && "6 = 1+2+3");
                    }
                    

                    (contexte : enseigner la programmation en intégrant tôt l'impérieuse nécessité d'écrire des tests, et de préférence avant de coder ce qui doit être testé)

                    -
                    Edité par michelbillaud 16 juin 2022 à 14:00:06

                    • Partager sur Facebook
                    • Partager sur Twitter
                      16 juin 2022 à 17:03:13

                      Pour faire comprendre pourquoi on devrait écrire un test avant d'implémenter, il faut juste faire comprendre une idée très simple.

                      Les tests (par exemple, ceux que tu as écrit) sont une expression des spécifications du programme que l'on réalise.

                      Les spécifications sont (devraient être) fournies au développeur en amont, et décrivent le comportement du programme attendu. Si elles ne lui sont pas fournies, ou fournies de façon trop générale ou imprécise, il a intérêt à travailler de concert avec son donneur d'ordre pour comprendre dans le détail ce qu'il veut et établir ces spécifications avec un degré de détail suffisant pour lui permettre de comprendre les comportements attendus.

                      Les tests ne font qu'exprimer sous la forme de code (non encore fonctionnel) le comportement attendu que l'on se propose d'implémenter.

                      Lorsque le test passe, on a implémenté une partie du comportement attendu.

                      Les tests écrits sous cette forme sont compréhensibles par le donneur d'ordre (il peuvent même l'être sous une forme plus proche du langage courant) et peuvent être validés par lui.

                      Ils deviennent une partie de la documentation de référence du projet.

                      Dans un exercice, le donneur d'ordre c'est l'enseignant et les comportement attendus qu'il décrit sont dans l'énoncé et indications qu'il fournit.

                      • on traduit une première indication sous la forme d'un test, testant le cas le plus simple, et on constate que le test échoue (s'il n'échoue pas alors qu'on n'est pas sensé avoir implémenté le comportement en question, il faut savoir pourquoi)
                      • on code (juste) ce qu'il faut pour faire passer le test
                      • on vérifie si on ne doit pas refactoriser le code pour l'améliorer et on vérifie que les tests passent toujours

                      On passe au test suivant, testant un cas plus complexe ou plus général.

                      On répète ces micro-cycles (dits "Red - Green - Refactor") jusqu'à ce que l'on ait écrit tous les tests décrivant les différents comportements attendus, à chaque itération l'intégralité des tests sont exécutés pour vérifier l'absence de régressions. Quand on n'a plus rien à tester, on a terminé l'exercice, et on peut le prouver.

                      Ecrire un test une fois que l'implémentation est faite n'est pas une bonne approche :

                      • c'est chiant à faire, on a la flemme de le faire (çà marche déjà, c'est une perte de temps, etc.),
                      • on a oublié tous les cas qu'on a voulu traiter lors du développement (et qui sont des cas qui apparaissent lors des itérations de développement et pas toujours directement des indications de l'énoncé)
                      • le code réalisé peut être difficilement testable, parce qu'on ne l'a pas conçu de façon à ce qu'il puisse être testé
                      • on risque d'écrire un test qui dépend de choix d'implémentation, et non pas des tests testant des comportements attendus

                      Ce dernier point est important. Un test ne devrait pas échouer parce que la façon dont on implémente le comportement attendu change. Si tel est le cas, cela veut dire que l'on ne teste pas le comportement attendu selon la spécification, mais une implémentation donnée. Le test doit exprimer le comportement attendu, rien d'autre.

                      C'est un des aspects les plus délicats de cette méthode. Cela force à établir des points d'entrée et de sortie qui sont une interface générique encapsulant les détails de l'implémentation.

                      C'est une "bonne chose", car cela permet à des équipes (d'étudiants / de développeurs) de travailler sur différentes parties d'un projet conséquent sans avoir à attendre que la partie des autres soit terminée, en se basant sur une interface dont les comportements attendus peuvent être simulés sur les parties qu'ils n'ont pas la charge de réaliser.

                      C'est (encore) un autre avantage de cette approche du développement.

                      -
                      Edité par Dlks 16 juin 2022 à 17:09:12

                      • Partager sur Facebook
                      • Partager sur Twitter
                        16 juin 2022 à 17:42:28

                        Je suis d'accord que c'est mieux de faire les tests avant de coder quand on connait déjà la spécification. Mais si on ne l'a pas la spécification ? Parce que par exemple on est en train de tester un truc et on sait pas trop encore à quoi ça va ressembler. Si on a juste une vague idée du truc mais pas le détail de chaque fonction, qu'est ce qu'elle fait, etc, comment on peut écrire le test pour tester une fonction si on ne sait pas encore ce que va faire la fonction ?
                        • Partager sur Facebook
                        • Partager sur Twitter
                          16 juin 2022 à 18:03:01

                          LucieNottin1 a écrit:

                          comment on peut écrire le test pour tester une fonction si on ne sait pas encore ce que va faire la fonction ?

                          Si on ne sait pas ce qu'elle va faire la fonction, on ne pourra pas l’écrire non plus !

                          Conclusion : on attends d'avoir les informations sur ce que doit faire la fonction, avant de faire quoi que ce soit !

                          • Partager sur Facebook
                          • Partager sur Twitter
                          ...
                            16 juin 2022 à 18:06:54

                            bool test() {
                                return true;
                            }
                            • Partager sur Facebook
                            • Partager sur Twitter

                            Le Tout est souvent plus grand que la somme de ses parties.

                              16 juin 2022 à 18:08:48

                              rouIoude a écrit:

                              LucieNottin1 a écrit:

                              comment on peut écrire le test pour tester une fonction si on ne sait pas encore ce que va faire la fonction ?

                              Si on ne sait pas ce qu'elle va faire la fonction, on ne pourra pas l’écrire non plus !

                              Conclusion : on attends d'avoir les informations sur ce que doit faire la fonction, avant de faire quoi que ce soit !

                              Justement, pour avoir des informations sur ce que va faire la fonction, il faut déjà avoir au moins une idée de ce qu'on doit faire, et je trouve qu'un bon moyen pour ça c'est de commencer à écrire le code qui fait ce qu'on doit faire, pour pouvoir ensuite le réorganiser en fonctions, etc, et c'est là qu'on saura exactement quelles sont les fonctions et qu'est ce qu'elles doivent faire non ?

                              • Partager sur Facebook
                              • Partager sur Twitter
                                16 juin 2022 à 18:15:40

                                Si je prend l'exemple du post courant.
                                Ça veut dire quoi "insérer au milieu"??
                                Je pourrais écrire différentes versions du code pour faire quelque chose du genre.
                                Mais je dois en recommencer une partie pour chaque nouvelle spécification.
                                L'exemple est simple.
                                Mais si ça conduit à l'écriture de dizaines de fonctions et des milliers de lignes de code, c'est auttre chose.
                                • Partager sur Facebook
                                • Partager sur Twitter

                                Le Tout est souvent plus grand que la somme de ses parties.

                                  16 juin 2022 à 18:28:00

                                  LucieNottin1 a écrit :

                                  Si on a juste une vague idée du truc mais pas le détail de chaque fonction, qu'est ce qu'elle fait, etc, comment on peut écrire le test pour tester une fonction si on ne sait pas encore ce que va faire la fonction ?

                                  Si tu ne sais pas quel est le comportement attendu de ton programme (ce qu'il doit faire), alors n'as effectivement pas la spécification des comportements attendus, et tu ne peux rien faire (tu ne peux même pas "tester un truc et on sait pas trop encore à quoi ça va ressembler" car tu ne sais rien).

                                  Si tu as la spécification des comportements attendus, mais que tu ne sais pas encore dans le détail comment tu vas l'implémenter, tu peux (dois) écrire un test qui décrit ce comportement. Le test va échouer, car tu n'auras pas encore écrit le reste du code et des fonctions auxiliaires nécessaires à l'obtention du résultat.

                                  Par exemple, on me demande d'écrire un programme qui, partir d'un nombre entier en chiffres, traduit ce nombre en lettres.

                                  Je n'ai, au départ, aucune idée de comment je vais réaliser cela, car je n'ai pas commencé ma phase exploratoire, mais à partir de l'information qu'on me donne, je peux quant même écrire ce test :

                                  assert(strcmp(convert_int_to_string(1), "un") == 0
                                                  && "1 -> un");
                                  

                                  Je peux écrire aussi un test pour 123456 qui renvoie "cent vingt trois mille quatre cent cinquante six" (et tout un tas d'autres), si je veux, mais on va commencer petit (et traiter des cas plus complexes ou plus généraux petit à petit).

                                  La compilation va échouer, car mon interface avec mon programme convert_int_to_string() ne sera pas écrite, et une fois que j'aurai écrit une charpente de fonction la compilation réussira, mais pas le test, car elle sera encore vide. C'est le point de départ de ton travail : faire en sorte d'écrire le code nécessaire pour que le test passe.

                                  Tu pourras aussi écrire des tests pour vérifier le comportement des fonctions auxiliaires, mais plus tu entreras dans le détail, plus tu risqueras de tester les détails de l'implémentation et plus ton test sera fragile et dépendant de choix pouvant évoluer.

                                  -
                                  Edité par Dlks 16 juin 2022 à 18:28:38

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    16 juin 2022 à 20:04:25

                                    Ce que décrit  LucieNottin1 est la situation lorsqu'on programme pour soi même, soit pour s'exercer tout seul, soit pour s'amuser.
                                    • Partager sur Facebook
                                    • Partager sur Twitter
                                      16 juin 2022 à 21:40:42

                                      robun a écrit:

                                      Ce que décrit  LucieNottin1 est la situation lorsqu'on programme pour soi même, soit pour s'exercer tout seul, soit pour s'amuser.


                                      Quand on met des lignes de code l'une après l'autre au hasard en se demandant ce que ça pourrait bien faire ?
                                      • Partager sur Facebook
                                      • Partager sur Twitter
                                        16 juin 2022 à 22:04:27

                                        michelbillaud a écrit:

                                        robun a écrit:

                                        Ce que décrit  LucieNottin1 est la situation lorsqu'on programme pour soi même, soit pour s'exercer tout seul, soit pour s'amuser.


                                        Quand on met des lignes de code l'une après l'autre au hasard en se demandant ce que ça pourrait bien faire ?


                                        Je dis pas qu'il faut taper au pif jusqu'à ce que ça marche x) Ce que je veux dire c'est que si je dois implémenter un truc pour résoudre un problème, j'ai tendance à commencer "au brouillon" histoire de "débroussailler le terrain" pour y voir plus clair. Une fois que j'y vois plus clair et que j'ai clairement en tête les contraintes liées au problème, je peux identifier quelles seront les fonctions qu'il y aura et qu'est ce qu'elles feront. Si par exemple je commence à me dire que je vais avoir une classe Ceci et une classe Cela qui fait ceci ou cela, c'est une décision qui n'est pas forcément informée sur la réalité du truc à implémenter. Peut être que "dans ma tête conceptuellement", ça avait du sens de séparer ceci et cela, mais qu'en fait on se rendrait compte en implémentant le truc que la séparation des 2 entraine par exemple des problèmes pour faire passer des infos de l'un à l'autre ou ce genre de chose. En commençant par "faire le truc le plus simple qui s'approche de la résolution du problème" sans se préoccuper au préalable de comment sera organisé, cela permet de ne pas passer à côté d'opportunités d'organisation. Puisque si je décide arbitrairement de séparer ceci et cela, je vis désormais dans un monde dans lequel ces 2 trucs sont séparés et je ne peux plus voir facilement une autre éventuelle organisation. Alors que si on se préoccupe juste du problème en lui même, le code commence dans un état "brouillon" mais au moins on voit clairement ce qu'il y a à faire et comment résoudre le problème puisqu'on l'a déjà sous les yeux ! A partir de là c'est très facile de réorganiser les choses en classes/fonctions et on est sûrs de prendre une décision informée
                                        • Partager sur Facebook
                                        • Partager sur Twitter
                                          17 juin 2022 à 1:10:52

                                          On nous dit souvent de commencer par une feuille de papier et un crayon.
                                          Et si la feuille était l'éditeur et le crayon était le clavier. C'est ce que je fais généralement.
                                          Je peux écrire du français (ou anglais ...) ou du pseudo-pseudo code ...
                                          Je pense qu'il faut tout de même un minimum de réflexion avant de taper quoi que ce soit.
                                          • Partager sur Facebook
                                          • Partager sur Twitter

                                          Le Tout est souvent plus grand que la somme de ses parties.

                                            17 juin 2022 à 8:23:11

                                            La programmation "exploratoire", c'est à dire commencer à programmer des trucs sans assurance de partir dans un plan général bien défini, ce n'est pas du tout incompatible avec la production des petits morceaux en se guidant sur les tests.

                                            Le but intermédiaire est de produire des petits bouts qui ne seront peut être pas conservés dans le programme final, ça ne veut pas dire qu'on peut se dispenser de coder au mieux ces petits bouts.

                                            -
                                            Edité par michelbillaud 17 juin 2022 à 8:27:28

                                            • Partager sur Facebook
                                            • Partager sur Twitter
                                              17 juin 2022 à 9:35:14

                                              michelbillaud a écrit:

                                              robun a écrit:

                                              Ce que décrit  LucieNottin1 est la situation lorsqu'on programme pour soi même, soit pour s'exercer tout seul, soit pour s'amuser.


                                              Quand on met des lignes de code l'une après l'autre au hasard en se demandant ce que ça pourrait bien faire ?


                                              Non : quand on n'a pas les spécifications précises. (Je rappelle son message : « Je suis d'accord que c'est mieux de faire les tests avant de coder quand on connait déjà la spécification. Mais si on ne l'a pas la spécification ? Parce que par exemple on est en train de tester un truc et on sait pas trop encore à quoi ça va ressembler. »)
                                              • Partager sur Facebook
                                              • Partager sur Twitter
                                                17 juin 2022 à 10:28:10

                                                robun a écrit:

                                                 Mais si on ne l'a pas la spécification ? Parce que par exemple on est en train de tester un truc et on sait pas trop encore à quoi ça va ressembler. »)


                                                Si on n'a pas la spécification, on ne sait pas ce que ça va faire, et effectivement on aura du mal à dire ce qui devrait se passer sur les exemples des tests, et donc c'est mal barré pour coder.

                                                Donc, on en revient à coder des petits bouts pour voir. Mais ces petits bouts, il faut bien qu'ils aient une spécification précise quand on se décide à les écrire. Même si la spécification doit évoluer ensuite, quand on se rendra compte que le petit bout (qui marche) ne rend pas tout à fait le service voulu pour le programme qu'on envisage.

                                                Et c'est là qu'on trouvera une grande utilité aux tests écrits au départ : ils fournissent une base pour vérifier que le code modifié marche toujours sur les tests déjà écrits, éventuellement modifiés pour tenir compte de la nouvelle idée.

                                                -
                                                Edité par michelbillaud 17 juin 2022 à 10:29:28

                                                • Partager sur Facebook
                                                • Partager sur Twitter

                                                insertion au milieu de la liste chainée

                                                × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                                                × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                                                • Editeur
                                                • Markdown