c# relacion ¿Por qué un operador de conversión implícito de<T> a<U> acepta<T?>?




relacion peso y presion arterial (3)

¿Por qué se compila el código del primer fragmento?

Un ejemplo de código de un código fuente de Nullable<T> que se puede encontrar here :

[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) {
    return value.Value;
}

[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) {
    return hasValue ? value : defaultValue;
}

La estructura Nullable<int> tiene un operador explícito Nullable<int> , así como el método GetValueOrDefault Uno de estos dos es usado por el compilador para convertir int? a T .

Después de eso, ejecuta el implicit operator Sample<T>(T value) .

Una imagen aproximada de lo que sucede es la siguiente:

Sample<int> sampleA = (Sample<int>)(int)a;

Si imprimimos typeof(T) dentro del Operador implícito de la Sample<T> , se mostrará: System.Int32 .

En su segundo escenario, el compilador no usa el implicit operator Sample<T> y simplemente asigna null a sampleB .

Este es un comportamiento extraño que no puedo entender. En mi ejemplo, tengo una Sample<T> clase Sample<T> y un operador de conversión implícito de T a Sample<T> .

private class Sample<T>
{
   public readonly T Value;

   public Sample(T value)
   {
      Value = value;
   }

   public static implicit operator Sample<T>(T value) => new Sample<T>(value);
}

El problema se produce cuando se utiliza un tipo de valor anulable para T , como int? .

{
   int? a = 3;
   Sample<int> sampleA = a;
}

Aquí está la parte clave:
En mi opinión, esto no debería compilarse porque la Sample<int> define una conversión de int a Sample<int> pero no de la int? para Sample<int> . ¡Pero compila y corre con éxito! (Por lo que quiero decir, se invoca el operador de conversión y se asignará 3 al campo de readonly ).

Y se pone aún peor. Aquí el operador de conversión no se invoca y sampleB se establecerá en null :

{
   int? b = null;
   Sample<int> sampleB = b;
}

Una gran respuesta probablemente se dividiría en dos partes:

  1. ¿Por qué se compila el código del primer fragmento?
  2. ¿Puedo evitar que el código se compile en este escenario?

Puedes echar un vistazo a cómo el compilador baja este código:

int? a = 3;
Sample<int> sampleA = a;

en this :

int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;

Debido a que Sample<int> es una clase, a su instancia se le puede asignar un valor nulo y con un operador implícito de este tipo, también se puede asignar el tipo subyacente de un objeto anulable. Así que asignaciones como estas son válidas:

int? a = 3;
int? b = null;
Sample<int> sampleA = a; 
Sample<int> sampleB = b;

Si Sample<int> sería una struct , eso, por supuesto, daría un error.

EDIT: ¿Por qué es esto posible? No lo pude encontrar en la especificación porque es una violación deliberada de la especificación y esto solo se mantiene por compatibilidad con versiones anteriores. Puedes leerlo en code :

VIOLACIÓN ESPECÍFICA DELIBERADA:
El compilador nativo permite una conversión "elevada" incluso cuando el tipo de retorno de la conversión no es un tipo de valor no anulable. Por ejemplo, si tenemos una conversión de struct S a string, entonces ¿una conversión "levantada" de S? El compilador nativo considera que la cadena existe, con la semántica de "s.HasValue? (string) s.Value: (string) null". El compilador de Roslyn perpetúa este error por motivos de compatibilidad con versiones anteriores.

Así es como este "error" se implemented en Roslyn:

De lo contrario, si el tipo de retorno de la conversión es un tipo de valor anulable, tipo de referencia o tipo de puntero P, entonces lo reducimos como:

temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)

Entonces, de acuerdo con las spec para un operador de conversión definido por el usuario T -> U , ¿existe un operador T? -> U? levantado T? -> U? T? -> U? donde T y U son tipos de valores no anulables. Sin embargo, dicha lógica también se implementa para un operador de conversión donde U es un tipo de referencia debido a la razón anterior.

PARTE 2 ¿Cómo evitar que el código se compile en este escenario? Bueno, hay una manera. Puede definir un operador implícito adicional específicamente para un tipo anulable y decorarlo con un atributo Obsolete . Eso requeriría que el parámetro de tipo T esté restringido a struct :

public class Sample<T> where T : struct
{
    ...

    [Obsolete("Some error message", error: true)]
    public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();
}

Este operador se elegirá como primer operador de conversión para el tipo anulable porque es más específico.

Si no puede hacer tal restricción, debe definir cada operador para cada tipo de valor por separado (si realmente está determinado, puede aprovechar la reflexión y la generación de código usando plantillas):

[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();

Eso daría un error si se hace referencia en cualquier lugar en el código:

Error CS0619 'Sample.implicit operator Sample (int?)' Está obsoleto: 'Algún mensaje de error'


Creo que se levanta el operador de conversión en acción. La especificación dice que:

Dado un operador de conversión definido por el usuario que convierte de un tipo de valor no anulable S a un tipo de valor no anulable T, existe un operador de conversión elevado que convierte de S? a T? Este operador de conversión elevado realiza un desenvolvimiento desde S? a S seguido de la conversión definida por el usuario de S a T seguida de un ajuste de T a T ?, ¿excepto que un S nulo? Se convierte directamente a un valor nulo de T ?.

Parece que no es aplicable aquí, porque mientras que el tipo S es el tipo de valor aquí ( int ), el tipo T no es el tipo de valor (clase de Sample ). Sin embargo, este problema en el repositorio de Roslyn indica que en realidad es un error en la especificación. Y la documentación del code Roslyn lo confirma:

Como se mencionó anteriormente, aquí divergimos de la especificación, de dos maneras. Primero, solo verificamos el formulario levantado si la forma normal no era aplicable. En segundo lugar, se supone que debemos aplicar la semántica de elevación solo si los parámetros de conversión y los tipos de retorno son ambos tipos de valores no anulables.

De hecho, el compilador nativo determina si se debe verificar un formulario levantado sobre la base de:

  • ¿Es el tipo que finalmente estamos convirtiendo de un tipo de valor anulable?
  • ¿Es el tipo de parámetro de la conversión un tipo de valor no anulable?
  • ¿Es el tipo que estamos convirtiendo en última instancia a un tipo de valor, tipo de puntero o tipo de referencia que acepta valores nulos?

Si la respuesta a todas esas preguntas es "sí", pasamos a Nullable y vemos si el operador resultante es aplicable.

Si el compilador siguiera la especificación, produciría un error en este caso como esperaba (y en algunas versiones anteriores lo hizo), pero ahora no lo hace.

Para resumir: creo que el compilador usa la forma elevada de su operador implícito, lo que debería ser imposible según la especificación, pero el compilador se aparta de la especificación aquí porque:

  • Se considera error en la especificación, no en el compilador.
  • Las especificaciones ya fueron violadas por el compilador anterior a Roslyn, y es bueno mantener la compatibilidad con versiones anteriores.

Como se describe en la primera cita que describe cómo funciona el operador levantado (además de que permitimos que T sea ​​el tipo de referencia), puede notar que describe exactamente lo que sucede en su caso. null valor null S ( int? ) se asigna directamente a T ( Sample ) sin operador de conversión, y el valor no nulo se desenvuelve a int y se ejecuta a través de su operador (el ajuste a T? obviamente no es necesario si T es el tipo de referencia).







value-type