泛型(Generics)是 Dart 语言中一个强大的特性,它允许你编写可以处理多种数据类型的代码,而无需为每种类型重复编写相同的逻辑。泛型的主要目的是提高代码的重用性、类型安全性和性能。
1. 为什么需要泛型?
考虑一个简单的例子,如果你想创建一个可以存储任何类型数据的列表,你可能会使用 List<dynamic>:
List<dynamic> dynamicList = [];
dynamicList.add(1);
dynamicList.add("hello");
dynamicList.add(true);
// 从列表中取出元素时,你需要进行类型转换,这可能导致运行时错误
String item = dynamicList[1]; // 运行时错误:_TypeError (type 'int' is not a subtype of type 'String')
这种方式虽然灵活,但失去了类型安全性。在编译时,Dart 无法知道 dynamicList[1] 的实际类型,只有在运行时才能发现类型不匹配的问题。泛型解决了这个问题,它允许你在编译时指定类型,从而在开发早期捕获类型错误。
2. 泛型类
你可以创建泛型类,使其能够操作多种类型的数据。在类名后面使用 <T>(或其他字母,如 E 代表元素,K 代表键,V 代表值)来声明一个或多个类型参数。
class Box<T> {
T value;
Box(this.value);
T getValue() {
return value;
}
}
void main() {
// 创建一个存储整数的 Box
Box<int> intBox = Box<int>(123);
print(intBox.getValue()); // 输出: 123
// 创建一个存储字符串的 Box
Box<String> stringBox = Box<String>("Hello Generics");
print(stringBox.getValue()); // 输出: Hello Generics
// 编译时错误:类型不匹配
// Box<int> anotherIntBox = Box<int>("abc");
}
3. 泛型方法
你也可以创建泛型方法,使其能够接受或返回泛型类型。在方法返回类型之前使用 <T> 来声明类型参数。
T firstElement<T>(List<T> list) {
// assert(list.isNotEmpty, "List cannot be empty");
return list[0];
}
void main() {
List<int> numbers = [1, 2, 3];
int firstNum = firstElement<int>(numbers); // 明确指定类型参数
print(firstNum); // 输出: 1
List<String> names = ["Alice", "Bob", "Charlie"];
String firstName = firstElement(names); // 类型推断,可以省略 <String>
print(firstName); // 输出: Alice
}
4. 泛型接口
接口也可以是泛型的,这允许实现该接口的类在实现时指定具体的类型。
abstract class Cache<T> {
void put(String key, T value);
T? get(String key);
}
class InMemoryCache<T> implements Cache<T> {
final Map<String, T> _cache = {};
@override
void put(String key, T value) {
_cache[key] = value;
}
@override
T? get(String key) {
return _cache[key];
}
}
void main() {
Cache<String> stringCache = InMemoryCache<String>();
stringCache.put("name", "Alice");
print(stringCache.get("name")); // Output: Alice
Cache<int> intCache = InMemoryCache<int>();
intCache.put("age", 30);
print(intCache.get("age")); // Output: 30
}
5. 泛型类型限制 (Bounded Type Parameters)
有时你可能希望限制泛型类型只能是特定类型或其子类型。可以使用 extends 关键字来指定类型参数的上限。
// 限制 T 必须是 num 或其子类型 (int, double)
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
double get doubleValue => value.toDouble();
}
void main() {
NumberBox<int> intBox = NumberBox<int>(10);
print(intBox.doubleValue); // Output: 10.0
NumberBox<double> doubleBox = NumberBox<double>(3.14);
print(doubleBox.doubleValue); // Output: 3.14
// 编译时错误:String 不是 num 的子类型
// NumberBox<String> stringBox = NumberBox<String>("hello");
}
泛型在 Flutter 开发中无处不在,它是 Flutter 框架实现类型安全和代码复用的基石。从 List<Widget> 到各种状态管理库,泛型都扮演着核心角色。理解泛型对于编写健壮、可维护的 Flutter 应用至关重要。
案例:一个通用的数据加载器组件
我们将创建一个通用的 DataLoader Widget,它可以加载任何类型的数据,并根据加载状态(加载中、加载成功、加载失败)显示不同的 UI。这个案例将演示泛型类和泛型方法在 Flutter 中的应用。
import 'package:flutter/material.dart';
// 定义一个泛型 Widget,用于加载和显示数据
class DataLoader<T> extends StatefulWidget {
final Future<T> Function() futureBuilder; // 返回 Future<T> 的函数
final Widget Function(T data) successWidgetBuilder; // 成功时构建 Widget 的函数
final Widget Function(Object error) errorWidgetBuilder; // 失败时构建 Widget 的函数
final Widget loadingWidget; // 加载时显示的 Widget
const DataLoader({
super.key,
required this.futureBuilder,
required this.successWidgetBuilder,
required this.errorWidgetBuilder,
this.loadingWidget = const CircularProgressIndicator(), // 默认加载指示器
});
@override
State<DataLoader<T>> createState() => _DataLoaderState<T>();
}
class _DataLoaderState<T> extends State<DataLoader<T>> {
late Future<T> _dataFuture; // 存储 Future 对象
@override
void initState() {
super.initState();
_dataFuture = widget.futureBuilder(); // 初始化 Future
}
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: _dataFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: widget.loadingWidget); // 加载中
} else if (snapshot.hasError) {
return Center(child: widget.errorWidgetBuilder(snapshot.error!)); // 加载失败
} else if (snapshot.hasData) {
return widget.successWidgetBuilder(snapshot.data!); // 加载成功
} else {
return const Center(child: Text("没有数据")); // 理论上不会发生
}
},
);
}
}
// 模拟数据服务
class MockDataService {
static Future<String> fetchStringData() async {
await Future.delayed(const Duration(seconds: 2));
// throw Exception("字符串数据加载失败!"); // 模拟错误
return "Hello from String Data!";
}
static Future<List<String>> fetchListData() async {
await Future.delayed(const Duration(seconds: 3));
return ["Item 1", "Item 2", "Item 3"];
}
static Future<Map<String, dynamic>> fetchMapData() async {
await Future.delayed(const Duration(seconds: 1));
return {"id": 1, "name": "Test User", "age": 25};
}
}
class GenericsExampleScreen extends StatelessWidget {
const GenericsExampleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("泛型应用案例"),
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("加载字符串数据:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
SizedBox(
height: 100,
child: DataLoader<String>(
futureBuilder: MockDataService.fetchStringData,
successWidgetBuilder: (data) => Text("成功: $data", style: const TextStyle(fontSize: 20, color: Colors.green)),
errorWidgetBuilder: (error) => Text("错误: $error", style: const TextStyle(fontSize: 16, color: Colors.red)),
),
),
const Divider(),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("加载列表数据:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
SizedBox(
height: 200,
child: DataLoader<List<String>>(
futureBuilder: MockDataService.fetchListData,
successWidgetBuilder: (data) => ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => ListTile(title: Text(data[index])),
),
errorWidgetBuilder: (error) => Text("错误: $error", style: const TextStyle(fontSize: 16, color: Colors.red)),
),
),
const Divider(),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("加载 Map 数据:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
SizedBox(
height: 150,
child: DataLoader<Map<String, dynamic>>(
futureBuilder: MockDataService.fetchMapData,
successWidgetBuilder: (data) => Column(
children: data.entries.map((entry) => Text("${entry.key}: ${entry.value}")).toList(),
),
errorWidgetBuilder: (error) => Text("错误: $error", style: const TextStyle(fontSize: 16, color: Colors.red)),
),
),
],
),
),
);
}
}
void main() {
runApp(const MaterialApp(home: GenericsExampleScreen()));
}
案例分析:
class DataLoader<T> extends StatefulWidget:DataLoader 被定义为一个泛型 Widget,类型参数 T 表示它将要加载的数据类型。这使得 DataLoader 可以用于加载任何类型的数据,而不仅仅是预定义的某种类型。final Future<T> Function() futureBuilder;:futureBuilder 是一个函数类型的属性,它返回一个 Future<T>。这意味着你可以传入任何返回 Future<T> 的函数,例如网络请求、数据库查询等。final Widget Function(T data) successWidgetBuilder;:successWidgetBuilder 也是一个函数类型的属性,它接收一个类型为 T 的数据,并返回一个 Widget。当数据加载成功时,DataLoader 会调用这个函数来构建显示数据的 UI。_DataLoaderState<T> extends State<DataLoader<T>>:State 类也需要是泛型的,并且其类型参数与 DataLoader 的类型参数保持一致。FutureBuilder<T>:Flutter 框架本身就大量使用了泛型。FutureBuilder 是一个非常常用的 Widget,它接收一个 Future 对象,并根据 Future 的状态(未完成、完成并成功、完成并失败)来重建 UI。FutureBuilder 也是泛型的,其类型参数与 Future 的类型参数一致。MockDataService 中的泛型方法:MockDataService 提供了 fetchStringData()、fetchListData() 和 fetchMapData() 等方法,它们返回不同泛型类型的 Future。这些方法可以作为 DataLoader 的 futureBuilder 参数传入。DataLoader<String>、DataLoader<List<String>>、DataLoader<Map<String, dynamic>>:在 GenericsExampleScreen 中,我们创建了 DataLoader 的不同实例,分别指定了不同的泛型类型。这展示了同一个 DataLoader 组件如何通过泛型来处理不同类型的数据。这个案例清晰地展示了 Dart 泛型在 Flutter 应用中的强大之处。通过泛型,我们可以创建高度可复用、类型安全且灵活的组件,从而大大提高开发效率和代码质量。泛型是 Flutter 框架设计的基础,也是编写高质量 Flutter 应用不可或缺的工具。