【问题标题】:Why does 'unbox.any' not provide a helpful exception text the way 'castclass' does?为什么“unbox.any”不像“castclass”那样提供有用的异常文本?
【发布时间】:2016-10-07 10:15:46
【问题描述】:

为了说明我的问题,请考虑以下简单示例 (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.
}

所以在我们尝试引用转换的情况下(对应于 CIL 指令castclass),抛出的异常包含以下形式的极好消息:

无法将“X”类型的对象转换为“Y”类型。

经验证据表明,此短信通常对需要处理问题的(有经验或无经验的)开发人员(错误修复人员)非常有帮助。

相比之下,我们在尝试拆箱 (unbox.any) 失败时收到的消息是相当不具信息性的。是否有任何技术原因必须如此?

指定的演员表无效。 [没有帮助]

换句话说,为什么我们没有收到像(我的话)这样的消息:

无法将“X”类型的对象拆箱为“Y”类型的值; 这两种类型必须一致。

分别(我再说一遍):

无法将空引用拆箱为不可空类型“Y”的值。

所以重复我的问题:错误消息在一种情况下是好的和信息丰富的,而在另一种情况下是很差的,这是否是“偶然的”?或者是否有技术原因导致运行时无法提供在第二种情况下遇到的实际类型的详细信息或极其困难?

(我在 SO 上看到了几个线程,我确信永远不会被问到失败拆箱的异常文本是否更好。)


更新:Daniel Frederico Lins Leite 的回答导致他在 CLR Github 上打开了一个问题(见下文)。这被发现是早期问题的副本(由 Jon Skeet 提出,人们几乎猜到了!)。因此,糟糕的异常消息没有充分的理由,人们已经在 CLR 中修复了它。所以我不是第一个对此感到疑惑的人。我们可以期待这一改进在 .NET Framework 中发布的那一天。

【问题讨论】:

  • 乔恩already asked this question。大致。同样的原因,这必须在 .NET 1.x 时代生成紧凑而快速的代码。如果您想要一个好的异常消息,那么您必须编写代码Convert.ToInt64(reference)。仍然非常紧凑,没有那么快。
  • @HansPassant 所以你链接的问题是关于为什么模式var nullable = box as int?; if (nullable.HasValue) { /* use nullable.Value */ }if (box is int) { var value = (int)box; /* use value */ } 慢得多。所以我知道后一个示例中使用的 CIL 指令 unbox.any 会很快,因为不涉及复制。在 .NET 1 天里,当简单值一直装在非泛型集合中时,它必须很快。 但它如何回答我的问题?在类型检查失败的“分支”中,我们不能在异常中加入更多细节吗?
  • 此外,castclass CIL 指令预计会非常快,并且在集合没有强类型化的 ArrayListHashtable 天必须如此之快。 castclass 没有复制,只是类型检查和引用到同一个地方。那么区别在哪里呢?在类型检查失败的情况下,castclass 会导致带有 from 类型和类型的“丰富”异常在异常消息中说明。

标签: c# .net clr cil unboxing


【解决方案1】:

TL;DR;

我认为运行时具有改进消息所需的所有信息。也许一些 JIT 开发人员可以提供帮助,因为不用说 JIT 代码非常敏感,有时出于性能或安全原因做出决策,外人很难理解。

详细说明

为了简化问题,我将方法更改为:

C#

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

Wich 为您提供带有类型名称的漂亮异常消息。

但是当你将一个对象转换为一个值类型时

C#

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

【讨论】:

  • 感谢您的分析。如果你在 Github 上创建了一个问题,那就太好了,然后你可以链接到这个 Stack Overflow 线程,我可以从这里链接到 Github。
  • 我已经创建了问题并在此处插入了链接。感谢您的建议。
  • 对你的 Github 问题有一个有趣的评论。我已将更新附加到我的问题文本中。
猜你喜欢
  • 2011-10-21
  • 1970-01-01
  • 2013-10-16
  • 1970-01-01
  • 1970-01-01
  • 2011-10-11
  • 1970-01-01
  • 2015-07-28
  • 2019-06-12
相关资源
最近更新 更多