一架梯子,一头程序猿,仰望星空!

Dart 泛型


泛型(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();