Faire des opérateurs de court-circuit || et && existent pour les booléens nullables? Le RuntimeBinder le pense parfois

Je lis la spécification du langage C # sur les opérateurs logiques conditionnels || et && , également appelés opérateurs logiques en court-circuit. Pour moi, cela ne semblait pas clair si ceux-ci existaient pour les booléens nullables, c’est-à-dire le type d’opérande Nullable (également écrit bool? ), Donc je l’ai essayé avec un typage non dynamic:

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

Cela a semblé 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, maintenant je le savais).

Cependant, je voulais aussi essayer avec dynamic liaison dynamic . 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 se déroule sans exception.

Eh bien, x et y ne sont pas surprenants, leurs déclarations entraînent la récupération des deux propriétés, et les valeurs résultantes sont les mêmes que prévu, x est true et y est null .

Mais l’évaluation pour xx de A || B A || B conduit à aucune exception de temps de liaison, et seule la propriété A été lue, pas B Pourquoi cela arrive-t-il? Comme vous pouvez le constater, nous pourrions changer le B getter pour renvoyer un object fou, comme "Hello world" , et xx serait toujours évalué à true sans problème de liaison …

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 bien sûr récupérées. Pourquoi est-ce autorisé par le classeur d’exécution? Si l’object renvoyé de B est remplacé par un object “mauvais” (comme une ssortingng ), une exception de liaison se produit.

Est-ce un comportement correct? (Comment pouvez-vous déduire cela de la spécification?)

Si vous essayez B comme premier opérande, les deux B || A B || A et B && A donnent une exception de classeur à l’exécution ( B | A et B & A fonctionnent correctement car tout est normal avec les opérateurs sans court-circuit | et & ).

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

Tout d’abord, merci de souligner que la spécification n’est pas claire sur le cas non-dynamic nullable-bool. Je vais corriger cela dans une prochaine version. Le comportement du compilateur est le comportement prévu; && et || ne sont pas censés travailler sur des bools nulles.

Le classeur dynamic ne semble toutefois pas implémenter cette ressortingction. Au lieu de cela, il lie les opérations de composant séparément: le & / | et le ?: . Ainsi, il est possible de se débrouiller si le premier opérande est true ou false (valeurs booléennes et donc autorisées comme premier opérande de ?: :), Mais si vous donnez null comme premier opérande (par exemple si vous essayez B && A dans l’exemple ci-dessus), vous obtenez une exception de liaison d’exécution.

Si vous y réfléchissez, vous pouvez voir pourquoi nous avons implémenté dynamic && et || de cette façon, au lieu d’une opération dynamic unique: les opérations dynamics sont liées à l’exécution une fois que leurs opérandes sont évaluées , de sorte que la liaison peut être basée sur les types d’exécution des résultats de ces évaluations. Mais une évaluation aussi impitoyable va à l’encontre de l’objective des opérateurs en court-circuit! Donc, à la place, le code généré pour dynamic && et || décompose l’évaluation en morceaux et procédera comme suit:

  • Evaluez l’opérande gauche (appelons le résultat x )
  • Essayez de le transformer en bool via une conversion implicite, ou les opérateurs true ou false (échec si impossible)
  • Utilisez x comme condition dans une opération ?:
  • Dans la vraie twig, utilisez x comme résultat
  • Dans la twig fausse, évaluez maintenant le deuxième opérande (appelons le résultat y )
  • Essayez de lier le & ou | opérateur basé sur le type d’exécution de x et y (échec 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 , le & ou | l’opérateur 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 dynamic && et || travailler sur des nullables. C’est juste qu’ils sont mis en œuvre d’une manière un peu trop légère, comparée à la situation statique. Cela devrait probablement être considéré comme un bogue, mais nous ne le réparerons jamais, car ce serait un changement radical. En outre, cela aiderait difficilement quiconque à resserrer le comportement.

J’espère que cela explique ce qui se passe et pourquoi! C’est un domaine insortingguant et je me trouve souvent déconcerté par les conséquences des décisions que nous avons sockets lors de la mise en œuvre de la dynamic. Cette question était délicieuse – merci de l’avoir évoqué!

Mads

Est-ce un comportement correct?

Oui, j’en suis sûr.

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

La section 7.12 de la spécification C # Version 5.0 contient des informations sur les opérateurs conditionnels && et || et comment la liaison dynamic les concerne. La section pertinente:

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

C’est le point clé qui répond à votre question, je pense. Quelle est la résolution qui se produit à 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ée 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 que 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 en utilisant les opérateurs false ou true . Ensuite, l’opérateur logique approprié est appelé. Dans cette optique, nous avons suffisamment d’informations pour répondre au rest 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 le || opérateur, nous soaps que cela est true(A) ? A : |(A, B) true(A) ? A : |(A, B) . Nous court-circuitons, donc nous n’obtiendrons pas une exception de temps contraignant. Même si A était false , nous ne pourrions toujours pas obtenir d’exception de liaison à l’exécution, en raison des étapes de résolution spécifiées. Si A est false , on fait alors le | 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 bien sûr récupérées. Pourquoi est-ce autorisé par le classeur d’exécution? Si l’object renvoyé de B est remplacé par un object “mauvais” (comme une chaîne), une exception de liaison se produit.

Pour des raisons similaires, celle-ci fonctionne également. && est évalué comme false(x) ? x : &(x, y) false(x) ? x : &(x, y) . A peut être converti avec succès en bool , il n’y a donc pas de problème. Parce que B est nul, l’opérateur & est levé (Section 7.3.7) de celui qui prend un bool à celui qui prend le bool? parameters, et il n’y a donc pas d’exception d’exécution.

Pour les deux opérateurs conditionnels, si B est autre chose qu’un bool (ou une dynamic nulle), la liaison d’exécution échoue car elle ne peut pas trouver de surcharge nécessitant un bool et un non-bool en tant que parameters. Cependant, cela ne se produit que si A ne parvient pas à satisfaire le premier conditionnel de l’opérateur ( true pour || , false pour && ). La raison en est que la liaison dynamic est assez paresseuse. Il n’essaiera pas de lier l’opérateur logique à moins que A soit faux et qu’il doive suivre ce chemin pour évaluer l’opérateur logique. Une fois que A ne parvient pas à satisfaire la première condition pour l’opérateur, il échouera avec l’exception de liaison.

Si vous essayez B comme premier opérande, les deux B || A et B && A donnent une exception de classeur d’exécution.

Si tout va bien, maintenant 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 ( false(B) ou true(B) ) avant de gérer l’opération logique. Bien sûr, B , étant null ne peut pas être converti en true ou en false , et donc l’exception d’exécution de liaison se produit.

Le type Nullable ne définit pas les 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;