Подтвердить что ты не робот

Почему "unbox.any" не предоставляет полезный текст исключения, как это делает "castclass"?

Чтобы проиллюстрировать мой вопрос, рассмотрим эти тривиальные примеры (С#):

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.
}

Итак, в тех случаях, когда мы пытаемся выполнить ссылочное преобразование (соответствующее инструкции CIL castclass), созданное исключение содержит отличное сообщение формы:

Невозможно применить объект типа "X" к типу "Y".

Эмпирические данные показывают, что это текстовое сообщение часто чрезвычайно полезно для (опытного или неопытного) разработчика (исправителя ошибок), которому необходимо решить проблему.

Напротив, сообщение, которое мы получаем, когда попытка попытки распаковки (unbox.any) терпит неудачу, довольно неинформативна. Есть ли какая-либо техническая причина, почему это должно быть так?

Указанный приказ недействителен. [НЕ ПОМОЩЬ]

Другими словами, почему мы не получаем сообщение типа (мои слова):

Невозможно удалить объект типа "X" в значение типа "Y"; два типа должны согласиться.

соответственно (мои слова снова):

Невозможно удалить ненулевую ссылку в значение типа NULL с нулевым значением.

Итак, чтобы повторить мой вопрос: "Случайно", что сообщение об ошибке в одном случае является хорошим и информативным, а в другом случае бедным? Или есть техническая причина, по которой было бы невозможно или было бы трудно или трудно, чтобы среда выполнения предоставляла детали фактических типов, встречающихся во втором случае?

(Я видел пару потоков здесь, на SO, которые, я уверен, никогда бы не спросили, был ли текст исключения для неудачных распаковщиков лучше.)


Обновление: ответ Дэниела Фредерико Линса Лейта привел к тому, что он открыл проблему в CLR Github (см. ниже). Это было обнаружено как дубликат более ранней версии (поднятой Джоном Скитом, люди почти догадывались об этом!). Поэтому не было веской причины для сообщения о бедных исключениях, и люди уже исправили его в CLR. Поэтому я не был первым, кто задумался об этом. Мы с нетерпением ждем того дня, когда это улучшение будет отправлено в .NET Framework.

4b9b3361

Ответ 1

TL; DR;

Я думаю, что во время выполнения есть вся информация, необходимая для улучшения сообщения. Может быть, какой-то разработчик JIT может помочь, потому что нет необходимости говорить, что код JIT очень чувствителен, а некоторые решения принимаются из-за производительности или соображений безопасности, которые очень трудно понять посторонним.

Подробное объяснение

Чтобы упростить задачу, я сменил метод на:

С#

void StringBuilderCast()
{
    object sbuilder = new StringBuilder();
    string s = (string)sbuilder;
}

IL

.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

Важными кодами операций являются:

http://msdn.microsoft.com/library/system.reflection.emit.opcodes.newobj.aspx http://msdn.microsoft.com/library/system.reflection.emit.opcodes.castclass.aspx

И общий макет памяти:

Thread Stack                        Heap
+---------------+          +---+---+----------+
| some variable |    +---->| L | T |   DATA   |
+---------------+    |     +---+---+----------+
|   sbuilder2   |----+
+---------------+

T = Instance Type  
L = Instance Lock  
Data = Instance Data

Итак, в этом случае среда выполнения знает, что у нее есть указатель на StringBuilder и он должен передать его в строку. В этой ситуации он имеет всю информацию чтобы дать вам лучшее исключение, насколько это возможно.

Если мы увидим в JIT https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/interpreter.cpp#L6137 мы сделаем что-то вроде этого

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

если мы копаем этот метод

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/eedbginterfaceimpl.cpp#L1633

и важная часть:

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);
    } 

Важная часть здесь - метод, который выдает исключение. Как вы видете он получает как текущий объект, так и тип, который вы пытаетесь выполнить.

В конце метод Throw вызывает этот метод:

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/excep.cpp#L13997

COMPlusThrow(kInvalidCastException, IDS_EE_CANNOTCAST, strCastFromName.GetUnicode(), strCastToName.GetUnicode());

который дает вам хорошее сообщение об исключении с именами типов.

Но когда вы бросаете объект в тип значения

С#

void StringBuilderToLong()
{
    object sbuilder = new StringBuilder();
    long s = (long)sbuilder;
}

IL

.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
}

важный код операции:
http://msdn.microsoft.com/library/system.reflection.emit.opcodes.unbox_any.aspx

и мы можем видеть поведение UnboxAny здесь 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;
        }
}

Хорошо... по крайней мере, кажется возможным дать лучшее сообщение об исключении. Если вы помните, когда в сообщении было хорошее сообщение, вызов был:

COMPlusThrow(kInvalidCastException, IDS_EE_CANNOTCAST, strCastFromName.GetUnicode(), strCastToName.GetUnicode());

и менее информативное сообщение было:

COMPlusThrow(kInvalidCastException);

Поэтому я думаю, что можно улучшить сообщение, сделав

auto thCastFrom = obj->GetTypeHandle();
auto thCastTo = TypeHandle(boxTypeClsHnd);
RealCOMPlusThrowInvalidCastException(thCastFrom, thCastTo);

Я создал следующую проблему в coreclr github, чтобы узнать, что мнение разработчиков Microsoft.

https://github.com/dotnet/coreclr/issues/7655