Question 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 le 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 valeurs booléennes nulles, c'est-à-dire le type d'opérande Nullable<bool> (aussi écrit bool?), donc je l'ai essayé avec la saisie non dynamique:

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

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

Cependant, je voulais essayer avec dynamic la liaison aussi bien. 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.

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 comme prévu, x est true et y est null.

Mais l'évaluation pour xx de A || B conduire à aucune exception de temps de liaison, et seulement la propriété A a été lu, pas B. Pourquoi cela arrive-t-il? Comme vous pouvez le constater, nous pourrions changer la B getter pour retourner un objet fou, comme "Hello world", et xx serait encore évaluer à true sans problèmes de liaison ...

Évaluer A && B (pour yy) conduit é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'objet renvoyé de B est changé en un "mauvais" objet (comme un string), une exception contraignante se produit.

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

Si tu essayes B comme premier opérande, les deux B || A et B && A donner une exception de classeur à l'exécution (B | A et B & A fonctionne bien 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.)


86
2017-12-16 16:11


origine


Réponses:


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

Le classeur dynamique ne semble toutefois pas implémenter cette restriction. Au lieu de cela, il lie les opérations du composant séparément: le &/| et le ?:. Ainsi, il est possible de se débrouiller si le premier opérande se trouve être true ou false (qui sont des 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é la dynamique && et || de cette façon au lieu d'une opération dynamique importante: les opérations dynamiques sont liées à l'exécution après que leurs opérandes soient évalués, afin que la liaison puisse ê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'objectif des opérateurs en court-circuit! Au lieu de cela, le code généré pour dynamique && 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 un bool via une conversion implicite, ou la true ou false opérateurs (échec si impossible)
  • Utilisation x comme condition dans un ?: opération
  • Dans la vraie branche, utilisez x Par conséquent
  • Dans la fausse branche, à présent évaluer le second 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: le ?: l'opérateur traite avec succès le premier opérande en tant que non nullable booléen, le & ou | l'opérateur le traite avec succès comme un nullable booléen, et les deux ne coordonnent jamais pour vérifier qu'ils sont d'accord.

Donc, ce n'est pas que dynamique && et || travailler sur des nullables. Il est juste qu'ils soient mis en œuvre d'une manière un peu trop indulgente par rapport au cas 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 intriguant et je me trouve souvent déconcerté par les conséquences des décisions que nous avons prises lors de la mise en œuvre de la dynamique. Cette question était délicieuse - merci de l'avoir évoqué!

Mads


65
2017-12-17 18:56



Est-ce un comportement correct?

Oui, je suis sûr que c'est le cas.

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 dynamique les concerne. La section pertinente:

Si un opérande d'un opérateur logique conditionnel a le type dynamique à la compilation, alors l'expression est liée dynamiquement (§7.2.2). Dans ce cas, le type à la compilation de l’expression est dynamique et le la résolution décrite ci-dessous aura lieu à l'exécution en utilisant le type d'exécution de ces opérandes qui ont le type dynamique à 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 utilisant le false ou true les opérateurs. Ensuite, l'opérateur logique approprié est appelé. Dans cette optique, 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 le || opérateur, nous savons qu'il suit 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 serions encore ne pas obtenir d'exception de liaison à l'exécution, en raison des étapes de résolution spécifiées. Si A est false, nous faisons ensuite la | 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'objet renvoyé de B est remplacé par un objet "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). A peut être converti avec succès en bool, donc il n'y a pas de problème là-bas. Car B est nul, le & l'opérateur est levé (Section 7.3.7) de celui qui prend un bool à celui qui prend le bool? paramètres, 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 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 A ne parvient pas à satisfaire la première condition pour l'opérateur (true pour ||, false pour &&). La raison en est que la liaison dynamique est assez paresseuse. Il n'essaiera pas de lier l'opérateur logique sauf si A est faux et doit 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 pour résoudre cet opérateur conditionnel consiste à prendre le premier opérande, B, et utilisez l’un des opérateurs de conversion bool (false(B) ou true(B)) avant de manipuler l’opération logique. Bien sûr, B, étant null ne peut pas être converti en soit true ou false, et donc l'exception d'exécution de liaison se produit.


6
2017-12-17 09:16



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;

-1
2017-12-24 10:35