为什么 'unbox.any' 不像 'castclass' 那样提供有用的异常文本?
Why does 'unbox.any' not provide a helpful exception text the way 'castclass' does?
为了说明我的问题,请考虑以下简单示例 (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
),抛出的异常包含以下形式的极好的消息:
Unable to cast object of type 'X' to type 'Y'.
经验证据表明,这条短信通常对需要处理问题的(有经验的或没有经验的)开发人员(错误修复者)非常有帮助。
相比之下,我们在尝试拆箱 (unbox.any
) 失败时收到的消息相当无用。有什么技术原因必须如此吗?
Specified cast is not valid. [NOT HELPFUL]
换句话说,为什么我们没有收到像(我的话)这样的消息:
Unable to unbox an object of type 'X' into a value of type 'Y';
the two types must agree.
分别(又是我的话):
Unable to unbox a null reference into a value of the non-nullable type 'Y'.
所以重复我的问题:是否 "accidental" 一种情况下的错误消息很好且信息丰富,而另一种情况下的错误消息很差?或者是否有技术原因导致运行时无法或极其困难地提供第二种情况下遇到的实际类型的详细信息?
(我在 SO 上看到了几个主题,我敢肯定,如果拆箱失败的异常文本更好,我肯定不会被问到。)
更新:Daniel Frederico Lins Leite 的回答导致他在 CLR Github 上提出了一个问题(见下文)。这被发现是早期问题的重复(由 Jon Skeet 提出,人们几乎猜到了!)。因此,糟糕的异常消息没有充分的理由,人们已经在 CLR 中修复了它。所以我不是第一个对此感到疑惑的人。我们可以期待这一改进在 .NET Framework 中发布的那一天。
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
如果我们深入研究这个方法
重要的部分是:
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方法调用了这个方法:
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 开发人员的意见。
为了说明我的问题,请考虑以下简单示例 (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
),抛出的异常包含以下形式的极好的消息:
Unable to cast object of type 'X' to type 'Y'.
经验证据表明,这条短信通常对需要处理问题的(有经验的或没有经验的)开发人员(错误修复者)非常有帮助。
相比之下,我们在尝试拆箱 (unbox.any
) 失败时收到的消息相当无用。有什么技术原因必须如此吗?
Specified cast is not valid. [NOT HELPFUL]
换句话说,为什么我们没有收到像(我的话)这样的消息:
Unable to unbox an object of type 'X' into a value of type 'Y'; the two types must agree.
分别(又是我的话):
Unable to unbox a null reference into a value of the non-nullable type 'Y'.
所以重复我的问题:是否 "accidental" 一种情况下的错误消息很好且信息丰富,而另一种情况下的错误消息很差?或者是否有技术原因导致运行时无法或极其困难地提供第二种情况下遇到的实际类型的详细信息?
(我在 SO 上看到了几个主题,我敢肯定,如果拆箱失败的异常文本更好,我肯定不会被问到。)
更新:Daniel Frederico Lins Leite 的回答导致他在 CLR Github 上提出了一个问题(见下文)。这被发现是早期问题的重复(由 Jon Skeet 提出,人们几乎猜到了!)。因此,糟糕的异常消息没有充分的理由,人们已经在 CLR 中修复了它。所以我不是第一个对此感到疑惑的人。我们可以期待这一改进在 .NET Framework 中发布的那一天。
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
如果我们深入研究这个方法
重要的部分是:
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方法调用了这个方法:
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 开发人员的意见。