commutateur avec var / null comportement étrange

91

Compte tenu du code suivant:

string someString = null;
switch (someString)
{
    case string s:
        Console.WriteLine("string s");
        break;
    case var o:
        Console.WriteLine("var o");
        break;
    default:
        Console.WriteLine("default");
        break;
}

Pourquoi l'instruction switch correspond-elle case var o?

C'est ma compréhension qui case string sne correspond pas quand s == nullparce que (effectivement) (null as string) != nullévalue faux. IntelliSense sur VS Code me dit que oc'est stringaussi. Des pensées?


Similaire à: cas de commutation C # 7 avec des vérifications nulles

Budi
la source
9
Confirmé. J'adore cette question, surtout avec l'observation qui oest string(confirmée avec les génériques - c'est-à-dire Foo(o)Foo<T>(T template) => typeof(T).Name) - c'est un cas très intéressant où string xse comporte différemment que var xmême quand xest tapé (par le compilateur) commestring
Marc Gravell
7
Le cas par défaut est le code mort. Croyez que nous devrions lancer un avertissement là-bas. Vérification.
JaredPar
13
Il m'est étrange que les concepteurs C # aient décidé d'autoriser du tout vardans ce contexte. Cela semble être le genre de chose que je trouverais en C ++, pas dans un langage censé conduire le programmeur "dans le gouffre du succès". Ici, varc'est à la fois ambigu et inutile, des choses que la conception C # semble généralement s'efforcer d'éviter.
Peter Duniho
1
@PeterDuniho Je ne dirais pas inutile; l'expression entrante vers le switchpourrait être imprononçable - types anonymes, etc. et ce n'est pas ambigu - le compilateur connaît clairement le type; il est juste déroutant (pour moi du moins) que les nullrègles soient si différentes!
Marc Gravell
1
@PeterDuniho fait amusant - nous avons jadis recherché les règles formelles d'affectation définie de la spécification C # 1.2, et le code d'expansion illustratif contenait la déclaration de variable à l' intérieur du bloc (là où elle se trouve maintenant); il ne s'est déplacé à l' extérieur qu'en 2.0, puis de nouveau à l'intérieur lorsque le problème de capture était évident.
Marc Gravell

Réponses:

69

Dans une switchinstruction de correspondance de modèle utilisant un casepour un type explicite, il est demandé si la valeur en question est de ce type spécifique ou d'un type dérivé. C'est l'équivalent exact deis

switch (someString) {
  case string s:
}
if (someString is string) 

La valeur nulln'a pas de type et ne satisfait donc à aucune des conditions ci-dessus. Le type statique de someStringn'entre en jeu dans aucun des deux exemples.

Le vartype bien que dans la correspondance de modèle agit comme un joker et correspondra à n'importe quelle valeur, y compris null.

Le defaultcas ici est un code mort. Le case var ocorrespondra à n'importe quelle valeur, nulle ou non nulle. Un cas non par défaut l'emporte toujours sur un cas par défaut et ne defaultsera donc jamais atteint. Si vous regardez l'IL, vous verrez qu'il n'est même pas émis.

En un coup d'œil, il peut sembler étrange que cela compile sans aucun avertissement (m'a définitivement jeté). Mais cela correspond au comportement C # qui remonte à 1.0. Le compilateur autorise les defaultcas même lorsqu'il peut prouver de manière triviale qu'il ne sera jamais atteint. Considérez comme exemple ce qui suit:

bool b = ...;
switch (b) {
  case true: ...
  case false: ...
  default: ...
}

Ici defaultne sera jamais atteint (même pour ceux boolqui ont une valeur qui n'est pas 1 ou 0). Pourtant, C # a permis cela depuis 1.0 sans avertissement. La correspondance de modèle est juste en ligne avec ce comportement ici.

JaredPar
la source
4
Le vrai problème est que le compilateur "montre" varqu'il est de type stringalors que ce n'est vraiment pas le cas (honnêtement, je ne sais pas quel type devrait être, certes)
shmuelie
@shmuelie le type de vardans l'exemple est calculé comme étant string.
JaredPar
5
@JaredPar merci pour les idées ici; Personnellement, je soutiendrais davantage l'émission d'avertissements même si ce n'était pas le cas auparavant, mais je comprends les contraintes de l'équipe linguistique. Avez-vous déjà envisagé un "mode whinge about tout" (éventuellement activé par défaut), vs "legacy stoic mode" (option)? peutcsc /stiffUpperLip
Marc Gravell
3
@MarcGravell, nous avons une fonctionnalité appelée ondes d'avertissement qui vise à faciliter l'introduction de nouveaux avertissements, moins de compatibilité. Essentiellement, chaque version du compilateur est une nouvelle vague et vous pouvez activer les avertissements via / wave: 1, / wave: 2, / wave: all.
JaredPar
4
@JonathanDickinson Je ne pense pas que cela montre ce que vous pensez que cela montre. Cela montre simplement que a nullest une stringréférence valide , et toute stringréférence (y compris null) peut être implicitement convertie (en préservant les références) en objectréférence, et toute objectréférence qui est nullpeut être convertie avec succès (explicite) vers tout autre type, toujours en cours null. Pas vraiment la même chose en termes de système de type compilateur.
Marc Gravell
22

Je rassemble plusieurs commentaires Twitter ici - c'est en fait nouveau pour moi, et j'espère que jaredpar interviendra avec une réponse plus complète, mais; version courte telle que je la comprends:

case string s:

est interprété comme if(someString is string) { s = (string)someString; ...ou if((s = (someString as string)) != null) { ... }- ce qui implique un nulltest - qui a échoué dans votre cas; inversement:

case var o:

où le compilateur résout otel quel stringsimplement o = (string)someString; ...- pas de nulltest, malgré le fait qu'il semble similaire en surface, juste avec le compilateur fournissant le type.

enfin:

default:

ici ne peut pas être atteint , car le cas ci-dessus attrape tout. Cela peut être un bogue du compilateur dans la mesure où il n'a pas émis d'avertissement de code inaccessible.

Je conviens que c'est très subtil et nuancé et déroutant. Mais apparemment, le case var oscénario a des utilisations avec une propagation nulle ( o?.Length ?? 0etc.). Je suis d' accord qu'il est étrange que cela fonctionne si très différemment entre var oet string s, mais il est ce que le compilateur fait actuellement.

Marc Gravell
la source
14

C'est parce que case <Type>correspond au type dynamique (à l'exécution) et non au type statique (à la compilation). nulln'a pas de type dynamique, il ne peut donc pas correspondre string. varest juste la solution de secours.

(Publier parce que j'aime les réponses courtes.)

user541686
la source