Court-circuiter les opérateurs || et && existe-t-il pour les booléens nullables? Le RuntimeBinder le pense parfois

84

J'ai lu la spécification du langage C # sur les opérateurs logiques conditionnels || et &&, également connus sous le nom d'opérateurs logiques de court-circuit. Pour moi, il ne semblait pas clair si ceux-ci existaient pour les booléens nullables, c'est-à-dire le type d'opérande Nullable<bool>(également écrit bool?), alors je l'ai essayé avec un typage non dynamique:

bool a = true;
bool? b = null;
bool? xxxx = b || a;  // compile-time error, || can't be applied to these types

Cela semblait régler la question (je ne pouvais pas comprendre clairement la spécification, mais en supposant que l'implémentation du compilateur Visual C # était correcte, je le savais maintenant).

Cependant, je voulais aussi essayer la dynamicreliure. J'ai donc essayé ceci à la place:

static class Program
{
  static dynamic A
  {
    get
    {
      Console.WriteLine("'A' evaluated");
      return true;
    }
  }
  static dynamic B
  {
    get
    {
      Console.WriteLine("'B' evaluated");
      return null;
    }
  }

  static void Main()
  {
    dynamic x = A | B;
    Console.WriteLine((object)x);
    dynamic y = A & B;
    Console.WriteLine((object)y);

    dynamic xx = A || B;
    Console.WriteLine((object)xx);
    dynamic yy = A && B;
    Console.WriteLine((object)yy);
  }
}

Le résultat surprenant est que cela fonctionne sans exception.

Eh bien, xet ce yn'est pas surprenant, leurs déclarations conduisent à la récupération des deux propriétés et les valeurs résultantes sont comme prévu, xest trueet yest null.

Mais l'évaluation de xxof A || Bconduit à aucune exception de temps de liaison, et seule la propriété a Aété lue, non B. Pourquoi cela arrive-t-il? Comme vous pouvez le voir, nous pourrions changer le Bgetter pour renvoyer un objet fou, comme "Hello world", et xxnous évaluerions toujours truesans problèmes de liaison ...

L'évaluation A && B(pour yy) ne conduit également à aucune erreur de temps de liaison. Et ici, les deux propriétés sont récupérées, bien sûr. Pourquoi est-ce autorisé par le classeur d'exécution? Si l'objet renvoyé de Best changé en un objet "incorrect" (comme a string), une exception de liaison se produit.

Ce comportement est-il correct? (Comment pouvez-vous déduire cela de la spécification?)

Si vous essayez en Btant que premier opérande, les deux B || Aet B && Adonnent une exception de reliure d'exécution ( B | Aet B & Afonctionnent correctement car tout est normal avec des opérateurs sans court-circuit |et &).

(Essayé avec le compilateur C # de Visual Studio 2013 et la version d'exécution .NET 4.5.2.)

Jeppe Stig Nielsen
la source
4
Il n'y a aucune instance de Nullable<Boolean>impliqué du tout, seuls les booléens encadrés sont traités comme dynamic- votre test avec bool?n'est pas pertinent. (Bien sûr, ce n'est pas une réponse complète, seulement le germe d'une.)
Jeroen Mostert
3
Le A || Bfait un certain sens, en ce que vous ne voulez pas évaluer à Bmoins que ce Asoit faux, ce qui n'est pas le cas. Donc, vous ne connaissez jamais vraiment le type de l'expression. La A && Bversion est plus surprenante - je vais voir ce que je peux trouver dans les spécifications.
Jon Skeet
2
@JeroenMostert: Eh bien, à moins que le compilateur ne décide que si le type de Aest boolet la valeur de Best null, alors un bool && bool?opérateur pourrait être impliqué.
Jon Skeet
4
Fait intéressant, il semble que cela ait exposé un bogue de compilation ou de spécification. La spécification C # 5.0 pour &&parle de le résoudre comme s'il l'était à la &place, et inclut spécifiquement le cas où les deux opérandes sont bool?- mais la section suivante à laquelle elle fait référence ne gère pas le cas Nullable. Je pourrais ajouter une sorte de réponse plus détaillée à ce sujet, mais cela ne l'expliquerait pas complètement.
Jon Skeet
14
J'ai envoyé un e-mail à Mads à propos du problème de spécification, pour voir si c'est juste un problème dans la façon dont je le lis ...
Jon Skeet

Réponses:

67

Tout d'abord, merci de souligner que la spécification n'est pas claire sur le cas nullable-bool non dynamique. Je corrigerai cela dans une prochaine version. Le comportement du compilateur est le comportement prévu; &&et ||ne sont pas censés fonctionner sur les bools nullables.

Le classeur dynamique ne semble pas implémenter cette restriction, cependant. Au lieu de cela, il lie les opérations des composants séparément: le &/ |et le ?:. Ainsi, il est capable de se débrouiller si le premier opérande se trouve être trueou false(qui sont des valeurs booléennes et donc autorisées comme premier opérande de ?:), mais si vous donnez nullcomme premier opérande (par exemple si vous essayez B && Adans l'exemple ci-dessus), vous faites obtenir une exception de liaison d'exécution.

Si vous y réfléchissez, vous pouvez voir pourquoi nous avons implémenté dynamique &&et de ||cette façon au lieu de comme une grande opération dynamique: les opérations dynamiques sont liées au moment de l' exécution après que leurs opérandes sont évalués , de sorte que la liaison puisse être basée sur les types d'exécution des résultats de ces évaluations. Mais une évaluation aussi enthousiaste va à l'encontre de l'objectif de court-circuiter les opérateurs! Donc, à la place, le code généré pour dynamique &&et ||divise l'évaluation en morceaux et se déroulera comme suit:

  • Evaluez l'opérande de gauche (appelons le résultat x)
  • Essayez de le transformer en une boolconversion implicite via, ou les opérateurs trueou false(échouer si impossible)
  • Utiliser xcomme condition dans une ?:opération
  • Dans la vraie branche, utilisez xcomme résultat
  • Dans la fausse branche, évaluez maintenant le deuxième opérande (appelons le résultat y)
  • Essayez de lier l' opérateur &ou en |fonction du type d'exécution de xet y(échoue si impossible)
  • Appliquer l'opérateur sélectionné

C'est le comportement qui laisse passer certaines combinaisons "illégales" d'opérandes: l' ?:opérateur traite avec succès le premier opérande comme un booléen non nullable , l' opérateur &ou le |traite avec succès comme un booléen nullable , et les deux ne se coordonnent jamais pour vérifier qu'ils sont d'accord .

Donc ce n'est pas si dynamique && et || travailler sur les nullables. C'est juste qu'ils se trouvent être implémentés d'une manière un peu trop clémente, par rapport au cas statique. Cela devrait probablement être considéré comme un bogue, mais nous ne le corrigerons jamais, car ce serait un changement radical. De plus, cela n'aiderait guère personne à resserrer le comportement.

Espérons que cela explique ce qui se passe et pourquoi! C'est un domaine intriguant, et je me trouve souvent déconcerté par les conséquences des décisions que nous avons prises lorsque nous avons mis en place une dynamique. Cette question était délicieuse - merci de l'avoir soulevée!

Mads

Mads Torgersen - MSFT
la source
Je peux voir que ces opérateurs de court-circuit sont spéciaux, car avec la liaison dynamique, nous ne sommes pas vraiment autorisés à connaître le type du deuxième opérande dans le cas où nous court-circuitons. Peut-être que la spécification devrait le mentionner? Bien sûr, puisque tout ce qui se trouve à l'intérieur d'un dynamicest encadré, nous ne pouvons pas faire la différence entre un bool?qui HasValueet un "simple" bool.
Jeppe Stig Nielsen
6

Ce comportement est-il correct?

Oui, j'en suis presque sûr.

Comment pouvez-vous déduire cela de la spécification?

Section 7.12 de C # Spécification Version 5.0, contient des informations concernant les opérateurs conditionnels &&et ||et la façon dont la liaison dynamique se rapporte à eux. La section pertinente:

Si un opérande d'un opérateur logique conditionnel a le type dynamique à la compilation, alors l'expression est dynamiquement liée (§7.2.2). Dans ce cas, le type au moment de la compilation de l'expression est dynamique, et la résolution décrite ci-dessous aura lieu au moment de l'exécution en utilisant le type au moment de l'exécution des opérandes qui ont le type dynamique au moment de la compilation.

C'est le point clé qui répond à votre question, je pense. Quelle est la résolution qui se produit au moment de l'exécution? La section 7.12.2, Opérateurs logiques conditionnels définis par l'utilisateur explique:

  • L'opération x && y est évaluée comme T.false (x)? x: T. & (x, y), où T.false (x) est une invocation de l'opérateur false déclaré dans T, et T. & (x, y) est une invocation de l'opérateur sélectionné &
  • L'opération x || y est évalué comme T.true (x)? x: T. | (x, y), où T.true (x) est une invocation de l'opérateur true déclaré dans T, et T. | (x, y) est une invocation de l'opérateur sélectionné |.

Dans les deux cas, le premier opérande x sera converti en booléen à l'aide des opérateurs falseou true. Ensuite, l'opérateur logique approprié est appelé. Dans cet esprit, nous avons suffisamment d'informations pour répondre au reste de vos questions.

Mais l'évaluation pour xx de A || B ne conduit à aucune exception de temps de liaison, et seule la propriété A a été lue, pas B. Pourquoi cela se produit-il?

Pour l' ||opérateur, nous savons que cela suit true(A) ? A : |(A, B). Nous court-circuitons, donc nous n'obtiendrons pas d'exception de temps contraignante. Même si Ac'était le cas false, nous n'obtiendrions toujours pas d'exception de liaison d'exécution, en raison des étapes de résolution spécifiées. Si Ac'est le cas false, nous faisons alors l' |opérateur, qui peut gérer avec succès les valeurs nulles, conformément à la section 7.11.4.

L'évaluation de A && B (pour yy) n'entraîne également aucune erreur de temps de liaison. Et ici, les deux propriétés sont récupérées, bien sûr. Pourquoi est-ce autorisé par le classeur d'exécution? Si l'objet retourné par B est changé en un objet "incorrect" (comme une chaîne), une exception de liaison se produit.

Pour des raisons similaires, celui-ci fonctionne également. &&est évalué comme false(x) ? x : &(x, y). Apeut être converti avec succès en a bool, il n'y a donc pas de problème. Comme il Best nul, l' &opérateur est levé (section 7.3.7) de celui qui prend a boolà celui qui prend les bool?paramètres, et il n'y a donc aucune exception d'exécution.

Pour les deux opérateurs conditionnels, s'il Bs'agit d'autre chose qu'un bool (ou une dynamique nulle), la liaison d'exécution échoue car elle ne peut pas trouver une surcharge qui prend un bool et un non-bool comme paramètres. Cependant, cela ne se produit que si Ane satisfait pas la première condition de l'opérateur ( truepour ||, falsepour &&). La raison pour laquelle cela se produit est que la liaison dynamique est assez paresseuse. Il n'essaiera pas de lier l'opérateur logique à moins que ce Asoit faux et il doit emprunter ce chemin pour évaluer l'opérateur logique. Une fois Aéchoue à satisfaire la première condition de l'opérateur, elle échouera avec l'exception de liaison.

Si vous essayez B comme premier opérande, les deux B || A et B && A donnent une exception au liant d'exécution.

J'espère que vous savez déjà pourquoi cela se produit (ou j'ai fait un mauvais travail en expliquant). La première étape de la résolution de cet opérateur conditionnel consiste à prendre le premier opérande B, et à utiliser l'un des opérateurs de conversion booléenne (false(B) ou true(B)) avant de traiter l'opération logique. Bien sûr, Bêtre nullne peut pas être converti en trueou false, et donc l'exception de liaison d'exécution se produit.

Christopher Currens
la source
Il n'est pas surprenant que dynamicla liaison se produise au moment de l'exécution en utilisant les types réels des instances, pas les types au moment de la compilation (votre premier devis). Votre deuxième citation n'est pas pertinente car aucun type ici ne surcharge le operator trueet operator false. Un explicit operatorretour boolest autre chose que operator trueet false. Il est difficile de lire la spécification de quelque manière que ce soit qui le permet A && B(dans mon exemple), sans également autoriser a && boù les aet bsont statiquement typés booléens nullables, c'est bool? a-à- dire et bool? b, avec liaison au moment de la compilation. Pourtant, cela est interdit.
Jeppe Stig Nielsen
-1

Le type Nullable ne définit pas d'opérateurs logiques conditionnels || et &&. Je vous suggère le code suivant:

bool a = true;
bool? b = null;

bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
Thomas Papamihos
la source