Pourquoi cette condition (null ||! TryParse) aboutit-elle à «l'utilisation d'une variable locale non affectée»?

98

Le code suivant entraîne l' utilisation de la variable locale non attribuée "numberOfGroups" :

int numberOfGroups;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Cependant, ce code fonctionne bien (cependant, ReSharper dit que le = 10est redondant):

int numberOfGroups = 10;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Est-ce que je manque quelque chose ou est-ce que le compilateur n'aime pas mon ||?

J'ai réduit cela à la dynamiccause des problèmes ( optionsc'était une variable dynamique dans mon code ci-dessus). La question demeure, pourquoi je ne peux pas faire ça ?

Ce code ne compile pas :

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        dynamic myString = args[0];

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Cependant, ce code fait :

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        var myString = args[0]; // var would be string

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Je n'avais pas réalisé que ce dynamicserait un facteur.

Brandon Martinez
la source
Ne pensez pas que c'est assez intelligent pour savoir que vous n'utilisez pas la valeur transmise à votre outparamètre comme entrée
Charleh
3
Le code donné ici ne montre pas le comportement décrit; cela fonctionne très bien. Veuillez publier un code qui démontre réellement le comportement que vous décrivez et que nous pouvons compiler nous-mêmes. Donnez-nous le dossier complet.
Eric Lippert
8
Ah, maintenant nous avons quelque chose d'intéressant!
Eric Lippert
1
Il n'est pas trop surprenant que le compilateur soit dérouté par cela. Le code d'assistance pour le site d'appel dynamique a probablement un flux de contrôle qui ne garantit pas l'affectation au outparamètre. Il est certainement intéressant de considérer quel code d'assistance le compilateur devrait produire pour éviter le problème, ou si c'est même possible.
CodesInChaos
1
À première vue, cela ressemble à un bug.
Eric Lippert

Réponses:

73

Je suis presque sûr que c'est un bogue du compilateur. Bonne trouvaille!

Edit: ce n'est pas un bug, comme le démontre Quartermeister; dynamic pourrait implémenter un trueopérateur étrange qui pourrait empêcher yl'initialisation.

Voici un repro minimal:

class Program
{
    static bool M(out int x) 
    { 
        x = 123; 
        return true; 
    }
    static int N(dynamic d)
    {
        int y;
        if(d || M(out y))
            y = 10;
        return y; 
    }
}

Je ne vois aucune raison pour laquelle cela devrait être illégal; si vous remplacez dynamic par bool, il se compile très bien.

Je rencontre actuellement l'équipe C # demain; Je vais leur en parler. Toutes mes excuses pour l'erreur!

Eric Lippert
la source
6
Je suis juste content de savoir que je ne deviens pas fou :) Depuis, j'ai mis à jour mon code pour ne compter que sur TryParse, donc je suis prêt pour le moment. Merci pour votre perspicacité!
Brandon Martinez
4
@NominSim: Supposons que l'analyse d'exécution échoue: une exception est alors levée avant la lecture du local. Supposons que l'analyse à l'exécution réussisse: à l'exécution, soit d est vrai et y est défini, soit d est faux et M définit y. Dans tous les cas, y est défini. Le fait que l'analyse soit différée jusqu'à l'exécution ne change rien.
Eric Lippert
2
Au cas où quelqu'un serait curieux: je viens de vérifier, et le compilateur Mono fait les choses correctement. imgur.com/g47oquT
Dan Tao
17
Je pense que le comportement du compilateur est en fait correct, car la valeur de dpeut être d'un type avec un trueopérateur surchargé . J'ai posté une réponse avec un exemple où aucune branche n'est prise.
Quartermeister
2
@Quartermeister auquel cas le compilateur Mono se trompe :)
porges
52

Il est possible que la variable soit désassignée si la valeur de l'expression dynamique est d'un type avec un opérateur surchargétrue .

L' ||opérateur invoquera l' trueopérateur pour décider s'il doit évaluer le côté droit, puis l' ifinstruction invoquera l' trueopérateur pour décider s'il doit évaluer son corps. Pour un normal bool, ceux-ci renverront toujours le même résultat et donc exactement un sera évalué, mais pour un opérateur défini par l'utilisateur, il n'y a pas une telle garantie!

Sur la base de la repro d'Eric Lippert, voici un programme court et complet qui démontre un cas où aucun chemin ne serait exécuté et la variable aurait sa valeur initiale:

using System;

class Program
{
    static bool M(out int x)
    {
        x = 123;
        return true;
    }

    static int N(dynamic d)
    {
        int y = 3;
        if (d || M(out y))
            y = 10;
        return y;
    }

    static void Main(string[] args)
    {
        var result = N(new EvilBool());
        // Prints 3!
        Console.WriteLine(result);
    }
}

class EvilBool
{
    private bool value;

    public static bool operator true(EvilBool b)
    {
        // Return true the first time this is called
        // and false the second time
        b.value = !b.value;
        return b.value;
    }

    public static bool operator false(EvilBool b)
    {
        throw new NotImplementedException();
    }
}
Quartermeister
la source
8
Bon travail ici. Je l'ai transmis aux équipes de test et de conception C #; Je verrai s'ils ont des commentaires là-dessus quand je les verrai demain.
Eric Lippert
3
Cela m'est très étrange. Pourquoi devrait dêtre évalué deux fois? (Je ne conteste pas que clairement est , comme vous l' avez montré.) Je me serais attendu le résultat évalué de true( à partir de la première invocation de l' opérateur, cause en ||) être « passé le long de » la ifdéclaration. C'est certainement ce qui se passerait si vous y mettiez un appel de fonction, par exemple.
Dan Tao
3
@DanTao: l'expression dn'est évaluée qu'une seule fois, comme vous vous y attendez. C'est l' trueopérateur qui est appelé deux fois, une fois ||et une fois if.
Quartermeister
2
@DanTao: Cela pourrait être plus clair si nous les mettons sur des déclarations séparées comme var cond = d || M(out y); if (cond) { ... }. Nous évaluons dd' abord pour obtenir une EvilBoolréférence d'objet. Pour évaluer le ||, nous appelons d'abord EvilBool.trueavec cette référence. Cela renvoie true, donc nous court-circuitons et n'invoquons pas M, puis attribuons la référence à cond. Ensuite, nous passons à la ifdéclaration. L' ifinstruction évalue sa condition en appelant EvilBool.true.
Quartermeister
2
Maintenant c'est vraiment cool. Je n'avais aucune idée qu'il y avait un opérateur vrai ou faux.
IllidanS4 veut que Monica revienne
7

De MSDN (c'est moi qui souligne):

Le type dynamique permet aux opérations dans lesquelles il se produit de contourner la vérification de type à la compilation . Au lieu de cela, ces opérations sont résolues au moment de l'exécution . Le type dynamique simplifie l'accès aux API COM telles que les API Office Automation, ainsi qu'aux API dynamiques telles que les bibliothèques IronPython et au modèle d'objet de document HTML (DOM).

Le type dynamic se comporte comme un objet de type dans la plupart des cas. Cependant, les opérations qui contiennent des expressions de type dynamic ne sont pas résolues ou vérifiées par le compilateur.

Étant donné que le compilateur ne contrôle ni ne résout aucune opération contenant des expressions de type dynamic, il ne peut garantir que la variable sera affectée via l'utilisation de TryParse().

NominSim
la source
Si la première condition est remplie, numberGroupsest affectée (dans le if truebloc), sinon, la deuxième condition garantit l'affectation (via out).
leppie
1
C'est une pensée intéressante, mais le code se compile correctement sans le myString == null(en se fondant uniquement sur le TryParse).
Brandon Martinez
1
@leppie Le fait est que puisque la première condition (en fait donc l' ifexpression entière ) implique une dynamicvariable, elle n'est pas résolue au moment de la compilation (le compilateur ne peut donc pas faire ces hypothèses).
NominSim
@NominSim: Je vois votre point :) +1 Cela pourrait être un sacrifice du compilateur (enfreindre les règles C #), mais d'autres suggestions semblent impliquer un bogue. L'extrait d'Eric montre que ce n'est pas un sacrifice, mais un bug.
leppie
@NominSim Cela ne peut pas être vrai; ce n'est pas parce que certaines fonctions du compilateur sont différées qu'elles le sont toutes. Il existe de nombreuses preuves pour montrer que dans des circonstances légèrement différentes, le compilateur effectue l'analyse d'affectation définie sans problème, malgré la présence d'une expression dynamique.
dlev