c# - specification - Reparto vs ''como'' operador revisado
common language specification (5)
Sé que ya hay varios mensajes relacionados con la diferencia entre los lanzadores y el operador as
. Todos en su mayoría reiteran los mismos hechos:
- El operador
as
no lanzará, pero devolverá elnull
si falla el lanzamiento. - En consecuencia, el operador
as
solo trabaja con tipos de referencia - El operador
as
no utilizará operadores de conversión definidos por el usuario
Las respuestas tienden entonces a debatir sin cesar sobre cómo usar o no usar lo uno o lo otro y las ventajas y desventajas de cada uno, incluso su desempeño (lo que no me interesa en absoluto).
Pero hay algo más en el trabajo aquí. Considerar:
static void MyGenericMethod<T>(T foo)
{
var myBar1 = foo as Bar; // compiles
var myBar2 = (Bar)foo; // does not compile (''Cannot cast expression of
// type ''T'' to type ''Bar'')
}
Por favor, no importa si este ejemplo obviamente contrario es una buena práctica o no. Mi preocupación aquí es la disparidad muy interesante entre los dos en que el reparto no se compilará mientras as
hace. Realmente me gustaría saber si alguien podría arrojar algo de luz sobre esto.
Como se observa a menudo, el operador as
ignora las conversiones definidas por el usuario, pero en el ejemplo anterior, es claramente el más capaz de los dos. Tenga en cuenta que as
lo as
respecta al compilador, no hay conexión conocida entre el tipo T y Bar (desconocido en tiempo de compilación). El reparto es enteramente ''run-time''. ¿Debemos sospechar que el reparto se resuelve, total o parcialmente, en tiempo de compilación y no as
operador?
Por cierto, la adición de una restricción de tipo, como es lógico, corrige la conversión, por lo tanto:
static void MyGenericMethod<T>(T foo) where T : Bar
{
var myBar1 = foo as Bar; // compiles
var myBar2 = (Bar)foo; // now also compiles
}
¿Por qué el operador as
compila y el elenco no?
¿Debemos sospechar que el reparto se resuelve, total o parcialmente, en tiempo de compilación y no como operador?
Usted mismo dio la respuesta al comienzo de su pregunta: "El operador as no usará operadores de conversión definidos por el usuario"; mientras tanto, el cast lo hace , lo que significa que debe encontrar esos operadores ( o su ausencia ) en el momento de la compilación.
Tenga en cuenta que, en lo que respecta al compilador, no hay conexión conocida entre el tipo T y Bar (desconocido en tiempo de compilación).
El hecho de que el tipo T sea desconocido significa que el compilador no puede saber si no hay conexión entre él y Bar.
Tenga en cuenta que (Bar)(object)foo
no funciona, porque ningún tipo puede tener un operador de conversión a Objeto [ya que es la clase base de todo], y se sabe que la conversión de objeto a Barra no tiene que tratar con una conversión operador.
El compilador no sabe cómo generar código que funcione para todos los casos.
Considere estas dos llamadas:
MyGenericMethod(new Foo1());
MyGenericMethod(new Foo2());
ahora asuma que Foo1
contiene un operador de conversión que puede convertirlo en una instancia de Bar
, y que Foo2
desciende de Bar
lugar. Obviamente, el código involucrado dependerá en gran medida de la T
real que usted pase.
En su caso particular, usted dice que el tipo ya es un tipo de Bar
por lo que, obviamente, el compilador solo puede hacer una conversión de referencia, porque sabe que es seguro, que no se está realizando ninguna conversión o que es necesario.
Ahora, as
conversión es más "exploratoria", no solo no considera las conversiones de los usuarios, sino que explícitamente permite el hecho de que la conversión no tiene sentido, por lo que el compilador deja que se deslice.
Es una cuestión de tipo seguridad.
Cualquier T
no se puede convertir a una Bar
, pero cualquier T
se puede "ver" as
una Bar
ya que el comportamiento está bien definido incluso si no hay conversión de T
a Bar
.
La primera compila simplemente porque así es as
se define la palabra clave as
. Si no se puede lanzar, devolverá null
. Es seguro porque la palabra clave as
no causa ningún problema de tiempo de ejecución. El hecho de que puede o no haber comprobado que la variante sea nula es otra cuestión.
Piense as
como un método TryCast.
Para abordar su primera pregunta: no es solo que el operador as
no tenga en cuenta las conversiones definidas por el usuario, sino que es relevante. Lo que es más relevante es que el operador de reparto hace dos cosas contradictorias. El operador de yeso significa:
Sé que esta expresión del tipo Foo en tiempo de compilación será en realidad un objeto de la barra de tipo runtime. Compilador, les estoy contando este hecho ahora para que puedan usarlo. Por favor genere el código asumiendo que estoy en lo correcto; si soy incorrecto, entonces puedes lanzar una excepción en el tiempo de ejecución.
Sé que esta expresión del tipo Foo en tiempo de compilación será realmente del tipo Foo en tiempo de ejecución. Existe una forma estándar de convertir algunas o todas las instancias de Foo en una instancia de Bar. Compilador, genere dicha conversión, y si resulta en tiempo de ejecución que el valor que se está convirtiendo no es convertible, entonces lance una excepción en tiempo de ejecución.
Esos son los opuestos . Buen truco, tener un operador que haga cosas opuestas.
El operador as
en contraste solo tiene el primer sentido. Y as
solo lo hacen las conversiones de boxeo , unboxing y preservación de la representación . Un reparto puede hacer todo eso más conversiones adicionales que cambian la representación. Por ejemplo, la conversión de int a short cambia la representación de un entero de cuatro bytes a un entero de dos bytes.
Es por eso que los modelos "crudos" no son legales en genéricos sin restricciones; porque el compilador no tiene suficiente información para averiguar qué tipo de lanzamiento es: boxeo, desempaquetado, preservación de la representación o cambio de representación. La expectativa de los usuarios es que una conversión en código genérico tiene toda la semántica de una conversión en código más fuertemente tipado, y no tenemos manera de generar ese código de manera eficiente.
Considerar:
void M<T, U>(T t, out U u)
{
u = (U)t;
}
¿Esperas que funcione? Qué código generamos que podemos manejar:
M<object, string>(...); // explicit reference conversion
M<string, object>(...); // implicit reference conversion
M<int, short>(...); // explicit numeric conversion
M<short, int>(...); // implicit numeric conversion
M<int, object>(...); // boxing conversion
M<object, int>(...); // unboxing conversion
M<decimal?, int?>(...); // lifted conversion calling runtime helper method
// and so on; I could give you literally hundreds of different cases.
Básicamente, tendríamos que emitir código para la prueba que inició el compilador nuevamente , hizo un análisis completo de las expresiones y luego emitió un nuevo código. Implementamos esa característica en C # 4; se llama "dinámico" y si ese es el comportamiento que desea, puede sentirse libre de usarlo.
No tenemos ninguno de estos problemas con as
, porque as
solo hace tres cosas. Hace conversiones de boxeo, conversiones de desempaquetado y pruebas de tipo, y podemos generar fácilmente el código que hace esas tres cosas.