【Dart语言】二、类型

基本类型

Dart 语言对以下方面提供了特殊支持:

  • 数字 (int, double)
  • 字符串 (String)
  • 布尔值 (bool)
  • 记录 ((value1, value2))
  • 函数 (Function)
  • 列表 (List, 也成为数组)
  • 集合 (Set)
  • 映射集合 (Map)
  • 符文 (Runes; 通常由字符API代替)
  • 符号 (Symbol)
  • 值为null (Null)

这种支持包括使用字面量创建对象的能力。例如,'this is a string' 是一个字符串字面量,而 true 是一个布尔字面量。

因为 Dart 中每个变量都引用一个对象——即一个类的实例——因此你通常可以使用构造函数来初始化变量。一些内置类型有自己的构造函数。例如,你可以使用 Map() 构造函数来创建一个映射(Map)。

其他一些类型在 Dart 语言中也有特殊的作用:

  • Object:除 Null 外所有 Dart 类的超类。
  • Enum:所有枚举的超类。
  • Future 和 Stream:用于异步支持。
  • Iterable:用于 for-in 循环和同步生成器函数中。
  • Never:表示一个表达式永远不会成功完成求值。通常用于总是抛出异常的函数。
  • dynamic:表示你想禁用静态检查。通常应该使用 Object 或 Object? 代替。dynamic 类型允许你在运行时动态地调用属性和方法,但会失去编译时的类型检查。
  • void:表示一个值永远不会被使用。常用作返回类型。如果一个函数没有返回值,或者其返回值被忽略,则可以使用 void 类型。

Object、Object?、Null 和 Never 类具有特殊的作用。要了解这些作用,请查阅“理解空安全”部分。

数字

Dart中的数字有两种类型:

int

整数值范围不超过64位,具体取决于平台。在原生平台上,整数取值范围可以从 -2^{63} 至2^{63} -1。在web端上,整数被表示为 JavaScript 数字( 64位浮点数 ,没有小数部分 ),其取值范围可以从-2^{53}2^{53} -1。

double

64位(双精度)浮点数,按照IEEE 754标准规定。

int 和 double 都是 num 的子类型。num 类型包含了基本的运算符,如 +、-、/ 和 *,以及 abs()、ceil() 和 floor() 等方法。(位运算符,如 >>,是在 int 类中定义的。)如果num及其子类型不具备您所需要的功能,那么dart:math库可能具备。

整数是没有小数点的数字。下面是一些定义整数字面值的例子:

var x = 1;
var hex = 0xDEADBEEF;

如果一个数字包含一个小数,它就是双精度数。下面是一些定义双字面值的例子:

var y = 1.1;
var exponents = 1.42e5;

您也可以将变量声明为num,如果这样做,该变量可以同时具有整型和双精度值。

num x = 1; // x既可以是整型值也可以是双精度值
x += 2.5;

整数字面值在必要时会自动转换为双精度:

double z = 1; // 相当于double z = 1.0。

下面是如何将字符串转换为数字的方法,反之亦然:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2); // 保留小数点后2位,四舍五入
assert(piAsString == '3.14');

数字字面量是编译时常量。许多算术表达式也是编译时常量,只要它们的操作数是求值为数字的编译时常量。

const msPerSecond = 1000; // msPerSecond会根据字面量值断定类型
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;

字符串

一个 Dart 字符串(String 对象)包含一个UTF-16代码单元序列。你可以使用单引号或双引号来创建字符串:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

你可以通过使用 ${expression} 将表达式的值嵌入到字符串中。如果表达式是一个标识符(比如一个变量),你可以省略 {}(比如 $str )。为了获取与对象对应的字符串,Dart 会调用该对象的 toString() 方法。

var str = "ABcd";
print("内容:$str"); // 打印:打印内容:ABcd
print("都转成大写:${str.toUpperCase()}"); // 打印:都转成大写:ABCD
print(str == str.toUpperCase()); // 打印:false (Dart判断字符串是区分大小写的)

== 操作符测试两个对象是否相等。如果两个字符串包含相同的代码单元序列,则它们是等效的(相同的字母如A在大写和小写的单元序列是不相同的)。

您可以使用相邻的字符串字面值或+操作符连接字符串:

var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 == 'String concatenation works even over '
        'line breaks.'); // 结果true

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');  // 结果true

要创建一个多行字符串,请使用带单引号或双引号的三个连续引号:

/// 三个单引号
var s1 = '''
You can create
multi-line strings like this one.
''';

/// 三个双引号
var s2 = """This is also a
multi-line string.""";

你可以通过在字符串前加 r 来创建一个“原始”字符串(即字符串字面量是什么样就显示什么样,包括Unicode字符不会显示对应的内容,如‌\u0041不会显示字母A):

// 如换行符\n 将失去它的换行特性并被显示出来
var s = r'In a raw string, not even \n gets special treatment.'; 

字符串字面量是编译时常量,只要任何插值表达式都是编译时常量,并且其计算结果为null、数值、字符串或布尔值。

// 这些在const string中有效。
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// 这些在const字符串中不起作用。
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

const validConstString = '$aConstNum $aConstBool $aConstString';

// 不起作用,那是因为invalidConstString是一个const常量,aNum、aBool和aString是非const常量,可以改变值。
// const invalidConstString = '$aNum $aBool $aString $aConstList'; 

布尔值

为了表示布尔值,Dart有一个名为bool的类型。只有两个对象具有bool类型:布尔字面值true和false,它们都是编译时常量。

// 布尔型字面值常量
const a = true;
const b = false;

var c = true;
var d = false;

bool e = true;
bool f = false;

// 检查是否有空字符串
var fullName = '';
print(fullName.isEmpty); // 显示 true

// 检查0
var hitPoints = 0;
print(hitPoints == 0);// 显示true

// 检查null
var unicorn = null;
print(unicorn == null);// 显示true

Runes 和 grapheme clusters

在 Dart 中,runes 暴露了字符串的 Unicode 码点。你可以使用 characters 包来查看或操作用户感知的字符,也就是 Unicode(扩展)grapheme clusters

Unicode 为全球所有书写系统中使用的每个字母、数字和符号定义了唯一的数值。由于 Dart 字符串是 UTF-16 代码单元的序列,因此在字符串中表示 Unicode 码点需要特殊的语法。通常,表示一个 Unicode 码点的方式是 \uXXXX,其中 XXXX 是一个 4 位的十六进制值。例如,心形字符 (♥) 的表示是 \u2665。如果要指定多于或少于 4 位的十六进制值,可以将值放在花括号中。例如,大笑的表情符号 (😆) 的表示是 \u{1f606}。

如果你需要读取或写入单个 Unicode 字符,可以使用 characters 包为 String 定义的 characters getter。返回的 Characters 对象将字符串表示为一个 grapheme clusters 的序列。以下是使用 characters API 的示例:

import 'package:characters/characters.dart'; // 官方维护包 characters: ^1.4.0

void main() {
  var hi = 'Hi 🇩🇰';// 🇩🇰在Unicode中表示的是丹麦国旗表情符号

  print('String is "$hi"\n');

  // 长度
  print('String.length: ${hi.length}');
  print('Characters.length: ${hi.characters.length}\n'); // characters会把表情符号当成单个字符
  // 最后一个字符
  print('The string ends with: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}\n');

  // 跳过最后一个字符
  print('Substring -1: "${hi.substring(0, hi.length - 1)}"');
  print('Skipping last character: "${hi.characters.skipLast(1)}"\n');

  // 替换字符
  var newHi = hi.characters.replaceAll('🇩🇰'.characters, '🇺🇸'.characters);
  print('Change flag: "$newHi"');
}

可以看出,当文本内容含有Unicode表情符号的时候,使用 characters 包的API操作字符比较方便快捷,上面代码输出如下内容:

String is "Hi 🇩🇰"

# 长度

String.length: 7
Characters.length: 4

# 最后一个字符

The string ends with: �
The last character: 🇩🇰

# 跳过最后一个字符

Substring -1: "Hi 🇩�"
Skipping last character: "Hi "

# 替换字符

Change flag: "Hi 🇺🇸"

有关如何使用 characters 包来操作字符串的详细信息,请参阅 characters 包的 示例 和 API 参考文档

Symbol(符号)

​Symbol 是一种用于存储人类可读字符串和经过优化以供计算机使用的字符串之间的关系的方法。

Symbol obj = Symbol('name');  

上面 name 字符串必须是有效的公共 Dart 成员名称、公共构造函数名称或者库名称。

Dart 中的 Symbol(符号)对象用于指定在 Dart 编程语言中声明的运算符或标识符。尽管在日常编程中可能不常直接使用 Symbol,但在特定场景下,Symbol 具有重要的作用和用例。以下是对Dart 中 Symbol 的详细解释和用例:

作用

1、反射机制

  • Symbol 在 Dart 的反射机制中扮演着关键角色。反射允许程序在运行时检查和操作类型信息,如类、方法、构造函数等。
  • 通过 Symbol,可以获取或引用类的镜像信息,从而实现对类的动态分析和操作。

2、元数据提取

  • Symbol 用于从库中提取元数据,元数据是关于数据的数据,通常用于提供关于程序结构的额外信息。
  • 通过 Symbol,可以访问库或类上的元数据,这对于构建动态 API 或进行运行时分析非常有用。

3、API设计

  • 在设计 API 时,Symbol 提供了一种通过名称引用标识符的方式,这种方式在代码压缩或混淆后仍然有效。
  • 由于标识符的名称在代码压缩后可能会改变,但标识符的 Symbol 不会改变,因此使用Symbol 可以确保 API 的稳定性和可靠性。

用例

1、检查库中是否存在特定类

  • 可以使用 Symbol 来表示库名和类名,然后通过反射机制检查库中是否存在该特定类。
  • 这在动态加载库或插件时非常有用,可以确保所需的类在运行时可用。

2、获取类的成员信息

  • 通过 Symbol,可以获取类的成员信息,如方法、属性、构造函数等。
  • 这对于构建基于反射的工具或框架非常有用,如依赖注入框架、自动化测试框架等。

3、实现动态调用

  • 可以使用 Symbol 来表示方法名或属性名,然后通过反射机制实现动态调用或访问。
  • 这在需要根据不同条件调用不同方法或访问不同属性时非常有用。

4、构建动态 API

  • 通过 Symbol,可以构建动态 API,这些 API 在运行时根据上下文或配置动态生成。
  • 这对于需要高度灵活性和可扩展性的应用程序非常有用。

以下是一个展示了如何使用 Symbol 和反射机制来检查库中是否存在特定类:

library example_lib;
import 'dart:mirrors';

class ExampleClass {
  void exampleMethod() {
    print('Inside exampleMethod');
  }
}

void main() {
  Symbol libSymbol = Symbol('example_lib'); // 库名称
  Symbol classSymbol = Symbol('ExampleClass'); // 类名称

  MirrorSystem mirrorSystem = currentMirrorSystem(); // [MirrorSystem]用于反射一组相关库的接口
  LibraryMirror libMirror = mirrorSystem.findLibrary(libSymbol); // 通过库名查找库(库镜像)
  Map<Symbol, DeclarationMirror> declarations = libMirror.declarations; // 返回库中实际给出的声明的不可变映射
  
  // 是否有ExampleClass类
  if (libMirror != null && declarations.containsKey(classSymbol)) {
    print('有');
  } else {
    print('没有');
  }
}

在这个示例中,我们首先定义了一个库 example_lib 和一个类 ExampleClass。然后,在 main 函数中,我们使用 Symbol 来表示库名和类名,并通过反射机制检查库中是否存在该特定类。如果找到该类,则打印一条消息;否则,打印另一条消息。


综上所述,Dart 中的 Symbol 在反射机制、元数据提取、API 设计等方面具有重要的作用和用例。虽然在日常编程中可能不常直接使用 Symbol,但在需要动态分析和操作类型信息的场景中,Symbol 具有不可替代的作用。

记录 (Records) 

记录是一种匿名、不可变的聚合类型。与其他集合类型一样,它们允许您将多个对象组合成一个对象。但与其它集合类型不同的是,记录是固定大小的、异构的且有类型的。

记录是实际的值;您可以将它们存储在变量中、进行嵌套、在函数之间传递它们,并将它们存储在列表、映射和集合等数据结构中。

记录语法

记录表达式是用逗号分隔的命名字段或位置字段列表,用圆括号括起来。

var record = ('first', a: 2, b: true, 'last');

记录类型注释是用逗号分隔的类型列表,它们被括在圆括号中。您可以使用记录类型注释来定义返回类型和参数类型。例如,以下(int, int)语句是记录类型注释:

(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

记录表达式和类型注释中的字段反映了函数中形参和实参的工作方式。位置字段直接放在括号内:

// 在变量声明中记录类型注释:
(String, int) record;

// 用一个记录表达式初始化它:
record = ('A string', 123);

在记录类型注释中,命名字段位于所有位置字段之后的类型和名称对的花括号分隔部分中。在记录表达式中,名称放在每个字段值之前,后面有一个冒号:

// 在变量声明中记录类型注释:
({int a, bool b}) record;

// 用一个记录表达式初始化它:
record = (a: 123, b: true);

记录类型中命名字段的名称是该记录类型定义的一部分,或者说属于其结构。两个具有不同名称命名字段的记录具有不同的类型:

({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// 编译错误!这些记录的类型不同,因为两个具有不同名称命名字段的记录具有不同的类型
// recordAB = recordXY;

在记录类型注释中,你也可以为位置字段命名,但这些名称仅用于文档说明,并不影响记录的类型:

(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

recordAB = recordXY; // OK.

记录字段

记录字段可以通过内置 getter 访问。记录是不可变的,所以字段没有 setter。

命名字段公开相同名称的 getter。位置字段暴露了名称 $<position> 的 getter,跳过了命名字段:

var record = ('first', a: 2, b: true, 'last');

print(record.$1); // 打印 'first'
print(record.a); // 打印 2
print(record.b); // 打印 true
print(record.$2); // 打印 'last'

记录类型

没有针对单个记录类型的类型声明。记录是基于其字段的类型进行结构类型化的。一个记录的形状(即其字段的集合、字段的类型以及(如果有的话)字段的名称)唯一地确定了记录的类型。

记录中的每个字段都有自己的类型。同一记录中的字段类型可以不同。无论从记录中访问哪个字段,类型系统都知道每个字段的类型:

(num, Object) pair = (42, 'a');

var first = pair.$1; // 静态类型‘ num ’,运行时类型‘ int ’。
var second = pair.$2; // 静态类型‘ Object ’,运行时类型‘ String ’。

考虑两个不相关的库,它们创建了具有相同字段集的记录。尽管这两个库之间没有耦合关系,但类型系统仍然能够理解这些记录属于相同的类型。

记录相等性

如果两个记录具有相同的形状(即字段集),并且它们的对应字段具有相同的值,则这两个记录是相等的。由于命名字段的顺序不是记录形状的一部分,因此命名字段的顺序不会影响记录的相等性。

例如:

(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);

print(point == color); // 打印 'true'.
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);

print(point == color); // 打印 'false'.

记录会根据其字段的结构自动定义 hashCode 和 == 方法。

多返回值

记录允许函数将多个值打包在一起返回。要从返回的记录中检索值,请使用模式匹配将这些值解构到局部变量中。

// 以记录的形式返回多个值
(String name, int age) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

final json = <String, dynamic>{
  'name': 'Dash',
  'age': 10,
  'color': 'blue',
};

// 使用带有位置字段的记录模式进行解构
var (name, age) = userInfo(json);

/* 相当于:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

print("名字$name,年龄$age岁");

你也可以使用其命名字段,并通过冒号 : 语法来解构一个记录。

({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// 使用带有命名字段的记录模式进行解构
final (:name, :age) = userInfo(json);

你可以不使用记录从函数中返回多个值,但其他方法都有各自的缺点。例如,创建一个类会更加冗长,而使用其他集合类型如 List 或 Map 则会失去类型安全性。

记录的多返回值和异型特性使得不同类型的 Future 可以并行化,关于这一点,你可以在dart:async 文档中查阅到相关信息。

集合 (Collections)

Dart 内置了对 list、set 和 map 集合的支持。要了解有关配置集合包含的类型的更多信息,请参阅泛型。

列表

几乎每种编程语言中最常见的集合可能是数组,即对象的有序组合。在 Dart 中,数组是 List 对象,因此大多数人直接称它们为列表。

Dart 中的列表字面量是由方括号([ ])包围的、由逗号分隔的表达式或值组成的列表。下面是一个简单的 Dart 列表:

var list = [1, 2, 3];

提示!

Dart 会推断出该列表的类型为 List<int>。如果您尝试向此列表中添加非整数对象,分析器或运行时将引发错误。有关更多信息,请阅读有关类型推断的内容。

在 Dart 集合字面量的最后一个项(元素)后面可以添加一个逗号。这个尾随逗号不会影响集合,但有助于防止复制粘贴错误。

var list = [
  'Car',
  'Boat',
  'Plane', // 最后一项添加逗号
];

列表使用从0开始的索引,其中 0 是第一个值的索引,list.length - 1 是最后一个值的索引。你可以使用 .length 属性获取列表的长度,并使用下标运算符([ ])访问列表的值。

var list = [1, 2, 3];
print(list.length == 3); // 打印 true
print(list[1] == 2); // 打印 true

list[1] = 1; // 重新赋值给下标是1的元素
print(list[1] == 1); // 打印 true

要创建一个编译时常量列表,请在列表字面量前添加 const 关键字:

var constantList = const [1, 2, 3]; // 也可以这样:const List constantList = [1, 2, 3];
// constantList[1] = 1; // 这一行将导致一个错误(运行时会报错)

有关列表的更多信息,请参阅  dart:core 文档

集合

Dart 中的集合(Set)是由唯一元素组成的无序集合。Dart 通过集合字面量和 Set 类型来提供对集合的支持。

下面是一个简单的 Dart 集合,使用 set 字面量创建:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

提示

Dart 会推断出 halogens 的类型为 Set<String>。如果你尝试向集合中添加错误类型的值,分析器或运行时将会报错。

要创建一个空集合,可以使用带有类型参数的 {},或者将一个 {} 赋值给一个类型为 Set 的变量。

var names = <String>{};
// Set<String> names = {}; // 这个也行
// var names = {}; // 创建一个map,而不是一个set

Set 还是 map ?

映射 map 字面量的语法与集合 set 字面量的语法相似。由于映射 map 字面量先被引入,所以空的大括号 {} 默认是映射 Map 类型。如果你忘记在空的大括号 {} 或其赋值的变量上添加类型注解,那么 Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

使用 add() 或 addAll() 方法向现有集合中添加元素。

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens); // halogens 是上面例子的变量

使用 .length 来获取集合中的元素数量:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens); // halogens是上面例子的变量
print(elements.length == 5); // 打印 false ,因为halogens元素数量是5,加上elements原有的数量就是6了

要在编译时创建一个常量set,请在set字面量之前添加 const 关键字:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // 这行代码将会导致一个错误。

有关集合的更多信息,请参阅  dart:core 文档

映射表

一般来说,map 是一个将键和值关联起来的对象。键和值都可以是任何类型的对象。每个键只出现一次(唯一的,当出现相同的键时最后一个键会覆盖前面的键),但你可以多次使用相同的值。Dart 语言通过 map 字面量和 Map 类型来提供对映射表的支持。

下面是两个简单的 Dart 映射表,使用 map 字面值创建:

var gifts = {
  // 键:    值
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

提示!

Dart 会推断 gifts 的类型为 Map<String, String>,而 nobleGases 的类型为 Map<int, String>。如果你尝试向这两个 map 中的任何一个添加类型不匹配的值,分析器或运行时将会报错。

你可以使用 Map 构造函数创建相同的对象:

var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

提示

如果您使用 c# 或 Java 等语言,您可能希望看到 new Map() ,而不是 Map()。在 Dart 中,new 关键字是可选的。

另外 Map 是一个抽象接口,按理接口是不能直接实例化的,但 Map 的构造函数定义成了 external factory Map()(外部的工厂构造函数),所以上面才可以使用 Map 构造函数创建对象。

使用下标赋值运算符([ ]=)向现有的 map 中添加一个新的键值对:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加键值对

使用下标运算符 ([ ]) 从 map 中检索(获取)值:

var gifts = {'first': 'partridge'};
print(gifts['first'] == 'partridge'); // 打印 true

如果你在 map 中查找一个不存在的键,你会得到 null 作为返回值:

var gifts = {'first': 'partridge'};
print(gifts['fifth'] == null); // 打印 true

使用 .length 来获取 map 中键值对的数量:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
print(gifts.length == 2); // 打印 true

要创建一个编译时常量 map ,请在 map 字面量之前添加 const 关键字:

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

// constantMap[2] = 'Helium'; // 这一行将导致一个错误。

有关映射表的更多信息,请参阅  dart:core 文档

运算符

扩展运算符

Dart 支持在 list、map 和 set 字面量中使用扩展运算符(...)和空安全扩展运算符(...?)。扩展运算符提供了一种简洁的方式,可以将多个值插入到集合中。

例如,您可以使用扩展运算符(...)将一个列表的所有值插入到另一个列表中:

var list = [1, 2, 3];
var list2 = [0, ...list];
print(list2.length == 4); // 打印 true

如果扩展运算符右侧的表达式可能为 null,您可以使用空安全扩展运算符(...?)来避免异常:

var list2 = [0, ...?list];
print(list2.length == 1); // 如果list为null,则打印 true,反之可能打印 false 或者 true 

有关使用扩展运算符的更多详细信息和示例,请参阅 扩展运算符提案

控制流运算符

Dart 提供了 collection if 和 collection for,它们可以在 list、map 和 set 字面量中使用。您可以使用这些运算符通过条件判断(if)和重复(for)来构建集合。

以下是一个使用 collection if 运算符来创建一个包含三到四个元素的 list 的示例:

var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet']; // 如果promoActive是true,则nav数组的元素数量是四个,反之是三个

Dart 也支持在 collection 字面量内部使用 if-case 结构:

var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];

下面是一个使用 collection for 在将 list 中的元素添加到另一个 list 之前对其进行操作的示例:

var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
print(listOfStrings[1] == '#1'); // 打印 true

上面的 collection for 例子与扩展运算符 (...) 比较相似,扩展运算符只能把另一个 list 的所有值原封的传递过去,而 collection for 运算符可以先操作后再传递。

有关使用 collection if 和 collection for 的详细信息和示例,请参阅控制流集合提案

泛型 (Generics)

如果你查看基本数组类型 List 的 API 文档,你会发现该类型的实际形式是 List<E>。<...> 符号表示 List 是一个泛型(或参数化)类型——即一个具有形式类型参数的类型。按照惯例,大多数类型变量都使用单字母命名,如 E、T、S、K 和 V。

为什么要使用泛型?

类型安全通常需要泛型,但它们的好处不仅仅是允许代码运行:

  • 正确指定泛型类型会生成质量更高的代码。
  • 你可以使用泛型来减少代码重复。

如果你希望 list 只包含字符串,你可以将其声明为 List<String>(读作“list of string”)。这样,你、你的同事以及你的工具都能检测到向该 list 赋值非字符串类型很可能是一个错误。下面是一个例子。

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 错误

使用泛型的另一个原因是减少代码重复。泛型允许你在多种类型之间共享单一的接口和实现,同时仍然可以利用静态分析的优势。例如,假设你创建了一个用于缓存对象的接口:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

你发现你需要这个接口的字符串专用版本,所以你创建了另一个接口:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

后来,你决定你还需要这个接口的数字专用版本……你懂的。

泛型类型可以省去你创建所有这些接口的麻烦。相反,你可以创建一个接受类型参数的单一接口。

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

在这段代码中,T 是占位符类型。你可以将其视为一个开发者稍后会定义的类型的占位符。

使用集合字面量

List、set 和 map 字面量可以被参数化。参数化字面量与你之前见过的字面量相似,只是在方括号之前添加了<类型>(对于列表和集合)或<键类型, 值类型>(对于映射)。以下是使用类型化字面量的一个示例:

var names = <String>['Seth', 'Kathy', 'Lars']; // list 泛型字面量
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'}; // set 泛型字面量
var pages = <String, String>{ // map 泛型字面量
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

使用带构造函数的参数化类型

在使用构造函数时,如果要指定一个或多个类型,请在类名之后立即使用尖括号 (<...>) 来包含这些类型。例如:

// set的<...>里只能指定一个类型,这里指定String类型,from是Set的一个factory(工厂)构造函数  
var names = <String>['Seth', 'Kathy', 'Lars']; 
var nameSet = Set<String>.from(names);

下面代码创建了一个map,其中键是整数类型,而值是View类型:

// <...>中指定了两个类型,其中键(第一个类型)是int类型,值(第二个类型)是View类型
var views = Map<int, View>(); 

泛型集合及其包含的类型

Dart 的泛型类型是具体化的,这意味着它们在运行时携带自己的类型信息。例如,你可以测试一个集合的类型。

var names = <String>[]; // 创建一个 list泛型集合
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

提示

相比之下,Java 中的泛型使用类型擦除(erasure),这意味着泛型类型参数在运行时会被移除。在 Java 中,你可以测试一个对象是否是 List,但是你无法测试它是否是List<String>。

限制参数化类型

在实现泛型类型时,您可能希望限制可以作为参数提供的类型,以确保参数必须是特定类型的子类型。您可以使用 extends 关键字来实现这一点。

一个常见的用例是通过使类型成为 Object 的子类型(而不是默认的 Object? )来确保类型不可为空。

class Foo<T extends Object> {
  // 提供给Foo的T类型参数必须是非空的,由于 Object 是所有类型的基类,所以T可以是任何类型
}

除了 Object 之外,你还可以使用 extends 关键字与其他类型一起使用。以下是一个扩展SomeBaseClass 的例子,这样你就可以在类型T的对象上调用 SomeBaseClass 的成员了:

class Foo<T extends SomeBaseClass> {
  // 实现在这里…
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

可以使用 SomeBaseClass 或其任何子类型作为泛型参数:

var someBaseClassFoo = Foo<SomeBaseClass>(); // 使用SomeBaseClass作为泛型参数
var extenderFoo = Foo<Extender>();// 使用SomeBaseClass的子类Extender作为泛型参数

也可以不指定泛型参数:

var foo = Foo();
print(foo); // Foo<SomeBaseClass>’的实例

指定任何非 SomeBaseClass 类型都会导致错误:

var foo = Foo<Object>(); // 编译分析错误,因为 Object 不是 SomeBaseClass 的子类

使用泛型方法

方法和函数也允许类型参数:

T first<T>(List<T> ts) { 
  // 进行一些初始工作或错误检查,然后……
  T tmp = ts[0];
  // 进行一些额外的检查或处理…
  return tmp;
}

看 first 后面的<T>表示这是一个泛型方法或函数,占位符为T字母,当参数 List<T> 的T是什么类型时,方法或函数里的T就表示什么类型。

在这里,first 方法上的泛型类型参数 <T> 允许你在多个地方使用类型参数 T:

  • 函数的返回类型为 (T)。
  • 参数的类型为 (List<T>)。
  • 局部变量的类型为 (T tmp)。

别名 (Typedefs)

类型别名(通常称为 typedef,因为它是用关键字 typedef 声明的)是引用类型的一种简明方式。下面是声明和使用名为 IntList 的类型别名的示例:

typedef IntList = List<int>; // List<int>泛型的别名是IntList 
IntList il = [1, 2, 3];

类型别名可以有类型参数:

typedef ListMapper<X> = Map<X, List<X>>;

void main() {
  Map<String, List<String>> m1 = {}; // 罗嗦的
  ListMapper<String> m2 = {}; // 同样的东西,但更短更清晰

  m1["01"] = ["A","B"];
  m1["02"] = ["C","D"];

  m2["11"] = ["AA","BB"];
  m2["22"] = ["CC","DD"];

  print(m1); // 打印 {01: [A, B], 02: [C, D]}
  print(m2); // 打印 {11: [AA, BB], 22: [CC, DD]}
}

版本提示

在 Dart 2.13 版本之前,类型定义仅限于函数类型。要使用新的类型定义,至少需要 Dart 2.13 版本的语言。

在大多数情况下,我们建议使用内联函数类型而不是函数类型定义。然而,函数类型定义仍然有其用处:

typedef Compare<T> = int Function(T a, T b); // 函数类型定义

int sort(int a, int b) => a - b;

void main() {
  print(sort is Compare<int>); // 打印 true, 因为Compare<int>就是一个有两个传参的函数(Function(T a, T b))
}

类型系统

详见类型系统

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值