在 Dart 中,泛型(Generics)是一种用于编写灵活、可重用代码的强大工具。通过泛型,我们可以编写能够处理多种数据类型的类、方法或接口,而无需为每种数据类型重复编写代码。泛型的核心理念是允许在编写代码时使用占位符类型,然后在实际使用时提供具体的类型。本文将详细介绍 Dart 中的泛型,包括集合(List、Set、Map)的泛型接口,以及自定义泛型类和方法的实现。
什么是泛型
泛型使得类型参数化成为可能,允许我们编写与多种类型数据兼容的代码。通过泛型,可以使代码在处理不同类型时具有更强的灵活性和可扩展性,而不必牺牲类型检查和类型安全。
泛型的核心语法
泛型的语法通常使用尖括号 <>
来表示类型参数,例如 List<int>
或 Map<String, dynamic>
。
List<int> numbers = [1, 2, 3, 4];
Map<String, String> countries = {'US': 'United States', 'IN': 'India'};
在上面的示例中,List<int>
表示一个包含整数的列表,而 Map<String, String>
表示一个键和值都是字符串的映射。
集合的泛型接口
Dart 提供了许多常用的集合类型,如 List
、Set
和 Map
,它们都是通过泛型接口定义的。通过泛型,集合可以处理不同类型的元素,并在编译时确保类型安全。
泛型 List
List
是 Dart 中最常用的集合之一,它是一个有序的对象集合。通过泛型,我们可以指定 List
中存储的元素类型。
示例:
void main() {List<int> intList = [1, 2, 3, 4]; // 仅允许存储整数List<String> stringList = ['Dart', 'Flutter']; // 仅允许存储字符串print(intList); // 输出: [1, 2, 3, 4]print(stringList); // 输出: [Dart, Flutter]
}
在这个例子中,我们定义了两个泛型 List
,一个存储整数,另一个存储字符串。通过泛型,我们可以确保列表中的所有元素都是指定类型。
泛型 Set
Set
是 Dart 中的另一种集合类型,它存储一组唯一的元素。与 List
类似,Set
也可以通过泛型指定存储的元素类型。
示例:
void main() {Set<String> names = {'Alice', 'Bob', 'Charlie'}; // 仅允许存储字符串names.add('Dart'); // 添加元素names.add('Alice'); // 重复元素不会被添加print(names); // 输出: {Alice, Bob, Charlie, Dart}
}
在这个例子中,Set<String>
表示一个存储字符串的集合。由于 Set
保证元素的唯一性,因此重复添加的元素将被忽略。
泛型 Map
Map
是键值对的集合,每个键都与一个值关联。通过泛型,我们可以分别为 Map
中的键和值指定类型。
示例:
void main() {Map<String, int> ages = {'Alice': 25, 'Bob': 30}; // 键为字符串,值为整数ages['Charlie'] = 28; // 添加新的键值对print(ages); // 输出: {Alice: 25, Bob: 30, Charlie: 28}
}
在这个例子中,Map<String, int>
表示键为字符串、值为整数的映射。通过泛型,我们可以确保每个键都是字符串,每个值都是整数。
自定义泛型类
除了使用集合类型的泛型接口,Dart 还允许我们创建自定义泛型类。泛型类使我们能够创建适用于多种数据类型的类,而无需为每种数据类型分别定义类。
定义泛型类
自定义泛型类时,我们可以在类名后使用尖括号 <T>
来声明泛型类型参数 T
。T
是占位符,表示任何类型。
示例:
class Box<T> {T? value;Box(this.value);void display() {print('The value is $value');}
}void main() {Box<int> intBox = Box<int>(123); // 创建一个存储整数的 BoxBox<String> stringBox = Box<String>('Hello'); // 创建一个存储字符串的 BoxintBox.display(); // 输出: The value is 123stringBox.display(); // 输出: The value is Hello
}
在这个例子中,Box
类是一个泛型类,它可以存储任何类型的数据。通过 <T>
声明的泛型参数 T
,我们可以在类的属性和方法中使用这个类型。
多个泛型参数
自定义泛型类时,Dart 允许我们使用多个泛型参数。这对于需要处理多个不同类型的类非常有用。
示例:
class Pair<K, V> {K key;V value;Pair(this.key, this.value);void display() {print('Key: $key, Value: $value');}
}void main() {Pair<String, int> pair = Pair<String, int>('Age', 30);pair.display(); // 输出: Key: Age, Value: 30
}
在这个例子中,Pair<K, V>
类定义了两个泛型参数 K
和 V
,分别表示键和值的类型。这样,我们可以创建一个键值对类,适用于任何类型的键和值。
自定义泛型方法
除了泛型类,Dart 还支持定义泛型方法。泛型方法允许我们在方法内部使用占位符类型,并在调用方法时指定具体类型。
定义泛型方法
定义泛型方法时,可以在方法名称之前使用尖括号声明泛型参数。例如:
T findMax<T extends Comparable<T>>(T a, T b) {if (a.compareTo(b) > 0) {return a;} else {return b;}
}void main() {print(findMax(5, 10)); // 输出: 10print(findMax('apple', 'banana')); // 输出: banana
}
在这个例子中,findMax
方法使用泛型参数 T
,它要求 T
必须实现 Comparable<T>
接口,以便可以调用 compareTo
方法。该方法返回两个参数中的较大值。
多个泛型参数的泛型方法
我们还可以为方法定义多个泛型参数。
示例:
K getFirst<K, V>(Map<K, V> map) {return map.keys.first;
}void main() {Map<String, int> ages = {'Alice': 25, 'Bob': 30};print(getFirst(ages)); // 输出: Alice
}
在这个例子中,getFirst
方法使用了两个泛型参数 K
和 V
,它返回映射中的第一个键。
泛型的上下限
Dart 的泛型允许我们对类型参数设置上下限,这样可以限制泛型的类型范围。通过 extends
关键字,可以指定泛型参数必须是某个类的子类或实现了某个接口。
泛型上限
使用 extends
可以为泛型参数设置上限,限制泛型只能是某个类或接口的子类。
示例:
class Animal {void speak() => print('Animal speaking');
}class Dog extends Animal {void bark() => print('Dog barking');
}class AnimalShelter<T extends Animal> {T? animal;AnimalShelter(this.animal);void makeSound() {animal?.speak();}
}void main() {AnimalShelter<Dog> dogShelter = AnimalShelter(Dog());dogShelter.makeSound(); // 输出: Animal speaking
}
在这个例子中,AnimalShelter<T>
使用了泛型上限 T extends Animal
,因此 T
必须是 Animal
类的子类。这样,我们可以确保 animal
对象始终具有 speak
方法。
总结
Dart 中的泛型是一个强大而灵活的工具,使得开发者可以编写更加通用和类型安全的代码。本文介绍了 Dart 中使用泛型的基本语法,包括 List
、Set
和 Map
等集合的泛型接口,以及如何定义自定义泛型类和方法。通过使用泛型,开发者能够提高代码的可维护性和可扩展性。
无论是在创建集合类型、还是在构建复杂的数据结构时,掌握泛型能够帮助你编写更具适应性和效率的代码。