我刚刚发布了这个问题的答案,但我并不完全相信我的答案。我想知道两件事,请考虑此代码:
class Foo<T>
{
void SomeMethod()
{
string str = "foo";
Foo<T> f = str as Foo<T>;
}
}
根据C# Specification 5.0
,as operator
有两种不同的转换。
因为,如果
E
的编译时类型不dynamic
,则操作E as T
产生与E is T ? (T)(E) : (T)null
如果
E
的编译时类型是dynamic
,则与强制转换运算符不同,as operator
不是动态绑定的(§7.2.2)。因此,在这种情况下,扩展是:E is T ? (T)(object)(E) : (T)null
这是无效的,因为(Foo<T>)str
str is Foo<T> ? (Foo<T>)str : (Foo<T>)null;
我认为它应该翻译为:
str is Foo<T> ? (Foo<T>)(object)str : (Foo<T>)null;
但规范说这只发生在E
类型dynamic
时。
所以我的问题是:
- 编译器是否将此表达式转换为通常无效的代码?
- 当
E
类型是动态的时,为什么首先将E
转换为object
,然后在(T)E
完全有效时T
?
编译器是否将此表达式转换为通常 无效?
在盯着规范大约一个小时后,我开始说服自己,这只是规范中被忽视的边缘情况。请注意,这只是 C# 语言编辑器使用 is
运算符的语义表达as
运算符的一种方式。
编译器实际上并没有将as
运算符转换为具有is
的三元运算符。它将发出一个 IL 调用来isinst
,无论是as
还是is
:
IL_0000: nop
IL_0001: ldstr "foo"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: isinst class ConsoleApplication2.Foo`1<!T>
IL_000d: stloc.1
IL_000e: ret
查看已编译的 DLL,as
运算符保持不变。
当 E 的类型是动态的时,为什么它首先将 E 强制转换为对象,然后将 T 转换为对象 而 (T)E 是完全有效的?
这在规范的细则中进行了描述:
如果 E 的编译时类型是动态的,则与强制转换运算符不同 As 运算符未动态绑定 (§7.2.2)。因此, 在这种情况下,扩展为:
E is T ? (T)(object)(E) : (T)null
需要对object
进行投射,以便对dynamic
对象使用as
。 as
是编译时操作dynamic
而对象仅在运行时绑定。
编译器实际上将dynamic
类型对象视为类型 object
开始:
class Foo<T>
{
public void SomeMethod()
{
dynamic str = "foo";
Foo<T> f = str as Foo<T>;
}
}
str
实际上一开始就被视为object
:
.class private auto ansi beforefieldinit Foo`1<T>
extends [mscorlib]System.Object
{
// Methods
.method public hidebysig
instance void SomeMethod () cil managed
{
// Method begins at RVA 0x2050
// Code size 15 (0xf)
.maxstack 1
.locals init (
[0] object,
[1] class Foo`1<!T>
)
IL_0000: nop
IL_0001: ldstr "foo"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: isinst class Foo`1<!T>
IL_000d: stloc.1
IL_000e: ret
} // end of method Foo`1::SomeMethod
}
编辑:
在与托管语言团队的 Vladimir Reshetnikov 交谈后,他解释了从"as operator"到"cast operator"的表示语义实际上试图表达的内容:
我同意,规范中也有一些不精确的语言。它说,如果涉及开放类型,则"as"运算符始终适用,但随后根据强制转换来描述其评估,这在某些情况下可能无效。应该说,扩展中的强制转换不表示正常的 C# 强制转换运算符,而只是表示"as"运算符中允许的转换。我会记下来修复它。谢谢!