# Dart教程 - 8 泛型

当在 Dart 中使用泛型(Generics),我们可以将类型参数化,从而实现更通用和可重用的代码。泛型允许我们编写一次代码,适用于多种类型,提高了代码的灵活性和可维护性。

我们前面在使用容器的时候,已经接触过泛型了。

举个栗子:

List使用泛型:

void main(List<String> args) {
  // 不使用泛型,没有类型限制
  var nameList_1 = ['Doubi', 'Erbi', 'Niubi', 111];
  print(nameList_1.runtimeType); // List<Object>

  // 使用泛型进行类型限制,只能放字符串类型
  var nameList_2 = <String>['Doubi', 'Erbi', 'Niubi'];
  
  List<String> nameList_3 = ['Doubi', 'Erbi', 'Niubi'];
  //nameList_3.add(123);  // 报错,List<String>中只能放String类型数据
}
1
2
3
4
5
6
7
8
9
10
11

同样,Map也可以指定泛型:

void main(List<String> args) {
  // 不使用泛型,key和value可以是任意类型
  var map_1 = {123: 'one', 'name': 'Doubi', 'age': 12};

  // 使用泛型,key和value只能是字符串,否则报错
  Map<String, String> map_2 = {'name': 'Doubi', 'age': "12"};
}
1
2
3
4
5
6
7

我们也可以使用泛型机制创建我们的泛型类、泛型方法和泛型接口。

# 8.1 泛型类

我们在开发的时候,经常需要请求服务器,获取一些信息,例如获取学生的信息,或者获取老师的信息等。

那么我们会定义相关的业务类,举个栗子:

定义学生类:

class Student {
  String name;
  int age;

  Student(this.name, this.age);
}
1
2
3
4
5
6

定义老师类:

class Teacher {
  String name;
  int age;

  Teacher(this.name, this.age);
}
1
2
3
4
5
6

我们一般会定义一个返回的结果类,然后在类中封装返回码和返回的数据,通过这个结果类返回学生或老师的信息。

举个栗子:

class DataResult {
  int messageCode;
  Object? data;

  // 默认构造方法
  DataResult(this.messageCode, this.data);

  // 错误的时候,返回错误码,没有数据
  DataResult.error(this.messageCode);
}
1
2
3
4
5
6
7
8
9
10

因为我们返回的数据可能是不同的类型,所以我们将数据的类型定义为Object类型。因为有时候可能只需要返回信息码,没有数据信息,所以这里定义data可以为空,使用 Object? 定义。

测试:

import 'DataResult.dart';
import 'Student.dart';
import 'Teacher.dart';

void main() {
  // 封装数据
  Student stu = Student("Doubi", 12);
  DataResult result1 = DataResult(200, stu);

  Teacher tea = Teacher("Niubi", 32);
  DataResult result2 = DataResult(200, tea);

  // 获取数据
  if (result1.data.runtimeType == Student) {
    Student stu2 = result1.data as Student; // 需要进行类型转换
    print(stu2.name);
  }

  if (result2.data.runtimeType == Teacher) {
    Teacher tea2 = result2.data as Teacher; // 需要进行类型转换
    print(tea2.name);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上面的代码中,我们先将学生和老师的信息封装到了DataResult中。然后重新从DataResult获取老师和学生的信息。

但是获取到信息以后,需要使用 as 进行类型转换,如果使用 Student stu2 = result1.data; 直接赋值代码会报错,因为 data 是Object类型。为了安全起见,我们使用了 runtimeType 进行类数据的类型判断,然后再进行了转换。

这个时候我们可以使用泛型,使用泛型我们可以编写更通用、可重用的代码,以适应不同类型的数据。

修改上面的代码:

class DataResult<T> {
  int messageCode;
  T? data;

  // 默认构造方法
  DataResult(this.messageCode, this.data);

  // 错误的时候,返回错误码,没有数据
  DataResult.error(this.messageCode);
}
1
2
3
4
5
6
7
8
9
10

在类名后面添加 <T> 表示当前类声明的泛型,泛型可以任意字母,一般使用 T ,然后可以使用 T 声明变量 T? data;

测试一下代码:

import 'DataResult.dart';
import 'Student.dart';
import 'Teacher.dart';

void main() {
  // 封装数据
  Student stu = Student("Doubi", 12);
  DataResult<Student> result1 = DataResult(200, stu);

  Teacher tea = Teacher("Niubi", 32);
  DataResult<Teacher> result2 = DataResult(200, tea);

  Student stu2 = result1.data!; // 不需要进行类型转换
  print(stu2.name);

  Teacher tea2 = result2.data!; // 不需要进行类型转换
  print(tea2.name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这个在创建 DataResult 对象的时候,使用泛型,然后在通过对象来获取 data 的时候,就不用类型转换了。

# 8.2 泛型函数

泛型函数是一种具有泛型类型参数的函数,可以在函数签名中使用类型参数来实现更通用和灵活的代码。泛型函数可以在参数、返回类型或函数体中使用这些类型参数。

举个栗子:

我们写一个函数获取列表的最后一个元素:

// 获取列表的最后一个元素
T getLast<T>(List<T> list) {
  return list[list.length - 1];
}

void main() {
  List<int> intList = [1, 2, 3];
  List<String> strList = ['a', 'b', 'c', 'd'];

  int i = getLast<int>(intList);
  print(i);

  String s = getLast<String>(strList);
  print(s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中定义了一个泛型方法 T getLast<T>(List<T> list),函数名后面的 <T> 定义函数中用到的泛型类型,前面的 T 表示返回值的类型。

通过使用泛型定义,我们不同数据类型的列表都可以调用,而且在获取到元素后,不要类型转换。

其实在上面的代码 int i = getLast<int>(intList); 写成:

int i = getLast(intList);
1

也是可以的,int i = getLast(intList);:这是一种类型推断的方式,编译器根据变量 i 的声明类型 int 推断出要调用 getLast<int>,并根据传递的参数 intList 的类型 List<int> 推断出泛型函数的类型参数 Tint。因为类型推断可以根据上下文自动推断类型参数,所以在这种情况下,我们可以省略 <int>

# 8.3 泛型接口

在 Dart 中,泛型接口是指可以具有泛型类型参数的接口。泛型接口允许在接口定义中使用类型参数,并在实现接口时指定具体的类型参数。

举个栗子:

定义一个泛型接口。

// 定义一个泛型接口
abstract class Repository<T> {
  void save(T data);
  T findById(int id);
}
1
2
3
4
5

下面写一个类来实现这个泛型接口:

// 实现泛型接口
class UserRepository implements Repository<String> {
  
  void save(String data) {
    print('Saving user: $data');
  }

  
  String findById(int id) {
    return 'User with ID $id';
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

我们在实现接口的时候,指定了泛型的类型,那么类中用到泛型的地方,类型就确定确定了。

我们还可以定义其他的子类来实现该接口,并使用不同的数据类型。

调用:

void main() {
  UserRepository userRepository = UserRepository();

  userRepository.save('John Doe');
  String user = userRepository.findById(1);
  print(user);
}
1
2
3
4
5
6
7

# 8.4 泛型约束

我们在使用泛型的时候,还可以使用 extends 关键字来进行约束。

举个栗子:

// 获取列表的最后一个元素
T getLast<T extends num>(List<T> list) {
  return list[list.length - 1];
}
1
2
3
4

上面定义的泛型 <T extends num> 表示泛型 T必须是 num 的子类型。

所以在调用的时候,列表的数据类型需要时num的子类,例如 int 或 double,如果设置为其他类型就会报错,例如下面传入 String 类型的列表就会报错。

// 获取列表的最后一个元素
T getLast<T extends num>(List<T> list) {
  return list[list.length - 1];
}

void main() {
  List<int> intList = [1, 2, 3];
  int i = getLast(intList);
  print(i);

  List<double> strList = [1.0, 2.0, 3.0];
  double d = getLast(strList);
  print(d);

  //String s = getLast<String>(strList); // 代码报错,String不是num的子类
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

通过泛型约束,我们可以在泛型代码中引入类型约束,提供更多的类型安全性和编译时检查。这有助于减少类型错误,并在编译时捕获一些潜在的问题。