535 votes

Comportement personnalisé de la conversion implicite à l’opérateur null-fusionnant curieux

Note: il semble que cela ait été corrigé dans Roslyn

Cette question a été soulevée lors de la rédaction de ma réponse à ce un seul, qui parle de l'associativité de l' null-coalescence de l'opérateur.

Juste pour rappel, l'idée de la valeur null est-coalescence de l'opérateur est qu'une expression de la forme

x ?? y

la première évalue x, alors:

  • Si la valeur de x est null, y est évaluée et qui est le résultat de l'expression
  • Si la valeur de x est non-nulle, y est pas évalué, et la valeur de x est le résultat final de l'expression, après une conversion vers le type de compilation d' y si nécessaire

Maintenant, généralement il n'y a pas besoin d'une conversion, ou c'est juste à partir d'un type nullable à un non nullable un - généralement les types sont les mêmes, ou tout simplement de (dis) int? de int. Toutefois, vous pouvez créer votre propre conversion implicite des opérateurs, et ceux-ci sont utilisées, le cas échéant.

Pour le cas simple d' x ?? y, je n'ai pas vu tout comportement étrange. Cependant, avec (x ?? y) ?? z je vois certains comportement déroutant.

Voici une courte mais complète du programme de test, les résultats sont dans les commentaires:

using System;

public struct A
{
    public static implicit operator B(A input)
    {
        Console.WriteLine("A to B");
        return new B();
    }

    public static implicit operator C(A input)
    {
        Console.WriteLine("A to C");
        return new C();
    }
}

public struct B
{
    public static implicit operator C(B input)
    {
        Console.WriteLine("B to C");
        return new C();
    }
}

public struct C {}

class Test
{
    static void Main()
    {
        A? x = new A();
        B? y = new B();
        C? z = new C();
        C zNotNull = new C();

        Console.WriteLine("First case");
        // This prints
        // A to B
        // A to B
        // B to C
        C? first = (x ?? y) ?? z;

        Console.WriteLine("Second case");
        // This prints
        // A to B
        // B to C
        var tmp = x ?? y;
        C? second = tmp ?? z;

        Console.WriteLine("Third case");
        // This prints
        // A to B
        // B to C
        C? third = (x ?? y) ?? zNotNull;
    }
}

Nous avons donc trois personnalisés types de valeur, A, B et C, avec les conversions de A à B, A, C et B to C.

Je peux comprendre les deux cas le deuxième et le troisième cas... mais pourquoi est-il un supplément de A à B de conversion dans le premier cas? En particulier, j'aimerais vraiment avoir attendu le premier cas deuxième cas être la même chose - c'est juste de l'extraction d'une expression dans une variable locale, après tout.

Les élèves de ce qui se passe? Je suis extrêmement hesistant à pleurer "bug" quand il s'agit pour le compilateur C#, mais je suis perplexe quant à ce qui se passe...

EDIT: Ok, c'est du plus mauvais exemple de ce qui se passe, grâce au configurateur de réponse, ce qui me donne une raison de plus de penser que c'est un bug. EDIT: L'échantillon n'a pas même besoin de deux null-coalescence des opérateurs maintenant...

using System;

public struct A
{
    public static implicit operator int(A input)
    {
        Console.WriteLine("A to int");
        return 10;
    }
}

class Test
{
    static A? Foo()
    {
        Console.WriteLine("Foo() called");
        return new A();
    }

    static void Main()
    {
        int? y = 10;

        int? result = Foo() ?? y;
    }
}

La sortie de ce est:

Foo() called
Foo() called
A to int

Le fait qu' Foo() est appelée deux fois ici, est très surprenant pour moi - je ne vois aucune raison pour que l'expression doit être évaluée à deux reprises.

416voto

Eric Lippert Points 300275

Merci à tous ceux qui ont contribué à l'analyse de cette question. C'est clairement un bug du compilateur. Il semble se produire uniquement lorsque il y a une levée de conversion impliquant deux types nullables sur le côté gauche de la coalescence de l'opérateur.

Je n'ai pas encore identifié précisément les choses vont mal, mais à un certain moment au cours de la "nullable abaissement" de la phase de compilation -- après l'analyse initiale, mais avant la génération de code -- nous de réduire l'expression

result = Foo() ?? y;

à partir de l'exemple ci-dessus pour l'équivalent moral de:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

Clairement que c'est inexact; l'abaissement est

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

Ma meilleure supposition basée sur mon analyse est que le nullable optimizer va sur les rails d'ici. Nous avons un nullable optimiseur qui ressemble à des situations où l'on sait qu'une expression particulière de type nullable ne peut pas être null. Considérez les points suivants naïf analyse: on peut d'abord dire que

result = Foo() ?? y;

est le même que

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

et puis, on pourrait dire que

conversionResult = (int?) temp 

est le même que

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

Mais l'optimiseur peut intervenir et de dire "oh, attendez une minute, nous l'avons déjà vérifié que le temp n'est pas nulle; il n'y a pas besoin de les vérifier pour les nuls un deuxième temps, juste parce que nous sommes l'appel d'une levée opérateur de conversion". Nous avions à optimiser pour le juste

new int?(op_Implicit(temp2.Value)) 

Ma conjecture est que nous sommes quelque part la mise en cache le fait que l'optimisation de la forme d' (int?)Foo() est new int?(op_implicit(Foo().Value)) mais qui n'est effectivement pas l'optimisation de la forme que nous voulons; nous voulons l'optimisation de la forme de Foo()-remplacé-à-temporaire-et-puis-convertis.

De nombreux bugs dans le compilateur C# sont le résultat d'une mauvaise mise en cache des décisions. Un mot pour le sage: à chaque fois que vous le cache un fait pour une utilisation ultérieure, vous êtes potentiellement créer une incohérence doit quelque chose de pertinent changement. Dans ce cas pertinents chose qui a changé de poste de l'analyse initiale, c'est que l'appel à Foo() doit toujours être réalisé en une extraction de temporaire.

Nous avons fait beaucoup de réorganisation de la nullable réécriture de passer en C# 3.0. Le bug se reproduit en C# 3.0 et 4.0, mais pas en C# 2.0, ce qui signifie que le bug était probablement mon mauvais. Désolé!

Je vais avoir un bug dans la base de données et nous allons voir si nous pouvons obtenir ce fixe pour une future version de la langue. Merci encore à tous pour votre analyse, il a été très utile!

84voto

configurator Points 15594

Cela est très certainement un bug.

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

Ce code va afficher:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

Qui m'a fait penser que la première partie de chaque ?? fusionnent expression est évaluée deux fois. Ce code prouvé:

B? test= (X() ?? Y());

sorties:

X()
X()
A to B (0)

Cela semble se produire que lorsque l'expression nécessite une conversion entre les deux types nullables; j'ai essayé diverses combinaisons avec l'un des côtés étant une chaîne, et aucun d'entre eux la cause de ce comportement.

54voto

user7116 Points 39829

Si vous regardez le code généré pour la Gauche regroupés cas, il ne fait quelque chose comme ceci (csc /optimize-):

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

Trouver un autre, si vous utilisez first , il va générer un raccourci si les deux a et b sont nulles et de retour c. Pourtant, si l' a ou b est non nulle, il réévalue a dans le cadre de la conversion implicite en B avant de revenir qui de la a ou b est non-nulle.

À partir de C# 4.0 Spécification, §6.1.4:

  • Si le nullable de conversion est de S? de T?:
    • Si la valeur de la source est - null (HasValue de la propriété est - false), le résultat est le null de la valeur de type T?.
    • Sinon, la conversion est évaluée comme un déballage de S? de S, suivie par le sous-jacent de conversion de S de T, suivie par un emballage (§4.1.10) à partir de T de T?.

Cela semble expliquer la deuxième déballage-emballage combinaison.


Le C# 2008 et 2010 compilateur de produire de très semblable code, mais cela ressemble à une régression de la C# 2005 compilateur (8.00.50727.4927) qui génère le code suivant pour la ci-dessus:

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

Je me demande si ce n'est pas attribuable à l'ajout de la magie étant donné le type de système d'inférence?

16voto

Philip Rieck Points 21405

En fait, je vais l'appeler ce un bug maintenant, avec le plus clair exemple. Cela tient toujours, mais le double de l'évaluation n'est certainement pas bon.

Il semble que l' A ?? B est mis en œuvre en tant que A.HasValue ? A : B. Dans ce cas, il y a beaucoup de casting aussi (à la suite de l'ordinaire casting pour le ternaire ?: opérateur). Mais si vous ignorez tout cela, alors cela a un sens, basé sur la façon dont il est mis en œuvre:

  1. A ?? B s'étend à d' A.HasValue ? A : B
  2. A est notre x ?? y. L'élargir à d' x.HasValue : x ? y
  3. remplacer toutes les occurrences de A -> (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

Ici vous pouvez voir qu' x.HasValue est vérifié deux fois, et si x ?? y nécessite casting, x sera jeté deux fois.

Je l'avais mis simplement comme un artefact de la façon dont ?? est mis en œuvre, plutôt que d'un bug du compilateur. À emporter: Ne créez pas de conversion implicite des opérateurs avec des effets secondaires.

Il semble être un bug du compilateur tournant autour de la façon dont ?? est mis en œuvre. À emporter: ne pas nid de coalescence des expressions avec des effets secondaires.

12voto

Hasan Khan Points 20723
<pre><code></code><p>Réponse se trouve dans le code décompilé. Il est l’évaluation première expression deux fois. Je ne vois aucune raison de ré-évaluer l’expression nouveau. Je l’appellerais un bug.</p></pre>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X