点击在线阅读,体验更好 | 链接 |
---|---|
现代JavaScript高级小册 | 链接 |
深入浅出Dart | 链接 |
现代TypeScript高级小册 | 链接 |
类型兼容:协变和逆变
引言
在类型系统中,协变和逆变是对类型比较(类型兼容
)一种形式化描述。在一些类型系统中,例如 Java,这些概念是显式嵌入到语言中的,例如使用extends
关键字表示协变,使用super
关键字表示逆变。在其他一些类型系统中,例如 TypeScript,协变和逆变的规则是隐式嵌入的,通过类型兼容性检查来实现。
协变和逆变的存在使得类型系统具有更大的灵活性。例如,如果你有一个Animal
类型的数组,并且你有一个Dog
类型的对象(假设Dog
是Animal
的子类型),那么你应该能够将Dog
对象添加到Animal
数组中。这就是协变。反过来,如果你有一个处理Animal
类型对象的函数,并且你有一个Dog
类型的对象,你应该可以使用这个函数来处理Dog
对象。这就是逆变。
协变和逆变还可以帮助我们创建更通用的代码。例如,如果你有一个可以处理任何Animal
的函数,那么这个函数应该能够处理任何Animal
的子类型。这意味着,你可以编写一段只依赖于Animal
类型的代码,然后使用这段代码处理任何Animal
的子类型。
协变(Covariance)
协变描述的是如果存在类型A和B,并且A是B的子类型,那么我们就可以说由A组成的复合类型(例如Array<A>
或者(a: A) => void
)也是由B组成的相应复合类型(例如Array<B>
或者(b: B) => void
)的子类型。
让我们通过一个例子来理解协变。假设我们有两个类型Animal
和Dog
,其中Dog
是Animal
的子类型。
type Animal = { name: string };
type Dog = Animal & { breed: string };
let dogs: Dog[] = [{ name: "Fido", breed: "Poodle" }];
let animals: Animal[] = dogs; // OK because Dog extends Animal, Dog[] is a subtype of Animal[]
这里我们可以将类型为Dog[]
的dogs
赋值给类型为Animal[]
的animals
,因为Dog[]
是Animal[]
的子类型,所以数组是协变的。
协变:类型的向下兼容性
协变是类型系统中的一个基本概念,它描述的是类型的“向下兼容性”。如果一个类型A可以被看作是另一个类型B的子类型(即A可以被安全地用在期望B的任何地方),那么我们就说A到B是协变的。这是类型系统中最常见和直观的一种关系,例如在面向对象编程中的继承就是协变的一种表现。
在TypeScript中,所有的类型都是自身的子类型(即每个类型到自身是协变的),并且null
和undefined
类型是所有类型的子类型。除此之外,接口和类也可以通过继承来形成协变关系。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
let myDog: Dog = new Dog();
let myAnimal: Animal = myDog; // OK,因为Dog是Animal的子类型
这个例子中,我们可以将一个Dog
对象赋值给一个Animal
类型的变量,因为Dog
到Animal
是协变的。
在TypeScript中,泛型类型也是协变的。例如,如果类型A是类型B的子类型,那么Array<A>
就是Array<B>
的子类型。
let dogs: Array<Dog> = [new Dog()];
let animals: Array<Animal> = dogs; // OK,因为Array<Dog>是Array<Animal>的子类型
逆变(Contravariance)
逆变是协变的反面。如果存在类型A和B,并且A是B的子类型,那么我们就可以说由B组成的某些复合类型是由A组成的相应复合类型的子类型。
这在函数参数中最常见。让我们来看一个例子:
type Animal = { name: string };
type Dog = Animal & { breed: string };
let dogHandler = (dog: Dog) => { console.log(dog.breed); }
let animalHandler: (animal: Animal) => void = dogHandler; // Error!
在这个例子中,我们不能将类型为(dog: Dog) => void
的dogHandler
赋值给类型为(animal: Animal) => void
的animalHandler
。因为如果我们传递一个Animal
(并非所有的Animal
都是Dog
)给animalHandler
,那么在执行dogHandler
函数的时候,就可能会引用不存在的breed
属性。因此,函数的参数类型是逆变的。
逆变:类型的向上兼容性
逆变描述的是类型的“向上兼容性”。如果一个类型A可以被看作是另一个类型B的超类型(即B可以被安全地用在期望A的任何地方),那么我们就说A到B是逆变的。在函数参数类型的兼容性检查中,TypeScript使用了逆变。
type Handler = (arg: Animal) => void;
let animalHandler: Handler = (animal: Animal) => { /* ... */ };
let dogHandler: Handler = (dog: Dog) => { /* ... */ }; // OK,因为Animal是Dog的超类型
这个例子中,我们可以将一个处理`
Dog的函数赋值给一个处理
Animal的函数类型的变量,因为
Animal是
Dog的超类型,所以
(dog: Dog) => void类型是
(animal: Animal) => void`类型的子类型。
这看起来可能有些反直觉,但实际上是为了保证类型安全。因为在执行dogHandler
函数时,我们可以安全地传入一个Animal
对象,而不需要担心它可能不是Dog
类型。
协变与逆变的平衡
协变和逆变在大多数情况下都可以提供合适的类型检查,但是它们并非完美无缺。在实际应用中,我们必须关注可能的边界情况,以避免运行时错误。在某些情况下,我们甚至需要主动破坏类型的协变或逆变,以获得更强的类型安全。例如,如果我们需要向一个Dog[]
数组中添加Animal
对象,我们可能需要将这个数组的类型声明为Animal[]
,以防止添加不兼容的类型。
总的来说,协变和逆变是理解和应用TypeScript类型系统的重要工具,但我们必须在灵活性和类型安全之间找到合适的平衡。