Dart基础语法之内置数据类型

199 阅读21分钟

2.1 基本类型

Dart 语言对以下类型有特殊的支持:

  • Numbers(数值int,double)
  • Strings(字符串String)
  • Booleans(布尔bool)
  • Records (记录(value1,value2))
  • Lists(列表,也称为数组List)
  • Maps(映射Map)
  • Runes(在字符串中表示一个Unicode字符)
  • Symbols ( Symbol)
  • The value null (Null)

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

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

  • Object:除 Null 之外的所有 Dart 类的超类。
  • Enum:所有枚举的超类。
  • Future和Stream:用于异步支持。
  • Iterable:用于for-in 循环和同步生成器函数。
  • Never:指示表达式永远无法成功完成求值。最常用于总是抛出异常的函数。
  • dynamic:表示要禁用静态检查。通常你应使用Object或Object?代替。
  • void:表示从未使用过该值。通常用作返回类型。
Numbers

Dart 中的数值有两种类型:

int

整数值不大于 64 位, 具体取决于平台。在本机平台上,值可以从 -2 63到 2 63 - 1。在 Web 上,整数值表示为 JavaScript 数字(没有小数部分的 64 位浮点值),并且可以从 -253到 253 - 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 += 2.5;

必要时,整数文字会自动转换为双精度数:

double z = 1; // Equivalent to 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);
assert(piAsString == '3.14');

<font style="color:rgb(17, 121, 109);">int</font>类型指定传统的位移位(<font style="color:rgb(17, 121, 109);"><<</font>, <font style="color:rgb(17, 121, 109);">>></font>, <font style="color:rgb(17, 121, 109);">>>></font>)、补码(<font style="color:rgb(17, 121, 109);">~</font>)、与(<font style="color:rgb(17, 121, 109);">&</font>)、或(<font style="color:rgb(17, 121, 109);">|</font>)和异或(<font style="color:rgb(17, 121, 109);">^</font>)运算符,这些运算符对于操作和屏蔽位字段中的标志很有用。例如:

assert((3 << 1) == 6); // 0011 << 1 == 0110

assert((3 | 4) == 7); // 0011 | 0100 == 0111

assert((3 & 4) == 0); // 0011 & 0100 == 0000

以字面量定义的数值是编译期常量。许多算术表达式也同样是编译器常量,只要它们的操作数是编译期常数且最后得到一个数值。

const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;
字符串

Dart 的字符串是以 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} 将一个表达式插入到字符串中。如果这个表达式是一个标识符,你可以省略 {}。为了得到一个对象的字符串表示,Dart 会调用对象的 toString() 方法。

var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, '
        'which is very handy.');
assert('That deserves all caps. '
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. '
        'STRING INTERPOLATION is very handy!');

说明:== 运算符测试两个对象是否相等。两个字符串相等的条件是它们包含同样的编码单位序列。

你可以通过并排字符串字面量或者使用 + 运算符来串联字符串:

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

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

要创建多行字符串,使用三个引号(单引号或双引号)来标记:

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

你可以通过 r 前缀来创建一个”原始的“字符串:

var s = r'In a raw string, not even \n gets special treatment.';

如果这是一个普通字符串(没有前缀 r),"\n" 会被解释为一个换行符。但是,因为我们在字符串前加了 r,这个字符串被视为一个原始字符串,因此 \n 不会被解析为换行符,而是作为普通的两个字符 `` 和 n

只要所有的插值表达式是编译期常量,计算结果为 null 或者 数值、字符串、布尔值,那么这个字符串字面量就是编译期常量。

// 可作为常量字符串的组成部分
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// 不可作为常量字符串的组成部分
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [123];

const validConstString = '$aConstNum $aConstBool $aConstString';
// const invalidConstString = '$aNum $aBool $aString $aConstList';
布尔值

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

Dart 的类型安全意味着你不能使用像 if (nonbooleanValue) 这样的代码。取而代之,你需要明确地检查值,像是:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints == 0);

// Check for null.
var unicorn = null;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);
Runes和Grapheme Clusters

在 Dart 中,runes 用于暴露字符串的 Unicode 码位。你可以使用 characters 包来查看或操作用户感知的字符,也称为 Unicode(扩展的)字素簇(grapheme clusters)

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

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

import 'package:characters/characters.dart';

void main() {
  var hi = 'Hi 🇩🇰';
  print(hi);
  print('The end of the string: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}');
}

输出取决于您的环境,如下所示:

Hi 🇩🇰
The end of the string: ???
The last character: 🇩🇰
Symbols

一个 Symbols 对象表示 Dart 程序中已声明的运算符或标识符。你可能永远都不需要用到 symbols,但是它们对于通过名称来标识引用的接口非常重要,因为缩写改变标识符的名字但不改变标识符的 symbols。

要获取一个标识符的 symbol,使用 symbol 字面量,语法是 # 后面跟上标识符:

#radix
#bar

Symbol 字面量是编译期常量

2.2 Records

语言版本需要至少 3.0 。

Records是一种匿名、不可变的聚合类型。与其他集合类型一样,它们允许您将多个对象捆绑为一个对象。与其他集合类型不同,记录是固定大小、异构(就是在一个Record可以存在多种不同的类型)且类型化的。

Records是真实值;可以将它们存储在变量中、嵌套它们、将它们传递到函数或从函数传递它们,以及将它们存储在列表lists、maps和集合sets等数据结构中

Records表达式是以逗号分隔的命名或位置字段列表,括在括号中:

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

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

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

record表达式和类型注释中的字段反映了参数和实参如何在函数中起作用。位置字段直接进入括号内:

// Record type annotation in a variable declaration:
(String, intrecord;

// Initialize it with a record expression:
record = ('A string'123);

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

// Record type annotation in a variable declaration:
({int a, bool b}) record;

// Initialize it with a record expression:
record = (a: 123, b: true);

record类型中命名字段的名称是record类型定义或其形状的一部分。两个具有不同名称的命名字段的record具有不同的类型:

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

// Compile error! These records don't have the same type.
// recordAB = recordXY;

在record类型注释中,您还可以命名位置字段,但这些名称纯粹用于文档,不会影响记录的类型:

(int a, int b) recordAB = (12);
(int x, int y) recordXY = (34);

recordAB = recordXY; // OK.  recordAB与recordXY相同都是(int,int)

这类似于函数声明或函数 typedef 中的位置参数可以有名称,但这些名称不会影响函数的签名。

Record字段

record字段可通过内置 getter 访问。record是不可变的,因此字段没有 setter。

命名字段会公开同名的 getter。位置字段会公开名称为 的 getter $,跳过命名字段:

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

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

Record类型

单个Record类型没有类型声明。记录根据其字段的类型进行结构化类型划分。record的形状(其字段集、字段类型及其名称(如果有))唯一地决定了记录的类型。

record中的每个字段都有自己的类型。同一record中的字段类型可以不同。无论从record中访问哪个字段,类型系统都能识别每个字段的类型:

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

var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.

假设两个不相关的库创建了具有相同字段集的记录。即使库之间没有相互耦合,类型系统也会认为这些记录属于同一类型。

Record相等

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

例如:

(int x, int y, int z) point = (123);
(int r, int g, int b) color = (123);

print(point == color); // Prints '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); // Prints 'false'. Lint: Equals on unrelated types.

record根据其字段的结构自动定义 hashCode 和 == 方法

多次返回

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

// Returns multiple values in a record:
(String name, int age) userInfo(Map<Stringdynamic> json) {
  return (json['name'as String, json['age'as int);
}

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

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

/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

你还可以使用冒号语法,通过其命名字段来解构record:,您可以在 模式类型页面上阅读更多相关信息:

({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// Destructures using a record pattern with named fields:
final (:name, :age) = userInfo(json);

2.3 集合

Dart 内置了对 list、set 和 map集合的支持

lists

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

lists内容由逗号分隔的表达式或值列表表示,括在方括号 ( []) 中。这是一个简单的 Dart 列表:

var list = [123];

// tip:Dart 推断其list类型为List<int>。如果您尝试将非整数对象添加到此列表,分析器或运行时会引发错误。有关更多信息,请阅读类型推断。

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

var list = [
  'Car',
  'Boat',
  'Plane',
];

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

var list = [123];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

要创建编译时常量列表,请const在列表文字前添加:

var constantList = const [123];
// constantList[1] = 1; // This line will cause an error.
sets

set是唯一项的无序集合,Dart 通过 set 字面量和 Set 类型来支持 set。

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

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

tip:Dart 推断 halogens 的类型为 Set。如果你试图添加错误的类型到这个 set 中,分析器或运行时会抛出一个错误。

要创建一个空的 set,使用 {} 并提供一个类型参数,或者使用 {} 指向带类型的 Set:

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

:::info Set 还是 map? Map 的字面量语法和 set 的字面量语法很相似。因为 map 字面量的优先级更高,{} 默认表示 Map 类型。如果你忘记了 {} 的类型注解或者它指向的变量,Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

:::

使用 add() 或 addAll() 来向已存在的 set 中添加元素

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);

用于.length获取set中的项目数:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);

要创建一个 set 作为编译期常量,在 set 字面量前使用 const。

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

通常来说,映射是一个关联了键和值的对象。键和值都可以是任意类型的对象。“键”是唯一的,但是你可以多次使用相同的“值”。Dart 通过映射字面量和 Map 类型来支持映射。

下面是几个简单的 Dart 映射,使用字面量创建:

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

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

:::info 说明:Dart 推断 gifts 拥有类型 Map<String, String> ,而 nobleGases 拥有类型 Map<int, String> 。如果你试图添加错误的类型到上面的映射中,分析器或者运行时会报告一个错误。要了解更多信息,请参阅 类型推断

:::

你可以通过 Map 的构造函数创建同样的对象:

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

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

:::info 如果你之前使用过 C# 或 Java 等语言,你可能会希望看到<font style="color:rgb(17, 121, 109);background-color:rgb(231, 248, 255);">new Map()</font> 而不是<font style="color:rgb(17, 121, 109);background-color:rgb(231, 248, 255);">Map()</font>。在 Dart 中,<font style="color:rgb(17, 121, 109);background-color:rgb(231, 248, 255);">new</font>关键字是可选的。有关详细信息,请参阅使用构造函数

:::

使用下标赋值运算符将新的键值对添加到现有映射(<font style="color:rgb(17, 121, 109);">[]=</font>):

var gifts = {'first''partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair

使用下标运算符从映射中检索值(<font style="color:rgb(17, 121, 109);">[]</font>):

var gifts = {'first''partridge'};
assert(gifts['first'] == 'partridge');

如果您查找不在Map中的键,您将得到null以下结果:

var gifts = {'first''partridge'};
assert(gifts['fifth'] == null);

使用 .length 去获得映射中键值对的数量:

var gifts = {'first''partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

要创建一个作为编译期常量的映射,在映射字面量前加上 const:

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

// constantMap[2] = 'Helium'; // 这一行会引发一个错误
运算符

拓展运算符

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

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

var list = [123];
var list2 = [0, ...list];
assert(list2.length == 4);

如果扩展运算符右边的表达式可能为空,则可以使用可识别空值的扩展运算符 ( ...?) 来避免异常:

var list2 = [0, ...?list];
assert(list2.length == 1);

控制流运算符

Dart 提供了collection if和collection for给list、map和set字面量使用。可以使用这些运算符通过条件 ( if) 和重复 ( for)构建集合for。

下面是使用collection if 创建包含三或四个项目的列表的示例:

var nav = ['Home''Furniture''Plants', if (promoActive) 'Outlet'];

Dart 还支持集合字面量中的if-case:

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

下面是一个使用collection for 操作一个列表的项目然后再将它们添加到另一个列表的示例:

var listOfInts = [123];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');

2.4 泛型

如果你查看基本数组类型 List 的 API 文档,你会发现它的类型其实是 List。<...> 标记表示 List 是一个”泛型“(或带参数的)类——具有形式上的类型参数的类型。按照惯例,类型变量的名字是单个字母,比如 E,T,S,K,和 V。

** 为什么要使用泛型?**
泛型通常是类型安全所必需的,但它们的好处不仅仅是让你的代码运行:

  • 正确指定泛型类型可以生成更好的代码。
  • 您可以使用泛型来减少代码重复。

如果您希望列表仅包含字符串,则可以将其声明为List(读作“字符串列表”)。这样您、您的同事和您的IDE就可以检测到将非字符串分配给列表可能是一个错误。以下是一个例子:

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

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

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> {
  getByKey(String key);
  void setByKey(String key, T value);
}

在此代码中,T 是替代类型。它是一个占位符,您可以将其视为开发人员稍后将定义的类型。

使用集合字面量

列表和映射字面量可以是参数化的。参数化的字面量就像你之前见过的字面量,只是在左括号前加上了 (对于列表)或 <keyType, valueType>(对于映射)。下面是一个使用类型字面量的例子:

var names = <String>['Seth''Kathy''Lars'];
var uniqueNames = <String>{'Seth''Kathy''Lars'};
var pages = <StringString>{
  'index.html''Homepage',
  'robots.txt''Hints for web robots',
  'humans.txt''We are people, not machines'
};

在构造函数中使用参数类型

要在使用构造函数时指定一个或多个类型,请将类型放在<...>类名后面的尖括号 ( ) 中。例如:

var nameSet = Set<String>.from(names);

以下代码创建一个具有整数键和 View 类型值的映射:

var views = Map<int, View>();

泛型集合和它们包含的类型

Dart 的泛型类是“实体化”的,这意味着它们在运行期携带了自己的类型信息。因此,你可以检测一个集合的类型:

var names = <String>[];
names.addAll(['Seth''Kathy''Lars']);
print(names is List<String>); // true

限制参数类型

实现一个泛型时,你可能想要限制它的参数类型。你可以使用 extends 做到这点。

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

class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

使用 SomeBaseClass 或者它的子类作为泛型参数是可以的:

class Foo<T extends SomeBaseClass{
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

不指定通用参数也是可以的:

var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

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

var foo = Foo<Object>();

使用泛型方法

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

first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

这里 first () 中的泛型参数允许你在以下几个地方使用类型参数 T:

在函数的返回类型中(T)。

在参数类型中(List)。

在局部变量的类型中(T tmp)。

2.5 Typedefs

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

typedef IntList = List<int>;
IntList il = [123];

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

typedef ListMapper<X= Map<XList<X>>;
Map<StringList<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

在大多数情况下,我们建议使用内联函数类型,而不是函数的 typedef。但是,函数 typedef 仍然有用:

typedef Compare<T> = int Function(T a, T b);

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

void main() {
  assert(sort is Compare<int>); // True!
}

2.6 类型系统

Dart 是类型安全的编程语言:Dart 使用静态类型检查和运行时检查的组合来确保变量的值始终与变量的静态类型或其他安全类型相匹配。尽管类型是必需的,但由于类型推断,类型的注释是可选的。

静态类型检查的一个好处是能够使用 Dart 的静态分析器在编译时找到错误。

可以向泛型类添加类型注释来修复大多数静态分析错误。最常见的泛型类是集合类型 List 和 Map<K,V>

例如,在下面的代码中,main() 创建一个列表并将其传递给 printInts(),由 printInts() 函数打印这个整数列表。

void printInts(List<int> a) => print(a);

void main() {
  final list = [];
  list.add(1);
  list.add('2');
  printInts(list);
}

上面的代码在调用 printInts(list) 时会在 list (高亮提示)上产生类型错误:

error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable

高亮错误是因为产生了从 List 到 List 的不正确的隐式转换。 list 变量是 List 静态类型。这是因为 list 变量的初始化声明 var list = [] 没有为分析器提供足够的信息来推断比 dynamic 更具体的类型参数。 printInts() 函数需要 List 类型的参数,因此导致类型不匹配。

在创建 list 时添加类型注释 (代码中高亮显示部分)后,分析器会提示无法将字符串参数分配给 int 参数。删除 list.add("2") 中的字符串引号使代码通过静态分析并能够正常执行。

void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

什么是类型安全

类型安全是为了确保程序不会进入某些无效状态。安全的类型系统意味着程序永远不会进入表达式求值为与表达式的静态类型不匹配的值的状态。例如,如果表达式的静态类型是 String ,则在运行时保证在评估它的时候只会获取字符串。

Dart 的类型系统,同 Java 和 C#中的类型系统类似,是安全的。它使用静态检查(编译时错误)和运行时检查的组合来强制执行类型安全。例如,将 String 分配给 int 是一个编译时错误。如果对象不是字符串,使用 as String 将对象转换为字符串时,会由于运行时错误而导致转换失败。

类型安全的好处

安全的类型系统有以下几个好处:

  • 在编译时就可以检查并显示类型相关的错误。
    安全的类型系统强制要求代码明确类型,因此在编译时会显示与类型相关的错误,这些错误可能在运行时可能很难发现。
  • 代码更容易阅读。
    代码更容易阅读,因为我们信赖一个拥有指定类型的值。在类型安全的 Dart 中,类型是不会骗人的。因为一个拥有指定类型的值是可以被信赖的。
  • 代码可维护性更高。
    在安全的类型系统下,当更改一处代码后,类型系统会警告因此影响到的其他代码块。
  • 更好的 AOT 编译。
    虽然在没有类型的情况下可以进行 AOT 编译,但生成的代码效率要低很多。

静态检查中的一些技巧

大多数静态类型的规则都很容易理解。下面是一些不太明显的规则:

  • 重写方法时,使用类型安全返回值。
  • 重写方法时,使用类型安全的参数。
  • 不要将动态类型的 List 看做是有类型的 List。

让我们通过下面示例的类型结构,来更深入的了解这些规则

重写方法时,使用类型安全返回值

子类方法中返回值类型必须与父类方法中返回值类型的类型相同或是其子类型。考虑 Animal 类中的 Getter 方法:

class Animal {
  void chase(Animal a) { ... }
  Animal get parent => ...
}

父类Getter 方法返回一个 Animal 。在 HoneyBadger 子类中,可以使用 HoneyBadger(或 Animal 的任何其他子类型)替换 Getter 的返回值类型,但不允许使用其他的无关类型。

class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  HoneyBadger get parent => ...
}
此种写法会报错
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  Root get parent => ...
}

重写方法时,使用类型安全的参数

子类方法的参数必须与父类方法中参数的类型相同或是其参数的父类型。不要使用原始参数的子类型,替换原有类型,这样会导致参数类型"收紧"。

考虑 Animal 的 chase(Animal) 方法:

class Animal {
  void chase(Animal a) { ... }
  Animal get parent => ...
}

chase() 方法的参数类型是 Animal 。一个 HoneyBadger 可以追逐任何东西。因此可以在重写 chase() 方法时将参数类型指定为任意类型 (Object) 。

class HoneyBadger extends Animal {
  @override
  void chase(Object a) { ... }

  @override
  Animal get parent => ...
}

Mouse 是 Animal 的子类,下面的代码将 chase() 方法中参数的范围从 Animal 缩小到 Mouse 。

报错:static analysis: failure
class Mouse extends Animal { ... }

class Cat extends Animal {
  @override
  void chase(Mouse a) { ... }
}

下面的代码不是类型安全的,因为 a 可以是一个 cat 对象,却可以给它传入一个 alligator 对象。

Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.

不要将动态类型的 List 看做是有类型的 List

当期望在一个 List 中可以包含不同类型的对象时,动态列表是很好的选择。但是不能将动态类型的 List 看做是有类型的 List 。

这个规则也适用于泛型类型的实例。

下面代码创建一个 Dog 的动态 List ,并将其分配给 Cat 类型的 List ,表达式在静态分析期间会产生错误。

void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

运行时检查

运行时检查工具会处理分析器无法捕获的类型安全问题。

例如,以下代码在运行时会抛出异常,因为将 Dog 类型的 List 赋值给 Cat 类型的 List 是错误的:

void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>;
}

类型推断

分析器 (analyzer) 可以推断字段,方法,局部变量和大多数泛型类型参数的类型。当分析器没有足够的信息来推断出一个特定类型时,会使用 <font style="color:rgb(17, 121, 109);">dynamic</font> 作为类型。

下面是在泛型中如何进行类型推断的示例。在此示例中,名为arguments的变量包含一个 Map ,该 Map 将字符串键与各种类型的值配对。

如果显式键入变量,则可以这样写:

Map<String, dynamic> arguments = {'argA''hello''argB'42};

或者,使用var让 Dart 来推断类型:

var arguments = {'argA''hello''argB'42}; // Map<String, Object>

Map 字面量从其条目中推断出它的类型,然后变量从 Map 字面量的类型中推断出它的类型。在此 Map 中,键都是字符串,但值具有不同的类型( String 和 int ,它们具有共同的上限类型 Object )。因此,Map 字面量的类型为 <font style="color:rgb(17, 121, 109);">Map<String, Object></font> ,也就是 <font style="color:rgb(17, 121, 109);">arguments</font> 的类型。

字段和方法推断

重写父类的且没有指定类型的字段或方法,继承父类中字段或方法的类型。

没有声明类型且不存在继承类型的字段,如果在声明时被初始化,那么字段的类型为初始化值的类型。

静态字段推断

静态字段和变量的类型从其初始化程序中推断获得。需要注意的是,如果推断是个循环,推断会失败(也就是说,推断变量的类型取决于知道该变量的类型)。

局部变量推断

在不考虑连续赋值的情况下,局部变量如果有初始化值的情况下,其类型是从初始化值推断出来的。这可能意味着推断出来的类型会非常严格。如果是这样,可以为他们添加类型注释。

✗ static analysis: failure

var x = 3; // x is inferred as an int.
x = 4.0;
num y = 3; // A num can be double or int.
y = 4.0;

参数类型推断

构造函数调用的类型参数和 泛型方法 调用是根据上下文的向下信息和构造函数或泛型方法的参数的向上信息组合推断的。如果推断没有按照意愿或期望进行,那么你可以显式的指定他们的参数类型。

// Inferred as if you wrote <int>[].
List<int> listOfInt = [];

// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];

// Inferred as Iterable<int>.
var ints = listOfDouble.map((x) => x.toInt());

在最后一个示例中,根据向下信息 <font style="color:rgb(17, 121, 109);">x</font> 被推断为 <font style="color:rgb(17, 121, 109);">double</font> 。闭包的返回类型根据向上信息推断为 <font style="color:rgb(17, 121, 109);">int</font> 。在推断 <font style="color:rgb(17, 121, 109);">map()</font> 方法的类型参数:<font style="color:rgb(17, 121, 109);"><int></font> 时,Dart 使用此返回值的类型作为向上信息。

替换类型

当重写方法时,可以使用一个新类型(在新方法中)替换旧类型(在旧方法中)。类似地,当参数传递给函数时,可以使用另一种类型(实际参数)的对象替换现有类型(具有声明类型的参数)要求的对象。什么时候可以用具有子类型或父类型的对象替换具有一种类型的对象那?

从_消费者_和_生产者_的角度有助于我们思考替换类型的情况。消费者接受类型,生产者产生类型。

可以使用父类型替换消费者类型,使用子类型替换生产者类型。

下面让我们看一下普通类型赋值和泛型类型赋值的示例。

普通类型赋值

将对象赋值给对象时,什么时候可以用其他类型替换当前类型?答案取决于对象是消费者还是生产者。

分析以下类型层次结构:

思考下面示例中的普通赋值,其中 <font style="color:rgb(17, 121, 109);">Cat c</font>消费者<font style="color:rgb(17, 121, 109);">Cat()</font>生产者

Cat c = Cat();

在消费者的位置,任意类型(<font style="color:rgb(17, 121, 109);">Animal</font>)的对象替换特定类型(<font style="color:rgb(17, 121, 109);">Cat</font>)的对象是安全的。因此使用 <font style="color:rgb(17, 121, 109);">Animal c</font> 替换 <font style="color:rgb(17, 121, 109);">Cat c</font> 是允许的,因为 Animal 是 Cat 的父类。

Animal c = Cat();

但是使用 <font style="color:rgb(17, 121, 109);">MaineCoon c</font> 替换 <font style="color:rgb(17, 121, 109);">Cat c</font> 会打破类型的安全性,因为父类可能会提供一种具有不同行为的 Cat ,例如 Lion :

✗ static analysis: failure

MaineCoon c = Cat();

在生产者的位置,可以安全地将生产类型 (Cat) 替换成一个更具体的类型 (MaineCoon) 的对象。因此,下面的操作是允许的:

Cat c = MaineCoon();

泛型赋值

上面的规则同样适用于泛型类型吗?是的。考虑动物列表的层次结构— Cat 类型的 List 是 Animal 类型 List 的子类型,是 MaineCoon 类型 List 的父类型。

在下面的示例中,可以将 <font style="color:rgb(17, 121, 109);">MaineCoon</font> 类型的 List 赋值给 <font style="color:rgb(17, 121, 109);">myCats</font> ,因为 <font style="color:rgb(17, 121, 109);">List<MaineCoon></font><font style="color:rgb(17, 121, 109);">List<Cat></font> 的子类型:

List<MaineCoon> myMaineCoons = ...
  List<Cat> myCats = myMaineCoons;

从另一个角度看,可以将 <font style="color:rgb(17, 121, 109);">Animal</font> 类型的 List 赋值给 <font style="color:rgb(17, 121, 109);">List<Cat></font> 吗?

List<Animal> myAnimals = ...
  List<Cat> myCats = myAnimals;

这个赋值不能通过静态分析,因为它创建了一个隐式的向下转型 (downcast),这在非 <font style="color:rgb(17, 121, 109);">dynamic</font> 类型中是不允许的,比如 <font style="color:rgb(17, 121, 109);">Animal</font>

若要这段代码能够通过静态分析,需要使用一个显式转换,这可能会在运行时导致失败。

List<Animal> myAnimals = ...
  List<Cat> myCats = myAnimals as List<Cat>;

不过,显式转换在运行时仍然可能会失败,这取决于转换被转换内容的实际类型 (此处是 <font style="color:rgb(17, 121, 109);">myAnimals</font>)。

方法

在重写方法中,生产者和消费者规则仍然适用。例如:

对于使用者(例如 <font style="color:rgb(17, 121, 109);">chase(Animal)</font> 方法),可以使用父类型替换参数类型。对于生产者(例如 <font style="color:rgb(17, 121, 109);">父类</font> 的 Getter 方法),可以使用子类型替换返回值类型。