【问题标题】:Why is "dynamic" not covariant and contravariant with respect to all types when used as a generic type parameter?当用作泛型类型参数时,为什么“动态”对于所有类型都不是协变和逆变的?
【发布时间】:2011-06-21 00:46:38
【问题描述】:

我想知道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);       
    }
}

带有c1c2 的前两个语句表明基本的协变和逆变是有效的。然后我使用c3c4 来表明dynamic 可以以相同的方式用作泛型类型参数。

带有c5c6 的语句表明从dynamicobject 的转换始终有效。这并不奇怪,因为object 是所有其他类型的祖先。

c7c8 的最终实验是我开始感到困惑的地方。这意味着返回dynamic 对象的方法不能替代返回string 对象的方法,同样接受string 对象的方法也不能采用dynamic 对象。带有赋值和方法调用的最后两条语句表明情况显然并非如此,因此我感到困惑。

我想了想,想知道这是否是为了防止程序员在类型转换之间使用ICovariance&lt;dynamic&gt;作为垫脚石,这会导致运行时错误,例如:

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 的语义在赋值和协变/逆变与泛型之间存在差异?”

【问题讨论】:

  • 希望 Eric 来访并给我们启示 :)
  • 我正在尽我所能地投射Summon Eric Lippert
  • 各位,如果有什么想引起我注意的,请使用我博客上的“联系”链接。
  • 我不敢相信它成功了!
  • 你们太古怪了。

标签: c# .net generics dynamic c#-4.0


【解决方案1】:

我想知道当用作泛型类型参数时,动态是否在语义上等同于对象。

你的猜想完全正确。

“动态”作为一种类型只不过是“对象”,戴着一顶有趣的帽子,帽子上写着“与其对这个类型对象的表达式进行静态类型检查,不如生成在运行时进行类型检查的代码” .在所有其他方面,动态只是对象,故事的结尾。

我很好奇为什么存在这种限制,因为在为变量或形式参数赋值时两者是不同的。

从编译器的角度考虑,然后从 IL 验证器的角度考虑。

当您为变量赋值时,编译器基本上会说“我需要生成代码,将某个类型的值隐式转换为变量的确切类型”。编译器生成执行此操作的代码,IL 验证器验证其正确性。

即编译器生成:

Frob x = (Frob)whatever;

但将转换限制为隐式转换,而不是显式转换。

当值是动态的时,编译器基本上会说“我需要生成在运行时询问这个对象的代码,确定它的类型,再次启动编译器,然后吐出一小块 IL 来转换这个对象的任何内容“

也就是说,编译器生成以下道德等价物:

Frob x = MakeMeAConversionFunctionAtRuntime<Frob>((object)whatever);

验证者甚至没有眨眼。验证者看到一个返回 Frob 的方法。如果该方法无法将“whatever”转换为 Frob,则该方法可能会抛出异常;无论哪种方式,只有一个 Frob 被写入 x。

现在想想你的协方差情况。从 CLR 的角度来看,没有所谓的“动态”。只要你有一个“动态”的类型参数,编译器就会简单地生成“对象”作为类型参数。 “动态”是 C# 语言功能,而不是公共语言运行时功能。如果“对象”上的协变或逆变不合法,那么“动态”上的协变或逆变也不合法。编译器无法生成任何 IL 来使 CLR 的类型系统以不同的方式工作。

这就解释了为什么你观察到从List&lt;dynamic&gt;List&lt;object&gt; 的转换;编译器知道它们是相同的类型。规范实际上指出这两种类型之间有一个identity 转换;它们是相同的类型。

这一切都有意义吗?你似乎对动态的设计原则很感兴趣;与其尝试自己从第一原理和实验中推断出它们,不如省去麻烦并阅读Chris Burrows' blog articles on the subject。他完成了大部分功能的实现和相当数量的功能设计。

【讨论】:

  • 感谢您的深入解释。听起来这不是 PL/类型理论问题,而是更多地受到工程决策和 CLR 的影响。
  • @ide:“动态”根本没有类型理论。这一切都是为了让 C# 程序员更容易与来自其他语言的弱类型对象模型进行互操作。目的不是让“动态”成为类型系统中的一等类型;目的是减少编写与 Python/Ruby/COM/Excel/任何对象对话的 C# 代码对灵魂的破坏。
  • @EricLippert 嗨,我知道这已经很长时间了,但也许你会在这里帮助我。我有一个关于动态的问题:stackoverflow.com/questions/57270215/…。我已经阅读了您的答案,并看到您建议您可以使用 object 而不是 dynamic,它应该几乎相同,但在我的情况下并非如此。当对象一编译正常时,动态语法不会编译。你知道这是为什么吗?
  • @EricLippert 您是否知道该博客文章现在是否托管在其他地方?
【解决方案2】:

对于这个:

ICovariance<string> c7 = new Covariance<dynamic>();

原因很明显,如果可以的话,你可以这样做:

c7.Method().IndexOf(...);

它肯定会失败,除非dynamic 不是string 或有那些方法。

因为(即使在所有更改之后)c# 不是动态语言。只有在绝对安全时才允许协方差。您当然可以跳起来并在dynamic 变量上调用IndexOf,但您不能让您的API 用户无意中这样做。例如,如果您返回这样的 ICovariance&lt;string&gt;dynamic 卧底调用代码可能会失败!

记住规则,如果有从 DB 的转换,DB 是协变的。在这种情况下,没有从 dynamicstring 的转换。

但是dynamicobject 是协变的,因为一切都源自它。

【讨论】:

  • 但是无论如何你都失去了dynamic 的类型安全性。 dynamic d = 0; d.IndexOf(...);。我的问题是为什么这种行为不会扩展到泛型。
  • @ide 当你写评论时我只是在输入解释:) 基本上这是因为你的代码的调用者不知道你的界面中有什么。他们期待字符串,不应该关心。
  • @ide 有趣的是,类型安全并没有丢失!它只是为不同类型的动态调度提供语法糖。例如反射。这就是为什么我发现 dynamic 除了 COM 互操作之外几乎没有意义。
  • @ide:安德烈是正确的,当前的动态实现只能在动态类型的代码中失败。您的提议将使所有泛型动态类型化,而不管它们看起来具有什么静态类型。
  • @Ben Voigt 我真的希望(并相信)它不是当前的,而是内部实施。 C# 是静态类型的,这就是它的特点。
【解决方案3】:

因为动态和协变/逆变关键字太新了?

我猜你有点回答了你自己的问题。赋值语句中的赋值类型安全被放宽了,因为动态就是这样工作的;它缩短了编译时类型检查,因此您可以对编译器不知道的对象进行您期望的赋值。

但是,通用协方差/逆变是严格控制的;如果不使用 in/out 关键字(在 C# 4.0 中与 dynamic 一起引入),您将无法转换任何一种方式。通用参数,即使允许协变/逆变,也要求类型位于继承层次结构的同一分支中。字符串不是动态的,动态的也不是字符串(尽管两者都是对象,而动态可能指的是可以作为字符串访问的内容),因此协变/逆变检查中固有的通用类型检查失败,而 OTOH ,编译器被明确告知忽略大多数涉及动态的非泛型操作。

【讨论】:

    【解决方案4】:

    “为什么动态的语义在赋值和协变/逆变与泛型之间存在差异?”

    答案是,当使用泛型时,您是从数据类型本身中抽象出来的。但是,这也意味着泛型足够通用,所有类型都将共享相同的功能。

    因此,如果您有“ICovariance c9 = new Covariance();”,则动态和异常都没有相同的功能(作为基本类型)。此外,编译器不知道如何从动态转换为异常(即使它们都继承自对象)。

    如果在dynamicException(对象除外)之间存在显式继承层次结构,那么这将是可行的。

    有些原因是因为你可以沮丧,但不能向上。 EG,如果异常继承自动态,那就没问题了。如果 dynamic 从 Exception 继承,那将是一个向上的交易,这是不行的,因为可能存在 'dynamic's data is not present inException` 的情况。

    .NET 内置了这些显式类型转换,您可以在 System.Convert 对象中看到它们的作用。但是,如果没有自定义代码,就无法轻松地在彼​​此之间隐式或显式转换超级特定的类型。这就是多类型失败的原因之一(就像 'ICovariance c9 = new Covariance();` 的情况)。这也是为了保护类型安全而构建的。

    【讨论】:

      猜你喜欢
      • 2013-11-29
      • 2011-02-02
      • 1970-01-01
      • 2018-12-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多