在 Dart 中,泛型(Generics)是一种强大的功能,允许你编写能够操作不同数据类型的类、方法或接口,而不需要在编写代码时指定具体的类型。这种灵活性提高了代码的复用性,并且在编译时提供了类型检查,从而减少了运行时错误。
一、基本概念
泛型的核心思想是让你在编写类、方法或接口时,先定义一种类型参数(type parameter),然后在使用时再指定具体的类型。这意味着你可以创建一个“模板”,让它在不同的上下文中使用不同的类型。
例如,你可以创建一个泛型列表类,来存储任意类型的元素:
class Box<T> {T value;Box(this.value);T getValue() {return value;}
}void main() {var intBox = Box<int>(42); // 泛型实例化为 intvar stringBox = Box<String>("Hello");print(intBox.getValue()); // 42print(stringBox.getValue()); // Hello
}
在上面的代码中,Box
是一个泛型类,T
是它的类型参数,可以在实例化 Box
时指定具体的类型,比如 int
或 String
。
二、泛型的用途
1、泛型类
你可以在类中使用泛型来创建更灵活、可复用的类。例如,List
就是一个泛型类,它可以存储任何类型的数据。
class Container<T> {T? value;void setValue(T value) {this.value = value;}T? getValue() {return value;}
}
2、泛型方法
除了泛型类,Dart 还允许你为方法定义泛型。这使得方法在调用时可以处理不同类型的参数。
T identity<T>(T value) {return value;
}void main() {print(identity<int>(42)); // 输出: 42print(identity<String>("Hello, Dart!")); // 输出: Hello, Dart!
}
在上面的例子中,identity
方法是泛型的,它可以接收任何类型的参数并返回相同类型的值。
3、泛型接口
Dart 中的接口也可以使用泛型。通过泛型接口,你可以确保实现类在类型安全的情况下使用某些操作。
abstract class Printable<T> {void print(T value);
}class StringPrinter implements Printable<String> {void print(String value) {print("Printing: $value");}
}void main() {var printer = StringPrinter();printer.print("Hello, Dart!"); // 打印: Printing: Hello, Dart!
}
三、类型约束
有时你可能希望限制泛型类型的范围。Dart 允许你为泛型类型添加约束,以确保它只能是某些类型的子类。约束通过 extends
关键字实现。
1、基本类型约束
class NumberBox<T extends num> {T value;NumberBox(this.value);T getValue() {return value;}
}void main() {var intBox = NumberBox<int>(42); // 允许var doubleBox = NumberBox<double>(3.14); // 允许// var stringBox = NumberBox<String>("Hello"); // 错误,String 不是 num 的子类
}
在这个例子中,NumberBox
类被限制为只能使用 num
类型及其子类型,如 int
或 double
。
2、复杂类型约束
你可以使用更复杂的约束来限制泛型类型,例如要求泛型类型必须实现某个接口。
class Printer<T extends Printable> {void print(T value) {value.print(value);}
}class StringPrinter implements Printable<String> {void print(String value) {print("Printing: $value");}
}void main() {var printer = Printer<StringPrinter>();var stringPrinter = StringPrinter();printer.print(stringPrinter); // 打印: Printing: StringPrinter
}
在这个例子中,Printer
类的泛型参数被限制为实现了 Printable
接口的类型。
四、通配符类型(类型参数的上界和下界)
Dart 支持在泛型中使用类型参数的“上界”(extends)和“下界”(super),类似于 Java 泛型的通配符。
1、上界(extends
)
可以限制泛型的类型必须是某个类的子类或接口的实现类。
void printNumbers<T extends num>(T number) {print(number);
}void main() {printNumbers<int>(42); // 输出: 42printNumbers<double>(3.14); // 输出: 3.14
}
2、下界(super
)
虽然 Dart 目前没有提供直接的 super
关键字来限制下界类型,但可以通过其他方式来实现类似的效果(例如通过使用协变或逆变)。
五、泛型与集合类
Dart 中的许多集合类(如 List
, Set
, Map
)都支持泛型。这样可以确保集合中的元素类型是一致的,避免类型错误。
List<int> numbers = [1, 2, 3, 4];
Set<String> names = {"Alice", "Bob"};Map<String, int> ages = {"Alice": 30, "Bob": 25};
六、泛型与协变和逆变
协变(covariance)和逆变(contravariance)是泛型类型之间的一种关系。它们控制类型如何在继承体系中变化,但 Dart 中的泛型主要是协变的。你可以在某些情况下使用 covariance
关键字来控制参数类型的协变行为。
1、什么是型变
型变指的是在泛型类型参数之间的继承关系。它描述了如果一个类型 A
是另一个类型 B
的子类型,是否可以在泛型类中通过子类型替代父类型。直观地说,型变决定了你是否可以将子类类型赋给父类类型,特别是在泛型类中。型变有三种主要类型:协变、逆变和不变。
举个例子,假设有如下类结构:
class Fruit {}
class Orange extends Fruit {}
接下来,我们有一个泛型类 Crate<T>
:
class Crate<T> {T item;Crate(this.item);
}
假设我们想知道 Crate<Orange>
是否是 Crate<Fruit>
的子类型。直觉可能会告诉你:“因为 Orange
是 Fruit
的子类,所以 Crate<Orange>
应该是 Crate<Fruit>
的子类型”。但是,答案是 No,在 dart 语言中两者没有关系,Dart 语言中 没有 默认的协变或逆变关系,所以 Crate<Orange>
和 Crate<Fruit>
是没有继承关系的,它们完全是独立的。
简单来说,型变就是指 Crate 和 Crate 是什么关系这个问题,对于不同的答案,有如下几个术语。
- invariance(不型变):也就是说,Crate 和 Crate 之间没有关系。
- covariance(协变):也就是说,Crate 是 Crate 的子类型。
- contravariance(逆变):也就是说,Crate 是 Crate 的子类型。
注意:
- 上面在解释协变、逆变概念时的说法只是为了帮助理解,这种说法对于Java而言并不准确。在 Dart 中,Crate 和 Crate 永远没有关系,对于协变应该这么说, Crate 是 Crate<? extends Fruit> 的子类型,逆变则是,Crate 是 Crate<? super Orange> 的子类型。
- 子类(subclass) 和 **子类型(subtype)**不是一个概念,子类一定是子类型,子类型不一定是子类,例如,Crate 是 Crate<? extends Fruit> 的子类型,但是Crate 并不是 Crate<? extends Fruit> 的子类。
接下来的内容会详细解释在 Dart 中如何通过协变、逆变和不变来控制这些类型关系。
2、什么是协变
协变(covariant)允许你使用更具体的子类型替代父类型。也就是说,协变表示,如果类型 A
是类型 B
的子类,那么 Container<A>
可以被视为 Container<B>
的子类型。协变通常适用于输出类型,意味着你可以将返回更具体类型的函数赋值给返回更广泛类型的函数。
协变的特点:
- 协变适用于返回类型或获取数据的场景。
- 你可以使用子类型替代父类型。
协变的示例:
class Animal {void sound() => print("Animal sound");
}class Dog extends Animal {void sound() => print("Woof");
}class Box<T> {T item;Box(this.item);T getItem() => item;
}void main() {Box<Dog> dogBox = Box(Dog());Box<Animal> animalBox = dogBox; // Box<Dog> 可以赋值给 Box<Animal>animalBox.getItem().sound(); // 输出: Woof
}
在这个例子中,Box<Dog>
可以被赋值给 Box<Animal>
,这是因为 Dog
是 Animal
的子类。我们通过 Box
的返回类型实现了协变。dogBox
作为 Box<Dog>
被赋给了 Box<Animal>
类型的变量,说明输出类型是协变的。
3、什么是逆变
逆变(contravariant)与协变相反,逆变允许你使用更广泛的父类型替代子类型。也就是说,逆变表示,如果类型 A
是类型 B
的子类,那么 Function<B>
可以被视为 Function<A>
的子类型。逆变通常适用于输入类型,意味着你可以将接受父类类型的函数赋值给接受子类类型的函数。
逆变的特点:
- 逆变适用于参数类型或输入数据的场景。
- 你可以使用父类型替代子类型。
逆变的示例:
假设我们有以下类定义:
class Animal {void sound() => print("Animal sound");
}class Dog extends Animal {void sound() => print("Woof");
}class Processor<T> {void process(T item) {item.sound();}
}void main() {Processor<Animal> animalProcessor = Processor();Processor<Dog> dogProcessor = animalProcessor; // Processor<Animal> 可以赋值给 Processor<Dog>dogProcessor.process(Dog()); // 输出: Woof
}
在这个例子中,Processor<Animal>
被赋值给了 Processor<Dog>
,这是因为 Processor<T>
是逆变的(特别是对于 T
)。我们把父类 Animal
的 Processor
用来处理子类 Dog
的对象,这就实现了逆变。我们在这里看到的是,输入类型(即方法参数类型)支持逆变。
4、什么是不变
不变(invariant)表示泛型类型的参数没有任何协变或逆变的关系,也就是说,子类型和父类型的泛型类型是完全不同的。换句话说,Container<Dog>
和 Container<Animal>
之间没有任何继承关系,它们是完全独立的。通常在 Dart 中,默认的泛型类型是不变的。
不变的特点:
- 不变表示泛型类型参数没有协变或逆变关系。
- 泛型类的参数类型不能被替换。
不变的示例:
class Animal {void makeSound() => print("Animal sound");
}class Dog extends Animal {void makeSound() => print("Dog barking");
}class Box<T> {final T value;Box(this.value);T get() => value;
}void main() {Box<Dog> dogBox = Box(Dog());Box<Animal> animalBox = dogBox; // 错误:Box<Dog> 不能赋值给 Box<Animal>
}
在这个例子中,Box<Dog>
和 Box<Animal>
之间没有任何关系,因为 Dart 中泛型是默认不变的。你不能将一个 Box<Dog>
赋值给一个 Box<Animal>
,即使 Dog
是 Animal
的子类。这种类型的限制是由不变(invariance)特性控制的。
5、总结
- 协变(Covariant):允许子类型替代父类型,通常适用于返回值类型。
- 逆变(Contravariant):允许父类型替代子类型,通常适用于输入参数类型。
- 不变(Invariant):没有协变或逆变关系,泛型类型参数完全不允许替换。
理解型变的概念以及如何在泛型类型中应用协变、逆变和不变,可以帮助你在编写泛型代码时提高类型安全性和灵活性。
七、泛型的类型推断
在 Dart 中,当你使用泛型时,编译器可以根据你提供的类型推断出泛型的具体类型。比如:
var list = <int>[1, 2, 3];
这里,编译器自动推断 list
是一个 List<int>
。
八、泛型的类型擦除
和 Java 类似,Dart 的泛型也会进行类型擦除。在运行时,泛型类型不再保留其具体的类型信息,这意味着泛型类型只是用来在编译时进行类型检查,运行时并没有保存泛型类型的具体信息。
List<int> intList = [1, 2, 3];
print(intList.runtimeType); // 输出: List<int> 但实际上是 List
总结
- 泛型 提供了灵活性和类型安全,允许你在不同的上下文中使用不同类型。
- 你可以为类、方法、接口和集合使用泛型来确保类型一致性。
- 类型约束 使得你可以限制泛型类型的范围,确保类型安全。
- 泛型在集合类、接口、方法等方面有广泛应用,提升了代码的复用性和安全性。
Dart 的泛型是编译时检查的,能有效避免很多常见的运行时错误,是现代编程语言中的重要特性之一。
关于泛型的协变和逆变可以参考链接:https://www.jianshu.com/p/0c2948f7e656