c# - number - ¿Por qué ''unbox.any'' no proporciona un texto de excepción útil como ''castclass''?
type number c# (1)
Para ilustrar mi pregunta, considere estos ejemplos triviales (C #):
object reference = new StringBuilder();
object box = 42;
object unset = null;
// CASE ONE: bad reference conversions (CIL instrcution 0x74 ''castclass'')
try
{
string s = (string)reference;
}
catch (InvalidCastException ice)
{
Console.WriteLine(ice.Message); // Unable to cast object of type ''System.Text.StringBuilder'' to type ''System.String''.
}
try
{
string s = (string)box;
}
catch (InvalidCastException ice)
{
Console.WriteLine(ice.Message); // Unable to cast object of type ''System.Int32'' to type ''System.String''.
}
// CASE TWO: bad unboxing conversions (CIL instrcution 0xA5 ''unbox.any'')
try
{
long l = (long)reference;
}
catch (InvalidCastException ice)
{
Console.WriteLine(ice.Message); // Specified cast is not valid.
}
try
{
long l = (long)box;
}
catch (InvalidCastException ice)
{
Console.WriteLine(ice.Message); // Specified cast is not valid.
}
try
{
long l = (long)unset;
}
catch (NullReferenceException nre)
{
Console.WriteLine(nre.Message); // Object reference not set to an instance of an object.
}
Entonces, en los casos en que intentamos una conversión de referencia (correspondiente a la instrucción CIL castclass
), la excepción lanzada contiene un excelente mensaje de la forma:
No se puede convertir el objeto de tipo ''X'' para escribir ''Y''.
La evidencia empírica muestra que este mensaje de texto suele ser extremadamente útil para los desarrolladores (con experiencia o sin experiencia) (solucionadores de errores) que necesitan lidiar con el problema.
En contraste, el mensaje que recibimos cuando falla un intento de unbox.any
( unbox.any
) es bastante no informativo. ¿Hay alguna razón técnica por la que esto debe ser así?
El elenco especificado no es válido. [NO ES ÚTIL]
En otras palabras, ¿por qué no recibimos un mensaje como (mis palabras):
No se puede desempaquetar un objeto de tipo ''X'' en un valor de tipo ''Y''; Los dos tipos deben estar de acuerdo.
respectivamente (mis palabras de nuevo):
No se puede desempaquetar una referencia nula en un valor del tipo ''Y'' no anulable.
Entonces, para repetir mi pregunta: ¿es "accidental" que el mensaje de error en un caso sea bueno e informativo, y en el otro caso sea malo? ¿O existe una razón técnica por la cual sería imposible, o prohibitivamente difícil, que el tiempo de ejecución proporcione detalles de los tipos reales encontrados en el segundo caso?
(He visto un par de hilos aquí en TAN que estoy seguro de que nunca me habrían preguntado si el texto de excepción para unboxings fallidos hubiera sido mejor).
Actualización: la respuesta de Daniel Frederico Lins Leite lo llevó a abrir un problema en el CLR Github (ver más abajo). Se descubrió que esto era un duplicado de un problema anterior (planteado por Jon Skeet, la gente casi lo adivina). Así que no había una buena razón para el pobre mensaje de excepción, y la gente ya lo había corregido en el CLR. Así que no fui el primero en preguntarme acerca de esto. Podemos esperar el día en que se presente esta mejora en .NET Framework.
TL; DR;
Creo que el tiempo de ejecución tiene toda la información necesaria para mejorar el mensaje. Tal vez algún desarrollador de JIT podría ayudar, porque no hace falta decir que el código JIT es muy sensible y, en ocasiones, las decisiones se toman debido a razones de rendimiento o seguridad, que es muy difícil de entender para un extraño.
Explicación detallada
Para simplificar el problema cambié el método a:
DO#
void StringBuilderCast()
{
object sbuilder = new StringBuilder();
string s = (string)sbuilder;
}
ILLINOIS
.method private hidebysig
instance void StringBuilderCast() cil managed
{
// Method begins at RVA 0x214c
// Code size 15 (0xf)
.maxstack 1
.locals init (
[0] object sbuilder,
[1] string s
)
IL_0000: nop
IL_0001: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: castclass [mscorlib]System.String
IL_000d: stloc.1
IL_000e: ret
} // end of method Program::StringBuilderCast
Los opcodes importantes aquí son:
http://msdn.microsoft.com/library/system.reflection.emit.opcodes.newobj.aspx http://msdn.microsoft.com/library/system.reflection.emit.opcodes.castclass.aspx
Y el diseño general de la memoria es:
Thread Stack Heap
+---------------+ +---+---+----------+
| some variable | +---->| L | T | DATA |
+---------------+ | +---+---+----------+
| sbuilder2 |----+
+---------------+
T = Instance Type
L = Instance Lock
Data = Instance Data
Entonces, en este caso, el tiempo de ejecución sabe que tiene un puntero a un StringBuilder y debería convertirlo en una cadena. En esta situación, tiene toda la información necesaria para darle la mejor excepción posible.
Si vemos en el JIT https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/interpreter.cpp#L6137 veremos algo así como
CORINFO_CLASS_HANDLE cls = GetTypeFromToken(m_ILCodePtr + 1, CORINFO_TOKENKIND_Casting InterpTracingArg(RTK_CastClass));
Object * pObj = OpStackGet<Object*>(idx);
ObjIsInstanceOf(pObj, TypeHandle(cls), TRUE)) //ObjIsInstanceOf will throw if cast can''t be done
Si nos metemos en este método.
y la parte importante sería:
BOOL fCast = FALSE;
TypeHandle fromTypeHnd = obj->GetTypeHandle();
if (fromTypeHnd.CanCastTo(toTypeHnd))
{
fCast = TRUE;
}
if (Nullable::IsNullableForType(toTypeHnd, obj->GetMethodTable()))
{
// allow an object of type T to be cast to Nullable<T> (they have the same representation)
fCast = TRUE;
}
// If type implements ICastable interface we give it a chance to tell us if it can be casted
// to a given type.
else if (toTypeHnd.IsInterface() && fromTypeHnd.GetMethodTable()->IsICastable())
{
...
}
if (!fCast && throwCastException)
{
COMPlusThrowInvalidCastException(&obj, toTypeHnd);
}
La parte importante aquí es el método que lanza la excepción. Como puede ver, recibe tanto el objeto actual como el tipo que intenta convertir.
Al final, el método Throw llama a este método:
COMPlusThrow(kInvalidCastException, IDS_EE_CANNOTCAST, strCastFromName.GetUnicode(), strCastToName.GetUnicode());
Que le da el bonito mensaje de excepción con los nombres de tipo.
Pero cuando estás lanzando un objeto a un tipo de valor
DO#
void StringBuilderToLong()
{
object sbuilder = new StringBuilder();
long s = (long)sbuilder;
}
ILLINOIS
.method private hidebysig
instance void StringBuilderToLong () cil managed
{
// Method begins at RVA 0x2168
// Code size 15 (0xf)
.maxstack 1
.locals init (
[0] object sbuilder,
[1] int64 s
)
IL_0000: nop
IL_0001: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: unbox.any [mscorlib]System.Int64
IL_000d: stloc.1
IL_000e: ret
}
El código de operación importante aquí es:
http://msdn.microsoft.com/library/system.reflection.emit.opcodes.unbox_any.aspx
y podemos ver el comportamiento de UnboxAny aquí https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/interpreter.cpp#L8766
//GET THE BOXED VALUE FROM THE STACK
Object* obj = OpStackGet<Object*>(tos);
//GET THE TARGET TYPE METADATA
unsigned boxTypeTok = getU4LittleEndian(m_ILCodePtr + 1);
boxTypeClsHnd = boxTypeResolvedTok.hClass;
boxTypeAttribs = m_interpCeeInfo.getClassAttribs(boxTypeClsHnd);
//IF THE TARGET TYPE IS A REFERENCE TYPE
//NOTHING CHANGE FROM ABOVE
if ((boxTypeAttribs & CORINFO_FLG_VALUECLASS) == 0)
{
!ObjIsInstanceOf(obj, TypeHandle(boxTypeClsHnd), TRUE)
}
//ELSE THE TARGET TYPE IS A REFERENCE TYPE
else
{
unboxHelper = m_interpCeeInfo.getUnBoxHelper(boxTypeClsHnd);
switch (unboxHelper)
{
case CORINFO_HELP_UNBOX:
MethodTable* pMT1 = (MethodTable*)boxTypeClsHnd;
MethodTable* pMT2 = obj->GetMethodTable();
if (pMT1->IsEquivalentTo(pMT2))
{
res = OpStackGet<Object*>(tos)->UnBox();
}
else
{
CorElementType type1 = pMT1->GetInternalCorElementType();
CorElementType type2 = pMT2->GetInternalCorElementType();
// we allow enums and their primtive type to be interchangable
if (type1 == type2)
{
res = OpStackGet<Object*>(tos)->UnBox();
}
}
//THE RUNTIME DOES NOT KNOW HOW TO UNBOX THIS ITEM
if (res == NULL)
{
COMPlusThrow(kInvalidCastException);
//I INSERTED THIS COMMENTS
//auto thCastFrom = obj->GetTypeHandle();
//auto thCastTo = TypeHandle(boxTypeClsHnd);
//RealCOMPlusThrowInvalidCastException(thCastFrom, thCastTo);
}
break;
case CORINFO_HELP_UNBOX_NULLABLE:
InterpreterType it = InterpreterType(&m_interpCeeInfo, boxTypeClsHnd);
size_t sz = it.Size(&m_interpCeeInfo);
if (sz > sizeof(INT64))
{
void* destPtr = LargeStructOperandStackPush(sz);
if (!Nullable::UnBox(destPtr, ObjectToOBJECTREF(obj), (MethodTable*)boxTypeClsHnd))
{
COMPlusThrow(kInvalidCastException);
//I INSERTED THIS COMMENTS
//auto thCastFrom = obj->GetTypeHandle();
//auto thCastTo = TypeHandle(boxTypeClsHnd);
//RealCOMPlusThrowInvalidCastException(thCastFrom, thCastTo);
}
}
else
{
INT64 dest = 0;
if (!Nullable::UnBox(&dest, ObjectToOBJECTREF(obj), (MethodTable*)boxTypeClsHnd))
{
COMPlusThrow(kInvalidCastException);
//I INSERTED THIS COMMENTS
//auto thCastFrom = obj->GetTypeHandle();
//auto thCastTo = TypeHandle(boxTypeClsHnd);
//RealCOMPlusThrowInvalidCastException(thCastFrom, thCastTo);
}
}
}
break;
}
}
Bueno ... al menos, parece posible dar un mejor mensaje de excepción. Si recuerdas cuando la excepción tenía un mensaje bonito, la llamada fue:
COMPlusThrow(kInvalidCastException, IDS_EE_CANNOTCAST, strCastFromName.GetUnicode(), strCastToName.GetUnicode());
y el mensaje menos informativo fue:
COMPlusThrow(kInvalidCastException);
Así que creo que es posible mejorar el mensaje haciendo
auto thCastFrom = obj->GetTypeHandle();
auto thCastTo = TypeHandle(boxTypeClsHnd);
RealCOMPlusThrowInvalidCastException(thCastFrom, thCastTo);
He creado el siguiente problema en el github coreclr para ver cuáles son las opiniones de los desarrolladores de Microsoft.