为什么是“动态”?用作泛型类型参数时,所有类型不是协变和逆变吗?
我想知道用作泛型类型参数时 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
进行的最终实验是我开始感到困惑的地方。这意味着返回动态
对象的方法不能替代返回string
对象的方法,同样,接受string
对象的方法不能采用<代码>动态。最后两个带有赋值和方法调用的语句表明情况显然并非如此,因此我感到困惑。
我想了一下,想知道这是否是为了防止程序员使用 ICovariance
作为类型转换之间的垫脚石,从而导致运行时错误,例如:
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
的语义在赋值和赋值之间不同与泛型的协变/逆变?”
I am wondering if dynamic
is semantically equivalent to object
when used as a generic type parameter. If so, I am curious why this limitation exists since the two are different when assigning values to variables or formal parameters.
I've written a small experiment in C# 4.0 to tease apart some of the details. I defined some simple interfaces and implementations:
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) { }
}
The interesting details of the experiment:
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);
}
}
The first two statements with c1
and c2
demonstrate that basic covariance and contravariance are working. I then use c3
and c4
to show that dynamic
can be used as a generic type parameter in the same fashion.
The statements with c5
and c6
reveal that a conversion from dynamic
to object
is always valid. This isn't really too surprising, since object
is an ancestor of all other types.
The final experiment with c7
and c8
is where I start to become confused. It implies that methods that return dynamic
objects are not substitutes for methods that return string
ones, and similarly that methods that accept string
objects cannot take dynamic
ones. The final two statements with the assignment and method call show this is clearly not the case, hence my confusion.
I thought about this a little, and wondered if this is to prevent programmers from using ICovariance<dynamic>
as a stepping stone between type conversions that would result in run-time errors, such as:
ICovariance<dynamic> c9 = new Covariance<Exception>();
ICovariance<string> c10 = c9;
// While this is definitely not allowed:
ICovariance<string> c11 = new Covariance<Exception>();
However, this is unconvincing in the case of dynamic
since we lose type-safety anyway:
dynamic v1 = new Exception();
string v2 = v1;
Put another way, the question is "Why does the semantics of dynamic
differ between assignment and covariance/contravariance with generics?"
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
你的猜想是完全正确的。
“动态”作为一种类型只不过是戴着一顶滑稽帽子的“对象”,这顶帽子说“不是对类型对象的这个表达式进行静态类型检查,而是生成在运行时进行类型检查的代码”。在所有其他方面,动态只是对象,故事的结局。
从编译器的角度考虑,然后从 IL 验证器的角度考虑。
当您为变量赋值时,编译器基本上会说“我需要生成从这样那样类型的值到变量的确切类型的隐式转换”。编译器生成执行此操作的代码,然后 IL 验证器验证其正确性。
也就是说,编译器生成:
但将转换限制为隐式转换,而不是显式转换。
当值是动态的时,编译器基本上会说“我需要生成在运行时询问该对象的代码,确定其类型,再次启动编译器,并吐出一小块 IL,将该对象的任何内容转换为类型这个变量,运行该代码,并将结果分配给这个变量,如果其中任何一个失败,则抛出。”
也就是说,编译器生成的道德等价物是:
验证者对此甚至不眨眼。验证者看到一个返回 Flob 的方法。如果该方法无法将“任何内容”转换为 Flob,则该方法可能会抛出异常;无论哪种方式,除了 Flob 之外什么都不会被写入 x 中。
现在考虑一下你的协方差情况。从 CLR 的角度来看,不存在“动态”这样的东西。只要有“动态”类型参数,编译器就会简单地生成“对象”作为类型参数。 “动态”是 C# 语言功能,而不是公共语言运行时功能。如果“对象”上的协变或逆变不合法,那么“动态”上的协变或逆变也不合法。编译器无法生成任何 IL 来使 CLR 的类型系统以不同的方式工作。
这就解释了为什么您观察到存在从
List
到List
这一切有意义吗?您似乎对动态背后的设计原则非常感兴趣;您可以省去麻烦并阅读Chris Burrows 关于该主题的博客文章。他负责该功能的大部分实现和相当多的设计。
Your conjecture is completely correct.
"dynamic" as a type is nothing more than "object" with a funny hat on, a hat that says "rather than doing static type checking for this expression of type object, generate code that does the type checking at runtime". In all other respects, dynamic is just object, end of story.
Think about it from the compiler's perspective and then from the IL verifier's perspective.
When you're assigning a value to a variable, the compiler basically says "I need to generate code that does an implicit conversion from a value of such and such a type to the exact type of the variable". The compiler generates code that does that, and the IL verifier verifies its correctness.
That is, the compiler generates:
But limits the conversions to implicit conversions, not explicit conversions.
When the value is dynamic, the compiler basically says "I need to generate code that interrogates this object at runtime, determines its type, starts up the compiler again, and spits out a small chunk of IL that converts whatever this object is to the type of this variable, runs that code, and assigns the result to this variable. And if any of that fails, throw."
That is, the compiler generates the moral equivalent of:
The verifier doesn't even blink at that. The verifier sees a method that returns a Frob. That method might throw an exception if it is unable to turn "whatever" into a Frob; either way, nothing but a Frob ever gets written into x.
Now think about your covariance situation. From the CLR's perspective, there is no such thing as "dynamic". Everywhere that you have a type argument that is "dynamic", the compiler simply generates "object" as a type argument. "dynamic" is a C# language feature, not a Common Language Runtime feature. If covariance or contravariance on "object" isn't legal, then it isn't legal on "dynamic" either. There's no IL that the compiler can generate to make the CLR's type system work differently.
This then explains why it is that you observe that there is a conversion from, say,
List<dynamic>
to and fromList<object>
; the compiler knows that they are the same type. The specification actually calls out that these two types have an identity conversion between them; they are identical types.Does that all make sense? You seem very interested in the design principles that underly dynamic; rather than trying to deduce them from first principles and experiments yourself, you could save yourself the bother and read Chris Burrows' blog articles on the subject. He did most of the implementation and a fair amount of the design of the feature.
对于这个:
原因很明显,如果可能的话你可以这样做:
并且它肯定会失败,除非
dynamic
不是string
或具有这些方法。因为(即使在所有更改之后)c# 不是动态语言。仅当绝对安全时才允许协方差。您当然可以立即行动并在
dynamic
变量上调用IndexOf
,但您不能让 API 的用户无意中执行此操作。例如,如果您返回这样一个带有dynamic
的ICovariance
秘密调用代码可能会失败!请记住规则,如果存在从
D
到B
的强制转换,则D
与B
是协变的。在这种情况下,没有从dynamic
到string
的转换。但是
dynamic
与object
是协变的,因为一切都是从它派生的。for this one:
reason is obvious, if it was possible then you could do:
and it will definitely fail, except if
dynamic
is notstring
or has those method.since (even after all the changes) c# is not dynamic language. Covariance is allowed only when it is definitely safe. You can of course shot into your feet and call
IndexOf
ondynamic
variable, but you can't let users of your API to do it unintentionally. For example if you return such aICovariance<string>
withdynamic
undercover calling code might fail!Remember the rule,
D
is covariant toB
if there is a cast fromD
toB
. In this case there is no cast fromdynamic
tostring
.But
dynamic
is covariant toobject
just because everything is derived from it.因为动态和协变/逆变关键字太新了?
我猜你已经回答了你自己的问题。赋值语句中的赋值类型安全性有所放松,因为这就是动态的工作原理;它缩短了编译时类型检查,因此您可以根据编译器不知道的对象进行您期望的分配。
然而,通用协变/逆变是严格控制的;如果不使用 in/out 关键字(在 C# 4.0 中也与动态一起引入),您将无法以任何方式进行转换。即使允许协变/逆变,泛型参数也要求类型位于继承层次结构的同一分支中。字符串不是动态的,动态也不是字符串(尽管两者都是对象,动态可能指的是可以作为字符串访问的内容),因此协方差/逆变检查中固有的通用类型检查失败,而 OTOH ,编译器被明确告知忽略大多数涉及动态的非泛型操作。
Because dynamic and covariant/contravariant keywords are so new?
I would guess that you kind of answered your own question. Assignment type-safety is relaxed in assignment statements, because that's how dynamic works; it short-circuits compile-time type-checking so you can make assignments you EXPECT to work from objects the compiler has no clue about.
However, generic covariance/contravariance is rigidly controlled; without the use of the in/out keywords (which were also introduced alongside dynamic in C# 4.0) you couldn't convert either way. The generic parameters, even with co/contravariance allowed, require the types to be in the same branch of the inheritance hierarchy. A String is not a dynamic and a dynamic is not a string (though both are Objects and a dynamic may refer to what could be accessed as a string), so the generic type-checking inherent in the covariance/contravariance checks fails, while OTOH, the compiler is expressly told to ignore most non-generic operations involving dynamic.
“为什么动态的语义在泛型的赋值和协变/逆变之间有所不同?”
答案是,当使用泛型时,您是从数据类型本身中抽象出来的。然而,它也意味着泛型足够通用,所有类型都将共享相同的功能。
因此,如果您有“ICovariance c9 = new Covariance();”,则动态和异常都不具有相同的功能(作为基本类型)。更重要的是,编译器不知道如何从动态转换为异常(即使它们都继承自对象)。
如果
dynamic
和Exception
(对象除外)之间存在显式继承层次结构,那么这还可以。某种程度上的原因是因为你可以低头,但不能向上。 EG,如果异常继承自动态,那就没问题了。如果dynamic继承自Exception,那么这将是一个向上转型的交易,这是不行的,因为可能存在“dynamic
的数据不存在于
Exception”中的情况。.NET 内置了这些显式类型转换,您可以在 System.Convert 对象中看到它们的运行情况。然而,如果没有自定义代码,超级特定的类型无法轻松地在彼此之间隐式或显式转换。这就是多种类型失败的原因之一(就像 'ICovariance c9 = new Covariance();` 的情况一样)。这也是为了保持类型安全而构建的。
"Why does the semantics of dynamic differ between assignment and covariance/contravariance with generics?"
The answer is that when using generics you are abstracted from the data type itself. However, it also implies that generic is generic enough that all types will share the same functionality.
So if you have 'ICovariance c9 = new Covariance();` both dynamic and exception do not have the same functionalities (as base types). More over, the compiler doesn't have a clue as to how to convert from dynamic to exception (even though they both inherit from object).
If there was an explicit inheritance hierarchy between
dynamic
andException
(other than object), than this would be somewhat ok.The reason somewhat is because you can downcast, but not upcast. EG, if exception inherits from dynamic, than it would be fine. If dynamic inherits from Exception it would be an upcast kinda deal and that would not be ok, since there could be the condition where the 'dynamic
's data is not present in
Exception`..NET has these explicit typecasts built in, and you can see them in action in the
System.Convert
object. However, types that are super specific cannot be easily implicitly or explicitly casted between one another without custom code. And this is one of the reasons why having multi-types fails (as is the case with 'ICovariance c9 = new Covariance();` ). This is also built to preserve type safety.