泛型

generics

泛型的基本概念与作用

理论: 什么是泛型,为什么我们需要泛型?

泛型(Generics)是一种编程语言特性,允许您编写可以处理多种数据类型的代码,而不是预先指定单一的具体数据类型。它使得代码更加灵活、可重用,并能在编译时提供更强的类型安全性。

为什么要使用泛型?

  1. 代码重用(Code Reusability):
  • 如果没有泛型,您可能需要为每种数据类型编写几乎相同的代码。例如,一个存储整数的列表和一个存储字符串的列表,它们的底层逻辑可能完全相同,但由于类型不同,您不得不创建两个独立的实现。泛型允许您编写一个通用的“列表”实现,可以存储任何类型的数据。
  1. 类型安全(Type Safety):
  • 在编译时捕获类型不匹配的错误,而不是在运行时才发现。没有泛型时,我们有时会使用Object类型来处理各种数据,但这会丧失类型信息,导致运行时类型转换错误(CastError)。泛型通过在编译时强制类型检查,保护我们免受此类错误的影响。
  1. 可读性与清晰性(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方法接受Objectpop方法返回Object
  • intStackstringStack的使用看起来没问题,因为我们始终放入并取出相同类型的数据。
  • 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编译器非常擅长推断泛型函数的类型参数。大多数情况下,您不需要显式地指定它们。只有当编译器无法推断出意图时,才需要手动指定(例如,当参数是dynamicObject?且您希望一个更具体的类型时)。
  • 方法与局部函数: 泛型不仅可以用于顶层函数,也可以用于类的方法和局部函数。
  • 泛型参数的范围: 泛型类型参数T的范围仅限于定义它的函数体。

泛型接口(Generic Interfaces)

理论: 在接口(或抽象类)中使用泛型

Dart中并没有像Java那样的interface关键字来显式定义接口。在Dart中,所有的类都隐式地定义了一个接口。因此,当我们在Dart中谈论“泛型接口”时,我们通常指的是带有泛型类型参数的抽象类(abstract class)或一个普通类,并通过implementsextends关键字来使用它作为接口或基类。

通过在抽象类(或普通类)中引入泛型类型参数,我们可以定义一个通用的契约,该契约适用于多种数据类型。实现或继承这个泛型接口/抽象类的类,必须提供这些类型参数的具体实现。

定义带有泛型的抽象类/接口:

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>

假设我们想创建一个处理器类,它只能处理数字类型(intdouble)。我们可以使用num作为类型约束。numintdouble的父类。

// 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")时,编译器会报错,因为Stringextends 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的泛型是比纯粹的类型擦除要“重新化”得多,它允许在运行时获取泛型类型参数。

如何在运行时进行泛型相关的类型检查?

  1. is关键字(Type Checking): object is Type: 检查object是否是Type或其子类型。对于泛型,它会检查泛型参数本身。
  2. as关键字(Type Casting): object as Type: 将object转换为Type。如果转换失败,会抛出TypeError(在旧版本中可能为_CastError)。对于泛型,它会尝试保留并验证泛型参数。
  3. 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中,即使intObjectnum的子类型,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>之间的特殊情况)。这是为了确保集合的类型安全。如果你需要处理一个包含intdouble的列表,应该直接声明为List<num>

泛型在集合中的应用(Generics in Collections)

理论: Dart核心库中List<E>, Map<K, V>, Set<E>等如何使用泛型

Dart的核心集合库是泛型使用的最佳例子。ListMapSet是最常用的集合类型,它们都利用泛型来提供类型安全、代码重用和效率。

  • 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(空安全时代,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>