Generic Type Variance in Dart
摘要
从C#到Kotlin,很多编程语言都支持类型「变体」(或型变,variance) 的特性,Dart在未来的版本中也会加入「声明处型变」(declaration-site variance) 的支持。在这篇学习笔记中,梳理了几种变体的含义,以及它们在Dart中的实现情况。
支持特性,带来便利的同时也带来了相应的复杂度。比如应该如何判定两个泛型类型之间父/子类型关系?例如,以下语句通常会被认为是合理的:
List integers = [1, 2];
List numbers = integers;
一组「整数」可以认为也是一组「数字」,看起来合情合理。然而,能够通过编译的语句:,却会导致运行时错误,因为实际上具有类型,并不能接纳一个浮点数。
于是就需要引入类型参数变体(
变体的种类
一共有三种类型的变体:
Producer / Consumer
上面这些不常见的英文单词容易混淆、晦涩难懂,笔者经过反复阅读文档之后仍然记不住(这也是写下这篇笔记的原因)。通过参考作者关于Java类型通配符的助记词:PECS,即,生产者-Extens,消费者-Super,我们会发现,借用「生产者、消费者」的概念同样有助于理解、记忆上述不同种类的变体。
生产者
纯粹的「生产者」只产出不消费,定义为
class Producer {
T get next => …;
}
Producer numbers = Producer();
print(numbers.next);
输出,而是的一种,因此把它理解为「的生产者」是自然且安全的。
在
class Producer {
void add(T a) { … }
^ The 'out' type parameter 'T' can't be used in an 'in' position.
}
目前为止Dart的默认模式类似于
Iterable numbers = [1, 2];
numbers = numbers.map((x) => x * 2);
当然这样会缺少上述「out类型参数不能用于in的位置」的约束。
消费者
纯粹的「消费者」只负责消费而不产出,和「生产者」正好相反,因此属于
class Consumer {
void eat(T a) { … }
}
这时可以接收任何类型的输入,这是最直观的传统用法,并不需要引入新语法,例如:
final consumer = Consumer();
consumer.eat(1.0);
consumer.eat(2);
但是当我们需要限制输入数据的类型时,就是
final Consumer consumer = Consumer();
consumer.eat(10);
接受类型的输入,当然也能接受类型的输入,所以使用一个类型的变量来引用它是安全的。这样,编译器就能帮助我们避免输入不希望的数据类型,如。
当然,在
class Consumer {
final T first;
^ The 'in' type parameter 'T' can't be used in an 'out' position.
}
生产者 + 消费者
既输出又输入的情况就只能是
假设能够在一个类上同时使用
// Dart目前的默认模式,没有约束
class ReadWrite {
T read() {}
void write(T a) {}
}
考虑以下两种场景:
// 1. covariance
ReadWrite nrw = ReadWrite();
nrw.write(0.0);
两个语句分开看都没问题,但实际上只能接收,而不应该允许输入。
// 2. contravariance
ReadWrite irw = ReadWrite(); // 假设能够编译通过
int x = irw.read();
这里正好相反,我们认为输出类型的数值,但实际上它却存在输出的可能性,因此这种写法也应该禁止。
我们可以看到上面这两种场景是自相矛盾的,所以对于既输出又输入的情况,就只能限制类型参数必须相同了,定义关键字是:
class ReadWrite {
T read() {}
void write(T a) {}
}
此时,上面例子中的两个赋值语句都会导致编译错误,实际上只能使用相同的类型参数去引用:
ReadWrite irw = ReadWrite();
irw.write(10);
int x = irw.read();
// 如果希望适用面更广,就需要使用父类了
ReadWrite nrw = ReadWrite()
..write(11)
..write(2.0);
num x = nrw.read();
Use-site Variance
上面的例子可以看到
void pipeline(ReadWrite from, ReadWrite to) {
to.write(from.read());
}
// 不能这样做
pipeline(ReadWrite(), ReadWrite());
^ The argument type 'ReadWrite' can't be assigned to the parameter type 'ReadWrite'.
将类型的数据作为类型取出然后写入,这个合理的要求也被拒绝了。
为了更好的解决这个问题,就需要引入,可以理解为在使用时对数据类型做一些临时的约束(在允许范围内),以便满足处理的需求,这样就比较灵活了。
如果Dart支持
void pipeline(ReadWrite from, ReadWrite to) {
to.write(from.read());
}
// 允许
pipeline(ReadWrite(), ReadWrite());
意思就是限制了函数只能把作为一个「生产者」来使用。这样编译器就会明白:此处将一个赋值给一个变量是安全的,编译就能够通过了,当然编译器也同时会禁止去扮演一个消费者的角色。
不过目前
void pipeline(ReadWrite from, ReadWrite to) {
to.write(from.read());
}
然而此时的编译器无法阻止函数作出这样的操作,将会导致运行时错误。这类问题目前只能依靠开发者本身,通过Code review或者运行时错误分析去发现了,这也说明了
现状
Dart的
analyzer:
enable-experiment:
- variance