2.2 泛型

基础知识

泛型(Generics)是 Dart 语言中一个强大的特性,它允许你编写可以处理多种数据类型的代码,而无需为每种类型重复编写相同的逻辑。泛型的主要目的是提高代码的重用性、类型安全性和性能。

1. 为什么需要泛型?

考虑一个简单的例子,如果你想创建一个可以存储任何类型数据的列表,你可能会使用 List<dynamic>

dart
复制代码
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 代表值)来声明一个或多个类型参数。

dart
复制代码
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> 来声明类型参数。

dart
复制代码
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. 泛型接口

接口也可以是泛型的,这允许实现该接口的类在实现时指定具体的类型。

dart
复制代码
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 关键字来指定类型参数的上限。

dart
复制代码
// 限制 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 开发中无处不在,它是 Flutter 框架实现类型安全和代码复用的基石。从 List<Widget> 到各种状态管理库,泛型都扮演着核心角色。理解泛型对于编写健壮、可维护的 Flutter 应用至关重要。

案例:一个通用的数据加载器组件

我们将创建一个通用的 DataLoader Widget,它可以加载任何类型的数据,并根据加载状态(加载中、加载成功、加载失败)显示不同的 UI。这个案例将演示泛型类和泛型方法在 Flutter 中的应用。

dart
复制代码
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 StatefulWidgetDataLoader 被定义为一个泛型 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。这些方法可以作为 DataLoaderfutureBuilder 参数传入。
  • DataLoader<String>DataLoader<List<String>>DataLoader<Map<String, dynamic>>:在 GenericsExampleScreen 中,我们创建了 DataLoader 的不同实例,分别指定了不同的泛型类型。这展示了同一个 DataLoader 组件如何通过泛型来处理不同类型的数据。

这个案例清晰地展示了 Dart 泛型在 Flutter 应用中的强大之处。通过泛型,我们可以创建高度可复用、类型安全且灵活的组件,从而大大提高开发效率和代码质量。泛型是 Flutter 框架设计的基础,也是编写高质量 Flutter 应用不可或缺的工具。