65.9K
CodeProject 正在变化。 阅读更多。
Home

C#, Java 和 Scala 中的协变/逆变规则对比

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (3投票s)

2015年4月24日

CPOL

16分钟阅读

viewsIcon

22256

downloadIcon

87

不同的编程语言支持方差(协变/逆变)的方式不同。本文的目的是将 C#、Java 和 Scala 中所有支持的方差类型集中起来进行比较,并探讨语言设计者做出某些架构决策的原因。

引言

我假设读者熟悉协变和逆变的基本概念。尽管如此,我仍然无法省略这里的定义,尽管我清楚没有示例很难理解方差的要点。好吧,前面有很多易于理解的示例。

如果 AB 的子类型,那么 F(A)F(B) 的子类型,那么一个复杂类型 F(T) 在类型参数 T 上是协变的。

如果 AB 的子类型,那么 F(B)F(A) 的子类型,那么一个复杂类型 F(T) 在类型参数 T 上是逆变的。

如果一个复杂类型 F(T) 在类型参数 T 上既不是协变也不是逆变,那么它就是非变的(invariant)。

预告提示:这里的复杂类型 F 可以是数组、泛型类型等等。

目录

在此,我将简要总结文章将要讨论的要点,包括语言是否支持特定方差功能的说明。

 

C#

Java

Scala

数组协变

+

(运行时不安全)

+

(运行时不安全)

_

(数组本身是非变的)

 

当然,Java 的“协变”数组有支持。

数组逆变

_

_

_

泛型方差

(协变/逆变)

+

由泛型类型创建者定义(定义点)。

 

(仅限于泛型接口和泛型委托)

+

由泛型类型的客户端使用通配符定义(使用点)。

+

由泛型类型创建者定义(定义点)。

 

此外,还有存在类型(existential types)涵盖了 Java 的通配符功能。

重写:返回类型协变

_

+

+

重写:参数类型逆变

_

_

_

阅读本文时,您可以直接深入您感兴趣的语言,但我建议您通读全文:这样您将能更好地掌握一般概念。

协变/逆变规则对比

数组协变

C#

我们来看下面的例子

Cat[] cats = new Cat[] { new Cat(), new Cat() };
Animal[] animals = cats; //*
animals[0] = new Dog(); //**runtime(not compile-time) error here.

此代码可以编译通过,没有错误。这意味着 C# 中的数组是协变的,因为我们可以在期望 Animal[] 数组的地方使用 Cat[] 数组(参见 * 行)。

但很明显,最后一行(**)违背常理。事实上,代码在运行时会因 ArrayTypeMismatchException 而失败。所以,从形式上讲,C# 支持数组协变,但它不安全,并且没有完全由编译器强制执行。支持这种协变主要是因为它模仿了 Java 的支持。当时,C# 需要非常接近 Java 以在 Java 社区广泛传播这门新语言。现在,两门语言的道路已经分道扬镳,但对“有缺陷”的数组协变的却支持已经深深植根于 CLR,可能永远不会改变。

Java

与 C# 类似,相同的代码在 Java 中也有类似的表现,除了运行时我们会收到 Java 的 ArrayStoreException

Scala

Java 数组(内部)并不表示为单一类型,而是九种不同的类型:一种用于引用数组,另八种用于每种基本类型(int、short、float 等)。对于 Scala 语言设计者来说,支持与 Java 的互操作性,同时将数组纳入 Scala 丰富的集合层次结构是一个真正的挑战。因此,Scala 的数组由泛型 Array[T] 表示,它直接映射到 Java 的数组 T[]。它们在字节码中的表示相同,因此您可以在 Java 和 Scala 之间双向传递数组。 

我们稍后将讨论泛型方差,但现在让我们试着理解为什么 Scala 的架构决定使 Array[T] 非变。考虑 Scala 中的以下代码

val cats: Array[Cat] = Array[Cat](new Cat(), new Cat())
val animals: Array[Animal] = cats //*compile-time error here
animals.update(0, new Dog())

凭借 Scala 编译器的能力,我们在这里会收到编译时错误(* 行)。否则,就有可能像在 Java(和 C#)中那样破坏类型安全。有趣的是,并非所有 Scala 的集合都具有相同的行为。让我们在示例中将 Array 更改为 List

val cats:List[Cat] = List[Cat](new Cat(), new Cat())
val animals:List[Animal] = cats //OK
val newAnimals = animals.updated(0, new Dog())

这段代码可以在 Scala 中编译,并且在运行时也绝对安全。为什么 List 可以这样做而 Array 不可以?答案是可变性。Scala 中的数组是可变的,因此无法保证类型安全,原因与 Java(C#)中的数组(也是可变的)无法保证类型安全的原因相同。另一方面,Scala 的 List不可变的(Scala 也有可变列表,但这里我们谈论的是不可变列表)。因此,在更新列表中的元素时,实际上我们创建了一个包含旧元素(以及新更新的元素)的 List。换句话说,对于不可变列表,可以保证 List 不会被原地更新而导致运行时不一致。

数组协变简要总结

只有只读(不可变)数组才能真正协变。但它们不是不可变的。当我们更新数组中的某个元素时,我们不会得到一个新数组,只是原地更新目标数组。这就是为什么不可能让数组真正协变,同时又保证运行时安全。语言设计者需要做出一个艰难的选择。关于哪种更好,是支持像 C# 和 Java 那样对数组进行“有缺陷”的协变,还是做出审慎的决定使它们非变(Scala),这是一个有争议的问题。

数组逆变

C#/Java/Scala

这三个语言都不支持数组逆变。尽管 C# 和 Java 支持数组协变(运行时不安全)——但允许数组逆变是不切实际的。让我们试着找出原因。      

让我们想象一下,如果下面的代码能工作(实际上它不能)

Animal[] animals = new Animal[] { new Cat(), new Cat() };
Dog[] dogs = animals; //compile-time error here, but let's imagine that it works           
dogs[0] = new Dog();

数组的实际类型是 Animal(它可以包含猫和狗),所以从数据更改的角度来看,将一个元素从 Cat 替换为 Dog,即使是通过 dogs 变量,也没有什么可怕的。但是,如果我们无法在编译时保证数组中没有 Cat,我们如何通过 dogs 变量读取 Animal 数组中的元素呢?也许语言设计者可以实现某种解决方法,例如在运行时执行不兼容的读取操作时失败,但这种数组本质上将是无用的。所以,我们的结论是:

只有写时数组才能是逆变的。

泛型方差

C#

C# 中的泛型协变

我们来看下面的代码: 

interface IAnimalFarm<out T> where T: Animal
{
   T ProduceAnimal();
}
                     
class CatFarm : IAnimalFarm<Cat>
{
  public Cat ProduceAnimal()
  {
      return new Cat();
  }
}

现在我们准备尝试使用泛型协变了

IAnimalFarm<Cat> catFarm = new CatFarm();
IAnimalFarm<Animal> animalFarm = catFarm; //* OK, because covariant
Animal animal = animalFarm.ProduceAnimal();

这段代码(所有注意力都集中在 * 标记的行上)可以编译通过。这意味着编译器保证通过 animalFarm 变量处理 CatFarm 是安全的。事实上,如果我们调用返回 Animal 类型的方法 ProduceAnimal,而返回对象的实际类型是 Cat,会发生什么?答案是什么都没有,因为,由于赋值兼容性,将一个更具体类型(Cat)的值赋给一个不那么具体类型(Animal)的变量是可以的。

基本上,为了在泛型类型参数上是协变的,类型应该只在方法的输出位置包含泛型参数。在我们的例子中,这意味着为了协变,IAnimalFarm 应该只在方法的输出位置包含泛型类型参数 T

为什么有这样的限制?

考虑以下层次结构,其中泛型类型参数 T 同时出现在 IAnimalFarm 接口方法的输出和输入位置。

interface IAnimalFarm<T> where T : Animal
{
  T ProduceAnimal();
  void FeedAnimal(T animal);
}

class AnimalFarm : IAnimalFarm<Animal>
{
  public Animal ProduceAnimal()
  {
     return new Animal();
  }
              
  public void FeedAnimal(Animal animal)
  {
     //feed animal
  }
}

class CatFarm : IAnimalFarm<Cat>
{
  public Cat ProduceAnimal()
  {
     return new Cat();
  }
               
  public void FeedAnimal(Cat animal)
  {
     //feed cat
  }
}

假设 IAnimalFarm 支持协变。这意味着以下代码是合法的

IAnimalFarm<Cat> catFarm = new CatFarm();
IAnimalFarm<Animal> animalFarm = catFarm; //* compile-time error, but imagine it works
animalFarm.FeedAnimal(new Dog());

在这里,我们通过 animalFarm 变量 (*) 处理 CatFarm。这似乎没问题。但随后我们试图通过 animalFarm 变量喂养一个 Dog 对象(而对象的底层类型是 CatFarm)。所以,基本上我们试图在猫的农场里喂狗——狗不会高兴的。这个样本中的每一行看起来都合理,但结合起来却产生了不安全的行为。

正如你所见,对泛型类型参数位置(仅输出)进行编译时限制的原因很清楚:提供运行时安全。你还记得,在数组的情况下,我们决定支持协变,即使数组的元素可以同时出现在数组操作的输入和输出位置,为此付出了运行时安全的代价。在泛型的情况下,你有编译时支持,但会得到一定的灵活性。

C# 中的泛型逆变

我们来看下面的代码

interface IAnimalFarm<in T> where T : Animal
{
   void FeedAnimal(T animal);
}

class AnimalFarm : IAnimalFarm<Animal>
{
   public void FeedAnimal(Animal animal)
   {
      //feed animal
   }
}

以及下面使用逆变的代码

IAnimalFarm<Animal> animalFarm = new AnimalFarm();
IAnimalFarm<Cat> catFarm = animalFarm; //OK, because contravariant
catFarm.FeedAnimal(new Cat());

这段代码可以编译通过。这意味着编译器保证通过 catFarm 变量处理 AnimalFarm 是安全的。事实上,如果我们传递 Cat 对象作为参数调用 FeedAnimal,就不会发生任何错误。AnimalFarmFeedAnimal 需要 Animal 对象,但由于赋值兼容性,传递一个更具体的对象(Cat)是没问题的。

为了在泛型类型参数上是逆变的,类型应该只在方法的输入位置包含泛型参数。在我们的例子中,这意味着为了逆变,IAnimalFarm 应该只在方法的输入位置包含泛型类型参数 T

为什么有这样的限制?

再次考虑以下层次结构,其中泛型类型参数 T 同时出现在 IAnimalFarm 接口方法的输出和输入位置。

interface IAnimalFarm<T> where T : Animal
{
  T ProduceAnimal();
  void FeedAnimal(T animal);
}

class AnimalFarm : IAnimalFarm<Animal>
{
  public Animal ProduceAnimal()
  {
     return new Animal();
  }
              
  public void FeedAnimal(Animal animal)
  {
     //feed animal
  }
}

class CatFarm : IAnimalFarm<Cat>
{
  public Cat ProduceAnimal()
  {
     return new Cat();
  }
              
  public void FeedAnimal(Cat animal)
  {
     //feed cat
  }
}

假设 IAnimalFarm 支持逆变。这意味着以下代码是合法的

IAnimalFarm<Animal> animalFarm = new AnimalFarm();
IAnimalFarm<Cat> catFarm = animalFarm; //* compile-time error, but imagine it works
Cat animal = catFarm.ProduceAnimal();

我们正在通过 catFarm 变量处理 AnimalFarm,然后试图产生 Cat。但是我们正在处理的底层对象是 AnimalFarm 类型,所以动物农场只能产生一个抽象的 Animal,而不是一个具体的 Cat。同样,每一行都是合理的,但结合起来却产生了不安全的行为。

关于 C# 一些方差限制的重要说明
  • 泛型类型方差仅限于泛型接口和泛型委托。

  • 方差仅适用于引用类型的泛型类型参数。

让我们稍微思考一下为什么存在这些限制。

那么,为什么 C# 中的泛型是非变的?你可能已经明白,一个类需要只包含输出方法参数(才能协变)并且只包含输入方法参数(才能逆变)。关键在于对类来说很难保证这一点:例如,一个协变类(按 T 类型参数)不能有 T 类型的字段,因为你可以写入这些字段。对于真正不可变的类来说,这将工作得很好,但 C# 目前还没有对不可变性的全面支持。但说实话,我感觉我们可能会在未来看到对它的更好支持。

为什么泛型不支持值类型?简短的回答是,只有在 CLR 不需要更改(转换)泛型类型参数的值时,方差才有效。转换分为表示保留表示更改。表示保留转换的一个例子是引用的强制转换操作:执行强制转换时,您不会更改原始对象(引用指向的对象);您只是验证对象是否与应用的类型兼容并获得新的引用。表示更改转换的例子包括用户定义的转换、从 int 到 double 的转换、装箱和拆箱。对于 CLR,所有引用看起来都一样——它只是内存中实际对象的地址(取决于机器是 32 位还是 64 位)。这就是为什么它可以使用 IAnimalFarm<Cat> 而不是 IAnimalFarm<Animal> 而无需更改数据表示。对于某些值类型转换(例如装箱/拆箱),您无法说同样的话,因此方差将无法在 IEnumerable<int> IEnumerable<object> 之间工作。换句话说,保证变体转换是表示保留的最简单方法是仅允许引用类型

关于 Java/Scala 的快速说明(超前于本书内容):Java 和 Scala 中的泛型是完全的编译时构造。由于类型擦除过程,运行时没有关于泛型类型参数的信息。所有泛型参数都保留为 Object(引用类型),包括值类型(原始类型)。这就是为什么 Java/Scala 中的值类型数据表示没有问题——每个引用看起来都一样。这是使用类型擦除(JVM)相对于具体化泛型(CLR)的少数优点之一。

Java

Java 对泛型中的方差问题有另一种解决方案。正如你最近看到的,在 C# 中,泛型类型的创建者实际上负责使其成为非变/协变/逆变。这种方法被称为定义点方差注解。另一方面,在 Java 中,泛型类型的客户端决定是否将其视为非变/协变/逆变。这被称为使用点方差注解。

考虑以下代码:

interface AnimalFarm<T>
{
   T produceAnimal();
}

class CatFarm implements AnimalFarm<Cat>
{
   public Cat produceAnimal()
   {
       return new Cat();
   }
}

现在让我们尝试以协变的方式使用它

AnimalFarm<Cat> catFarm = new CatFarm();
AnimalFarm<Animal> animalFarm = catFarm; //* compile-time error
Animal animal = animalFarm.produceAnimal();

编译器不允许这样做(* 标记的行),因为 Java 中的泛型类型默认是非变的。但是,我们可以通过通配符“强制”泛型类型协变。下面的例子可以工作

AnimalFarm<Cat> catFarm = new CatFarm();
AnimalFarm<? extends Animal> animalFarm = catFarm; //OK
Animal animal = animalFarm.produceAnimal();

在“客户端”指定方差的好处是,即使你有一个有问题的泛型类型(泛型类型参数同时出现在方法的输入和输出位置),你仍然可以以协变/逆变的方式使用它。这种方法的坏处是方差没有被泛型类型创建者的设计所包含。相反,这个泛型类型的客户端应该费尽心思去思考如何正确使用它。

通配符 <? extends Animal> 意味着 animalFarm 可以持有任何 AnimalFarm<T> 类型的对象,其中泛型类型参数(T)是 Animal 的子类型。显然,Cat 类型参数满足此条件。

考虑以下示例

interface AnimalFarm<T>
{
   T produceAnimal();
   void feedAnimal(T animal);
}

class AnimalFarmDefault implements AnimalFarm<Animal>
{
   public Animal produceAnimal()
   {
       return new Animal();
   }

   public void feedAnimal(Animal animal)
   {
       //feed animal
   }
}

class CatFarm implements AnimalFarm<Cat>
{
   public Cat produceAnimal()
   {
       return new Cat();
   }

   public void feedAnimal(Cat animal)
   {
       //feed cat
   }
}

你还记得,在 C# 中,类似的泛型类型将是非变的,因为泛型类型参数同时出现在方法的输入和输出位置。

在 Java 中,默认情况下它也是非变的,但使用通配符,泛型类型的客户端可以指定如何处理它。

你可以将其视为协变

AnimalFarm<Cat> catFarm = new CatFarm();
AnimalFarm<? extends Animal> animalFarm = catFarm; //OK
Animal animal = animalFarm.produceAnimal();

或者视为逆变

AnimalFarm<Animal> animalFarm = new AnimalFarmDefault();
AnimalFarm<? super Cat> catFarm = animalFarm; //OK
catFarm.feedAnimal(new Cat());

通配符 <? super Cat> 意味着 catFarm 可以持有任何 AnimalFarm<T> 类型的对象,其中泛型类型参数(T)是 Cat 的超类型。当然,Animal 类型参数满足此条件。

这里的想法是,当你将泛型类型视为协变时,你只能访问泛型类型参数出现在方法输出位置的方法。当你将泛型类型视为逆变时,你只能访问泛型类型参数出现在方法输入位置的方法。

顺便说一句,通配符方差不仅限于接口——你也可以以方差的方式使用泛型类。

Scala

考虑以下示例

trait AnimalFarm[T]
{
  def produceAnimal(): T
}

class CatFarm extends AnimalFarm[Cat]{
  def produceAnimal(): Cat = new Cat()
}

不出所料,下面的例子无法编译,因为默认情况下 Scala 中的泛型类型是非变的

val catFarm:AnimalFarm[Cat] = new CatFarm()
val animalFarm: AnimalFarm[Animal] = catFarm //Compile-time error
val animal: Animal = animalFarm.produceAnimal()

但你可以这样使其协变

trait AnimalFarm[+T]
{
  def produceAnimal(): T
}

class CatFarm extends AnimalFarm[Cat]{
  def produceAnimal(): Cat = new Cat()
}

正如你所见,唯一的区别是“+”号,它表示该 trait 对于 T 类型参数是协变的。现在你可以使用协变了

val catFarm:AnimalFarm[Cat] = new CatFarm()
val animalFarm: AnimalFarm[Animal] = catFarm //OK
val animal: Animal = animalFarm.produceAnimal()

你看到这与 C# 使用的方法类似。你是在定义点指定类型是协变的,而不是像 Java 那样在使用点

类似地,你可以使用“-”号使 trait(或类)逆变

trait AnimalFarm[-T]
{
  def feedAnimal(animal: T): Unit
}

class AnimalFarmDefault extends AnimalFarm[Animal]
{
  def feedAnimal(animal: Animal): Unit = {
    //feed animal
  }
}

并将其用作逆变

val animalFarm:AnimalFarm[Animal] = new AnimalFarmDefault()
val catFarm: AnimalFarm[Cat] = animalFarm //OK
catFarm.feedAnimal(new Cat())

这一切看起来都像 C# 中的方差,除了两个重要点:首先,我们不限于 trait——我们可以将相同的规则应用于 Scala 的类。另一件事是,Scala 通过下界和上界为管理泛型约束提供了更大的灵活性。

还记得我们讨论过 C# 中的“有问题的”例子,即泛型类型参数同时出现在输入和输出位置。让我们在 Scala 中重现类似的情况

trait AnimalFarm[T]
{
  def produceAnimal(): T
  def feedAnimal(animal: T): Unit
}

class AnimalFarmDefault extends AnimalFarm[Animal]{
  def produceAnimal(): Animal = new Animal()
  def feedAnimal(animal: Animal): Unit = {
    //feed animal
  }
}

class CatFarm extends AnimalFarm[Cat]{
  def produceAnimal(): Cat = new Cat()
  def feedAnimal(animal: Cat): Unit = {
    //feed animal
  }
}

AnimalFarm trait 也是非变的,就像 C# 中类似的接口一样。它不能被简单地添加“+”号到类型参数前面,使其协变。如果我们想使 trait 协变,我们仍然需要处理类型参数也出现在 feedAnimal 方法的输入位置这一事实。在 C# 中,我们将不得不放弃使接口协变的愿望。

但在 Scala 中,我们可以这样做

trait AnimalFarm[+T]
{
  def produceAnimal(): T
  def feedAnimal[S >: T](animal: S): Unit
}

class AnimalFarmDefault extends AnimalFarm[Animal]{
  def produceAnimal(): Animal = new Animal()
  def feedAnimal[S >: Animal](animal: S): Unit = {
    //feed animal
  }
}

class CatFarm extends AnimalFarm[Cat]{
  def produceAnimal(): Cat = new Cat()
  def feedAnimal[S >: Cat](animal: S): Unit = {
    //feed animal
  }
}

并将其用作协变

val catFarm:AnimalFarm[Cat] = new CatFarm()
val animalFarm: AnimalFarm[Animal] = catFarm //OK
val animal: Animal = animalFarm.produceAnimal()
animalFarm.feedAnimal(new Dog) //still OK!

很酷的是,你甚至可以在不损害类型安全的情况下调用协变类型上的 feedAnimal。让我们通过以下方法为例来探讨它是如何工作的

 def feedAnimal[S >: Cat](animal: S): Unit = {
   //feed animal
 }

下界(例如 [S >: Cat])指定了一个自反关系,这意味着你可以将任何是 Cat 超类型的类型(S)的对象传递给该方法。如果你传递一个 Cat 对象,那么 CatCat 之间的公共超类型就是 Cat 本身,所以 S 变成 Cat。如果你传递一个 Animal 对象,那么 AnimalCat 之间的公共超类型就是 Animal,所以 S 变成 Animal。如果你传递一个 Dog 对象,那么 DogCat 之间的公共超类型又是 Animal,所以 S 变成 Animal。拥有这个智能推理机制,编译器可以保证类型安全永远不会受到损害。

泛型方差简要总结

正如你所见,C# 和 Scala 在处理方差方面采用了类似的方法,即在定义点指定,尽管在约束规则和其他分歧细节上存在本质区别。Java 采用另一种方法,即在使用点通过通配符指定方差。

严格来说,Scala 也通过存在类型提供了使用点方差,以涵盖 Java 的通配符功能并解决一些互操作性问题。即便如此,这也不是 Scala 解决方差挑战的理念。对 Scala 的架构师来说,首选的方法是使用定义点方差。不过,你可以在附件的压缩包/GitHub 上找到存在类型方差的示例。

重写:返回类型协变

如果一个语言支持返回类型协变,意味着你可以用派生类的方法(返回更具体的类型)覆盖基类的方法(返回不那么具体的类型)。

C#

考虑下面的例子(在 C# 中无法编译)

class AnimalFarm
{
   public virtual Animal ProduceAnimal()
   {
      return new Animal();
   }
}

class CatFarm: AnimalFarm
{
    public override Cat ProduceAnimal() //compile-time error
    {
        return new Cat();
    }
}

正如你所见,C# 不支持返回类型协变。此外,CLR 本身也不支持。所以,我们不太可能在 C# 中看到这个特性。

Java

令人惊讶(或不惊讶)的是,类似的例子在 Java 中可以工作

class AnimalFarm
{
   public Animal produceAnimal()
   {
       return new Animal();
   }
}

class CatFarm extends AnimalFarm
{
   @Override //OK
   public Cat produceAnimal()
    {
        return new Cat();
    }
}

Java 从 JAVA 5.0 开始支持返回类型协变

Scala

在 Scala 中也有效

class AnimalFarm
{
  def produceAnimal(): Animal = new Animal()
}

class CatFarm extends AnimalFarm
{
  override def produceAnimal(): Cat = new Cat() //OK
}

不出所料,Scala 支持返回类型协变。不奇怪,因为 Scala 是一个基于 JVM 的语言,与 Java 兼容。

重写:参数类型逆变

如果一个语言支持参数类型逆变,意味着你可以用派生类的方法(具有更不具体的类型参数)覆盖基类的方法(具有更具体的类型参数)。

这三个语言都不支持参数类型逆变

C#

class AnimalFarm
{
   public virtual void FeedAnimal(Cat animal)
   {                    
   }
}

class CatFarm : AnimalFarm
{
   public override void FeedAnimal(Animal animal) //compile-time error
   {                    
   }
}

Java

class AnimalFarm
{
   public void feedAnimal(Cat animal)
   {
   }
}

class CatFarm extends AnimalFarm
{
   @Override //compile-time error
   public void feedAnimal(Animal animal)
   {
   }
}

Scala

class AnimalFarm
{
  def feedAnimal(animal: Cat)={
  }
}

class CatFarm extends AnimalFarm
{
  override def feedAnimal(animal: Animal)= { //compile-time error
  }
}

但是为什么不支持?参数类型逆变有什么问题?似乎支持它并没有什么坏处。乍一看确实如此。但问题在于,将其添加到语言中会产生许多争议情况:例如,如何区分重载和重写?返回类型可以协变,因为返回类型在重载时不被考虑:因此没有歧义。但是方法参数方法签名的一部分,并且它们重载时被考虑,因此在重载和重写之间可能存在潜在的歧义。还有更多潜在的问题,但这是最明显的。

结论

正如你所见,方差是一件相当有趣(有时甚至是复杂)的事情。语言设计者需要在整个过程中做出很多妥协:丰富语言以满足现代需求,同时处理庞大的遗留代码库。比较不同语言解决相似问题的方法非常有趣。你可以在附件的压缩包和 GitHub 上找到所有示例。感谢您的关注。下次再见!

© . All rights reserved.