泛型(generics)编程机制最主要的目的也是为了复用代码,熟悉java/c++的人应该对泛型不陌生,Dart也支持泛型编程,前面的章节大家知道List、Set、Map都是泛型类型,支持任意数据类型,本章介绍如果自定义泛型类。
1.为什么使用泛型
大家使用List的时候,可以通过传递不同的类型参数给List, 让List支持各种数据类型,可以大致知道,泛型是跟类型参数有关。
我们先通过一些例子,理解泛型机制是如何复用代码的。
// 我们实现一个处理int数据类型的栈。
// 栈是一种先进后出的数据结构。
class StackInt {
// 栈容量大小,能存储多少个元素
int _capacity;
// 栈顶指针
int _last;
// 栈数据存储在List数组中
List<int> _data;
// 构造方法,接收一个容量参数cap,设置栈能存储多少个元素
StackInt(int cap) : _capacity = cap, _data = List<int>(cap), _last = -1;
// 出栈
int pop() {
if (_last > -1) {
// 没有越界的话,获取栈顶元素
int v = _data[_last];
_last--;
return v;
}
else {
return null;
}
}
// 入栈
bool push(int v) {
if (_last + 1 == _capacity) {
// 超出栈空间容量
return false;
}
_last++;
_data[_last] = v;
}
}
使用StackInt例子:
void main() {
var s = StackInt(10);
// 入栈
s.push(1);
s.push(2);
s.push(3);
// 出栈
print(s.pop());
print(s.pop());
print(s.pop());
}
输出:
3
2
1
上面我们实现了一个处理int数据类型的栈,那么如果我们要处理String类型怎么办?
复制粘贴,再写一遍。
class StackString {
// 栈容量大小,能存储多少个元素
int _capacity;
// 栈顶指针
int _last;
// 栈数据存储在List数组中
List<String> _data;
// 构造方法,接收一个容量参数cap,设置栈能存储多少个元素
StackString(int cap) : _capacity = cap, _data = List<String>(cap), _last = -1;
// 出栈
String pop() {
if (_last > -1) {
// 没有越界的话,获取栈顶元素
String v = _data[_last];
_last--;
return v;
}
else {
return null;
}
}
// 入栈
bool push(String v) {
if (_last + 1 == _capacity) {
// 超出栈空间容量
return false;
}
_last++;
_data[_last] = v;
}
}
上面代码跟处理int类型的逻辑是一模一样的代码,区别是处理的数据类型变了,如果我们要处理N种数据类型就要复制粘贴代码N次,代码非常臃肿。
下面介绍泛型机制如何复用代码。
2.自定义泛型类
泛型类,其实就是给类传入一些特殊的参数,我们通常叫泛型参数,使用泛型参数替代代码中需要变化的数据类型或者参数。
泛型参数使用<>符号,包裹起来,多个泛型参数使用逗号分隔。
提示:我们一般习惯使用大写字母代表泛型参数。
接上面的例子,我们将栈的实现改成泛型类。
// 自定义泛型类,在类名后面增加泛型参数定义。
// 这里定义了一个泛型参数T,而且设计约定T只能传入数据类型。
// 注意: 这个约定不是什么强制约定就是我们自己规定的,如果你传入的不是数据类型编译器处理不了就报错了。
class Stack<T> {
int _capacity;
int _last;
// 引用泛型参数,定义List处理T类型的数组
List<T> _data;
// 构造方法的初始化参数列表,也引用了参数T,初始化List
Stack(int cap) : _capacity = cap, _data = List<T>(cap), _last = -1;
// 出栈, 定义pop方法返回值类型为T
T pop() {
if (_last > -1) {
// 定义出栈元素类型为T
T v = _data[_last];
_last--;
return v;
}
else {
return null;
}
}
// 入栈, 定义push参数接收T类型数据
bool push(T v) {
if (_last + 1 == _capacity) {
// 超出栈空间容量
return false;
}
_last++;
_data[_last] = v;
}
}
大家如果写过前端代码,对模版技术一定非常熟悉,模版技术通常会在模版中定义一些特殊的标签(模版参数),然后通过使用实际的数据去替换这些模版标签,达到用同一个模版输出不同内容的目的。
泛型其实就是一种模版技术,我们定义一些泛型参数(模版参数),然后编译器会根据实际传入泛型参数,将泛型类(模版代码),转化成不同的类。
使用上面定义泛型类的例子:
void main() {
// 实例化栈对象,传入int作为泛型参数,栈容量为10
var s = Stack<int>(10);
// 入栈
s.push(1);
s.push(2);
s.push(3);
// 出栈
print(s.pop());
print(s.pop());
print(s.pop());
}
3.限制泛型参数类型
我们也可以限制泛型参数的类型。
限制泛型参数类型语法格式:<泛型参数 extends 父类>
例子:
// T 类型必须是BaseClass的子类
class Foo<T extends BaseClass> {
// 打印传入的类型
String toString() => "Instance of 'Foo<$T>'";
}
// Son继承BaseClass类
class Son extends BaseClass {...}
// 使用父类作为泛型参数是允许的
var foo1 = Foo<BaseClass>();
// 使用BaseClass的子类Son作为泛型参数
var foo2 = Foo<Son>();
// 如果不传入任何泛型参数,默认使用父类BaseClass作为泛型参数
var foo = Foo();