泛型
generics
泛型的基本概念与作用
理论: 什么是泛型,为什么我们需要泛型?
泛型(Generics)是一种编程语言特性,允许您编写可以处理多种数据类型的代码,而不是预先指定单一的具体数据类型。它使得代码更加灵活、可重用,并能在编译时提供更强的类型安全性。
为什么要使用泛型?
- 代码重用(
Code Reusability):
- 如果没有泛型,您可能需要为每种数据类型编写几乎相同的代码。例如,一个存储整数的列表和一个存储字符串的列表,它们的底层逻辑可能完全相同,但由于类型不同,您不得不创建两个独立的实现。泛型允许您编写一个通用的“列表”实现,可以存储任何类型的数据。
- 类型安全(
Type Safety):
- 在编译时捕获类型不匹配的错误,而不是在运行时才发现。没有泛型时,我们有时会使用
Object类型来处理各种数据,但这会丧失类型信息,导致运行时类型转换错误(CastError)。泛型通过在编译时强制类型检查,保护我们免受此类错误的影响。
- 可读性与清晰性(
Readability and Clarity):
- 泛型能够清晰地表明代码设计的意图,一眼就能看出某个类或函数期望处理的数据类型,提高了代码的可读性和可维护性。
类比: 想象一下一个万能工具箱。没有泛型时,你可能需要为存储螺丝刀专门做一个小盒子,为扳手做一个大盒子。而泛型就像一个印有“工具”标签的通用盒子,你可以把任何工具放进去,并且每个工具都清楚地知道它是什么,不会被误认为其他的。更进一步,如果我们说这个“工具”盒子只能放“手持工具”,这就是类型约束,它限制了你能放什么,但仍然很灵活。
示例:不使用泛型时可能遇到的问题
假设我们想创建一个简单的堆栈数据结构(Stack),可以存储任何类型的元素。
不使用泛型(使用Object类型)的实现
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
class Stack {
List _elements = []; // 使用非泛型List,可以存储任何类型的对象
void push(Object element) { // 接受任何Object类型
_elements.add(element);
}
Object pop() { // 返回Object类型,使用者需要自己进行类型转换
if (_elements.isEmpty) {
throw StateError("Stack is empty.");
}
return _elements.removeLast();
}
bool get isEmpty => _elements.isEmpty;
}
void main() {
print("--- 不使用泛型的Stack ---");
// 示例 1: 存储整数
var intStack = Stack();
intStack.push(10);
intStack.push(20);
print("弹出整数: ${intStack.pop()}"); // 运行时没问题
// 示例 2: 存储字符串
var stringStack = Stack();
stringStack.push("Hello");
stringStack.push("World");
print("弹出字符串: ${stringStack.pop()}"); // 运行时没问题
// 示例 3: 混入不同类型导致的问题
var mixedStack = Stack();
mixedStack.push(123); // 压入一个整数
mixedStack.push("Dart"); // 压入一个字符串
Object val = mixedStack.pop(); // 取出的是Object类型
print("取出: $val (类型: ${val.runtimeType})");
// 尝试将字符串强制转换为整数,编译时无警告,但运行时会报错!
try {
int number = mixedStack.pop() as int; // 预期是整数,但实际是字符串
print("成功转换为整数: $number");
} on TypeError catch (e) { // Dart 2.15 及以后为 TypeError,之前可能是 _CastError
print("运行时错误捕获: ${e.runtimeType} - ${e.toString()}");
} catch (e) {
print("其他错误: $e");
}
}
运行结果
--- 不使用泛型的Stack ---
弹出整数: 20
弹出字符串: World
取出: Dart (类型: String)
运行时错误捕获: TypeError - type 'String' is not a subtype of type 'int' in type cast
范例解释:
- 在
Stack类中,_elements是一个List(在现代Dart中,默认是List<dynamic>,这与List<Object?>类似),push方法接受Object,pop方法返回Object。 intStack和stringStack的使用看起来没问题,因为我们始终放入并取出相同类型的数据。mixedStack揭示了问题:- 我们
push(123)和push("Dart"),由于push接受Object,两者都能被接受。 - 当我们
pop()时,返回的也是Object。 - 在
try-catch块中,我们尝试将弹出的值(实际上是"Dart"字符串)强制转换为int。虽然代码在编译时没有报错(因为编译器认为一个Object类型的值在运行时可能是一个int),但在运行时,由于"Dart"确实不是一个int,Dart虚拟机抛出了TypeError(在较旧版本中可能是_CastError)。
- 我们
- 总结: 这种方式导致了类型信息的丢失,将类型错误的检测从编译时推迟到了运行时,增加了程序潜在的缺陷。这就是为什么我们需要泛型来在编译时提供更强的类型检查。
注意事项:
- 运行时类型不安全: 使用
Object(或dynamic)作为通用类型,虽然可以让代码处理各种数据,但会牺牲编译时期的类型检查,使得潜在的类型错误只能在程序运行时才能暴露,这通常是难以调试和修复的。 - 需要手动进行类型转换: 每次从
Object类型的容器中取出数据时,您都需要手动进行类型转换(如as String),这不仅增加了代码冗余,也增加了出错的风险。
泛型类(Generic Classes)
理论: 如何定义带有类型参数的类
泛型类(Generic Class)允许您定义一个类,该类在其声明中带有一个或多个类型参数。这些类型参数在类实例化时会被具体的类型替代,从而使类能够操作特定类型的数据,同时保持其内部逻辑的通用性。
定义泛型类时,在类名后面使用尖括号(< >)包含一个或多个类型参数。通常,类型参数使用单个大写字母表示,如T(Type)、E(Element)、K(Key)、V(Value)等,这是一种约定俗成的做法。
class MyGenericClass<T> {
// T 现在可以在类的成员变量、方法参数、方法返回值中使用
}
示例: Box<T>类的实现与使用
我们将使用泛型来改进之前Stack的例子,或者创建一个更简单的Box类,它可以存储任意类型的单个值。
泛型Box<T>类的实现:
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
class Box<T> { // T 是类型参数
T _value; // 类的内部成员现在是类型 T
Box(this._value); // 构造函数接收一个类型 T 的值
T getValue() { // 方法返回类型 T
return _value;
}
void setValue(T newValue) { // 方法参数是类型 T
_value = newValue;
}
String toString() {
return 'Box(value: $_value, type: ${T.runtimeType})';
}
}
void main() {
print("--- 使用泛型的Box<T> ---");
// 示例 1: 存储整数的 Box
var intBox = Box<int>(10); // 实例化时指定 T 为 int
print("intBox: $intBox");
print("获取 intBox 值: ${intBox.getValue()}");
intBox.setValue(20);
print("更新 intBox 值: ${intBox.getValue()}");
// intBox.setValue("Hello"); // 编译时报错:A value of type 'String' can't be assigned to a variable of type 'int'.
print('\n');
// 示例 2: 存储字符串的 Box
var strBox = Box<String>("Hello Dart"); // 实例化时指定 T 为 String
print("strBox: $strBox");
print("获取 strBox 值: ${strBox.getValue()}");
strBox.setValue("Hello Flutter");
print("更新 strBox 值: ${strBox.getValue()}");
// strBox.setValue(123); // 编译时报错:A value of type 'int' can't be assigned to a variable of type 'String'.
print('\n');
// 示例 3: Dart 的类型推断
// 如果构造函数参数明确,Dart 可以推断出泛型类型。
// 这里的 T 被推断为 double
var doubleBox = Box(3.14); // 相当于 Box<double>(3.14)
print("doubleBox (推断类型): $doubleBox");
print("获取 doubleBox 值: ${doubleBox.getValue()}");
print('\n');
// 示例 4: 使用 dynamic 或者 Object?
// 如果不指定泛型类型,默认会是 dynamic (Dart 2.x 历史遗留,或某些场景下)
// 或者 Object? (更符合现代Dart空安全推荐)
var dynamicBox = Box(null); // 当为null时,T 会被推断为 Object?
print("dynamicBox (推断类型): $dynamicBox");
dynamicBox.setValue("任意类型");
dynamicBox.setValue(false);
print("更新 dynamicBox 值: ${dynamicBox.getValue()}");
var objectBox = Box<Object?>(100); // 明确指定 Object?
print("objectBox: $objectBox");
objectBox.setValue("可以放字符串");
objectBox.setValue(3.14);
print("更新 objectBox 值: ${objectBox.getValue()}");
}
运行结果
--- 使用泛型的Box<T> ---
intBox: Box(value: 10, type: int)
获取 intBox 值: 10
更新 intBox 值: 20
strBox: Box(value: Hello Dart, type: String)
获取 strBox 值: Hello Dart
更新 strBox 值: Hello Flutter
doubleBox (推断类型): Box(value: 3.14, type: double)
获取 doubleBox 值: 3.14
dynamicBox (推断类型): Box(value: false, type: Object?)
更新 dynamicBox 值: false
objectBox: Box(value: 100, type: Object?)
更新 objectBox 值: 3.14
示例解释
Box<T>类定义了一个类型参数T。_value字段、Box构造函数参数、getValue返回类型以及setValue方法参数都使用了T,确保了类型的一致性。- 当我们创建
Box<int>或Box<String>时,我们明确指定了T的具体类型。 - 尝试将非指定类型的值赋给泛型字段或传递给泛型参数时,编译器会立即报错,而不是推迟到运行时,这增强了类型安全。
- Dart编译器具有强大的类型推断能力,如果构造函数参数的类型足够明确,可以省略掉
Box<double>中的<double>。 - 如果无法推断或故意不指定,泛型类型会默认推断为
Object?(在非空安全模式下可能为dynamic)。
注意事项:初始化泛型字段
- 非空安全模式下(Dart 2.12 之前): 泛型字段
T _value;默认是可空的,可能不会强制要求在构造函数中初始化。 - 空安全模式下(Dart 2.12 及之后): 泛型字段
T _value;默认是不可空的。如果您声明T _value;,则必须在构造函数中初始化它,或者将其声明为可空T? _value;。
class Box<T> {
T _value; // Error: Field '_value' has not been initialized.
// 必须像范例中 Box(this._value); 这样初始化
}
class NullableBox<T> {
T? _value; // OK, 允许为 null
NullableBox(); // 可以在构造函数中不初始化
}
- 占位符性质: 泛型类型参数T在编译时仅仅是类型的一个占位符,它本身不能直接拥有方法(除非通过类型约束)。例如,你不能直接在
Box<T>内部调用_value.toLowerCase(),因为T可能是int。必须在知道T具体是String的上下文才行,或通过类型约束T extends String。
泛型函数(Generic Functions)
理论: 如何在函数签名中引入类型参数
泛型函数(Generic Function)允许您编写一个函数,该函数可以操作各种类型的数据,而不需要为每种数据类型编写单独的函数实现。这与泛型类的目的类似,都是为了实现代码的重用和类型安全。
定义泛型函数时,类型参数放在函数名之后、圆括号()之前。
// 函数签名的类型参数
void myGenericFunction<T>(T arg) {
// T 可以在函数体内部作为类型使用
}
// 也可以有返回值
List<T> transformList<T>(List<T> input) {
// ...
return [];
}
示例: printList<T>(List<T> list)函数
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
// 泛型函数:可以打印任何类型的 List
void printList<T>(List<T> list) { // <T> 声明类型参数
print("--- 打印 List<${T.runtimeType}> ---"); // T.runtimeType 获取类型参数的实际运行时类型
for (T item in list) { // 遍历时,item 的类型就是 T
print(item);
}
}
// 另一个泛型函数:查找列表中第一个满足条件的元素
T? findFirst<T>(List<T> list, bool Function(T) test) {
for (T item in list) {
if (test(item)) {
return item;
}
}
return null; // 如果没找到,返回 null
}
// 另一个泛型函数:将一个 List 映射到另一个 List (稍微复杂一点的例子)
List<R> mapList<T, R>(List<T> inputList, R Function(T) mapper) {
List<R> result = [];
for (T item in inputList) {
result.add(mapper(item));
}
return result;
}
void main() {
print("--- 泛型函数示例 ---");
// 示例 1: 使用 printList 打印整数列表
List<int> numbers = [1, 2, 3, 4, 5];
printList<int>(numbers); // 明确指定类型参数 <int>
// Dart 的类型推断很强大,通常可以省略 <int>
printList(numbers); // 相当于 printList<int>(numbers);
print('\n');
// 示例 2: 使用 printList 打印字符串列表
List<String> names = ["Alice", "Bob", "Charlie"];
printList<String>(names); // 明确指定类型参数 <String>
printList(names); // 相当于 printList<String>(names);
print('\n');
// 示例 3: 使用 findFirst 查找第一个偶数
int? firstEven = findFirst<int>(numbers, (i) => i % 2 == 0);
// Dart 再次推断类型,可以省略 <int>
int? firstEvenInferred = findFirst(numbers, (i) => i % 2 == 0);
print("第一个偶数: $firstEven (推断: $firstEvenInferred)");
List<String> fruits = ["Apple", "Banana", "Cherry"];
String? fruitStartingWithB = findFirst(fruits, (s) => s.startsWith('B'));
print("第一个以 'B' 开头的水果: $fruitStartingWithB");
print('\n');
// 示例 4: 使用 mapList 转换列表
List<int> lengths = mapList<String, int>(names, (name) => name.length);
print("name 列表转换为长度: $lengths"); // [5, 3, 7]
List<double> doubledNumbers = mapList(numbers, (num) => num * 2.0); // 推断 T 为 int, R 为 double
print("numbers 列表元素翻倍: $doubledNumbers"); // [2.0, 4.0, 6.0, 8.0, 10.0]
// 注意:尝试传入错误类型会导致编译错误
// printList<String>(numbers); // 编译时报错:The argument type 'List<int>' can't be assigned to the parameter type 'List<String>'.
}
运行结果
--- 泛型函数示例 ---
--- 打印 List<int> ---
1
2
3
4
5
--- 打印 List<int> ---
1
2
3
4
5
--- 打印 List<String> ---
Alice
Bob
Charlie
--- 打印 List<String> ---
Alice
Bob
Charlie
第一个偶数: 2 (推断: 2)
第一个以 'B' 开头的水果: Banana
name 列表转换为长度: [5, 3, 7]
numbers 列表元素翻倍: [2.0, 4.0, 6.0, 8.0, 10.0]
范例解释
printList<T>(List<T> list)函数通过<T>声明了一个类型参数。这使得它能够接受任何List<T>类型的参数。- 函数内部,T类型被用于
item的类型声明,确保了在循环中对元素的操作是类型安全的。 findFirst展示了如何将泛型与函数类型一起使用,test回调函数的参数类型也被泛型 T 约束。mapList展示了可以定义多个类型参数<T, R>,实现更灵活的类型转换。- 当调用泛型函数时,您可以明确指定类型参数(如
printList<int>(numbers)),但是Dart的类型推断能力通常能让您省略它们(如printList(numbers)),编译器会自动根据传入的参数类型确定T的具体类型。 - 如果传入的参数类型与期望的泛型参数类型不匹配,编译器会立即报错,提供了强大的类型安全保证。
注意事项
- 类型推断(Type Inference): Dart编译器非常擅长推断泛型函数的类型参数。大多数情况下,您不需要显式地指定它们。只有当编译器无法推断出意图时,才需要手动指定(例如,当参数是
dynamic或Object?且您希望一个更具体的类型时)。 - 方法与局部函数: 泛型不仅可以用于顶层函数,也可以用于类的方法和局部函数。
- 泛型参数的范围: 泛型类型参数T的范围仅限于定义它的函数体。
泛型接口(Generic Interfaces)
理论: 在接口(或抽象类)中使用泛型
Dart中并没有像Java那样的interface关键字来显式定义接口。在Dart中,所有的类都隐式地定义了一个接口。因此,当我们在Dart中谈论“泛型接口”时,我们通常指的是带有泛型类型参数的抽象类(abstract class)或一个普通类,并通过implements或extends关键字来使用它作为接口或基类。
通过在抽象类(或普通类)中引入泛型类型参数,我们可以定义一个通用的契约,该契约适用于多种数据类型。实现或继承这个泛型接口/抽象类的类,必须提供这些类型参数的具体实现。
定义带有泛型的抽象类/接口:
abstract class Cache<K, V> { // 定义 K 和 V 两个类型参数
V? get(K key);
void set(K key, V value);
bool contains(K key);
void clear();
}
示例: Cache<K, V>接口
我们将定义一个泛型抽象类Cache<K, V>,并实现一个具体的InMemoryCache类。
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
// 定义一个泛型抽象类 (作为接口使用)
abstract class Cache<K, V> {
V? get(K key); // 根据键 K 获取值 V
void set(K key, V value); // 设置键 K 和值 V
bool contains(K key); // 检查是否包含键 K
void clear(); // 清空缓存
}
// 实现一个具体的缓存类
class InMemoryCache<K, V> implements Cache<K, V> {
final Map<K, V> _storage = {}; // 内部使用泛型 Map
V? get(K key) {
return _storage[key];
}
void set(K key, V value) {
_storage[key] = value;
}
bool contains(K key) {
return _storage.containsKey(key);
}
void clear() {
_storage.clear();
}
String toString() {
return 'InMemoryCache(size: ${_storage.length}, keys: ${_storage.keys.join(', ')})';
}
}
void main() {
print("--- 泛型接口 (抽象类) 示例 ---");
// 示例 1: 创建一个缓存 String 到 int 的对象
Cache<String, int> userAgeCache = InMemoryCache<String, int>();
userAgeCache.set("Alice", 30);
userAgeCache.set("Bob", 25);
print("userAgeCache: $userAgeCache");
print("Alice 的年龄: ${userAgeCache.get("Alice")}");
print("Bob 是否在缓存中: ${userAgeCache.contains("Bob")}");
print("Charlie 是否在缓存中: ${userAgeCache.contains("Charlie")}");
// userAgeCache.set("Charlie", "thirty"); // 编译时报错:The argument type 'String' can't be assigned to the parameter type 'int'.
print('\n');
// 示例 2: 创建一个缓存 int 到 String 的对象
Cache<int, String> productCache = InMemoryCache<int, String>();
productCache.set(101, "Laptop");
productCache.set(102, "Mouse");
print("productCache: $productCache");
print("产品 101: ${productCache.get(101)}");
// productCache.set("103", "Keyboard"); // 编译时报错:The argument type 'String' can't be assigned to the parameter type 'int'.
print('\n');
// 示例 3: 使用 `var` 进行类型推断
// Dart 会根据 InMemoryCache<String, double> 推断出 cache 变量也是 Cache<String, double> 类型
var priceCache = InMemoryCache<String, double>();
priceCache.set('Apple', 1.99);
priceCache.set('Banana', 0.79);
print('Price of Apple: ${priceCache.get('Apple')}');
priceCache.clear();
print('priceCache cleared: $priceCache');
}
运行结果:
--- 泛型接口 (抽象类) 示例 ---
userAgeCache: InMemoryCache(size: 2, keys: Alice, Bob)
Alice 的年龄: 30
Bob 是否在缓存中: true
Charlie 是否在缓存中: false
productCache: InMemoryCache(size: 2, keys: 101, 102)
产品 101: Laptop
Price of Apple: 1.99
priceCache cleared: InMemoryCache(size: 0, keys: )
范例解释
abstract class Cache<K, V>定义了一个泛型接口,它有两个类型参数K(Key)和V(Value)。缓存操作(get,set等)都使用这些类型参数来保证类型安全。InMemoryCache<K, V> implements Cache<K, V>实现了这个泛型接口。注意,在实现类中也必须传递泛型类型参数。_storage成员变量被声明为Map<K, V>,这确保了内部存储与接口定义的类型参数一致。- 当我们创建
userAgeCache(Cache<String, int>)时,它只接受String类型的键和int类型的值。尝试存储不同类型的值会导致编译时错误。 - 同样,
productCache(Cache<int, String>)只接受int类型的键和String类型的值。 - 通过这种方式,我们可以在编译时就捕捉到类型错误,避免了运行时的问题,并提供了极大的灵活性,可以为任何键值对类型创建缓存。
注意事项: Dart中没有专门的interface关键字,通常通过抽象类实现
- 隐式接口: 在Dart中,每个类都隐式地定义了一个接口。这意味着你可以用
implements关键字来实现任何类的(隐式)接口。 - 抽象类作为接口: 然而,当我们需要定义一个只包含抽象方法(没有实现)的契约,并且这个契约需要是泛型的时,抽象类(
abstract class)是更常用和更清晰的方式。它允许您定义抽象成员,强制实现类提供具体实现。 - 泛型参数的传递: 当一个类实现或继承一个泛型父类/接口时,它必须传递泛型参数。可以直接传递父类的类型参数(如
class Child<T> implements Parent<T>),也可以指定具体的类型(如class IntChild implements Parent<int>)。
泛型类型约束(Generic Type Constraints) - extends
理论: 限制类型参数的范围,确保类型安全和方法可用性
默认情况下,泛型类型参数T可以是任何类型(它实际上被隐式地约束为Object?)。但是,有时我们希望对泛型类型施加更严格的限制,以便我们可以在泛型代码内部安全地调用该类型所特有的方法,或者确保类型满足特定的行为。
这就是**类型约束(Type Constraints)**的作用。在Dart中,您可以使用extends关键字来指定一个类型参数必须是某个特定类型或其子类型。
class MyClass<T extends SomeType> {
// 在这里,T 保证是SomeType或其子类型
// 所以我们可以安全地调用 SomeType 的方法
}
void myGenericFunction<T extends SomeType>(T arg) {
// 在这里,arg 保证是SomeType或其子类型
// 所以我们可以安全地调用 SomeType 的方法
}
这意味着T将拥有SomeType的所有公共方法和属性。
示例: Processor<T extends num>
假设我们想创建一个处理器类,它只能处理数字类型(int或double)。我们可以使用num作为类型约束。num是int和double的父类。
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
// 定义一个处理器,它只能处理数字类型 (int 或 double)
// <T extends num> 表示 T 必须是 num 类型或其子类型
class NumericProcessor<T extends num> {
final T _data;
NumericProcessor(this._data);
// 我们可以安全地调用 num 类型T 的方法,例如 add
T add(T other) {
// 这里如果 T 是 int, 结果可能是 double (例如 5 + 3.0 = 8.0)
// Dart 中 num 的 + 运算符返回 num
// 为了让返回类型更明确,我们可以进行类型转换或使用具体的运算
// 简单起见,这里假设 T 总是可以进行加法运算
// 实际上,为了返回 T,需要更巧妙的实现,或者返回 num
// 如果我们想返回 T 类型,需要确保 T 具有 + 运算符且返回 T
// 很多时候,我们会返回 num,然后让外部去判断
// 简化处理,直接返回 num.
// return (_data + other) as T; // 编译时可能有警告,运行时可能出错
return (_data + other) as T; // Dart 2.12+ 编译器会智能处理,不会报错,但如果结果类型不兼容 T 仍运行时错
}
// 我们可以访问 num 类型的属性
bool get isNegative => _data.isNegative;
double toDouble() {
return _data.toDouble();
}
num get numberData => _data; // 将 T 赋值给 num 是安全的
}
// 另一个泛型函数,也带有类型约束
void printSum<T extends num>(T a, T b) {
// 因为 T extends num,所以我们可以放心地执行加法操作
// 注意,(a + b) 的结果类型是 num
print("Sum of $a and $b: ${a + b} (Type: ${(a + b).runtimeType})");
}
class Animal {
void speak() => print('Generic animal sound');
}
class Dog extends Animal {
void speak() => print('Woof!');
void fetch() => print('Fetching the ball...');
}
// 约束 T 必须是 Animal 或其子类
class AnimalShelter<T extends Animal> {
final List<T> _animals = [];
void admit(T animal) {
_animals.add(animal);
}
void dailyRoutine() {
print('--- Daily Routine ---');
for (var animal in _animals) {
animal.speak(); // 可以安全地调用 Animal 的方法
// animal.fetch(); // 编译时报错:The method 'fetch' isn't defined for the type 'Animal'.
// 因为 T 只是 Animal,不能保证所有的 Animal 都有 fetch 方法
}
}
T getFirstAnimal() => _animals.first;
}
void main() {
print("--- 泛型类型约束 (extends) 示例 ---");
// 示例 1: 使用 int 类型的 NumericProcessor
var intProcessor = NumericProcessor<int>(5);
print("intProcessor Data: ${intProcessor.numberData}");
print("intProcessor.isNegative: ${intProcessor.isNegative}");
// var sumInt = intProcessor.add(3); // (int + int) 结果为 int
// print("intProcessor sum: $sumInt (Type: ${sumInt.runtimeType})");
// 注意:在 Dart 中,int + int 结果是 int, 但 int + double 结果是 double.
// num 的 '+' 运算符的返回类型是 num。
// 为了 avoid as T 转换可能产生的运行时错误,建议返回 num
num sumInt = intProcessor._data + 3; // safe and clear
print("intProcessor sum: $sumInt (Type: ${sumInt.runtimeType})");
// 示例 2: 使用 double 类型的 NumericProcessor
var doubleProcessor = NumericProcessor<double>(10.5);
print("doubleProcessor Data: ${doubleProcessor.numberData}");
print("doubleProcessor.isNegative: ${doubleProcessor.isNegative}");
// double sumDouble = doubleProcessor.add(2.3); // (double + double) 结果为 double
// print("doubleProcessor sum: $sumDouble (Type: ${sumDouble.runtimeType})");
num sumDouble = doubleProcessor._data + 2.3;
print("doubleProcessor sum: $sumDouble (Type: ${sumDouble.runtimeType})");
// 示例 3: 在函数中使用类型约束
printSum(10, 20); // T 被推断为 int
printSum(1.5, 3.7); // T 被推断为 double
printSum(10, 5.5); // T 被推断为 num
// printSum("Hello", "World"); // 编译时报错:The argument type 'String' can't be assigned to the parameter type 'num'.
// 因为 String 不 extends num
print('\n');
// 示例 4: 使用 AnimalShelter
var dogShelter = AnimalShelter<Dog>(); // 只能收容 Dog 及其子类
dogShelter.admit(Dog());
// dogShelter.admit(Animal()); // 编译时报错:The argument type 'Animal' can't be assigned to the parameter type 'Dog'.
// 因为 Dog 是 Animal 的子类型,但 T 是 Dog,所以这里不能放 Animal
print('First animal in dog shelter: ${dogShelter.getFirstAnimal().runtimeType}');
dogShelter.dailyRoutine();
var animalShelter = AnimalShelter<Animal>(); // 可以收容任何 Animal
animalShelter.admit(Animal());
animalShelter.admit(Dog());
print('First animal in generic shelter: ${animalShelter.getFirstAnimal().runtimeType}');
animalShelter.dailyRoutine();
}
运行结果
--- 泛型类型约束 (extends) 示例 ---
intProcessor Data: 5
intProcessor.isNegative: false
intProcessor sum: 8 (Type: int)
doubleProcessor Data: 10.5
doubleProcessor.isNegative: false
doubleProcessor sum: 12.8 (Type: double)
Sum of 10 and 20: 30 (Type: int)
Sum of 1.5 and 3.7: 5.2 (Type: double)
Sum of 10 and 5.5: 15.5 (Type: double)
--- Daily Routine ---
Woof!
First animal in generic shelter: Animal
--- Daily Routine ---
Generic animal sound
Woof!
范例解释
NumericProcessor<T extends num>: 我们定义了一个泛型类,其类型参数T被约束为num或其子类型。这保证了在NumericProcessor内部,任何对_data值的操作(例如_data.isNegative或_data.toDouble())都是有效的,因为num类型提供了这些方法。- 当尝试创建
NumericProcessor<String>("hello")时,编译器会报错,因为String不extends num。这提供了编译时期的类型安全。 printSum<T extends num>函数也使用了类型约束,可以确保其参数a和b都是数字类型,从而可以安全地执行a + b操作。AnimalShelter<T extends Animal>示例展示了如何约束泛型为特定的基类。这允许我们在泛型代码中调用基类Animal的方法speak()。但是,您不能调用子类Dog独有的fetch()方法,因为T可能只是Animal。
注意事项
- 编译时检查: 类型约束的主要好处是在编译时提供更强的类型检查,确保您调用的方法或访问的属性对于约束类型是有效的。
- 多重约束(Dart不支持): 与其他语言(如Java)不同,Dart不直接支持在一个类型参数上指定多个
extends约束。例如,您不能写class Foo<T extends Bar & Baz>。如果需要类似的功能,通常需要创建一个新的抽象类或mixin来组合这些行为。 - 默认约束: 如果泛型类型参数没有明确的
extends约束,它会隐式地被视为T extends Object?。这意味着您可以对任何未约束的泛型类型调用Object?的方法,如toString()或runtimeType。 - 返回值类型: 当使用
extends约束进行运算时(如num类型的加法),运算结果的实际类型可能会比泛型参数T更宽泛(例如int + double得到double,而int泛型可能期望int)。需要谨慎处理返回值类型,可能需要返回更宽泛的类型(如num),或者在特定场景下接受潜在的运行时类型转换错误。
泛型与运行时类型检查(Generics and Runtime Type Checks)
理论: 泛型在运行时会被擦除(Type Erasure),以及如何进行运行时类型检查
Dart语言在运行时具有一定的类型信息(reified generics),但它并非完全的“重新化(reified)”。这意味着 Dart 在编译时会保留泛型类型信息,但在运行时,在某些场景下,具体类的类型参数会被擦除,只保留其原始类型。
类型擦除(Type Erasure) 意味着什么?
在Dart中,当你检查一个对象的运行时类型时(例如使用runtimeType),它会告诉你对象的实际类型,但对于泛型类型,它可能会告诉你它的“基类型”而不是具体的泛型参数。
例如: List<int>在运行时可能被视为_GrowableList<int>或类似的东西,并且其中的<int>信息在某些情况下是可用的。但是,如果你将List<int>赋值给一个List变量(没有泛型),那么具体的<int>信息就可能丢失。
然而,重要的是: Dart的泛型比Java或C#等语言中的“类型擦除”表现得更好。Dart的runtimeType操作符和is、as表达式在很大程度上能够保留和利用泛型类型信息。所以,严格来说,Dart的泛型是比纯粹的类型擦除要“重新化”得多,它允许在运行时获取泛型类型参数。
如何在运行时进行泛型相关的类型检查?
- is关键字(
Type Checking):object is Type: 检查object是否是Type或其子类型。对于泛型,它会检查泛型参数本身。 - as关键字(
Type Casting):object as Type: 将object转换为Type。如果转换失败,会抛出TypeError(在旧版本中可能为_CastError)。对于泛型,它会尝试保留并验证泛型参数。 runtimeType属性:object.runtimeType: 返回对象的实际运行时类型。这包括泛型类型参数的信息。
示例: is和as关键字配合泛型
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
class Container<T> {
T value;
Container(this.value);
String toString() {
return 'Container<$T>(value: $value)';
}
}
void main() {
print("--- 泛型与运行时类型检查 ---");
// 创建泛型实例
var intContainer = Container<int>(10);
var stringContainer = Container<String>("Hello");
var dynamicContainer = Container(true); // T 被推断为 bool
var objectContainer = Container<Object?>(null);
// 1. 使用 runtimeType 检查泛型类型
print("intContainer.runtimeType: ${intContainer.runtimeType}"); // Output: Container<int>
print("stringContainer.runtimeType: ${stringContainer.runtimeType}"); // Output: Container<String>
print("dynamicContainer.runtimeType: ${dynamicContainer.runtimeType}"); // Output: Container<bool>
print("objectContainer.runtimeType: ${objectContainer.runtimeType}"); // Output: Container<Object?>
print('\n');
// 2. 使用 'is' 关键字检查泛型类型
print("intContainer is Container<int>: ${intContainer is Container<int>}"); // true
print("intContainer is Container<num>: ${intContainer is Container<num>}"); // true (因为 int extends num)
print("intContainer is Container<double>: ${intContainer is Container<double>}"); // false
print("intContainer is Container: ${intContainer is Container}"); // true (只检查原始类型)
print("stringContainer is Container<String>: ${stringContainer is Container<String>}"); // true
print("stringContainer is Container<Object>: ${stringContainer is Container<Object>}"); // true (因为 String extends Object)
print("stringContainer is Container<int>: ${stringContainer is Container<int>}"); // false
print('\n');
// 3. 使用 'as' 关键字进行类型转换
Container<Object> objectGenericContainer = intContainer as Container<Object>; // int implicitly implements Object
print("intContainer as Container<Object>: $objectGenericContainer");
print("类型转换为 Object 后,runtimeType: ${objectGenericContainer.runtimeType}"); // runtimeType 仍然是 Container<int>
// 尝试错误的转换,会导致运行时错误
try {
Container<double> doubleGenericContainer = intContainer as Container<double>; // 编译时无警告,运行时会抛出 TypeError
print("intContainer as Container<double>: $doubleGenericContainer");
} on TypeError catch (e) {
print("捕获运行时错误 (TypeError): ${e.toString()}");
}
print('\n');
// 4. 泛型类型擦除的误解与 Dart 的实际行为
// 在 Dart 中,List<int> 和 List<String> 在运行时是不同的类型!
List<int> listOfInt = [1, 2, 3];
List<String> listOfString = ["a", "b"];
List<dynamic> listOfDynamic = [1, "b", true];
print("listOfInt.runtimeType: ${listOfInt.runtimeType}"); // _GrowableList<int>
print("listOfString.runtimeType: ${listOfString.runtimeType}"); // _GrowableList<String>
print("listOfDynamic.runtimeType: ${listOfDynamic.runtimeType}");// _GrowableList<dynamic>
print("listOfInt is List<int>: ${listOfInt is List<int>}"); // true
print("listOfString is List<String>: ${listOfString is List<String>}"); // true
// 重要的行为:
// List<int> 不是 List<Object>
// List<int> 不是 List<double>
print("listOfInt is List<Object>: ${listOfInt is List<Object>}"); // false,这与Java/C#不同,Dart保留了类型信息
print("listOfInt is List<num>: ${listOfInt is List<num>}"); // false,尽管 int extends num,但 List<int> 并非 List<num>
// 这是因为 List<int> 在运行时是针对 int 的,不能直接
// 保证它可以安全地存储任何 num 类型的元素(如 double)。
// 正确的处理方式:
// 如果你需要一个可以存储 int 和 double 的列表,你需要声明 List<num>
List<num> listOfNum = [1, 2.0, 3];
print("listOfNum.runtimeType: ${listOfNum.runtimeType}");
print("listOfInt is List<num>: ${listOfInt is List<num>}"); // 仍然是 false
// 但你可以将 List<int> 复制到 List<num>,因为 int 是 num 的子类型
List<num> anotherListOfNum = List<num>.from(listOfInt);
print("anotherListOfNum from listOfInt: $anotherListOfNum");
}
运行结果
--- 泛型与运行时类型检查 ---
intContainer.runtimeType: Container<int>
stringContainer.runtimeType: Container<String>
dynamicContainer.runtimeType: Container<bool>
objectContainer.runtimeType: Container<Object?>
intContainer is Container<int>: true
intContainer is Container<num>: true
intContainer is Container<double>: false
intContainer is Container: true
stringContainer is Container<String>: true
stringContainer is Container<Object>: true
stringContainer is Container<int>: false
intContainer as Container<Object>: Container<int>(value: 10)
类型转换为 Object 后,runtimeType: Container<int>
捕获运行时错误 (TypeError): type 'Container<int>' is not a subtype of type 'Container<double>' in type cast
listOfInt.runtimeType: _GrowableList<int>
listOfString.runtimeType: _GrowableList<String>
listOfDynamic.runtimeType: _GrowableList<dynamic>
listOfInt is List<int>: true
listOfString is List<String>: true
listOfInt is List<Object>: false
listOfInt is List<num>: false
listOfNum.runtimeType: _GrowableList<num>
anotherListOfNum from listOfInt: [1, 2, 3]
范例解释
runtimeType准确性:Dart的runtimeType属性保留了泛型类型参数信息。intContainer.runtimeType确实返回Container<int>,而不是简单的Container。这表明Dart在运行时确实“知道”泛型参数。is对泛型参数的检查:intContainer is Container<int>为true,符合预期。intContainer is Container<num>也为true,因为int是num的子类型。Dart的is运算符在这里是协变的(covariant)。intContainer is Container<double>为false,因为int不是double。intContainer is Container(不带泛型参数)为true,这只检查了原始类。
as类型转换:intContainer as Container<Object>成功,因为int可以向上转型为Object。转换后objectGenericContainer的运行时类型仍然是Container<int>,说明原始对象的类型参数信息没有丢失,只是我们以更宽泛的Container<Object>视角来引用它。intContainer as Container<double>导致运行时TypeError,因为int不能直接成为double。
- 集合泛型与
is运算符的细微之处:List<int>和List<String>在运行时是不同的类型(_GrowableList<int>和_GrowableList<String>)。- 关键点:
List<int> is List<Object>和List<int> is List<num>都为false!这与Java/C#的行为不同。在Dart中,即使int是Object或num的子类型,List<int>默认情况下也不会被认为是List<Object>或List<num>的子类型。这是Dart类型系统的一个设计选择,叫做“不变性(invariance)”或“严格类型协变(strict type covariance)”的应用,尤其是在集合类型上。主要原因是,如果List<int>能赋值给List<num>,那么List<num>就可能添加一个double进去,导致原List<int>存储了非int类型,这会破坏类型安全。 - 要实现将
List<int>的元素视为num,你需要创建一个新的List<num>并复制元素,例如List<num>.from(listOfInt)。
注意事项: 编译时类型与运行时类型
- 编译时安全但运行时可能失败:虽然泛型提供了强大的编译时类型检查,但
as强制类型转换仍然可能在运行时失败,如果实际对象与目标类型不兼容。 - Dart的泛型比纯粹的“类型擦除”更有信息:不要将Dart的泛型行为与Java等语言的纯类型擦除完全等同。Dart的
runtimeType通常能告诉您泛型参数。 is运算符对泛型集合的特殊性:记住List<SubType> is List<SuperType>在Dart中通常是false(除了List<FutureOr<T>>和List<T>之间的特殊情况)。这是为了确保集合的类型安全。如果你需要处理一个包含int和double的列表,应该直接声明为List<num>。
泛型在集合中的应用(Generics in Collections)
理论: Dart核心库中List<E>, Map<K, V>, Set<E>等如何使用泛型
Dart的核心集合库是泛型使用的最佳例子。List、Map和Set是最常用的集合类型,它们都利用泛型来提供类型安全、代码重用和效率。
List<E>: 表示一个元素的有序集合,其中E是集合中元素的类型。Map<K, V>: 表示一个键值对的集合,其中K是键的类型,V是值的类型。Set<E>: 表示一个元素的无序集合,其中E是集合中元素的类型,且集合中的元素是唯一的。 这些泛型集合在声明时就明确了它们将存储什么类型的元素,从而在编译时捕获类型不匹配的错误。
示例: 声明和操作各种泛型集合
// main.dart
// Dart SDK Version: >=2.12.0 <3.0.0
void main() {
print("--- 泛型在集合中的应用 ---");
// 1. List<E> (列表)
print("\n--- List<E> ---");
// 声明一个存储整数的列表
List<int> numbers = [10, 20, 30];
print("numbers: $numbers, Type: ${numbers.runtimeType}");
numbers.add(40);
// numbers.add("hello"); // 编译时报错:The argument type 'String' can't be assigned to the parameter type 'int'.
print("numbers (添加后): $numbers");
int firstNum = numbers[0]; // 返回类型直接就是 int,无需转换
print("firstNum: $firstNum");
// 也可以使用 var 配合类型推断
var names = <String>['Alice', 'Bob', 'Charlie']; // <String> 明确指定泛型
print("names: $names, Type: ${names.runtimeType}");
names.add('David');
// names.add(123); // 编译时报错
print("names (添加后): $names");
// 不指定泛型时 (仅在旧代码或特定场景下,非空安全默认推断 `List<dynamic>` 或 `List<Object?>`)
// 现代 Dart 强烈建议指定泛型类型
List anything = [1, 'hello', true]; // Dart 2.x 默认是 List<dynamic>
print("anything: $anything, Type: ${anything.runtimeType}");
anything.add({"key": "value"});
print("anything (添加后): $anything");
// 从 List<num> 到 List<int> 是不安全的,因为 List<num> 可以包含 double
// var numList = <num>[1, 2.0, 3];
// List<int> intListFromNum = numList; // 编译时报错:A value of type 'List<num>' can't be assigned to a variable of type 'List<int>'.
// Dart 集合是类型不变的 (invariant)
// 2. Map<K, V> (映射/字典)
print("\n--- Map<K, V> ---");
// 声明一个键为 String,值为 int 的映射
Map<String, int> ages = {
'Alice': 30,
'Bob': 25,
};
print("ages: $ages, Type: ${ages.runtimeType}");
ages['Charlie'] = 35;
// ages['David'] = "forty"; // 编译时报错:The argument type 'String' can't be assigned to the parameter type 'int'.
// ages[123] = 50; // 编译时报错:The argument type 'int' can't be assigned to the parameter type 'String'.
print("ages (添加后): $ages");
int aliceAge = ages['Alice']!; // 返回类型直接是 int?,使用 ! 确定非空
print("Alice's age: $aliceAge");
// 使用 var 配合类型推断
var userSettings = <String, bool>{
'darkMode': true,
'notifications': false,
};
print("userSettings: $userSettings, Type: ${userSettings.runtimeType}");
// 3. Set<E> (集合)
print("\n--- Set<E> ---");
// 声明一个存储字符串的集合
Set<String> uniqueColors = {'red', 'green', 'blue'};
print("uniqueColors: $uniqueColors, Type: ${uniqueColors.runtimeType}");
uniqueColors.add('yellow');
uniqueColors.add('red'); // 重复元素不会被添加
// uniqueColors.add(123); // 编译时报错
print("uniqueColors (添加后): $uniqueColors");
bool hasGreen = uniqueColors.contains('green');
print("Contains 'green': $hasGreen");
// 使用 var 配合类型推断
var productIds = <int>{101, 203, 405};
print("productIds: $productIds, Type: ${productIds.runtimeType}");
// 4. 类型推断和字面量
// 编译器可以根据字面量自动推断泛型类型
var inferredList = [1, 2, 3]; // List<int>
print("inferredList: ${inferredList.runtimeType}");
var inferredMap = {'name': 'Dart', 'version': 2.19}; // Map<String, Object> (因为 value 类型是 String 和 double)
print("inferredMap: ${inferredMap.runtimeType}");
var inferredSet = {'apple', 'banana', 'apple'}; // Set<String>
print("inferredSet: ${inferredSet.runtimeType}");
// 注意:如果字面量中没有提供足够的类型信息,会推断为最宽泛的类型
var mixedList = [1, "hello"]; // List<Object>
print("mixedList: ${mixedList.runtimeType}");
}
运行结果
--- 泛型在集合中的应用 ---
--- List<E> ---
numbers: [10, 20, 30], Type: _GrowableList<int>
numbers (添加后): [10, 20, 30, 40]
firstNum: 10
names: [Alice, Bob, Charlie], Type: _GrowableList<String>
names (添加后): [Alice, Bob, Charlie, David]
anything: [1, hello, true], Type: _GrowableList<Object>
anything (添加后): [1, hello, true, {key: value}]
--- Map<K, V> ---
ages: {Alice: 30, Bob: 25}, Type: _InternalLinkedHashMap<String, int>
ages (添加后): {Alice: 30, Bob: 25, Charlie: 35}
Alice's age: 30
userSettings: {darkMode: true, notifications: false}, Type: _InternalLinkedHashMap<String, bool>
--- Set<E> ---
uniqueColors: {red, green, blue}, Type: _Set<String>
uniqueColors (添加后): {red, green, blue, yellow}
Contains 'green': true
productIds: {101, 203, 405}, Type: _Set<int>
inferredList: _GrowableList<int>
inferredMap: _InternalLinkedHashMap<String, Object>
inferredSet: _Set<String>
mixedList: _GrowableList<Object>
范例解释
List<int> numbers: 声明一个只能存储整数的列表。尝试添加字符串numbers.add("hello")会导致编译时错误,这正是泛型带来的类型安全。访问numbers[0]时,返回值的类型直接就是int,无需手动强制转换。Map<String, int> ages: 声明一个键为String,值为int的映射。对键或值类型不匹配的赋值操作都会被编译器捕获。Set<String> uniqueColors: 声明一个只能存储字符串的集合。同理,类型不匹配的添加操作会被阻止。- 类型推断: Dart在使用集合字面量时具有强大的类型推断能力。例如
var inferredList = [1, 2, 3];会被自动推断为List<int>。如果字面量中存在多种类型(如[1, "hello"]),Dart 会推断出最公共的父类型,例如List<Object>。 - 集合的协变性(Covariance) 和不变性(Invariance):
- 在Dart中,
List<SubType>不能直接赋值给List<SuperType>(例如List<int>不能直接赋值给List<num>)。这是因为Dart的集合类型默认是不变的,这可以防止潜在的运行时类型错误(例如,如果List<int>被视为List<num>,你可能会无意中向其添加double,从而破坏其int泛型)。 - 如果你需要将一个具体类型的列表转换为一个更通用的类型,通常需要创建一个新的集合并复制元素,如
List<num>.from(listOfInt)。
- 在Dart中,
注意事项: 不指定泛型类型时的行为
- 在现代Dart(空安全时代,SDK >=2.12.0) 中,如果集合在声明时没有明确指定泛型类型,通常会根据上下文或元素推断为
List<Object?>、Map<Object?, Object?>、Set<Object?>或List<dynamic>等。 - 不指定泛型类型(例如
List list = []) 会使集合失去类型安全的好处,因为它可以存储任何类型的对象,这意味着您需要更多的运行时检查(is, as),并失去了编译时错误捕获的能力。 - 最佳实践: 始终为您的集合指定明确的泛型类型(如
List<String> userNames = []或var userNames = <String>[])。这不仅能提高代码的类型安全,还能提高可读性,并允许Dart编译器进行更有效的优化。 - 对于空集合的初始化
[]、{}建议显式声明泛型类型:var myList = <int>[];或List<int> myList = [];以避免默认为List<dynamic>或Map<dynamic, dynamic>。