Remarque: cela semble avoir été corrigé dans 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 opérateur null-coalescing .
Pour rappel, l’opérateur à coalescence nulle est qu’une expression de la forme
x ?? y
évalue d’abord x
, puis:
x
est nulle, y
est évalué et correspond au résultat final de l'expression.x
est non nulle, y
est pas évaluée et la valeur de x
est le résultat final de la expression, après conversion au type de compilation y
si nécessaireMaintenant généralement il n'y a pas besoin de conversion, ou c'est simplement d'un type nullable à un non nullable - généralement les types sont les mêmes, ou juste de (disons) int?
à int
. Cependant, vous pouvez créez vos propres opérateurs de conversion implicites, qui sont utilisés si nécessaire.
Pour le cas simple de x ?? y
, Je n'ai pas vu de 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ées, A
, B
et C
, avec des conversions de A en B, A en C et B en C.
Je peux comprendre à la fois le deuxième et le troisième cas ... mais pourquoi existe-t-il une conversion supplémentaire de A à B dans le premier cas? En particulier, je vraiment m'attendais à ce que le premier et le deuxième cas soient identiques - il s'agit simplement d'extraire une expression dans une variable locale, après tout.
Des preneurs sur ce qui se passe? Je suis extrêmement hésitant à crier "bug" quand il s'agit du compilateur C #, mais je suis perplexe sur ce qui se passe ...
EDIT: Ok, 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 bogue. EDIT: L'échantillon n'a même pas besoin de deux opérateurs à 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 que Foo()
soit appelé deux fois ici me surprend énormément - je ne vois aucune raison pour que l'expression soit évaluée deux fois.
Merci à tous ceux qui ont contribué à l’analyse de ce problème. C'est clairement un bug du compilateur. Cela ne semble se produire que lorsqu'il existe une conversion levée impliquant deux types nullables sur le côté gauche de l'opérateur en coalescence.
Je n'ai pas encore identifié où les choses tournent mal, mais à un moment donné pendant la phase de compilation "Nullable Bolding" - après l'analyse initiale mais avant la génération de code - nous réduisons l'expression
result = Foo() ?? y;
de l'exemple ci-dessus à l'équivalent moral de:
A? temp = Foo();
result = temp.HasValue ?
new int?(A.op_implicit(Foo().Value)) :
y;
Clairement, c'est inexact. l'abaissement correct est
result = temp.HasValue ?
new int?(A.op_implicit(temp.Value)) :
y;
D'après les analyses que j'ai effectuées jusqu'à présent, la meilleure hypothèse est que l'optimiseur nullable se désactive Rails ici. Nous disposons d'un optimiseur nullable qui recherche les situations dans lesquelles nous savons qu'une expression particulière de type nullable ne peut pas Considérons l’analyse naïve suivante: nous pourrions 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 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 avions optimiser loin pour
new int?(op_Implicit(temp2.Value))
Je suppose que nous sommes quelque part en train de mettre en cache le fait que la forme optimisée de (int?)Foo()
Est new int?(op_implicit(Foo().Value))
mais ce n'est pas réellement la forme optimisée que nous souhaitons; nous voulons la forme optimisée de Foo () - remplacé-par-temporaire-puis-converti.
De nombreux bogues dans le compilateur C # résultent de mauvaises décisions de mise en cache. Un mot au sage: chaque fois que vous cachez un fait pour l'utiliser plus tard, vous créez potentiellement une incohérence si quelque chose de pertinent change . 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 beaucoup réorganisé la passe de réécriture annulable 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 mon mauvais. Pardon!
Je vais faire entrer un bogue dans la base de données et nous verrons si nous pouvons le réparer pour une future version du langage. Merci encore à tous pour votre analyse. c'était très utile!
UPDATE: 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 dans Roslyn, voir ma série d'articles qui commence ici: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/ =
C'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 produira:
X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)
Cela m'a fait penser que la première partie de chaque ??
expression de coalesce est évaluée deux fois. Ce code l'a prouvé:
B? test= (X() ?? Y());
les sorties:
X()
X()
A to B (0)
Cela semble se produire uniquement lorsque l'expression nécessite une conversion entre deux types nullables; J'ai essayé diverses permutations avec l'un des côtés étant une chaîne, et aucune d'entre elles n'a provoqué ce comportement.
Si vous regardez le code généré pour le cas groupé à gauche, il 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);
}
Une autre découverte, si vous tilisezfirst
, un raccourci sera généré si a
et b
sont nuls et renvoient c
. Pourtant, si a
ou b
est non nul, il réévalue a
dans le cadre de la conversion implicite en B
avant de renvoyer lequel de a
ou b
n'est pas null.
À partir de la spécification C # 4.0, § 6.1.4:
- Si la conversion Nullable est de
S?
àT?
:
- Si la valeur source est
null
(la propriétéHasValue
estfalse
), le résultat est la valeurnull
de typeT?
.- Sinon, la conversion est évaluée en tant que décompression de
S?
àS
, suivi de la conversion sous-jacente deS
àT
, suivi d’un retour à la ligne (§ 4.1.10) deT
àT?
.
Cela semble expliquer la deuxième combinaison décollage-emballage.
Les compilateurs C # 2008 et 2010 produisent un code très similaire, mais cela ressemble à une régression du compilateur C # 2005 (8.00.50727.4927) qui génère le code suivant pour ce qui précède:
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 cela n’est pas dû au supplément magique attribué au système d’inférence de type?
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
est implémenté comme A.HasValue ? A : B
. Dans ce cas, il y a beaucoup de casting aussi (après le casting régulier pour le ternaire ?:
opérateur). Mais si vous ignorez tout cela, alors cela a du sens en fonction de la façon dont il est implémenté:
A ?? B
se développe en A.HasValue ? A : B
A
est notre x ?? y
. Étendre à x.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Ici vous pouvez voir que x.HasValue
est coché deux fois, et si x ?? y
nécessite un casting, x
sera jeté deux fois.
Je le résumerais 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 qu’un bug du compilateur.
Cela semble être un bug du compilateur qui tourne autour de la façon dont ??
est implémenté. À emporter: ne nichez pas d'expressions coalescentes avec des effets secondaires.
Je ne suis pas du tout un expert en C #, comme le montre l'historique de mes questions, mais j'ai essayé cela et je pense que c'est un bug ... mais en tant que débutant, je dois dire que je ne comprends pas tout ce qui se passe sur ici donc je vais supprimer ma réponse si je suis loin.
Je suis parvenu à cette conclusion bug
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'entiers nuls avec des magasins de sauvegarde. Je mets chacun à 4, puis je lance int? something2 = (A ?? B) ?? C;
( Code complet ici )
Cela ne fait que lire le A et rien d’autre.
Cette déclaration me semble devoir:
Donc, comme A n'est pas nul, il ne regarde que A et se termine.
Dans votre exemple, placer un point d'arrêt au premier cas montre que x, y et z ne sont pas nuls et que, par conséquent, je m'attendrais à ce qu'ils soient traités de la même manière que mon exemple moins complexe ... mais je crains que je ne suis trop d'un débutant en C # et ont manqué le point de cette question complètement!