Roslyn n'a pas réussi à compiler le code

95

Après avoir migré mon projet de VS2013 vers VS2015, le projet ne se construit plus. Une erreur de compilation se produit dans l'instruction LINQ suivante:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

Le compilateur renvoie une erreur:

Erreur CS0165 Utilisation de la variable locale non attribuée 'b'

Qu'est-ce qui cause ce problème? Est-il possible de le réparer via un paramètre de compilateur?

ramil89
la source
11
@BinaryWorrier: Pourquoi? Il ne l'utilise bqu'après avoir été affecté via un outparamètre.
Jon Skeet
1
La documentation de VS 2015 indique que «bien que les variables transmises comme arguments de sortie n'aient pas à être initialisées avant d'être transmises, la méthode appelée doit affecter une valeur avant le retour de la méthode». donc cela ressemble à un bogue oui, il est garanti d'être initialisé par ce tryParse.
Rup
3
Indépendamment de l'erreur, ce code illustre tout ce qui est mauvais dans les outarguments. Serait-ce TryParseretourné une valeur nullable (ou équivalent).
Konrad Rudolph
1
@KonradRudolph a l' where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bair beaucoup mieux
Rawling
2
Juste pour noter, vous pouvez simplifier cela en decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. J'ai ouvert un bug de Roslyn soulevant ceci.
Rawling

Réponses:

112

Qu'est-ce qui cause ce problème?

Cela ressemble à un bogue du compilateur. Du moins, ça l'a fait. Bien que les expressions decimal.TryParse(v, out a)et decimal.TryParse(v, out b)soient évaluées dynamiquement, je m'attendais à ce que le compilateur comprenne encore qu'au moment où il atteint a <= b, les deux aet bsont définitivement attribués. Même avec les bizarreries que vous pouvez créer dans le typage dynamique, je m'attendrais à n'évaluer a <= bqu'après avoir évalué les deux TryParseappels.

Cependant, il s'avère que grâce à l'opérateur et à la conversion délicate, il est tout à fait possible d'avoir une expression A && B && Cqui évalue Aet Cmais pas B- si vous êtes assez rusé. Voir le rapport de bogue de Roslyn pour l'exemple ingénieux de Neal Gafter.

Faire ce travail dynamicest encore plus difficile - la sémantique impliquée lorsque les opérandes sont dynamiques est plus difficile à décrire, car pour effectuer une résolution de surcharge, vous devez évaluer les opérandes pour savoir quels types sont impliqués, ce qui peut être contre-intuitif. Cependant, encore une fois Neal est venu avec un exemple qui montre que l'erreur du compilateur est nécessaire ... ce n'est pas un bug, il est un bug fix . D'énormes félicitations à Neal pour l'avoir prouvé.

Est-il possible de le réparer via les paramètres du compilateur?

Non, mais il existe des alternatives qui évitent l'erreur.

Premièrement, vous pouvez l'empêcher d'être dynamique - si vous savez que vous n'utiliserez jamais que des chaînes, vous pouvez alors utiliser IEnumerable<string> ou donner à la variable range vun type de string(ie from string v in array). Ce serait mon option préférée.

Si vous avez vraiment besoin de le garder dynamique, donnez simplement bune valeur pour commencer:

decimal a, b = 0m;

Cela ne fera aucun mal - nous savons qu'en fait votre évaluation dynamique ne fera rien de fou, vous finirez donc par attribuer une valeur à bavant de l'utiliser, ce qui rendra la valeur initiale non pertinente.

De plus, il semble que l'ajout de parenthèses fonctionne aussi:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Cela change le moment où les différentes parties de la résolution de surcharge sont déclenchées et rend le compilateur heureux.

Il reste un problème à résoudre - les règles de la spécification sur l'affectation définitive avec l' &&opérateur doivent être clarifiées pour indiquer qu'elles ne s'appliquent que lorsque l' &&opérateur est utilisé dans son implémentation "régulière" avec deux boolopérandes. Je vais essayer de m'assurer que cela est corrigé pour la prochaine norme ECMA.

Jon Skeet
la source
Ouais! L'application IEnumerable<string>ou l'ajout de parenthèses a fonctionné pour moi. Maintenant, le compilateur se construit sans erreur.
ramil89
1
utiliser decimal a, b = 0m;pourrait supprimer l'erreur, mais a <= butiliserait toujours 0m, car la valeur de sortie n'a pas encore été calculée.
Paw Baltzersen
12
@PawBaltzersen: Qu'est-ce qui vous fait penser cela? Il sera toujours attribué avant la comparaison - c'est juste que le compilateur ne peut pas le prouver, pour une raison quelconque (un bogue, en gros).
Jon Skeet
1
Avoir une méthode d'analyse sans effet secondaire ie. decimal? TryParseDecimal(string txt)peut être une solution aussi
zahir
1
Je me demande si c'est une initialisation paresseuse; il pense que "si le premier est vrai alors je n'ai pas besoin d'évaluer le second, ce qui signifie qu'il bpourrait ne pas être attribué"; Je sais que c'est un raisonnement invalide, mais cela explique pourquoi les parenthèses le
corrigent
16

Depuis que j'ai été tellement instruit dans le rapport de bogue, je vais essayer de l'expliquer moi-même.


Imagine Test un type défini par l'utilisateur avec un cast implicite boolqui alterne entre falseet true, en commençant par false. Pour autant que le compilateur le sache, le dynamicpremier argument du premier &&pourrait être évalué à ce type, il doit donc être pessimiste.

Si, alors, il laisse le code se compiler, cela peut arriver:

  • Lorsque le classeur dynamique évalue le premier &&, il effectue les opérations suivantes:
    • Évaluer le premier argument
    • C'est un T- implicitement jeté sur bool.
    • Oh, c'est false, donc nous n'avons pas besoin d'évaluer le deuxième argument.
    • Faites du résultat de l' &&évaluation comme premier argument. (Non, non false, pour une raison quelconque.)
  • Lorsque le classeur dynamique évalue le second &&, il effectue les opérations suivantes:
    • Évaluez le premier argument.
    • C'est un T- implicitement jeté sur bool.
    • Oh, c'est true, alors évaluez le deuxième argument.
    • ... Oh merde, bn'est pas assigné.

En termes de spécifications, en bref, il existe des règles spéciales "d'affectation définie" qui nous permettent de dire non seulement si une variable est "définitivement affectée" ou "non définitivement affectée", mais aussi si elle est "définitivement affectée après l' falseinstruction" ou "définitivement attribué après la truedéclaration ".

Celles-ci existent pour que, lorsqu'il traite avec &&and ||(et !et ??et ?:), le compilateur puisse examiner si des variables peuvent être affectées dans des branches particulières d'une expression booléenne complexe.

Cependant, ceux-ci ne fonctionnent que tant que les types des expressions restent booléens . Lorsqu'une partie de l'expression est dynamic(ou un type statique non booléen), nous ne pouvons plus dire de manière fiable que l'expression est trueou false- la prochaine fois que nous la castons boolpour décider quelle branche prendre, elle peut avoir changé d'avis.


Mise à jour: ceci est maintenant résolu et documenté :

Les règles d'affectation définies implémentées par les compilateurs précédents pour les expressions dynamiques permettaient certains cas de code pouvant entraîner la lecture de variables qui ne sont pas définitivement affectées. Voir https://github.com/dotnet/roslyn/issues/4509 pour un rapport à ce sujet.

...

En raison de cette possibilité, le compilateur ne doit pas permettre à ce programme d'être compilé si val n'a pas de valeur initiale. Les versions précédentes du compilateur (antérieures à VS2015) permettaient à ce programme de se compiler même si val n'a pas de valeur initiale. Roslyn diagnostique maintenant cette tentative de lecture d'une variable éventuellement non initialisée.

Rawling
la source
1
En utilisant VS2013 sur mon autre ordinateur, j'ai en fait réussi à lire la mémoire non attribuée en utilisant ceci. Ce n'est pas très excitant :(
Rawling
Vous pouvez lire des variables non initialisées avec un délégué simple. Créez un délégué qui accède outà une méthode qui a ref. Il le fera avec plaisir, et il affectera des variables, sans changer la valeur.
IllidanS4 veut que Monica revienne
Par curiosité, j'ai testé cet extrait avec C # v4. Par curiosité, cependant - comment le compilateur décide-t-il d'utiliser l'opérateur false/ truepar opposition à l'opérateur de conversion implicite? Localement, il appellera implicit operator boolle premier argument, puis invoquera le deuxième opérande, appelera operator falsele premier opérande, suivi par implicit operator boolle premier opérande à nouveau . Cela n'a pas de sens pour moi, le premier opérande devrait essentiellement se résumer à un booléen une fois, non?
Rob
@Rob Est-ce le cas dynamicenchaîné &&? Je l'ai vu en gros aller (1) évaluer le premier argument (2) utiliser le cast implicite pour voir si je peux court-circuiter (3) Je ne peux pas, alors évaluez le deuxième argument (4) maintenant je connais les deux types, je peut voir le meilleur &&est un &opérateur d'appel défini par l'utilisateur (5) falsesur le premier argument pour voir si je peux court-circuiter (6) Je peux (parce que falseet implicit boolpas d'accord), donc le résultat est le premier argument ... puis le suivant &&, (7) utiliser le cast implicite pour voir si je peux (à nouveau) court-circuiter.
Rawling
@ IllidanS4 Cela semble intéressant, mais je n'ai pas trouvé comment le faire. Pouvez-vous me donner un extrait?
Rawling
15

Ce n'est pas un bug. Voir https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 pour un exemple de la façon dont une expression dynamique de cette forme peut laisser une telle variable non attribuée.

Neal Gafter
la source
1
Comme ma réponse est acceptée et fortement votée, je l'ai modifiée pour indiquer la résolution. Merci pour tout votre travail à ce sujet - y compris pour m'expliquer mon erreur :)
Jon Skeet