2010-12-06 9 views
5

me encontré mirando al código que es similar a esto:¿Por qué/Cómo me permite .Net anular un Nullable <T> que es nulo?

private double? _foo; 

public double? Foo 
{ 
    get { return _foo; } 
    set { Baz(-value); } 
} 

public void Baz(double? value) 
{ 
    // Perform stuff with value, including null checks 
} 

Y en una prueba, que tenía la siguiente:

[Test] 
public void Foo_test() 
{ 
    ... 
    Foo = null; 
    ... 
} 

que esperaba que la prueba fracasaría (tiro), desde que pensé que realmente no se puede aplicar unario menos a null, que creí que sucedería en Foo s setter. Sin embargo, ocurrió lo inesperado y la prueba hicieron no fallan.

probablemente podría hacer una suposición informada en cuanto a por qué sucede esto, pero pensé que sería mejor si alguien que realmente sabe lo que está pasando me pudiera iluminar.

(Y antes de que alguien señala lo obvio:. Sí, definitivamente la negación debe ser empujado hacia abajo en las entrañas de Baz)

Editar y seguimiento

lo tanto, estoy agradecido por las aportaciones de Jon Skeet y JaredPar, quienes respondieron la parte Cómo de mi pregunta. Después de googlear encontré this blog post by Eric Lippert explaining the mathematical definition of 'lifted', lo cual tiene mucho sentido. También leo las partes relevantes de la especificación del lenguaje, que son bastante sencillas y explican la mecánica del levantamiento en C#.

Específicamente ahora sé que al principio del proceso de elevación, se realiza una comprobación nula en el operando, y si el operando es nulo, nulo será el resultado de aplicar el operador (unario menos, en mi caso). Entonces -null es null.

Ahora, para el "por qué" parte de la pregunta. Si defino mi propia (unario) operator - en un class (en la que me signo de un valor envuelta), e invocar este operador en una referencia nula, voy a conseguir un NullReferenceException. ¿Por qué es que 'levantar' se comporta de manera diferente? Estoy realmente interesado en el razonamiento detrás de esta decisión de diseño. Definitivamente soy no me opongo a la decisión.

+0

La negación definitivamente debe ser empujada al ... uhh ... Oh. No importa. :-P –

+0

Actualicé la pregunta con una segunda parte: * * "Why" *. –

Respuesta

12

¿Por qué es que 'levantar' se comporta de manera diferente? De hecho, estoy interesado en el razonamiento detrás de esta decisión de diseño.

Bueno, en primer lugar, no siempre debería obtener una excepción de referencia nula. Una llamada del operador se envía estáticamente; no necesita hacer una comprobación nula. Esto es perfectamente legal:

class P 
{ 
    public static P operator -(P p) 
    { return p; } 

    static void Main() 
    { 
    P p1 = null; 
    P p2 = -p1; 
    Console.WriteLine(p2 == null); 
    } 
} 

Realmente creo que lo que significa que su pregunta es "¿por qué los tipos de valor anulables consiguen automática elevación de las operaciones no anulables a nullable operaciones, pero los tipos de referencia se requieren para escribir la lógica específica de el operador si quieres semántica anulable? "

Es un desastre.

Lo que pasa es que los tipos de referencia fueron siempre nullable para empezar, por lo que siempre podría escribir ese código usted mismo. Dado que los tipos de valor anulables se agregaron más tarde, agregamos un mecanismo para que todos los operadores existentes que no admitan nulos de repente funcionen correctamente con tipos anulables, para que no tenga que escribir todo ese código aburrido usted mismo.

Sin embargo, hay un problema más general. El problema más general es que hemos combinado "nulo" para significar dos cosas diferentes. En el mundo del tipo de referencia, null significa "No me refiero a ningún objeto". En el mundo del tipo value, null significa lo que significa en las bases de datos: esta cantidad puede tener un valor pero no sabemos cuál es. ¿Cuáles son las cifras de ventas de noviembre si todo el personal de ventas no ha informado sus resultados todavía? Definitivamente hay un valor en dólares para esa cantidad, pero no sabemos qué es, entonces lo marcamos como nulo.

La aritmética elevada está diseñada para funcionar en la semántica de "base de datos nula"; null plus doce es nulo. Algo que no sabes más doce es algo más que no sabes.Desafortunadamente, dado que no agregamos la aritmética nullable a C# hasta la versión 2, ya había todo el equipo en su lugar para tratar nulo como la semántica "hay una referencia de objeto que no hace referencia a ningún objeto".

Si hubiésemos diseñado todo desde el principio, sospecho que (1) habría tipos de referencia anulables, tipos de referencias no anulables, tipos de valores que aceptan valores numéricos y tipos de valores que no admiten valores NULL, y (2) habría una una diferencia más claramente definida entre la referencia nula y el valor nulo, o su semántica estaría más alineada cuando se trata de la aritmética, y (3) habría un levantamiento automático de todas las operaciones no anulables para las operaciones que aceptan nulos.

Tenga en cuenta que VB/VBScript implement the more clear distinction by having two separate values: Null, which is database null, and Nothing, which is reference null.

+0

Cosas geniales, exactamente lo que estaba buscando/esperando. –

9

Puede negarlo porque así es como se definen los operadores unarios "levantados" ... si realiza alguna operación como negación, suma, resta, etc. y cualquiera de los dos operandos es nulo, el resultado también es nulo.

Para más detalles, véase la sección 7.3.7 de la especificación del lenguaje.

En este caso en particular:

Para los operadores unitarios (+, ++, -, -,, ~!) Una forma elevada de un operador existe si los tipos de operandos y resultados son tanto no tipos de valores anulables ¿La forma levantada se construye al agregar una sola? modificador para el operando y los tipos de resultados. El operador elevado produce un valor nulo si el operando es nulo. De lo contrario, el operador levantado desenvuelve el operando, aplica el operador subyacente y envuelve el resultado.

Tenga en cuenta que esto es específico del idioma que está utilizando, en particular, el & y | los operadores funcionan de forma diferente con los valores bool? en C# que en VB.

4

Esto es solo una característica de los valores que aceptan valores NULL en C#. Aplicar el conjunto de operadores unarios y binarios predefinidos a un valor que admite valores NULL produce null si cualquiera de las entradas que aceptan nulos es null.

Más información aquí, en la sección Operadores

EDITAR

Como Jon señaló esta regla no se cumple para los operadores como ||, true que tienen un no tipo de devolución que se puede nulificar

+1

... excepto cosas como 'null | true' que da "verdadero" en C#, pero Nothing en VB IIRC :) –

+2

@John gracias, actualizado. A decir verdad, simplemente evito los operadores de lógica booleana y los valores con nulos. Considero que las reglas son muy poco intuitivas y prefiero usar .HasValue explícito para evitar que se equivoquen. – JaredPar

+0

... o 'GetValueOrDefault()' - un método en 'Nullable <>' que parece ser usado raramente pero que es muy útil para evitar los operadores levantados. – Lucero

0

escribir esta expresión en el reloj de depuración o en la ventana inmediata:

-((int?)null) 

El resultado sigue siendo null, por lo que, evidentemente, está permitido.

+1

Es bastante evidente que está permitido por el hecho de que el código del OP se compila y se ejecuta sin errores. Está buscando una explicación sobre * por qué * esto sucede. –

+1

Si bien esto funciona correctamente en este escenario, tenga cuidado de usar la ventana inmediata en general para verificar los escenarios de codificación de casos de esquina. La ventana inmediata está mucho más cerca de un intérprete que una evaluación real. Se desviará del lenguaje real en ocasiones. – JaredPar

+1

Esto es como decir que funciona porque funciona. (Pero seré amable y no te daré un -1) –

Cuestiones relacionadas