Skip to content

协变与逆变 (Covariance and Contravariance)

协变(Covariance)和逆变(Contravariance)是类型系统中的重要概念,特别是在处理泛型和函数类型时。它们描述了子类型关系如何在不同上下文中传播。理解这些概念可以帮助你编写更加灵活且类型安全的代码。

1. 协变 (Covariance)

定义:如果 BA 的子类型,那么 F<B> 也是 F<A> 的子类型。这里 F 表示一个类型构造器,例如泛型或函数返回类型。

特点

  • 协变允许将更具体的类型赋值给更通用的类型。
  • 常见于输出位置,如返回值类型、读取属性等。

示例:泛型中的协变

typescript
class Animal {}
class Dog extends Animal {}

// 泛型数组是协变的
let animals: Animal[] = [];
let dogs: Dog[] = [new Dog()];

animals = dogs; // OK: Dog[] 是 Animal[] 的子类型

在这个例子中,Dog[] 可以赋值给 Animal[],因为 DogAnimal 的子类,所以 Dog[] 也是 Animal[] 的子类型。

示例:函数返回类型的协变

typescript
function getAnimal(): Animal {
  return new Animal();
}

function getDog(): Dog {
  return new Dog();
}

let getAnyAnimal: () => Animal = getDog; // OK: () => Dog 是 () => Animal 的子类型

在这里,getDog 函数的返回类型 DoggetAnyAnimal 返回类型 Animal 的子类型,因此可以进行赋值。

2. 逆变 (Contravariance)

定义:如果 BA 的子类型,那么 F<A>F<B> 的子类型。这意味着参数位置的类型是逆变的。

特点

  • 逆变允许将更通用的类型赋值给更具体的类型。
  • 常见于输入位置,如函数参数类型、写入属性等。

示例:函数参数类型的逆变

typescript
function acceptsAnimal(animal: Animal): void {
  console.log("It's an animal");
}

function acceptsDog(dog: Dog): void {
  console.log("It's a dog");
}

let acceptAnyAnimal: (animal: Animal) => void = acceptsDog; // Error: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
let acceptAnyDog: (dog: Dog) => void = acceptsAnimal; // OK: (animal: Animal) => void 是 (dog: Dog) => void 的子类型

在这个例子中:

  • acceptsDog 不能赋值给 acceptAnyAnimal,因为 acceptAnyAnimal 需要能够接受任何 Animal 类型的对象,而 acceptsDog 只能接受 Dog 类型的对象。
  • acceptAnyDog 可以赋值给 acceptsAnimal,因为 acceptsAnimal 接受的是更通用的 Animal 类型,它可以处理 Dog 类型的对象。

3. 不变 (Invariance)

有些情况下,类型既不是协变也不是逆变,而是不变的。这意味着即使 BA 的子类型,F<B> 也不是 F<A> 的子类型,反之亦然。

示例:双向绑定属性的不变性

typescript
class Container<T> {
  value: T;
}

let containerOfAnimals: Container<Animal>;
let containerOfDogs: Container<Dog>;

containerOfAnimals = containerOfDogs; // Error: Type 'Container<Dog>' is not assignable to type 'Container<Animal>'.
containerOfDogs = containerOfAnimals; // Error: Type 'Container<Animal>' is not assignable to type 'Container<Dog>'.

在这个例子中,Container<T>value 属性既可以用于读取也可以用于写入,因此它既不是协变也不是逆变,而是不变的。

总结

  • 协变:适用于输出位置,如返回值类型、读取属性等。如果 BA 的子类型,那么 F<B> 也是 F<A> 的子类型。
  • 逆变:适用于输入位置,如函数参数类型、写入属性等。如果 BA 的子类型,那么 F<A>F<B> 的子类型。
  • 不变:既不是协变也不是逆变,通常出现在需要双向访问(读/写)的场景中。

理解协变与逆变的概念对于正确使用泛型和函数类型非常重要。通过合理应用这些概念,你可以编写出更加灵活且类型安全的代码。

Released under the MIT License.