Hacer cortocircuitos a los operadores || y && existen para valores booleanos que aceptan valores NULL? El RuntimeBinder a veces lo cree

84

Leí la Especificación del lenguaje C # sobre los operadores lógicos condicionales || y &&, también conocidos como operadores lógicos de cortocircuito. Para mí, no parecía claro si estos existían para valores booleanos que aceptan valores NULL, es decir, el tipo de operando Nullable<bool>(también escrito bool?), así que lo probé con escritura no dinámica:

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

Eso pareció resolver la pregunta (no pude entender la especificación claramente, pero suponiendo que la implementación del compilador de Visual C # fuera correcta, ahora lo sabía).

Sin embargo, también quería probar con la dynamicencuadernación. Así que probé esto en su lugar:

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);
  }
}

El resultado sorprendente es que esto funciona sin excepción.

Bueno, xy yno es de extrañar, sus declaraciones llevan a que se recuperen ambas propiedades, y los valores resultantes son los esperados, xes truey yes null.

Pero la evaluación xxde no A || Bcondujo a ninguna excepción de tiempo de vinculación, y solo Ase leyó la propiedad , no B. ¿Por qué pasó esto? Como puede ver, podríamos cambiar el Bgetter para devolver un objeto loco, como "Hello world", y xxaún evaluaríamos truesin problemas de vinculación ...

La evaluación A && B(para yy) tampoco conduce a ningún error de tiempo de vinculación. Y aquí se recuperan ambas propiedades, por supuesto. ¿Por qué lo permite el archivador en tiempo de ejecución? Si el objeto devuelto Bse cambia a un objeto "incorrecto" (como a string), se produce una excepción de vinculación.

¿Es este el comportamiento correcto? (¿Cómo puedes inferir eso de la especificación?)

Si lo intenta Bcomo primer operando, ambos B || Ay B && Adan una excepción del enlazador en tiempo de ejecución ( B | Ay B & Afuncionan bien, ya que todo es normal con operadores sin cortocircuito |y &).

(Probado con el compilador C # de Visual Studio 2013 y la versión en tiempo de ejecución .NET 4.5.2.)

Jeppe Stig Nielsen
fuente
4
No hay instancias de Nullable<Boolean>involucrados en absoluto, solo booleanos en caja tratados como dynamic: su prueba con bool?es irrelevante. (Por supuesto, esta no es una respuesta completa, solo el germen de una.)
Jeroen Mostert
3
El A || Bhace que una cierta cantidad de sentido, en la que no se quiere evaluar Ba menos Aque es falso, que no lo es. Así que nunca se sabe el tipo de expresión. La A && Bversión es más sorprendente: veré qué puedo encontrar en la especificación.
Jon Skeet
2
@JeroenMostert: Bueno, a menos que el compilador decidiera que si el tipo de Aes booly el valor de Bes null, entonces un bool && bool?operador podría estar involucrado.
Jon Skeet
4
Curiosamente, parece que esto ha expuesto un error de compilación o especificación. La especificación C # 5.0 para &&habla de resolverlo como si fuera en su &lugar, y específicamente incluye el caso donde están ambos operandos bool?, pero la siguiente sección a la que se refiere no maneja el caso que acepta valores NULL. Podría agregar una especie de respuesta con más detalles sobre eso, pero no lo explicaría completamente.
Jon Skeet
14
Le envié un correo electrónico a Mads sobre el problema de las especificaciones, para ver si es solo un problema en cómo lo estoy leyendo ...
Jon Skeet

Respuestas:

67

En primer lugar, gracias por señalar que la especificación no es clara en el caso de bool que acepta valores NULL no dinámico. Lo arreglaré en una versión futura. El comportamiento del compilador es el comportamiento previsto; &&y ||se supone que no funcionan en bools que aceptan valores NULL.

Sin embargo, el enlazador dinámico no parece implementar esta restricción. En cambio, vincula las operaciones del componente por separado: el &/ |y el ?:. Por lo tanto, puede pasar si el primer operando es trueo false(que son valores booleanos y, por lo tanto, se permiten como el primer operando de ?:), pero si da nullcomo primer operando (por ejemplo, si lo intenta B && Aen el ejemplo anterior), lo hace obtener una excepción de enlace en tiempo de ejecución.

Si lo piensa, puede ver por qué implementamos dinámica &&y de ||esta manera en lugar de como una gran operación dinámica: las operaciones dinámicas se vinculan en tiempo de ejecución después de que se evalúan sus operandos , de modo que la vinculación puede basarse en los tipos de tiempo de ejecución de los resultados de esas evaluaciones. ¡Pero una evaluación tan entusiasta frustra el propósito de cortocircuitar a los operadores! Entonces, en cambio, el código generado para dinámica &&y ||divide la evaluación en partes y procederá de la siguiente manera:

  • Evaluar el operando izquierdo (llamemos al resultado x)
  • Intente convertirlo en una boolconversión implícita o en los operadores trueo false(fallar si no puede)
  • Utilizar xcomo condición en una ?:operación
  • En la rama verdadera, use xcomo resultado
  • En la rama falsa, ahora evalúe el segundo operando (llamemos al resultado y)
  • Intente vincular el operador &or |según el tipo de tiempo de ejecución de xy y(fallar si no puede)
  • Aplicar el operador seleccionado

Este es el comportamiento que deja pasar ciertas combinaciones "ilegales" de operandos: el ?:operador trata con éxito el primer operando como un booleano no anulable , el operador &or |lo trata con éxito como un booleano anulable y los dos nunca se coordinan para comprobar que están de acuerdo .

Entonces no es tan dinámico && y || trabajar en nullables. Es solo que se implementan de una manera un poco demasiado indulgente, en comparación con el caso estático. Esto probablemente debería considerarse un error, pero nunca lo arreglaremos, ya que sería un cambio importante. Además, no ayudaría a nadie a endurecer el comportamiento.

¡Ojalá esto explique lo que sucede y por qué! Esta es un área intrigante y, a menudo, me desconciertan las consecuencias de las decisiones que tomamos cuando implementamos Dynamic. Esta pregunta fue deliciosa, ¡gracias por mencionarla!

Mads

Mads Torgersen - MSFT
fuente
Puedo ver que estos operadores de cortocircuito son especiales, ya que con la vinculación dinámica realmente no se nos permite conocer el tipo del segundo operando en el caso en el que hacemos un cortocircuito. ¿Quizás la especificación debería mencionar eso? Por supuesto, dado que todo lo que hay dentro de a dynamicestá en caja, no podemos distinguir entre un bool?cuál HasValuey un "simple" bool.
Jeppe Stig Nielsen
6

¿Es este el comportamiento correcto?

Sí, estoy bastante seguro de que lo es.

¿Cómo puedes inferir eso de la especificación?

Sección 7.12 de C # Specification Version 5.0, tiene información con respecto a los operadores condicionales &&y ||y cómo unión dinámica se refiere a ellos. La sección relevante:

Si un operando de un operador lógico condicional tiene el tipo dinámico en tiempo de compilación, entonces la expresión está vinculada dinámicamente (§7.2.2). En este caso, el tipo de tiempo de compilación de la expresión es dinámico, y la resolución que se describe a continuación tendrá lugar en tiempo de ejecución utilizando el tipo de tiempo de ejecución de los operandos que tienen el tipo de tiempo de compilación dinámico.

Este es el punto clave que responde a su pregunta, creo. ¿Cuál es la resolución que ocurre en tiempo de ejecución? La sección 7.12.2, Operadores lógicos condicionales definidos por el usuario explica:

  • La operación x && y se evalúa como T.false (x)? x: T. & (x, y), donde T.false (x) es una invocación del operador falso declarado en T, y T. & (x, y) es una invocación del operador seleccionado &
  • La operación x || y se evalúa como T.verdadero (x)? x: T. | (x, y), donde T.true (x) es una invocación del operador true declarado en T, y T. | (x, y) es una invocación del operador | seleccionado.

En ambos casos, el primer operando x se convertirá en un bool usando los operadores falseo true. Entonces se llama al operador lógico apropiado. Con esto en mente, tenemos suficiente información para responder el resto de sus preguntas.

Pero la evaluación de xx de A || B no conduce a ninguna excepción de tiempo de vinculación, y solo se leyó la propiedad A, no B.

Para el ||operador, sabemos que sigue true(A) ? A : |(A, B). Hacemos un cortocircuito, por lo que no obtendremos una excepción de tiempo obligatorio. Incluso si lo Afuera false, todavía no obtendríamos una excepción de enlace en tiempo de ejecución, debido a los pasos de resolución especificados. Si Aes así false, hacemos el |operador, que puede manejar con éxito valores nulos, según la Sección 7.11.4.

La evaluación de A && B (para yy) tampoco conduce a ningún error de tiempo de vinculación. Y aquí se recuperan ambas propiedades, por supuesto. ¿Por qué lo permite el archivador en tiempo de ejecución? Si el objeto devuelto de B se cambia a un objeto "malo" (como una cadena), se produce una excepción de vinculación.

Por razones similares, este también funciona. &&se evalúa como false(x) ? x : &(x, y). Ase puede convertir correctamente a a bool, por lo que no hay ningún problema allí. Debido a que Bes nulo, el &operador se eleva (Sección 7.3.7) del que toma boola uno que toma los bool?parámetros y, por lo tanto, no hay excepción de tiempo de ejecución.

Para ambos operadores condicionales, si no Bes un bool (o una dinámica nula), el enlace en tiempo de ejecución falla porque no puede encontrar una sobrecarga que tome un bool y un no bool como parámetros. Sin embargo, esto solo ocurre si Ano se cumple el primer condicional del operador ( truepara ||, falsepara &&). La razón por la que esto sucede es porque el enlace dinámico es bastante lento. No intentará vincular el operador lógico a menos que Asea ​​falso y tenga que seguir ese camino para evaluar el operador lógico. Una vez que Ano cumpla con la primera condición para el operador, fallará con la excepción de vinculación.

Si prueba B como primer operando, ambos B || A y B && A dan una excepción de carpeta de tiempo de ejecución.

Con suerte, a estas alturas ya sabes por qué sucede esto (o hice un mal trabajo al explicar). El primer paso para resolver este operador condicional es tomar el primer operando, By usar uno de los operadores de conversión bool ( false(B)o true(B)) antes de manejar la operación lógica. Por supuesto, Bbeing nullno se puede convertir en trueo false, por lo que ocurre la excepción de enlace en tiempo de ejecución.

Christopher Currens
fuente
No es de extrañar que dynamicel enlace ocurra en tiempo de ejecución utilizando los tipos reales de las instancias, no los tipos de tiempo de compilación (su primera cita). Su segunda cita es irrelevante ya que ningún tipo aquí sobrecarga el operator truey operator false. Un explicit operatorregreso booles algo más que operator truey false. Es difícil leer la especificación de cualquier manera que lo permita A && B(en mi ejemplo), sin permitir también a && bdónde ay bson booleanos anulables de tipo estático, es decir , bool? ay bool? b, con enlace en tiempo de compilación. Sin embargo, eso no está permitido.
Jeppe Stig Nielsen
-1

El tipo que acepta valores NULL no define operadores lógicos condicionales || y &&. Te sugiero que sigas el código:

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
fuente