Remarque: cela semble avoir été corrigé à Roslyn
Cette question s'est posée lors de l'écriture de ma réponse à celle-ci , qui parle de l'associativité de l' opérateur de coalescence nulle .
Pour rappel, l'idée de l'opérateur de coalescence nulle est qu'une expression de la forme
x ?? y
évalue d'abord x
, puis:
- Si la valeur de
x
est nulle,y
est évaluée et c'est le résultat final de l'expression - Si la valeur de
x
n'est pas nulle,y
n'est pas évaluée et la valeur dex
est le résultat final de l'expression, après une conversion au type de compilationy
si nécessaire
Maintenant, généralement, il n'y a pas besoin de conversion, ou c'est juste d'un type nullable à un type non nullable - généralement les types sont les mêmes, ou simplement de (disons) int?
à int
. Cependant, vous pouvez créer vos propres opérateurs de conversion implicites et ceux-ci sont utilisés si nécessaire.
Pour le cas simple de x ?? y
, je n'ai vu aucun comportement étrange. Cependant, avec (x ?? y) ?? z
je vois un comportement déroutant.
Voici un programme de test court mais complet - 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 types de valeurs personnalisés A
, B
et C
, avec les conversions de A en B, A en C et B en C.
Je peux comprendre à la fois le deuxième cas et le troisième cas ... mais pourquoi y a- t -il une conversion A vers B supplémentaire dans le premier cas? En particulier, je m'attendais vraiment à ce que le premier et le deuxième cas soient la même chose - il s'agit juste d'extraire une expression dans une variable locale, après tout.
Des preneurs sur ce qui se passe? Je suis extrêmement réticent à crier "bug" en ce qui concerne le compilateur C #, mais je suis perplexe quant à ce qui se passe ...
EDIT: D'accord, voici un exemple plus méchant de ce qui se passe, grâce à la réponse du configurateur, ce qui me donne une raison supplémentaire de penser que c'est un bug. EDIT: L'échantillon n'a même plus besoin de deux opérateurs de coalescence nulle 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 ceci est:
Foo() called
Foo() called
A to int
Le fait d' Foo()
être appelé deux fois ici me surprend énormément - je ne vois aucune raison pour que l'expression soit évaluée deux fois.
la source
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Vous obtiendrez:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Réponses:
Merci à tous ceux qui ont contribué à l'analyse de ce problème. Il s'agit clairement d'un bug du compilateur. Cela semble se produire uniquement lorsqu'il y a une conversion levée impliquant deux types annulables sur le côté gauche de l'opérateur de coalescence.
Je n'ai pas encore identifié où précisément les choses tournent mal, mais à un moment donné lors de la phase de "réduction nulle" de la compilation - après l'analyse initiale mais avant la génération du code - nous réduisons l'expression
de l'exemple ci-dessus à l'équivalent moral de:
C'est manifestement incorrect; l'abaissement correct est
Ma meilleure supposition, basée sur mon analyse jusqu'à présent, est que l'optimiseur nullable va dérailler ici. Nous avons un optimiseur nullable qui recherche les situations où nous savons qu'une expression particulière de type nullable ne peut pas être null. Considérons l'analyse naïve suivante: nous pourrions d'abord dire que
est le même que
puis on pourrait dire que
est le même que
Mais l'optimiseur peut intervenir et dire "whoa, attendez une minute, nous avons déjà vérifié que temp n'est pas nul; il n'est pas nécessaire de le vérifier une seconde fois simplement parce que nous appelons un opérateur de conversion levé". Nous les ferions optimiser juste pour
Je suppose que nous mettons quelque part en cache le fait que la forme optimisée de
(int?)Foo()
estnew int?(op_implicit(Foo().Value))
mais ce n'est pas réellement la forme optimisée que nous voulons; nous voulons la forme optimisée de Foo () - remplacé-par-temporaire-puis-converti.De nombreux bogues dans le compilateur C # sont le résultat de mauvaises décisions de mise en cache. Un mot pour le sage: chaque fois que vous mettez en cache un fait pour une utilisation ultérieure, vous créez potentiellement une incohérence si quelque chose de pertinent devait changer . Dans ce cas, la chose pertinente qui a changé après l'analyse initiale est que l'appel à Foo () doit toujours être réalisé comme une extraction d'un temporaire.
Nous avons fait beaucoup de réorganisation de la passe de réécriture nullable en C # 3.0. Le bogue se reproduit en C # 3.0 et 4.0 mais pas en C # 2.0, ce qui signifie que le bogue était probablement ma faute. Désolé!
J'obtiendrai un bogue entré dans la base de données et nous verrons si nous pouvons le corriger pour une future version de la langue. Merci encore à tous pour votre analyse; ce fut très utile!
MISE À JOUR: J'ai réécrit l'optimiseur nullable à partir de zéro pour Roslyn; il fait maintenant un meilleur travail et évite ce genre d'erreurs étranges. Pour quelques réflexions sur le fonctionnement de l'optimiseur de Roslyn, consultez ma série d'articles qui commence ici: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
la source
C'est très certainement un bug.
Ce code affichera:
Cela m'a fait penser que la première partie de chaque
??
expression de fusion est évaluée deux fois. Ce code l'a prouvé:les sorties:
Cela semble se produire uniquement lorsque l'expression nécessite une conversion entre deux types nullables; J'ai essayé différentes permutations avec l'un des côtés étant une chaîne, et aucun d'entre eux n'a provoqué ce comportement.
la source
X() ?? Y()
développe en interneX() != null ? X() : Y()
, d'où la raison pour laquelle il serait évalué deux fois.Si vous regardez le code généré pour le cas groupé à gauche, il fait en fait quelque chose comme ceci (
csc /optimize-
):Une autre découverte, si vous l' utilisez
first
, générera un raccourci si les deuxa
etb
sont nuls et retournentc
. Pourtant, sia
oub
n'est pas nul, il réévaluea
dans le cadre de la conversion implicite enB
avant de renvoyer lequela
ou quib
n'est pas nul.D'après la spécification C # 4.0, §6.1.4:
Cela semble expliquer la deuxième combinaison déballage-emballage.
Le compilateur C # 2008 et 2010 produit un code très similaire, mais cela ressemble à une régression à partir du compilateur C # 2005 (8.00.50727.4927) qui génère le code suivant pour ce qui précède:
Je me demande si cela n'est pas dû à la magie supplémentaire donnée au système d'inférence de type?
la source
(x ?? y) ?? z
en lambdas imbriqués, ce qui garantit une évaluation dans l'ordre sans double évaluation. Ce n'est évidemment pas l'approche adoptée par le compilateur C # 4.0. D'après ce que je peux dire, la section 6.1.4 est abordée de manière très stricte dans ce chemin de code particulier et les temporaires ne sont pas élidés, ce qui entraîne la double évaluation.En fait, je vais appeler cela un bug maintenant, avec l'exemple plus clair. Cela tient toujours, mais la double évaluation n'est certainement pas bonne.
Il semble que
A ?? B
soit mis en œuvre en tant queA.HasValue ? A : B
. Dans ce cas, il y a aussi beaucoup de coulée (après la coulée régulière pour l'?:
opérateur ternaire ). Mais si vous ignorez tout cela, cela a du sens en fonction de la façon dont il est mis en œuvre:A ?? B
s'étend àA.HasValue ? A : B
A
est notrex ?? y
. Développer jusqu'àx.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Ici, vous pouvez voir que cette
x.HasValue
option est vérifiée deux fois et si ellex ?? y
nécessite un lancer,x
elle sera lancée deux fois.Je le mettrais simplement comme un artefact de la façon dontÀ emporter: ne créez pas d'opérateurs de casting implicites avec des effets secondaires.??
est implémenté, plutôt que comme un bug du compilateur.Il semble que ce soit un bogue du compilateur qui tourne autour de la façon dont il
??
est implémenté. À emporter: ne pas imbriquer les expressions coalescentes avec des effets secondaires.la source
A() ? A() : B()
cela évaluera probablementA()
deux fois, maisA() ?? B()
pas tellement. Et comme ça n'arrive qu'au casting ... Hmm ... Je viens de me convaincre que ça ne se comporte pas correctement.Je ne suis pas du tout un expert en C # comme vous pouvez le voir dans mon historique de questions, mais j'ai essayé et je pense que c'est un bug ... mais en tant que débutant, je dois dire que je ne comprends pas tout ici, donc je vais supprimer ma réponse si je suis loin.
Je suis arrivé à cette
bug
conclusion en créant une version différente de votre programme qui traite du même scénario, mais beaucoup moins compliquée.J'utilise trois propriétés d'entier nul avec des magasins de sauvegarde. Je mets chacun à 4 puis je lance
int? something2 = (A ?? B) ?? C;
( Code complet ici )
Cela lit juste le A et rien d'autre.
Cette déclaration me semble qu'elle devrait:
Donc, comme A n'est pas nul, il ne regarde que A et termine.
Dans votre exemple, mettre un point d'arrêt au premier cas montre que x, y et z ne sont pas tous nuls et donc, je m'attendrais à ce qu'ils soient traités de la même manière que mon exemple moins complexe .... mais je crains d'être trop d'un débutant C # et ont complètement raté le point de cette question!
la source
int
). Il pousse l'affaire plus loin dans un coin obscur en fournissant plusieurs conversions de types implicites. Cela nécessite que le compilateur modifie le type des données lors de la vérificationnull
. C'est à cause de ces conversions de types implicites que son exemple est différent du vôtre.