Voir le flux RSS

François DORIN

[Actualité] Retour sur une proposition pour C# 8 : les références nullables

Noter ce billet
par , 25/03/2018 à 19h43 (9562 Affichages)
L'erreur à un milliard de dollars
Connaissez-vous Tony Hoare ? Il y a de grandes chances que non. Pourtant, en tant que développeurs, vous utilisez très certainement le fruit de ses travaux. L'un d'entre eux, qu'il qualifie lui-même comme "l'erreur à 1 milliard de dollars", est le pointeur nul, objet du billet d'aujourd'hui.

Très pratique, le pointeur nul est présent dans de nombreux langages. C++, Java, C# pour ne prendre que les exemples de langages les plus répandus.

Par exemple, en C#, la valeur nulle est la valeur par défaut des types références. Si vous allouez un tableau d'objets, chaque case du tableau est initialisée… à nulle ! Ou encore, lorsqu'on souhaite préciser qu'une variable n'a pas de valeur, on l'initialise là aussi à nulle.

Mais qui n'a jamais eu à souffrir, durant l'exécution de son code, d'un NullPointerException ? Que le développeur à qui cela n'est jamais arrivé lève la main ! Cette erreur est fréquente (sans doute l'erreur la plus répandue !) et indique l'absence de valeur là où on en attend une. Cela oblige donc à tester la valeur de la variable avant de l'utiliser.

Et c'est là que réside le problème de la plupart des langages ayant cette notion. Il n'y a rien pour distinguer une valeur nulle fortuite d'une valeur nulle voulue.

Évolution du langage C#
Le lien avec C#8 ? Il y a actuellement de longues discussions autour d'une fonctionnalité portant le nom de "référence nullable"(nullable reference dans la langue de Shakespeare), qui permettrait justement de préciser si une référence peut être nulle ou non.

Si on passe outre l'aspect rupture du langage, quels seraient les avantages d'une telle approche ? Et bien le principal avantage est de permettre de corriger de nombreux bogues à la compilation, et non plus à l'exécution.

Ainsi, un code comme
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
if (hello.Length == 0)
{}

Générera une erreur si hello est une variable de type String?, mais pas d'erreur si elle est de type String.

Pour un type String?, il faudra vérifier que la référence est non nulle
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
if (hello != null && hello.Length == 0)
{}

En théorie, c'est très intéressant, mais cela soulève encore de nombreux problèmes, dont toutes les questions n'ont pas encore trouvé réponse. Par exemple, que se passe-t-il si une méthode teste le caractère nul d'une variable ? Par exemple :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
String? Hello = "world";
if (!String.IsNullOrEmpty(hello))
{
   int taille = hello.Length; // Générera une erreur !
}
Pourquoi une erreur ? Nous, en tant que développeurs, nous savons que la méthode String.IsNullOrEmpty teste le caractère nul ou vide d'une chaîne de caractères. Mais le compilateur ne le sait pas lui. Pour résoudre ce problème, deux approches sont envisageables :
  1. La première, rajouter un test de nullité dans la conditionnelle. Pas terrible. En plus d'être redondant, cela alourdit le code ;
  2. La seconde, introduire une nouvelle syntaxe, permettant de dire au compilateur "OK, je sais ce que je fais, ne génère pas d'erreur" (par exemple, int taille = hello.Length!;).


Des zones d'ombre...
De plus, si cette approche permettrait de résoudre certains problèmes, elle n'est pas sans en poser d'autres, qu'il va aussi falloir résoudre avant de pouvoir déployer cette fonctionnalité :
  • une valeur (notamment une propriété) peut très bien devenir nulle entre le moment où elle a été testée et où la valeur est effectivement utilisée ;
  • des méthodes qui retournent des valeurs non nulles pourront malgré tout retourner des valeurs nulles (cas d'une API qui n'aurait pas été mise à jour par exemple).


Une évolution ou une révolution ?
Le plus gros problème de cette approche reste qu'elle implique une évolution même du langage, évolution "cassante" changeant la sémantique de tout code écrit jusqu'à aujourd'hui. On comprend donc les longues discussions qu'il peut y avoir à ce sujet.

Illustrons ceci avec quelques exemples. Aujourd'hui, ce code est tout à fait valide :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
String hello = "World";
Hello = null;

Demain, ce code ne sera plus valide, et générera une erreur au niveau de la seconde ligne, lors de l'affectation de la valeur null.

Autrement dit, par défaut, les références ne seront plus nullables. Pour retrouver le comportement actuel, il faudra déclarer les variables comme étant nullables :
Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
String? hello = "World";
Hello = null; // Opération valide, puisque hello est nullable.

Pour éviter de casser tout le code existant, ce changement ne générera que des avertissements, et non des erreurs. Mais un code propre d'aujourd'hui pourra se retrouver avec des milliers d'avertissements demain !

Affaire à suivre
Les discussions sont loin d'être terminées. On parle déjà de C#8 alors que la prochaine version attendue de C# est la 7.3. Mais ce changement est loin d'être anodin et il est donc nécessaire de l'anticiper largement en amont, car s'il venait à être adopté, cela sera un changement majeur du langage comme jamais il n'en a connu jusqu'à aujourd'hui.

Sources
le projet C# sur github
exemple de discussion sur Reddit (213 commentaires au moment de l'écriture de ce billet, ce qui est énorme !)

Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Viadeo Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Twitter Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Google Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Facebook Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Digg Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Delicious Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog MySpace Envoyer le billet « Retour sur une proposition pour C# 8 : les références nullables » dans le blog Yahoo

Mis à jour 18/04/2018 à 14h39 par François DORIN

Catégories
DotNET , C#

Commentaires

Page 2 sur 2 PremièrePremière 12
  1. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ijk-ref
    Surement un peu Sauf que tu sais aussi (avec ton article) qu'il sera très difficile de faire passer une idée comme travaillée devant quelqu'un ne comprenant déjà ce qu'elle apporte ou résout.

    Le premier gros travail est d'être sûr qu'on parle bien de la même chose et que nos attentes d'un langage ne soient pas contradictoires. Par exemple il est quasi vain de parler d'améliorations de détection d'erreur à la compilation avec une personne ne jurant que par "dynamic".
    +1

    Citation Envoyé par ijk-ref
    C'est un peu le souci de penser que je devrais utiliser du "Pattern matching"
    -1

    Plus sérieusement, là où je suis d'accord avec toi sur le coup du dynamic (introduit à la base pour que le CLR puisse supporté des langages comme Python), sur le coup du pattern matching je ne suis pas d'accord, car je ne vois pas ce qu'apporte ta solution, hormis une syntaxe différente.

    Une des volontés de la gouvernance de C# est de garder une syntaxe le plus clair possible (et donc, éviter d'avoir 3 manières d'écrire la même chose). Les nouvelles syntaxes, lorsqu'elles sont introduites, ont une plus value qui justifie cette redondance.

    Citation Envoyé par ijk-ref
    Je ne connais pas d'autre moyen que d'utiliser des exemples particuliers pour expliquer un ajout qui sera tout sauf pour un usage "unique".
    Je ne dis pas qu'il ne faut pas utiliser des cas particuliers, je dis qu'il ne faut pas voir QUE les cas particuliers. Notamment, essayer de percevoir les détournements d'usage qui pourraient en être fait, les effets de bords, etc...

    Citation Envoyé par ijk-ref
    Ne te focalise pas trop sur ses difficultés (réelles) d'intégration (pas plus qu'une proposition Github) sinon tu ne verras jamais la "Lumière"
    Pour l'instant, j'essai d'abord de comprendre les tenants et aboutissants de ta proposition Chaque chose en son temps !

    Citation Envoyé par ijk-ref
    Voila, clairement je me suis pas du tout fait comprendre. Pourtant quand j'ai écrit :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    MyIntParse(str)->(var i) { Console.Writeline(i); }
    Tu avais l'air de comprendre que "i" est la sortie de la méthode. Donc l'inférence de type est parfaitement déterminé. Cette syntaxe permettrait de dire au compilateur de rentrer dans le block de code qui suit uniquement en cas de sortie normal de la méthode. Si la méthode arrive à une exception passer à la suite SANS lancer/initialiser l'exception.
    Je confirme : nous ne nous sommes pas compris. Tu parles de sortie "normale" que tu opposes aux "exceptions". Mais c'est déjà le système en vigueur à l'heure actuelle en C#. Avec ta proposition, l'idée serait d'avoir potentiellement plusieurs sorties normales. Dans ce cas, comment utiliser l'inférence de type ?

    De plus, si ton idée est finalement de permettre de considérer une exception comme une sortie normale, alors le problème est autre. Une exception, comme son nom l'indique, doit restée exceptionnelle. Pourquoi ? Car une exception, c'est un traitement lourd. Très lourd. Faire une procédure qui renvoie par une exception une valeur au lieu d'utiliser un paramètre de la fonction ou une valeur de retour, c'était facilement un facteur x1000 en terme de perte de performance en C++, ainsi qu'en Java, quand j'avais fait les tests sur une discussion animée et passionnée que j'avais eu avec un des responsables de la rubrique Java il y a quelques années (Michaël, si tu me lis, je te salue ).
    Donc, si l'idée est de "banaliser" l'usage des exceptions pour ce genre de cas de figure, pour ma part, je trouve que c'est une mauvaise idée.

    Citation Envoyé par ijk-ref
    Ne vois-tu pas l'intérêt pour nous et le compilateur de permettre une tel optimisation ? Sa syntaxe ne te plait peut-être pas car tu n'y vois pas une flèche "->" pour un retour de valeur. T'y vois plus le "=>" déclaratif et le remplaçant du "(*pointer)." en C++. Il y a p'être mieux. Si t'en suggères une plus approprié n'hésite pas. A défaut du TryParseOut qui n'est qu'une bidoule demandant au compilateur de faire plus qu'il ne devrait, Surement que tu préfèrerais :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    try(var i = MyIntParse(str)) { Console.Writeline(i); }
    Sauf que ma syntaxe est faite pour être bien plus générale et traiter d'autres cas.
    Clairement, le problème que j'ai avec la syntaxe que tu proposes n'est pas sur l'usage de ->, de => ou de n'importe quelle notation.

    Je reprends un de tes codes :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    MyParse("3.14")->(int r)
    {
      Console.Writeline("resultat : " + r);
    }
    |MyError()->()
    {
      Console("n'est pas un entier");
    }
    Ici, si je comprends bien, ce que tu veux dire, c'est qu'on appelle la méthode MyParse, avec le paramètre "3.14". Si on utilise la "sortie normale", on exécute le petit bout de code introduit par (int r), où r représente la valeur de retour.
    Si on ne fait pas la sortie "normale", on exécute le MyError(). Et c'est là que j'ai du mal, car le "MyError()" est juste une notation pour nommer la sortie. Or, là, on dirait l'appel à une fonction ! Il y a donc une terrible ambiguïté. Ou alors, je n'ai vraiment pas compris

    Citation Envoyé par ijk-ref
    Comme par exemple construire une méthode permettant de s'abonner à une liste d'évènements, d'attendre et de s'y désabonner au 1er évènement déclenché en sortant de la méthode avec les arguments de l'évènement dans block de code approprié. Non, on ne peut pas le faire actuellement avec vérification statique et une syntaxe lisible/concise !

    Si une tel traitement était spécifiquement codé en dur (dans le compilateur) avec sa propre syntaxe, une boucle traitant des messages/évènements pourrait ressembler à cela :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    while(true)
    {
      await message(MouseEventArgs r of MouseMove)
      {
        label.Content = "mouse location : " + r.GetPosition(this); 
      }
      or message(MouseButtonEventArgs r of MouseDown)
      {
        label.Content = "mouse button down : " + r.ChangedButton;
      }
      or message(MouseButtonEventArgs r of MouseUp)
      {
        label.Content = "mouse button up : " + r.ChangedButton;
      }
      or message(KeyEventArgs m of KeyEventArgs}
      {
        if(m.Key==Key.Escape) break;
      }
    }
    Sans vouloir rabâcher la même chose : le pattern matching introduit avec C#7
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     
    while(true)
    {
       var message = await GetMessage();
       switch(message) 
       {
          case MouseEvenrArgs r:
             label.Content = "mouse location : " + r.GetPosition(this); 
             break;
          case MouseButtonEventArgs r when button.Clicked = MouseDown:
             label.Content = "mouse button down : " + r.ChangedButton;
             break;
          case MouseButtonEventArgs r when button.Clicked = MouseUp:
             label.Content = "mouse button up : " + r.ChangedButton;
             break;
          case KeyEventArgs:
             if(m.Key==Key.Escape) break;
             break;
       }
    }
  2. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Ici, si je comprends bien, ce que tu veux dire, c'est qu'on appelle la méthode MyParse, avec le paramètre "3.14". Si on utilise la "sortie normale", on exécute le petit bout de code introduit par (int r), où r représente la valeur de retour.
    Si on ne fait pas la sortie "normale", on exécute le MyError(). Et c'est là que j'ai du mal, car le "MyError()" est juste une notation pour nommer la sortie. Or, là, on dirait l'appel à une fonction ! Il y a donc une terrible ambiguïté. Ou alors, je n'ai vraiment pas compris
    Oui on dirait, après j'avoue qu'en voulant simplifier pour que tu comprennes mieux j'ai apporté une grosse ambiguïté

    Cette simplification était dans un 1er temps de ne pas toucher à la syntaxe de la signature et au corps des méthodes.
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    MyIntParse(str)->(var i) { Console.Writeline(i); }
    Donc ici on utilise la méthode MyIntParse() dont le corps est écrit classiquement avec des "return" et des "throw new" des plus banales.
    Sauf qu'utilisé comme je l'indique, le compilateur se chargera alors d'en faire une copie en remplaçant les "throw new" (seulement ceux de la méthode !) par une sortie nous plaçant juste après le block.
    J'espère que tu vois clairement qu'il n'y a aucun surcouts. C'est "x1000" plus rapide qu'une interception d'exceptions et (légèrement) plus rapide aussi qu'un TryParseOut puisqu'on a pas à vérifier un booléen.

    Si j'ai réussi à être clair, on passe à l'étape suivante : nommer manuellement ses sorties. Car on ne s'en rend pas compte mais on transforme tous ce qui est "différent" en exceptions surtout parce qu'on n'a pas le choix. L'usage des exceptions déforme la réalité, une seconde sortie n'est pas forcement qu'une exception. D'ailleurs "bool TryParse(string s, out Int32 result)" ne renvoie pas vrai et exceptionnellement faux, non elle renvoie vrai ou faux et ces 2 valeurs doivent être traitées à égalité. Il n'y a donc pas de raison que la méthode "int Parse(string s)" ne fasse pas de même... enfin plutôt celle-ci "int Parse(string s) | void Error()"

    Je conviens que ça peut perturber au 1ère abord mais il faut comprendre qu'il n'y a PAS une méthode "MyParse()" et une autre méthode "MyError()". "int MyParse(string s) | void MyError()" désigne qu'une seule et unique méthode, dont le corps ressemblerait à ceci :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int MyParse(string s) | void MyError()
    {
      if(...)
      {
        MyParse.return 314;
      }
      MyError.return;
    }
    La force de cette syntaxe est que toutes les sorties ont la même... syntaxe ! Elles ont toutes un type sortie, un nom et autant de paramètres que nécessaire. On peut mettre les sorties dans l'ordre qu'on veut, ce n'est qu'une convention.

    Ensuite quand on utilise cette méthode, il faut bien voir le "|" devant le "MyError()" qui AMHA représente assez bien la continuité de la méthode, montrant une certaine liaison entre les blocks. Evidemment quand ça sort de nulle part c'est plus dur à comprendre

    Donc "MyError()" n'exécute rien pas plus que "MyParse(s)"... je comprends encore moins le sens de "exécuter le petit bout de code introduit par (int r)". C'est le tout qui exécute la méthode et unique méthode.

    Normalement si t'y vois plus clair je ne devrais pas à avoir t'expliquer pourquoi ton "dynamic" oups "pattern matching" n'est pas du tout la même chose

    A+
  3. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ijk-ref
    Oui on dirait, après j'avoue qu'en voulant simplifier pour que tu comprennes mieux j'ai apporté une grosse ambiguïté

    Cette simplification était dans un 1er temps de ne pas toucher à la syntaxe de la signature et au corps des méthodes.
    Ok donc sur ce point on s'est bien compris.

    Citation Envoyé par ijk-ref
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    MyIntParse(str)->(var i) { Console.Writeline(i); }
    Donc ici on utilise la méthode MyIntParse() dont le corps est écrit classiquement avec des "return" et des "throw new" des plus banales.
    Sauf qu'utilisé comme je l'indique, le compilateur se chargera alors d'en faire une copie en remplaçant les "throw new" (seulement ceux de la méthode !) par une sortie nous plaçant juste après le block.
    J'espère que tu vois clairement qu'il n'y a aucun surcouts. C'est "x1000" plus rapide qu'une interception d'exceptions et (légèrement) plus rapide aussi qu'un TryParseOut puisqu'on a pas à vérifier un booléen.
    Je pense que là aussi pas de soucis, on se comprend. Par contre, penser que cela sera plus rapide qu'un TryParseOut parce qu'il n'y aura pas de booléen a vérifier est une erreur. Tu ne verras pas cette vérification, mais le compilateur lui sera bien obligé de générer du code pour cela, pour savoir quel bloc de sortie exécuter. Ce sera juste du sucre syntaxique.

    Et crois moi, parfois, le compilateur fait un boulot monstre la dessus ! Il n'y a qu'a jeter un oeil au code généré lors de l'usage de async/await (toute une machine à état)

    Citation Envoyé par ijk-ref
    Si j'ai réussi à être clair, on passe à l'étape suivante : nommer manuellement ses sorties. Car on ne s'en rend pas compte mais on transforme tous ce qui est "différent" en exceptions surtout parce qu'on n'a pas le choix. L'usage des exceptions déforme la réalité, une seconde sortie n'est pas forcement qu'une exception.
    C'est une erreur que de penser qu'on a pas le choix. Il existe plusieurs manières de renvoyer plusieurs informations, et l'usage de out en est une.

    Citation Envoyé par ijk-ref
    D'ailleurs "bool TryParse(string s, out Int32 result)" ne renvoie pas vrai et exceptionnellement faux, non elle renvoie vrai ou faux et ces 2 valeurs doivent être traitées à égalité. Il n'y a donc pas de raison que la méthode "int Parse(string s)" ne fasse pas de même... enfin plutôt celle-ci "int Parse(string s) | void Error()"
    Pourquoi n'y aurait-il pas une raison ? Bien au contraire j'ai envie de dire ! Elles sont nommées différemment, et leur nom représente bien ce qu'elles font :
    • TryParse : on essaie de parser, donc on pense qu'il y a de forte chance pour que la saisie ne soit pas valide (par exemple, car saisie humaine) ;
    • Parse : on parse. L'appelant s'assure que le format de l'entrée est correct. Dans l'éventualité où cela ne serait pas le cas, on génère une exception.

    Pourqoi, dès lors, vouloir absolument qu'elles fassent la même chose ?

    Citation Envoyé par ijk-ref
    Je conviens que ça peut perturber au 1ère abord mais il faut comprendre qu'il n'y a PAS une méthode "MyParse()" et une autre méthode "MyError()". "int MyParse(string s) | void MyError()" désigne qu'une seule et unique méthode
    J'avais bien compris ce point. Mais justement, cette syntaxe ambiguë ne sera jamais acceptée. Puisqu'il s'agit de la même méthode, pourquoi ne reprend ton pas la liste des arguments au sein de la déclaration de MyError ?
    Avec cette syntaxe, on pourra penser que tu définies deux méthodes, MyParses et MyError, qui aurait le même corps !

    Citation Envoyé par ijk-ref
    dont le corps ressemblerait à ceci :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int MyParse(string s) | void MyError()
    {
      if(...)
      {
        MyParse.return 314;
      }
      MyError.return;
    }
    La force de cette syntaxe est que toutes les sorties ont la même... syntaxe ! Elles ont toutes un type sortie, un nom et autant de paramètres que nécessaire. On peut mettre les sorties dans l'ordre qu'on veut, ce n'est qu'une convention.
    Je propose ceci :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    (default => int, MyError => void) MyParse(string s)
    {
      if(...)
      {
        return 314; // sans rien, on utilise le chemin par défaut
      }
      return<MyError>; // ou return.MyError, mais dans ce cas on a l'impression que return n'est plus un mot clé mais un objet...
    }

    Les changements permettent :
    • de lever l’ambiguïté soulevé précédemment sur la gestion des paramètres ;
    • de garder le mot clé return pour le renvoi des informations. La syntaxe que tu proposes poses de nombreux soucis (on dirait qu'on accès à une propriété, le mot clé return n'est plus utilisé pour sortir de la fonction



    Citation Envoyé par ijk-ref
    Ensuite quand on utilise cette méthode, il faut bien voir le "|" devant le "MyError()" qui AMHA représente assez bien la continuité de la méthode, montrant une certaine liaison entre les blocks. Evidemment quand ça sort de nulle part c'est plus dur à comprendre
    Justement, pour ma part, je trouve que l'usage de | ne reflète pas du tout la continuité de la méthode. |, c'est l'opérateur binaire OU.

    Ici aussi, je propose de réécrire ta suggestion :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    MyParse("3.14")->(int r)
    {
      Console.Writeline("resultat : " + r);
    }
    |MyError()->()
    {
      Console("n'est pas un entier");
    }
    En ceci :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    switch(MyParse("3.14"))
    {
       case MyError:
          Console("n'est pas un entier");    
          break;
       default:
          Console.Writeline("resultat : " + value);  // le mot clé value se comporte comme dans un setter : il prend la valeur assignée (ici de retour de la fonction)
          break;
    }

    On réutilise ainsi une constructions déjà présente au sein du langage, tout en étant beaucoup plus clair je trouve.

    Citation Envoyé par ijk-ref
    Donc "MyError()" n'exécute rien pas plus que "MyParse(s)"... je comprends encore moins le sens de "exécuter le petit bout de code introduit par (int r)". C'est le tout qui exécute la méthode et unique méthode.
    Par le "petit bout de code introduit par ->(int r)" je faisais référence à cette ligne Console.Writeline("resultat : " + r);
    Citation Envoyé par ijk-ref
    Normalement si t'y vois plus clair je ne devrais pas à avoir t'expliquer pourquoi ton "dynamic" oups "pattern matching" n'est pas du tout la même chose
    Justement, je te tend des perches pour que tu puisses défendre ta proposition. Car comme c'est ta proposition, c'est bien à toi de faire le boulot Mais tu ne les saisies pas :/

    Pointe les différences, argumente, etc.

    Tu dis que c'est différent du pattern matching, sans préciser en quoi. Tu pourrais dire
    • que le pattern matching est dynamique alors que ta proposition est statique ;
    • qu'il est possible d'avoir plusieurs sorties avec le même type, ce qui n'est pas possible avec le pattern matching (mais on peut utiliser la clause where pour traiter différent cas)


    Maintenant, il reste encore beaucoup de boulot. Sur les impacts d'une telle approche, quid :
    • de la gestion des delegate ?
    • de la définition des événements ?
    • de la gestion de propriétés (qui ne sont que du sucres syntaxiques vers des méthodes get et set) ?
    • de la compatibilité avec d'autres langages managés ?
    • de la possibilité 'appeler une telle méthode manière classique ? En ne tenant compte que de la sortie "par défaut" par exemple ?
    • ...
  4. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Justement, je te tend des perches pour que tu puisses défendre ta proposition. Car comme c'est ta proposition, c'est bien à toi de faire le boulot Mais tu ne les saisies pas :/
    Mais J'ai bien l'intention d'en parler c'est juste que le reste était déjà très long à écrire. Je m'y colle...
  5. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Je pense que là aussi pas de soucis, on se comprend. Par contre, penser que cela sera plus rapide qu'un TryParseOut parce qu'il n'y aura pas de booléen a vérifier est une erreur. Tu ne verras pas cette vérification, mais le compilateur lui sera bien obligé de générer du code pour cela, pour savoir quel bloc de sortie exécuter. Ce sera juste du sucre syntaxique.

    Et crois moi, parfois, le compilateur fait un boulot monstre la dessus ! Il n'y a qu'a jeter un oeil au code généré lors de l'usage de async/await (toute une machine à état)
    Tu ne m'apprends rien là L'un de mes critères principaux est bien de penser à minimiser le travail à l'exécution.


    Citation Envoyé par François DORIN
    C'est une erreur que de penser qu'on a pas le choix. Il existe plusieurs manières de renvoyer plusieurs informations, et l'usage de out en est une.
    Et voila, erreur commune : plusieurs valeurs pour une sortie est une notion radicalement différent de plusieurs sorties pour une valeur chacune /!\

    Comme "a" et "à", dans des cas (comme à l'oral) ils peuvent se confondre. Il ne faut pourtant pas s'imaginer pouvoir remplacer l'un par l'autre !

    Citation Envoyé par François DORIN
    J'avais bien compris ce point. Mais justement, cette syntaxe ambiguë ne sera jamais acceptée. Puisqu'il s'agit de la même méthode, pourquoi ne reprend ton pas la liste des arguments au sein de la déclaration de MyError ?
    Hélas non tu n'as pas bien compris. Je t'ai dit que chaque sortie a les mêmes capacités. Il n'y en a pas une par défaut, chaque sortie peut avoir ses paramètres d'entrées. De ce fait :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int MyParse(string s) | void MyError(string s)
    {
      Console.Writeline(MyParse.s + " est une variable différent de " + MyError.s);
      if(...)
      {
        MyParse.return 314;
      }
      MyError.return;
    }
     
    // devrait s'utiliser ainsi :
    MyParse("3.14")->(int r)
    {
      Console.Writeline("resultat : " + r);
    }
    |MyError("1.618")->()
    {
      Console("n'est pas un entier");
    }
    Ce qui n'a aucun intérêt dans ce cas... mais en a bien dans d'autre... comme dans celui de ma boucle de messages - où t'es passé complètement à coté de cette notion essentielle :/

    J'espère que tu vois maintenant qu'il n'y a pas d'ambiguïté sur la gestion des paramètres.

    Citation Envoyé par François DORIN
    Avec cette syntaxe, on pourra penser que tu définies deux méthodes, MyParses et MyError, qui aurait le même corps !
    Et que pourra-t-on penser des références nullables ? Tous ces gens perdus sans leur null J'étends une syntaxe où la signature d'une méthode classique (fonctionnant toujours !) deviendrait un sous ensemble de ma signature étendue. Oui ça changerait notre perception sur ce que "doit" être une méthode... un peu comme découvrir que la Terre est ronde En tout cas je ne vois aucun problème de compréhension pour un compilateur.

    L'utilisation du "|" qui ne te pose pas de problème... mais quand même ... n'est pas arrêtée. Il y a p'être plus judicieux mais je n'ai pas trouvé.

    Citation Envoyé par François DORIN
    La syntaxe que tu proposes poses de nombreux soucis (on dirait qu'on accès à une propriété, le mot clé return n'est plus utilisé pour sortir de la fonction
    Woué on dirait qu'avec mon "MyParse.return" j'accède à une propriété et avec mon "MyParse.s" j'accède à une variable. Je crois qu'on peut s'en remettre

    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    switch(MyParse("3.14"))
    {
       case MyError:
          Console("n'est pas un entier");    
          break;
       default:
          Console.Writeline("resultat : " + value);  // le mot clé value se comporte comme dans un setter : il prend la valeur assignée (ici de retour de la fonction)
          break;
    }
    Ceci ne permettrait pas de mettre des paramètres à MyError si elle était définie ainsi.

    Sans parler du fait que je déteste la syntaxe du switch qui définit très mal ses blocks de code et bousille l'utilisation de break pour une boucle.

    - - - - - - - - - - - - - - - - -

    Bon on passe au truc
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    while(true)
    {
       var message = await GetMessage();
       switch(message) 
       {
          case MouseEvenrArgs r:
             label.Content = "mouse location : " + r.GetPosition(this); 
             break;
          case MouseButtonEventArgs r when button.Clicked = MouseDown:
             label.Content = "mouse button down : " + r.ChangedButton;
             break;
          case MouseButtonEventArgs r when button.Clicked = MouseUp:
             label.Content = "mouse button up : " + r.ChangedButton;
             break;
          case KeyEventArgs:
             if(m.Key==Key.Escape) break;
             break;
       }
    }

    T'as oublié de préciser la liste des évènements à traiter.
    - soit ton GetMessage traite tous les évènements "du monde" et c'est du travail donné inutilement... ne t'empêchant pas pour autant de devoir mettre ta méthode à jour en cas de nouvel évènement.
    - soit elle traite uniquement ceux pré-listée dans ton switch et donc rajouté une "case" te refera écrire ta méthode GetMessage.

    Dans tout les cas il n'y aucune vérification à la compilation des "case" rajoutés ou retirés par à rapport aux évènements traités réellement par ta méthode. De plus (comme tu sais) quelque soit l'évènement retourné, ton switch ne permet en aucun cas de savoir où aller précisément, il vérifiera chaque "case" un par un à l'exécution... c'est donc bien comparable à du bon gros "dynamic"


    Moi avec ma méthode étendue comme je peux préciser pour chaque sortie un paramètre d'entrée, ceci indique tout seul la liste des évènements à traiter pour ma méthode.
    De plus comme celle ci n'oublie pas d'être générique chaque sortie déduit donc son type de sortie grâce à sa variable d'entrée.
    Et pour finir quand la méthode sélectionne une sortie, le programme va directement au block de code concernant cette sortie. Il n'y a aucun besoin de test et de vérification dynamique de type !

    On serait largement gagnant en concision de code , en vérification à la compilation et en vitesse d'exécution.

    Citation Envoyé par François DORIN
    Maintenant, il reste encore beaucoup de boulot. Sur les impacts d'une telle approche
    Oui en y réfléchissant un peu rien ne semble insurmontable mais faudrait déjà comprendre l'intérêt brute de ma proposition avant de rentrer dans des détails t'en éloignant encore plus
  6. Avatar de François DORIN
    • |
    • permalink
    Bon, je reviens juste sur le point central que je n'ai pas compris, mais c'est un peu la pierre angulaire. Donc sans ça, tout le reste quasiment est caduc
    Citation Envoyé par ijk-ref
    Hélas non tu n'as pas bien compris. Je t'ai dit que chaque sortie a les mêmes capacités. Il n'y en a pas une par défaut, chaque sortie peut avoir ses paramètres d'entrées. De ce fait :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int MyParse(string s) | void MyError(string s)
    {
      Console.Writeline(MyParse.s + " est une variable différent de " + MyError.s);
      if(...)
      {
        MyParse.return 314;
      }
      MyError.return;
    }
     
    // devrait s'utiliser ainsi :
    MyParse("3.14")->(int r)
    {
      Console.Writeline("resultat : " + r);
    }
    |MyError("1.618")->()
    {
      Console("n'est pas un entier");
    }
    Ce qui n'a aucun intérêt dans ce cas... mais en a bien dans d'autre... comme dans celui de ma boucle de messages - où t'es passé complètement à coté de cette notion essentielle :/

    J'espère que tu vois maintenant qu'il n'y a pas d'ambiguïté sur la gestion des paramètres.
    Alors, désolé de te décevoir, mais si, il y a même encore plus d'ambiguïté car je ne vois pas du tout la vision que tu as de la gestion des paramètres.

    D'un côté, tu nous parle de fonction à multiples sorties, d'un autre, du fait que chaque sortie peut avoir ses paramètres d'entrée.
    Au moment où tu arrives sur ta ligne MyParse("3.14") tu exécutes la fonction à sorties multiples. On est bien d'accord ? Une fois seulement la fonction exécutée (et donc un return précisant la sortie à "utiliser" est atteint), on exécute le bloc de code correspondant (soit Console.Writeline("resultat : " + r); si on passe par la sortie MyParse, soit Console("n'est pas un entier"); si on passe par la sortie MyError.

    Maintenant, comment veux-tu passer un paramètre d'entrée à MyReturn ? Si c'est un paramètre d'entrée, tu dois réexécuter ta fonction, ce qui va à l'encontre de ce que tu disais précédemment.

    La seule possibilité que je vois pour ne pas réexécuter la fonction, et c'est celle que tu sembles mettre en avant, c'est de fournir l'intégralité des paramètres d'entrées au moment de l'appel à la fonction. J'y vois plusieurs gros inconvénients :
    • sous prétexte d'avoir des fonctions à sortie multiple (parqu'avoir plusieurs paramètres en sortie ne te convient pas), tu inondes les paramètres d'entrée. Bref, en voulant transformer une fonction avec plusieurs paramètres de sortie en une fonction à plusieurs sortie, tu génères une fonction avec plusieurs paramètres d'entrés, avec seulement certains qui seront effectivement utiles. Bref, tu ne fais que déplacer le problème ;
    • on a une inversion flagrante de causalité, dans le sens où on a des paramètres (conséquence) qui ne devraient être évalués qu'une fois une certaine condition (la cause) vérifiée (ici, le choix de la sortie). Avec une évaluation a priori, on met dans les conséquences avant la cause ;
    • perte de performance, dans la mesure où tous les paramètres d'entrée de toutes les sorties devront être évaluées a priori ;
    • que faire si on décide d'ignorer une sortie ? Les paramètres d'entrées ne seront pas fourni, donc erreur de compilation ?. Obliger à spécifier chaque sortie ? On va tomber dans les mêmes travers que Java et la gestion des try/catch (la gestion des exceptions et une obligation, et comme c'est "chiant", on a vu pulluler des bloc try/catch avec des catch vides).
  7. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Au moment où tu arrives sur ta ligne MyParse("3.14") tu exécutes la fonction à sorties multiples. On est bien d'accord ?
    Non, on n'exécute pas MyParse("3.14") mais MyParse("3.14") | MyError("1.618"). La méthode est bien exécutée qu'une seule fois et à l'intégralité de ses paramètres connues.

    Citation Envoyé par François DORIN
    sous prétexte d'avoir des fonctions à sortie multiple (parqu'avoir plusieurs paramètres en sortie ne te convient pas), tu inondes les paramètres d'entrée. Bref, en voulant transformer une fonction avec plusieurs paramètres de sortie en une fonction à plusieurs sortie, tu génères une fonction avec plusieurs paramètres d'entrés, avec seulement certains qui seront effectivement utiles. Bref, tu ne fais que déplacer le problème ;
    Sauf que comme je te l'ai dit, c'était seulement un exemple pour que tu comprennes la mécanique. Concrètement tu vois bien qu'il n'y a aucun sens à rajouter un paramètre à la sortie MyError. Donc je n'inonde rien du tout puisqu'il n'y a pas à en mettre.

    Certes l'utilisateur peut le faire. Il peut rajouter un paramètre qu'il n'a pas besoin pour ensuite dire qu'il est inutile. Il peut aussi utiliser "dynamic" partout, ce n'est pas pour autant qu'il faille le faire.

    Mettre des paramètres à une sortie est dans le but de devoir les utiliser même si on ne sort pas par cette sortie. Comme dans mon gestionnaire de messages/évènements où tous les paramètres permettent de s'abonner aux événements qu'ils désignent.

    Citation Envoyé par François DORIN
    perte de performance, dans la mesure où tous les paramètres d'entrée de toutes les sorties devront être évaluées a priori
    J'espère que tu comprends maintenant que non. Je répète quand même : on n'a pas à mettre des paramètres qu'il n'y a pas besoin.

    Citation Envoyé par François DORIN
    que faire si on décide d'ignorer une sortie ? Les paramètres d'entrées ne seront pas fourni, donc erreur de compilation ?. Obliger à spécifier chaque sortie ?
    T'y tiens toujours à tes paramètres d'entrées inutiles

    Par contre dans le cas d'une méthode "normale" où on transformerait ses exceptions par des sorties d'erreur (donc SANS paramètres sortis de je ne sais quelle dimension) oui il faut une syntaxe de simplification.

    genre var i = MyParse(str) |? 0; qui permettrait d'obtenir une valeur par défaut en cas d'autres sorties. Note : oui il y a du boulot de syntaxe à faire ici

    Ou encore il pourrait y avoir un var i = MyParse!(str) qui amènerait les sorties non traitées à lancer des exceptions classiques. Note : t'es obligé d'aimer car ça s'approche de la philosophie de tes références nullables

    Dans le cas de méthodes étendues avec des sorties avec paramètres, ca n'aurait tout simplement aucun sens. Puisque ce sont des paramètres utiles et nécessaires à la méthode.

    Citation Envoyé par François DORIN
    On va tomber dans les mêmes travers que Java et la gestion des try/catch (la gestion des exceptions et une obligation, et comme c'est "chiant", on a vu pulluler des bloc try/catch avec des catch vides).
    Je penses qu'on est plutôt sur la voie pour l'éviter

    Pour voir donne un exemple concret où des catch vides sont utilisés car trop de travail autrement.
  8. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ijk-ref
    Non, on n'exécute pas MyParse("3.14") mais MyParse("3.14") | MyError("1.618"). La méthode est bien exécutée qu'une seule fois et à l'intégralité de ses paramètres connues.
    Donc l'ensemble des paramètres d'une fonction, au lieu d'être disponible sur une seule ligne ou plusieurs lignes contiguës sont répartis sur plusieurs lignes non contiguës.

    Citation Envoyé par ijk-ref
    Sauf que comme je te l'ai dit, c'était seulement un exemple pour que tu comprennes la mécanique. Concrètement tu vois bien qu'il n'y a aucun sens à rajouter un paramètre à la sortie MyError. Donc je n'inonde rien du tout puisqu'il n'y a pas à en mettre.

    Certes l'utilisateur peut le faire. Il peut rajouter un paramètre qu'il n'a pas besoin pour ensuite dire qu'il est inutile. Il peut aussi utiliser "dynamic" partout, ce n'est pas pour autant qu'il faille le faire.
    Si cela n'a aucun sens, que ce soit pour cet exemple ou en général, alors pourquoi en donner la possibilité ? Outre la difficulté de lecture que je viens de mentionner plus haut, c'est inutile.

    Citation Envoyé par ijk-ref
    Mettre des paramètres à une sortie est dans le but de devoir les utiliser même si on ne sort pas par cette sortie. Comme dans mon gestionnaire de messages/évènements où tous les paramètres permettent de s'abonner aux événements qu'ils désignent.
    Si le but est de devoir les utiliser, alors autant regrouper l'ensemble des paramètres lors de l'appel à la méthode.

    Et avec "ta méthode" (pour reprendre ton expression), je ne vois pas comment tu peux ne t'abonner qu'à certains messages et pas à d'autres, puisque tu as besoin d'avoir tes paramètres d'entrée qui sont obligatoires ?

    Et tu mets ça en avant en disant que cela simplifierait grandement la gestion des messages, mais les paramètres que tu passes, il faut bien les gérer aussi. Donc il faut les préparer, les récupérer, etc. Tu le fais où ? Et comment ? Sans compter que je ne comprends pas ce que sont censés être ces paramètres. Les arguments de l'événement ? Pour un clic souris par exemple, quel bouton a été cliqué ? c'est ça ?

    Depuis le début, je te demande un exemple de code concret et fonctionnel en C# pour mettre en avant les apports de ta proposition quant à la gestion des événements, mais toujours rien. Il y a bien eu des pseudo codes, mais tu vois bien que c'est loin d'être suffisant pour te faire comprendre. Donc prend un exemple concret, avec des événements concrets et montre nous ton apport. Voici la trame que je souhaite :
    • Liste des événements / messages disponibles :
      • MouseMove
      • MouseClick
      • KeyPressed
    • un exemple en C# classique, qui fonctionne
    • un exemple avec ta proposition qui traite tous les événements ;
    • un exemple avec ta proposition qui ne traite que les événements souris ;
    • les exemples contiennent :
      • l'implémentation de la boucle de messages ;
      • l'implémentation de la fonction à sorties multiples, qui sera utilisé par la boucle de messages


    Avec ça, on aura une base saine pour les échanges.

    Citation Envoyé par ijk-ref
    T'y tiens toujours à tes paramètres d'entrées inutiles

    Par contre dans le cas d'une méthode "normale" où on transformerait ses exceptions par des sorties d'erreur (donc SANS paramètres sortis de je ne sais quelle dimension) oui il faut une syntaxe de simplification.

    genre var i = MyParse(str) |? 0; qui permettrait d'obtenir une valeur par défaut en cas d'autres sorties. Note : oui il y a du boulot de syntaxe à faire ici

    Ou encore il pourrait y avoir un var i = MyParse!(str) qui amènerait les sorties non traitées à lancer des exceptions classiques. Note : t'es obligé d'aimer car ça s'approche de la philosophie de tes références nullables

    Dans le cas de méthodes étendues avec des sorties avec paramètres, ca n'aurait tout simplement aucun sens. Puisque ce sont des paramètres utiles et nécessaires à la méthode.
    Je ne tiens à rien du tout. Cela fait parti des "détails" inhérents à ta solution. Aujourd'hui, quand une méthode renvoie une valeur, on peut choisir de l'ignorer (c'est le droit du programmeur).

    Si on étend ce principe aux fonctions à sorties multiples, le programmeur peut décider de gérer certaines sorties et pas d'autres (voir aucune). Donc, est-il possible d'ignorer certaines sorties ? Si oui, qu'advient-il des paramètres attachés à ces sorties ignorées ? C'est une véritable question qui appelle une véritable réponse. Pour l'instant, tu essaies de t'en sortir avec une pirouette rhétorique en prétendant que cela n'a aucun sens, alors que cela en a tout à fait un !

    Je n'aborde pas l'aspect syntaxe, car il faut déjà qu'on puisse s'entendre sur le coeur du problème avant d'aborder cela.

    Citation Envoyé par ijk-ref
    Note : t'es obligé d'aimer car ça s'approche de la philosophie de tes références nullables
    Je ne suis obligé à rien du tout. Ce n'est pas parce que je présente quelque chose que je suis pour.

    Citation Envoyé par ijk-ref
    Je penses qu'on est plutôt sur la voie pour l'éviter

    Pour voir donne un exemple concret où des catch vides sont utilisés car trop de travail autrement.
    Non, car on n'est pas en java et qu'une simple recherche te permettra de t'en rendre compte toi même. Donne déjà des exemples clairs pour illustrer "ta solution"
  9. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Et avec "ta méthode" (pour reprendre ton expression), je ne vois pas comment tu peux ne t'abonner qu'à certains messages et pas à d'autres, puisque tu as besoin d'avoir tes paramètres d'entrée qui sont obligatoires ?
    Là je ne comprends pas ou tu bloques. Ecrire une sortie Message(mouseMove)=>(var r) indiquerait à la méthode étendue de s'abonner justement à cet évènement au nom très explicite. Si je remplace mouseMove par KeyDown ça me semble évident que la méthode ne s'abonnera pas à l'évènement traité par mouseMove, non !?

    Je n'ai pas l'impression qu'il y ait vraiment besoin de savoir comment cela est géré en interne pour comprendre mais si tu veux du code fonctionnant...

    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
     
            private async void Window_Activated(object _sender, EventArgs _e)
            {                      
                var mouseMove = EventTools.Create((Action<MouseEventArgs> b) =>
                {
                    void _MouseMove(object sender, MouseEventArgs e) => b(e);
                    MouseMove += _MouseMove;                
                    return () => MouseMove -= _MouseMove;
                });
     
                var mouseDown = EventTools.Create((Action<MouseButtonEventArgs> b) =>
                {
                    void Methode(object sender, MouseButtonEventArgs e) => b(e);
                    MouseDown += Methode;
                    return () => MouseDown -= Methode;
                });
     
                var mouseUp = EventTools.Create((Action<MouseButtonEventArgs> b) =>
                {
                    void Methode(object sender, MouseButtonEventArgs e) => b(e);
                    MouseUp += Methode;
                    return () => MouseUp -= Methode;
                });
     
                var keyDown = EventTools.Create((Action<KeyEventArgs> b) =>
                {
                    void Methode(object sender, KeyEventArgs e) => b(e);
                    KeyDown += Methode;
                    return () => KeyDown -= Methode;
                });
     
                var loop_break0 = false;
                while (!loop_break0)
                {
                    var (output_index0, result0) = await EventTools.Subscribe(mouseMove, mouseDown, keyDown);
                    switch (output_index0)
                    {
                        case 0:
                            {
                                var r0 = (MouseEventArgs)result0;
                                label.Content = "position absolue : " + r0.GetPosition(this); 
                            }
                            break;
                        case 1:
                            {
                                var r0 = (MouseButtonEventArgs)result0;                            
                                var location = r0.GetPosition(this);
                                label.Content = "position relative : 0; 0";
     
                                var loop_break1 = false;
                                while (!loop_break1)
                                {
                                    var (output_index1, result1) = await EventTools.Subscribe(mouseMove, mouseUp);
                                    switch (output_index1)
                                    {
                                        case 0:
                                            {
                                                var r1 = (MouseEventArgs)result1;
                                                label.Content = "position relative : " + (r1.GetPosition(this) - location);
                                            }
                                            break;
                                        case 1:
                                            {
                                                var r1 = (MouseEventArgs)result1;
                                                label.Content = "position absolue : " + r1.GetPosition(this);
                                                loop_break1 = true;
                                            }
                                            break;
                                    }
                                }
                            }
                            break;
                        case 2:
                            {
                                var r0 = (KeyEventArgs)result0;
                                if (r0.Key == Key.Escape) loop_break0 = true;
                            }
                            break;
                    }
                }
     
                label.Content = "Il n'y a plus rien";
            }
     
            interface IEvent
            {
                Action Action(TaskCompletionSource<(int Index, object Result)> tcs, int index);
            }
     
            interface IEvent<out T> : IEvent
            {
     
            }
     
            class Event<T> : IEvent<T>
            {
                Func<Action<T>, Action> a;
     
                public Event(Func<Action<T>, Action> a) => this.a = a;            
     
                public Action Action(TaskCompletionSource<(int Index, object Result)> tcs, int index) => a((T r) => tcs.SetResult((index, r)));                                                
            }
     
            static class EventTools
            {            
                public static async Task<(int Index, object Result)> Subscribe(params IEvent[] events)
                {
                    var tcs = new TaskCompletionSource<(int Index, object Result)>();
     
                    Action[] disposes = new Action[events.Length];
     
                    for (int i = 0; i < events.Length; ++i) disposes[i] = events[i].Action(tcs, i);
     
                    var (index, result) = await tcs.Task;
     
                    for (int i = 0; i < disposes.Length; ++i) disposes[i]();
     
                    return (index, result);
                }
     
                public static Event<T> Create<T>(Func<Action<T>, Action> a) => new Event<T>(a);
            }
     
     
        }
    Ce qu'il fait : il entre dans une boucle pour afficher la position absolue du curseur. En appuyant sur une touche de la souris il rentre dans une seconde boucle affichant la position relative. Il quitte cette dernière en lâchant le bouton de la souris. Il quitte la boucle principale en appuyant sur la touche échappe du clavier.

    Ma version du traitement :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    while (true)
    {            
        await EventTools				
        .Event0(mouveMove)->(var r0)
        {
            label.Content = "position absolue : " + r0.GetPosition(this); 
        }
        |Event1(mouseDown)->(var r0)
        {                            
            var location = r0.GetPosition(this);
            label.Content = "position relative : 0; 0";
     
            while(true)
    	{										    
    	    await EventTools
    	    .Event0(mouveMove)->(var r1)
                {
                    label.Content = "position relative : " + (r1.GetPosition(this) - location);
                }
    	    |Event1(mouseUp)->(var r1)
    	    {
    	        var r1 = (MouseEventArgs)result;
                    label.Content = "position absolue : " + r1.GetPosition(this);
                    break;
    	    }
          }
       }
       |Event2(keyDown)->(var r0)
       {
           if (r0.Key == Key.Escape) break;				
       }
    }
    Mis à jour 08/04/2018 à 19h34 par ijk-ref
  10. Avatar de ijk-ref
    • |
    • permalink
    Définition des méthodes étendues utilisées :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    static class EventTools
    {            
    	public static async Task<T0> Event0(IEvent<T0> e0) | Task<T1> Event1<T1>(IEvent<T1> e1)
    	{
    		var (output_index, result) = await EventTools.Subscribe(e0, e1);
                    switch (output_index)
                    {
    			case 0: Event0.return (T0)result;
    			case 1: Event1.return (T1)result;			
    		}
    	}
     
    	public static async Task<T0> Event0(IEvent<T0> e0) | Task<T1> Event1<T1>(IEvent<T1> e1) | Task<T2> Event1<T2>(IEvent<T2> e2)
    	{
    		var (output_index, result) = await EventTools.Subscribe(e0, e1, e2);
                    switch (output_index)
                    {
    			case 0: Event0.return (T0)result;
    			case 1: Event1.return (T1)result;
    			case 2: Event2.return (T2)result;
    		}
    	}    
    }
    Mis à jour 08/04/2018 à 20h23 par ijk-ref
  11. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Depuis le début, je te demande un exemple de code concret et fonctionnel en C# pour mettre en avant les apports de ta proposition quant à la gestion des événements, mais toujours rien.
    Voila, voila... ?


    Citation Envoyé par François DORIN
    Je ne tiens à rien du tout. Cela fait parti des "détails" inhérents à ta solution. Aujourd'hui, quand une méthode renvoie une valeur, on peut choisir de l'ignorer (c'est le droit du programmeur).

    Si on étend ce principe aux fonctions à sorties multiples, le programmeur peut décider de gérer certaines sorties et pas d'autres (voir aucune). Donc, est-il possible d'ignorer certaines sorties ? Si oui, qu'advient-il des paramètres attachés à ces sorties ignorées ? C'est une véritable question qui appelle une véritable réponse. Pour l'instant, tu essaies de t'en sortir avec une pirouette rhétorique en prétendant que cela n'a aucun sens, alors que cela en a tout à fait un !
    Je ne sais pas si mon code permet de répondre à cette crainte car je ne suis pas sûr de te comprendre.

    Je peux tout aussi bien ne pas traiter une valeur de sortie comme nous le ferions avec un Int.Parse("42"); Qui serait ici un MyParse("42")->(int i); Ca n'a aucun intérêt mais bon... De plus personne ne s'amuse à écrire un switch sans mettre aucun case dedans. Si je mets un case c'est bien pour traiter une sortie spécifique... sinon je ne la mets pas.
  12. Avatar de François DORIN
    • |
    • permalink
    Bon, c'était "en souffrance", mais je n'ai pas reçu de notifications comme quoi tu avais répondu :/

    Citation Envoyé par ijk-ref
    Là je ne comprends pas ou tu bloques. Ecrire une sortie Message(mouseMove)=>(var r) indiquerait à la méthode étendue de s'abonner justement à cet évènement au nom très explicite. Si je remplace mouseMove par KeyDown ça me semble évident que la méthode ne s'abonnera pas à l'évènement traité par mouseMove, non !?
    Pourquoi je bloque ? La réponse est simple. D'un côté, tu dis que tous les paramètres liés à une sortie sont obligatoires. De l'autres, je te dis que je souhaite ignorer une sortie (par exemple, car l'événement mouseMove ne m'intéresse pas). Ma question est : comment fait-on ?

    Ici, dans le cas présent, tu t'en tires en générant des surcharges de ta fonction. Si je me souviens bien, tu disais que tu trouvais le système avec les TryParse inadapté car tu avais 3 fonctions qui avaient quasiment le même corps. Si je regarde les surcharges que tu évoques ici, tu as exactement le même problème. Sauf qu'il est à un autre endroit.

    Je comprends maintenant ce que tu souhaites faire. Mais je reste plus que sceptique, dans la mesure où il est tout à fait possible de faire cela avec les outils d'aujourd'hui, mais pas avec la syntaxe que tu attends. Exemple :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    while (!loop_break0)
    {
    	await EventTools.Subscribe(
    		new EventSubscriber(mouseMove, (MouseEventArgs args) => { label.Content = "position absolue : " + args.GetPosition(this); }),
    		new EventSubscriber(mouseDown, (MouseButtonEventArgs args) => 
    			{
    				var location = args.GetPosition(this);
    				label.Content = "position relative : 0; 0";
     
    				...
    			}),
    		new EventSubscriber(keyDown, (KeyEventArgs args) => { if (args.Key == Key.Escape) loop_break0 = true;})
    	)
    }

    Maintenant que je vois ce que tu veux faire, je me pose aussi des questions sur le bien-fondé de la démarche. Pendant des années, on a lutté pour simplifier le traitement des messages, rendre cela plus simple et cacher au maximum la logique de traitement afin de les simplifier, et là, tu cherches à refaire exactement le contraire. Aujourd'hui, on a de nombreux outils comme les delegates, les closures ou encore les événements qui permettent de gérer les choses simplement dans la très grande majorité des cas.

    Et là, tu cherches à remettre tout à plat. Reconstruire une file de message à partir des événements que tu traites ensuite comme dans l'ancien temps. Donc, oui, c'est compliqué, car c'est très loin de la philosophie du framework actuelle. Je pense effectivement qu'il n'est pas utile de faire cette proposition sur github, car 1) l'apport est plus que minime et 2) les inconvénients sont bien loin d'être négligeables.

    L'idée de base, qui était d'avoir des fonctions avec des sorties multiples, me parait intéressante. Mais tu veux absolument la mêler à une autre notion (la gestion de messages), complexifiant le problème (par exemple, en obligeant à supporter des paramètres pour chaque sortie).
  13. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Bon, c'était "en souffrance", mais je n'ai pas reçu de notifications comme quoi tu avais répondu :/
    Pas souci, le sujet mérite le temps (de la réflexion)... même involontaire

    Citation Envoyé par François DORIN
    Pourquoi je bloque ? La réponse est simple. D'un côté, tu dis que tous les paramètres liés à une sortie sont obligatoires. De l'autres, je te dis que je souhaite ignorer une sortie (par exemple, car l'événement mouseMove ne m'intéresse pas). Ma question est : comment fait-on ?

    Ici, dans le cas présent, tu t'en tires en générant des surcharges de ta fonction. Si je me souviens bien, tu disais que tu trouvais le système avec les TryParse inadapté car tu avais 3 fonctions qui avaient quasiment le même corps. Si je regarde les surcharges que tu évoques ici, tu as exactement le même problème. Sauf qu'il est à un autre endroit.
    Moué, en gros aujourd'hui on a une machine à café demandant 3 interventions humaines pour fonctionner. J'arrive et te proposant une nouvelle machine faisant le café en UNE intervention PLUS qu'elle permet de faire le thé en 3 interventions (que l'ancienne ne faisait pas du tout !). Et toi tu me dis que cette nouvelle machine ne simplifie rien du tout :/

    Mais dans les faits c'est plus "simple" ou plus "complexe" que cela. Il est complètement inutile de parler à notre niveau de "paramètres facultatifs" ou encore de rendre possible un nombre de sorties variables à l'instar de params pour les paramètres. Car cela détournera l'attention sur des sujets/problèmes secondaires et noiera encore plus le sujet principal. Alors je présente seulement le code avec le moins de concepts... pourtant bien pratique en l'état... tout comme les horribles Func<T0, R0>, Func<T0, T1, R0>, Func<T0, T1, T2, R0> ou encore les Tuple<T0, T1>, Tuple<T0, T1, T2> d'aujourd'hui.

    Citation Envoyé par François DORIN
    Je comprends maintenant ce que tu souhaites faire. Mais je reste plus que sceptique, dans la mesure où il est tout à fait possible de faire cela avec les outils d'aujourd'hui, mais pas avec la syntaxe que tu attends.
    Si mon idée te soulève le ventre, je n'imagine même pas comment t'aurais réagi à ta proposition à l'époque où cela n'existait pas. Mettre ses lignes de codes directement dans l'appel d'une méthode !!!

    C'est la Chose que j'essaie d'éviter. C'est comme si tu me proposais de remplacer foreach par :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    ForEach(list, (int e) =>
    {
       Console.WriteLine(e);
       ...
    });
    Le pas à pas ne fonctionne pas du tout de la même façon. Si t'avais à choisir entre débuger un programme avec des foreach classique ou celui ci ton choix serait vite fait

    Citation Envoyé par François DORIN
    Maintenant que je vois ce que tu veux faire, je me pose aussi des questions sur le bien-fondé de la démarche. Pendant des années, on a lutté pour simplifier le traitement des messages, rendre cela plus simple et cacher au maximum la logique de traitement afin de les simplifier, et là, tu cherches à refaire exactement le contraire. Aujourd'hui, on a de nombreux outils comme les delegates, les closures ou encore les événements qui permettent de gérer les choses simplement dans la très grande majorité des cas.
    Là on touche bien à un sujet de base

    On ne doit pas parler de la même chose... de la même simplification. La gestion actuelle des événements date de 95 avec l'arrivée de Delphi (rien de nouveau quoi). C'est très bien, on fait des beaux graphiques et on rajoutes du code d'évènements en cliquant sur des graphiques. Chaque évènement est traité dans son coin. Il n'y a donc pas vraiment d'ordre, on doit utiliser des variables "globales" si on veut des interactions plus poussées, une temporalité entre évènements.

    Citation Envoyé par François DORIN
    Et là, tu cherches à remettre tout à plat. Reconstruire une file de message à partir des événements que tu traites ensuite comme dans l'ancien temps. Donc, oui, c'est compliqué, car c'est très loin de la philosophie du framework actuelle.
    Oui je cherche bien d'une façon à remettre tout à plat. Mon approche permettrait de "voir" clairement l'ordre des événements, leur interactions entre eux, d'éliminer des variables "globales", d'utiliser un maximum de fonctions pures et de supprimer d'innombrable effets de bord. Les parties graphiques se rajouteraient à partir du code et non l'inverse. C'est bien sûr un sujet très complexe qu'il ne peut pas se faire seul (à moins d'être super-super-intelligent).

    J'espère que tu as quand même noté les très grosses différences avec l'ancien temps : typage statique fort et usage de await qui apporte plutôt un bon vent de fraicheur par rapport au traitement de l'ancien temps et de maintenant.

    Mais je ne cherche pas du tout à m'attaquer frontalement à ce sujet. Avoir un outil généraliste (tel que les méthodes à multi-sorties) permettrait à beaucoup d'intelligences d'imaginer d'autres façons de penser et de faire murir des sujets connexes en douceur sans forcer qui que ce soit.

    Citation Envoyé par François DORIN
    L'idée de base, qui était d'avoir des fonctions avec des sorties multiples, me parait intéressante. Mais tu veux absolument la mêler à une autre notion (la gestion de messages), complexifiant le problème (par exemple, en obligeant à supporter des paramètres pour chaque sortie).
    Si t'aimes l'idée de base, supporter des paramètres pour chaque sortie ne devrait pas te gêner plus que de mesure.
    (Une autre gestion de message n'a rien à voir là dedans.)
  14. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ijk-ref
    Pas souci, le sujet mérite le temps (de la réflexion)... même involontaire
    J'étais "désabonné" à la discussion. Normal que je ne recoive plus rien donc. J'avais du faire un mauvais clic au mauvais endroit !

    Citation Envoyé par ijk-ref
    Moué, en gros aujourd'hui on a une machine à café demandant 3 interventions humaines pour fonctionner. J'arrive et te proposant une nouvelle machine faisant le café en UNE intervention PLUS qu'elle permet de faire le thé en 3 interventions (que l'ancienne ne faisait pas du tout !). Et toi tu me dis que cette nouvelle machine ne simplifie rien du tout :/
    Sauf que tellement focaliser sur l'utilisation, tu en oublies les à-coté. Pour reprendre l'analogie de la machine à café, l'utilisation est simplifiée, mais l'installation et la maintenance sont complexifiée. Résultat des courses, au global, aucun des deux méthodes n'est mieux que l'autre. On perd autant de temps d'un côté comme de l'autre pour une complexité similaire.

    Citation Envoyé par ijk-ref
    Mais dans les faits c'est plus "simple" ou plus "complexe" que cela. Il est complètement inutile de parler à notre niveau de "paramètres facultatifs" ou encore de rendre possible un nombre de sorties variables à l'instar de params pour les paramètres. Car cela détournera l'attention sur des sujets/problèmes secondaires et noiera encore plus le sujet principal. Alors je présente seulement le code avec le moins de concepts... pourtant bien pratique en l'état... tout comme les horribles Func<T0, R0>, Func<T0, T1, R0>, Func<T0, T1, T2, R0> ou encore les Tuple<T0, T1>, Tuple<T0, T1, T2> d'aujourd'hui.
    Ben justement si ! Ce n'est pas en cachant les problèmes sous le tapis que les choses vont avancées

    Citation Envoyé par ijk-ref
    Si mon idée te soulève le ventre, je n'imagine même pas comment t'aurais réagi à ta proposition à l'époque où cela n'existait pas. Mettre ses lignes de codes directement dans l'appel d'une méthode !!!

    C'est la Chose que j'essaie d'éviter. C'est comme si tu me proposais de remplacer foreach par :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    ForEach(list, (int e) =>
    {
       Console.WriteLine(e);
       ...
    });
    Le pas à pas ne fonctionne pas du tout de la même façon. Si t'avais à choisir entre débuger un programme avec des foreach classique ou celui ci ton choix serait vite fait
    Pourtant, ça se fait. Pour le foreach, c'est une construction classique dès lors que l'on souhaite utiliser du parallélisme : Parallel.ForEach.

    Citation Envoyé par ijk-ref
    On ne doit pas parler de la même chose... de la même simplification. La gestion actuelle des événements date de 95 avec l'arrivée de Delphi (rien de nouveau quoi). C'est très bien, on fait des beaux graphiques et on rajoutes du code d'évènements en cliquant sur des graphiques. Chaque évènement est traité dans son coin. Il n'y a donc pas vraiment d'ordre, on doit utiliser des variables "globales" si on veut des interactions plus poussées, une temporalité entre évènements.

    Oui je cherche bien d'une façon à remettre tout à plat. Mon approche permettrait de "voir" clairement l'ordre des événements, leur interactions entre eux, d'éliminer des variables "globales", d'utiliser un maximum de fonctions pures et de supprimer d'innombrable effets de bord. Les parties graphiques se rajouteraient à partir du code et non l'inverse. C'est bien sûr un sujet très complexe qu'il ne peut pas se faire seul (à moins d'être super-super-intelligent).
    Dans ce cas, tu fais les choses à l'envers. Avant de définir une nouvelle syntaxe pour un nouveau paradigme, il faut définir ce nouveau paradigme

    Citation Envoyé par ijk-ref
    J'espère que tu as quand même noté les très grosses différences avec l'ancien temps : typage statique fort et usage de await qui apporte plutôt un bon vent de fraicheur par rapport au traitement de l'ancien temps et de maintenant.
    Oh que oui ! Quand je dois utiliser un langage sans typage fort et/ou statique (au hasard, javascript ), j'ai des crises d'urticaire. Le await a énormément simplifié beaucoup de code, notamment pour les GUI, mais pas que. J'ai même écrit un article dessus

    Citation Envoyé par ijk-ref
    Mais je ne cherche pas du tout à m'attaquer frontalement à ce sujet. Avoir un outil généraliste (tel que les méthodes à multi-sorties) permettrait à beaucoup d'intelligences d'imaginer d'autres façons de penser et de faire murir des sujets connexes en douceur sans forcer qui que ce soit.
    Effectivement, je pense que c'est un outil intéressant. Je n'ai pas encore assez de recul pour pouvoir en apprécier tous les cas d'usage et les inconvénients, mais le concept est intéressant.

    Citation Envoyé par ijk-ref
    Si t'aimes l'idée de base, supporter des paramètres pour chaque sortie ne devrait pas te gêner plus que de mesure.
    Comme déjà dit, et je ne répéterais pas le pourquoi, j'ai donné suffisamment d'arguments comme ça, la présence de paramètre pour les sorties génère son lot de problème. Donc autant l'idée de base me plait, autant la présence de paramètres sur chaque sortie m'horrifie
  15. Avatar de ijk-ref
    • |
    • permalink
    Citation Envoyé par François DORIN
    Ben justement si ! Ce n'est pas en cachant les problèmes sous le tapis que les choses vont avancées
    Expliquer à un nouveau le fonctionnement des méthodes/procédures/fonctions C# sans inclure directement le mot clé params... n'est PAS "cacher sous le tapis"


    Citation Envoyé par François DORIN
    Pourtant, ça se fait. Pour le foreach, c'est une construction classique dès lors que l'on souhaite utiliser du parallélisme : Parallel.ForEach.
    Ton exemple est complètement inadapté. Je te parle de code "suivable" en pas à pas. Du code aidant à avoir une lecture "linéaire sans changement de niveau" ce que permet de faire await et ma "proposition" pour simplifier des problèmes d'asynchrones et de sorties multiples. Le parallélisme est une toute autre notion qu'ils ne peuvent pas traiter directement par nature.

    Citation Envoyé par François DORIN
    Effectivement, je pense que c'est un outil intéressant. Je n'ai pas encore assez de recul pour pouvoir en apprécier tous les cas d'usage et les inconvénients, mais le concept est intéressant.

    Comme déjà dit, et je ne répéterais pas le pourquoi, j'ai donné suffisamment d'arguments comme ça, la présence de paramètre pour les sorties génère son lot de problème. Donc autant l'idée de base me plait, autant la présence de paramètres sur chaque sortie m'horrifie
    Certains de tes arguments me sont encore hélas impénétrables

    Sinon autre exemple : si j'ai besoin d'un foreach traitant à part son premier élément.

    Je pourrais bien sûr créer une méthode s'utilisant comme cela : ForEach(list, first : (int x) => { ... }, next : (int x) => { ... });.

    Mais tenant particulièrement à ce qu'il puisse s'utiliser comme un vrai foreach avec des continue et des break, je souhaite donc une écriture de ce genre :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    MyForEach(list)
    |First()->(int x)
    {
       ...
    }
    |Next()->(int x)
    {
       ...
    }
    Qu'est ce que tu suggérais ? (suggestion généraliste donc sans mots clés spécifiques pour uniquement ce cas)
  16. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ijk-ref
    Expliquer à un nouveau le fonctionnement des méthodes/procédures/fonctions C# sans inclure directement le mot clé params... n'est PAS "cacher sous le tapis"
    Dans ce cas, autant faire de même et éviter async/await. Mais cela n'empêche pas que ce sont des sujets qui devront être abordés.

    Citation Envoyé par ijk-ref
    Ton exemple est complètement inadapté. Je te parle de code "suivable" en pas à pas. Du code aidant à avoir une lecture "linéaire sans changement de niveau" ce que permet de faire await et ma "proposition" pour simplifier des problèmes d'asynchrones et de sorties multiples. Le parallélisme est une toute autre notion qu'ils ne peuvent pas traiter directement par nature.
    Croire qu'await ne modifie pas le débuggage pas à pas est une erreur. Await provoque une rupture du flux d'éxecution en rendant l'appel au code appelant. Donc clairement, ta proposition ne simplifie rien du tout par rapport à la mienne.


    Citation Envoyé par ijk-ref
    Sinon autre exemple : si j'ai besoin d'un foreach traitant à part son premier élément.

    Je pourrais bien sûr créer une méthode s'utilisant comme cela : ForEach(list, first : (int x) => { ... }, next : (int x) => { ... });.

    Mais tenant particulièrement à ce qu'il puisse s'utiliser comme un vrai foreach avec des continue et des break, je souhaite donc une écriture de ce genre :
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    MyForEach(list)
    |First()->(int x)
    {
       ...
    }
    |Next()->(int x)
    {
       ...
    }
    Qu'est ce que tu suggérais ? (suggestion généraliste donc sans mots clés spécifiques pour uniquement ce cas)
    Je suggère de ne pas utiliser une boucle foreach, car ce que tu essaies de faire est de planter un clou avec un tournevis. C'est possible, mais ce n'est pas l'outil le plus adapté.

    Une boucle foreach permet d'itérer sur une collection IEnumerable, c'est-à-dire de réaliser un traitement sur tous les éléments d'une collection. L'ordre dans lequel sont exécutés ces traitements n'est pas défini et donc dire "je traite le premier élément à part" n'a pas de sens dans la mesure où le premier élément n'est pas défini intrinsèquement par la boucle foreach, mais par la collection sous-jacente (et ce premier élément peut même varier d'une exécution à l'autre en fonction du type de la collection). Donc, pour moi, utiliser une boucle foreach dans ce cas relève d'une erreur de conception.

    Pour faire ce que tu souhaites faire, la bonne méthode est d'utiliser une boucle for (ou while), qui permet d'itérer sur une collection ordonnée, et donc de définir proprement la notion de premier élément.
Page 2 sur 2 PremièrePremière 12