我想知道dynamic
在用作泛型类型参数时是否在语义上等同于object
。如果是这样,我很好奇为什么存在这种限制,因为在为变量或形式参数赋值时两者是不同的。
我在C#4.0中编写了一个小实验来梳理一些细节。我定义了一些简单的接口和实现:
interface ICovariance<out T> { T Method(); }
interface IContravariance<in T> { void Method(T argument); }
class Covariance<T> : ICovariance<T>
{
public T Method() { return default(T); }
}
class Contravariance<T> : IContravariance<T>
{
public void Method(T argument) { }
}
实验的有趣细节:
class Variance
{
static void Example()
{
ICovariance<object> c1 = new Covariance<string>();
IContravariance<string> c2 = new Contravariance<object>();
ICovariance<dynamic> c3 = new Covariance<string>();
IContravariance<string> c4 = new Contravariance<dynamic>();
ICovariance<object> c5 = new Covariance<dynamic>();
IContravariance<dynamic> c6 = new Contravariance<object>();
// The following statements do not compile.
//ICovariance<string> c7 = new Covariance<dynamic>();
//IContravariance<dynamic> c8 = new Contravariance<string>();
// However, these do.
string s = new Covariance<dynamic>().Method();
new Contravariance<string>().Method((dynamic)s);
}
}
c1
和c2
的前两个陈述表明基本的协方差和逆变是有效的。然后,我使用c3
和c4
来表明dynamic
可以以相同的方式用作泛型类型参数。
c5
和c6
的陈述表明从dynamic
到object
的转换始终有效。这并不太令人惊讶,因为object
是所有其他类型的祖先。
c7
和c8
的最终实验是我开始感到困惑的地方。这意味着返回dynamic
个对象的方法不能替代返回string
个方法的方法,同样接受string
个对象的方法也不能取dynamic
个方法。分配和方法调用的最后两个语句显示情况显然不是这样,因此我感到困惑。
我想到了这一点,并想知道这是否是为了防止程序员使用ICovariance<dynamic>
作为类型转换之间的跳板,这会导致运行时错误,例如:
ICovariance<dynamic> c9 = new Covariance<Exception>();
ICovariance<string> c10 = c9;
// While this is definitely not allowed:
ICovariance<string> c11 = new Covariance<Exception>();
然而,在dynamic
的情况下,这是不可信的,因为我们无论如何都会失去类型安全性:
dynamic v1 = new Exception();
string v2 = v1;
换句话说,问题是“为什么dynamic
的语义在赋值和协方差/与泛型相反的方面有所不同?”
答案 0 :(得分:23)
我想知道当用作泛型类型参数时,dynamic在语义上是否等同于object。
您的推测完全正确。
“动态”作为一种类型只不过是带有滑稽帽子的“对象”,帽子上写着“而不是对类型对象的这个表达式进行静态类型检查,生成在运行时进行类型检查的代码” 。在所有其他方面,动态只是对象,故事结束。
我很好奇为什么存在这种限制,因为在为变量或形式参数赋值时两者是不同的。
从编译器的角度考虑,然后从IL验证者的角度考虑。
当您为变量赋值时,编译器基本上会说“我需要生成从这样的类型和类型的值到变量的确切类型进行隐式转换的代码”。编译器生成执行该操作的代码,IL验证程序验证其正确性。
即,编译器生成:
Frob x = (Frob)whatever;
但是将转化限制为隐式转化,而不是明确的转化。
当值是动态的时,编译器基本上会说“我需要生成在运行时询问此对象的代码,确定其类型,再次启动编译器,并吐出一小部分IL,它会转换此对象的任何内容对于这个变量的类型,运行该代码,并将结果分配给这个变量。如果其中任何一个失败,抛出。“
也就是说,编译器生成道德等价物:
Frob x = MakeMeAConversionFunctionAtRuntime<Frob>((object)whatever);
验证者甚至没有眨眼。验证者看到一个返回Frob的方法。如果它无法将“无论”变成Frob,那么该方法可能会抛出异常;无论哪种方式,只有一个Frob被写入x。
现在想想你的协方差情况。从CLR的角度来看,没有“动态”这样的东西。你有一个“动态”类型参数的地方,编译器只是生成“对象”作为类型参数。 “dynamic”是C#语言功能,而不是公共语言运行时功能。如果“对象”的协方差或逆变是不合法的,那么它对“动态”也是不合法的。编译器无法生成IL以使CLR的类型系统以不同的方式工作。
然后解释了为什么您发现存在从List<dynamic>
到List<object>
之间的转换;编译器知道它们是相同的类型。该规范实际上调用了这两种类型之间的身份转换;它们是相同的类型。
这一切都有意义吗?你似乎对充满活力的设计原则非常感兴趣;而不是试图从自己的第一原则和实验中推断出它们,你可以省去麻烦并阅读Chris Burrows' blog articles on the subject。他完成了大部分实现和相当多的功能设计。
答案 1 :(得分:2)
这个:
ICovariance<string> c7 = new Covariance<dynamic>();
原因很明显,如果有可能那么你可以这样做:
c7.Method().IndexOf(...);
并且肯定会失败,除非dynamic
不是string
或具有这些方法。
因为(即使在所有更改之后)c#也不是动态语言。只有在绝对安全的情况下才允许协方差。您当然可以站起来并在IndexOf
变量上调用dynamic
,但您无法让API的用户无意中执行此操作。例如,如果您返回ICovariance<string>
dynamic
D
秘密调用代码可能会失败!
如果有B
到D
的演员,请记住规则B
与dynamic
的协变。在这种情况下,没有string
到dynamic
的演员阵容。
但是object
与{{1}}的协变只是因为一切都是从它派生的。
答案 2 :(得分:1)
因为动态和协变/逆变关键字是如此新颖吗?
我猜你有点回答了自己的问题。在赋值语句中放宽赋值类型 - 安全性,因为这是动态的工作方式;它使编译时类型检查短路,这样你就可以通过编译器不知道的对象来进行EXPECT赋值工作。
然而,通用的协方差/逆变是严格控制的;如果不使用输入/输出关键字(也在C#4.0中与动态一起引入),您无法转换任何一种方式。即使允许co / contravariance,通用参数也要求类型位于继承层次结构的同一分支中。 String不是动态的,动态不是字符串(虽然两者都是对象,动态可以指代可以作为字符串访问的内容),因此协方差/逆变检查中固有的泛型类型检查失败,而OTOH ,明确告诉编译器忽略大多数涉及动态的非泛型操作。
答案 3 :(得分:0)
“为什么动态的语义在赋值和协方差/逆变与泛型之间是不同的?”
答案是,在使用泛型时,您将从数据类型本身中抽象出来。但是,它也意味着泛型足够通用,所有类型都将共享相同的功能。
因此,如果你有'ICovariance c9 = new Covariance();`动态和异常都没有相同的功能(如基类型)。更重要的是,编译器没有关于如何从动态转换为异常的线索(即使它们都从对象继承)。
如果dynamic
和Exception
(除了对象)之间存在显式的继承层次结构,那么这可能会有点好。
原因在某种程度上是因为你可以贬低,但不能贬低。 EG,如果异常继承自动态,那就好了。如果动态继承自Exception,它将是一个向上倾向的有点交易,这是不可能的,因为可能存在“动态's data is not present in
异常”的条件。
.NET内置了这些显式类型转换,您可以在System.Convert
对象中看到它们的运行情况。但是,如果没有自定义代码,则不能轻易地在彼此之间隐式或显式地转换超级特定的类型。这就是为什么多类型失败的原因之一(就像'ICovariance c9 = new Covariance();`)的情况一样。这也是为了保护类型安全。