c# - pulsaciones - relacion entre presion arterial y frecuencia cardiaca
¿Por qué un operador de conversión implícito de<T> a<U> acepta<T?>? (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:
- ¿Por qué se compila el código del primer fragmento?
- ¿Puedo evitar que el código se compile en este escenario?
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).
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''