# 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类型数据
}
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"};
}
2
3
4
5
6
7
我们也可以使用泛型机制创建我们的泛型类、泛型方法和泛型接口。
# 8.1 泛型类
我们在开发的时候,经常需要请求服务器,获取一些信息,例如获取学生的信息,或者获取老师的信息等。
那么我们会定义相关的业务类,举个栗子:
定义学生类:
class Student {
String name;
int age;
Student(this.name, this.age);
}
2
3
4
5
6
定义老师类:
class Teacher {
String name;
int age;
Teacher(this.name, this.age);
}
2
3
4
5
6
我们一般会定义一个返回的结果类,然后在类中封装返回码和返回的数据,通过这个结果类返回学生或老师的信息。
举个栗子:
class DataResult {
int messageCode;
Object? data;
// 默认构造方法
DataResult(this.messageCode, this.data);
// 错误的时候,返回错误码,没有数据
DataResult.error(this.messageCode);
}
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);
}
}
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);
}
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);
}
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);
}
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);
也是可以的,int i = getLast(intList);
:这是一种类型推断的方式,编译器根据变量 i
的声明类型 int
推断出要调用 getLast<int>
,并根据传递的参数 intList
的类型 List<int>
推断出泛型函数的类型参数 T
为 int
。因为类型推断可以根据上下文自动推断类型参数,所以在这种情况下,我们可以省略 <int>
。
# 8.3 泛型接口
在 Dart 中,泛型接口是指可以具有泛型类型参数的接口。泛型接口允许在接口定义中使用类型参数,并在实现接口时指定具体的类型参数。
举个栗子:
定义一个泛型接口。
// 定义一个泛型接口
abstract class Repository<T> {
void save(T data);
T findById(int id);
}
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';
}
}
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);
}
2
3
4
5
6
7
# 8.4 泛型约束
我们在使用泛型的时候,还可以使用 extends
关键字来进行约束。
举个栗子:
// 获取列表的最后一个元素
T getLast<T extends num>(List<T> list) {
return list[list.length - 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的子类
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过泛型约束,我们可以在泛型代码中引入类型约束,提供更多的类型安全性和编译时检查。这有助于减少类型错误,并在编译时捕获一些潜在的问题。