WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

Dart #47

Open WangShuXian6 opened 5 years ago

WangShuXian6 commented 5 years ago

Dart

https://dart.cn/guides/language/language-tour


Dart 开发语言概览

一个简单的 Dart 程序

// Define a function.
void printInteger(int aNumber) {
  print('The number is $aNumber.'); // Print to console.
}

// This is where the app starts executing.
void main() {
  var number = 42; // Declare and initialize a variable.
  printInteger(number); // Call a function.
}

//注释。

以双斜杠开头的一行语句称为单行注释。Dart 同样支持多行注释和文档注释。查阅注释获取更多相关信息。

void

一种特殊的类型,表示一个值永远不会被使用。类似于 main() 和 printInteger() 的函数,以 void 声明的函数返回类型,并不会返回值。

int

另一种数据类型,表示一个整型数字。 Dart 中一些其他的内置类型包括 String、List 和 bool。

42

表示一个数字字面量。数字字面量是一种编译时常量。

print()

一种便利的将信息输出显示的方式。

'...' (或 "...")

表示字符串字面量。

$variableName (或 ${expression})

表示字符串插值:字符串字面量中包含的变量或表达式。查阅字符串获取更多相关信息。

main() 一个特殊且 必须的 顶级函数,Dart 应用程序总是会从该函数开始执行。查阅 main() 函数 获取更多相关信息。

var 用于定义变量,通过这种方式定义变量不需要指定变量类型。这类变量的类型 (int) 由它的初始值决定 (42)。


重要概念

所有变量引用的都是 对象,每个对象都是一个 类 的实例。数字、函数以及 null 都是对象。除去 null 以外(如果你开启了 空安全), 所有的类都继承于 Object类。

尽管 Dart 是强类型语言,但是在声明变量时指定类型是可选的,因为 Dart 可以进行类型推断。在上述代码中,变量 number 的类型被推断为 int 类型。

如果你开启了 空安全,变量在未声明为可空类型时不能为 null。你可以通过在类型后加上问号 ? 将类型声明为可空。例如,int? 类型的变量可以是整形数字或 null。如果你 明确知道 一个表达式不会为空,但 Dart 不这么认为时,你可以在表达式后添加 !来断言表达式不为空(为空时将抛出异常)。例如:int x = nullableButNotNullInt!

如果你想要显式地声明允许任意类型,使用 Object?(如果你 开启了空安全)、 Object 或者 特殊类型 dynamic 将检查延迟到运行时进行。

Dart 支持泛型,比如 List<int>(表示一组由 int 对象组成的列表)或 List<Object>(表示一组由任何类型对象组成的列表)。

Dart 支持顶级函数(例如 main 方法),同时还支持定义属于类或对象的函数(即 静态 和 实例方法)。你还可以在函数中定义函数(嵌套 或 局部函数)。

Dart 支持顶级 变量,以及定义属于类或对象的变量(静态和实例变量)。实例变量有时称之为域或属性。

Dart 没有类似于 Java 那样的 public、protected 和 private 成员访问限定符。如果一个标识符以下划线 _ 开头则表示该标识符在库内是私有的。可以查阅 库和可见性 获取更多相关信息。

标识符 可以以字母或者下划线 _ 开头,其后可跟字符和数字的组合。

Dart 中 表达式 和 语句 是有区别的,表达式有值而语句没有。比如条件表达式 expression condition ? expr1 : expr2 中含有值 expr1 或 expr2。与 if-else 分支语句相比,if-else 分支语句则没有值。一个语句通常包含一个或多个表达式,但是一个表达式不能只包含一个语句。

Dart 工具可以显示 警告 和 错误 两种类型的问题。警告表明代码可能有问题但不会阻止其运行。错误分为编译时错误和运行时错误;编译时错误代码无法运行;运行时错误会在代码运行时导致 异常。


关键字

Dart 语言所使用的关键字

abstract2 dynamic2 implements2 show1
as2 else import2 static2
assert enum in super
async1 export2 in2 super
await3 extends is sync1
break external2 library2 this
case factory2 mixin2 throw
catch false new true
class final null try
const finally on1 typedef2
continue for operator2 var
covariant2 Function2 part2 void
default get2 rethrow while
deferred2 hide1 return with
do if set2 yield3

应该避免使用这些单词作为标识符。但是,带有上标的单词可以在必要的情况下作为标识符:

带有上标 1 的关键字为 上下文关键字,只有在特定的场景才有意义,它们可以在任何地方作为有效的标识符。

带有上标 2 的关键字为 内置标识符,其作用只是在JavaScript代码转为Dart代码时更简单,这些关键字在大多数时候都可以作为有效的标识符,但是它们不能用作类名或者类型名或者作为导入前缀使用。

带有上标 3 的关键字为 Dart 1.0 发布后用于 支持异步 相关内容。不能在由关键字 async、async 或 sync 标识的方法体中使用 await 或 yield 作为标识符。

其它没有上标的关键字为 保留字,均不能用作标识符。


变量 Variables

创建一个变量并将其初始化:

var name = 'Bob';

变量仅存储对象的引用

这里名为 name 的变量存储了一个 String 类型对象的引用,“Bob” 则是该对象的值。

name 变量的类型被推断为 String,但是你可以为其指定类型。 如果一个对象的引用不局限于单一的类型,可以将其指定为 Object(或 dynamic)类型。

Object name = 'Bob';

除此之外你也可以指定类型:

String name = 'Bob';

默认值

在 Dart 中,未初始化的变量拥有一个默认的初始化值:null。(如果你未迁移至 空安全,所有变量都为可空类型。) 即便数字也是如此,因为在 Dart 中一切皆为对象,数字也不例外。

int? lineCount;
assert(lineCount == null);

assert() 的调用将会在生产环境的代码中被忽略掉。 在开发过程中,assert(condition) 将会在 条件判断 为 false 时抛出一个异常。详情请查阅 Assert。


final 和 const

如果你不想更改一个变量,可以使用关键字 final 或者 const 修饰变量, 这两个关键字可以替代 var 关键字或者加在一个具体的类型前。 一个 final 变量只可以被赋值一次; 一个 const 变量是一个编译时常量(const 变量同时也是 final 的)。 顶层的 final 变量或者类的 final 变量在其第一次使用的时候被初始化。

实例变量 可以是 final 的但不可以是 const

创建并设置两个 final 变量:

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

你不能修改一个 final 变量的值

使用关键字 const 修饰变量表示该变量为 编译时常量。

如果使用 const 修饰类中的变量,则必须加上 static 关键字,即 static const(顺序不能颠倒)。 在声明 const 变量时可以直接为其赋值,也可以使用其它的 const 变量为其赋值:

const bar = 1000000; // 直接赋值 [Unit of pressure (dynes/cm2)]
const double atm = 1.01325 * bar; // 利用其它 const 变量赋值 (Standard atmosphere)

const 关键字不仅仅可以用来定义常量,还可以用来创建 常量值,该常量值可以赋予给任何变量。 可以将构造函数声明为 const 的,这种类型的构造函数创建的对象是不可改变的。

var foo = const [];
final bar = const [];
const baz = []; // 相当于 `const []` (Equivalent to `const []`)

如果使用初始化表达式为常量赋值可以省略掉关键字 const,比如上面的常量 baz 的赋值就省略掉了 const。 详情请查阅 不要冗余地使用 const。

没有使用 final 或 const 修饰的变量的值是可以被更改的,即使这些变量之前引用过 const 的值。 ``dart foo = [1, 2, 3]; // foo 的值之前为 const [] (Was const [])

常量的值不可以被修改

可以在常量中使用 类型检查和强制类型转换 (is 和 as)、 集合中的 if 以及 展开操作符 `...` 和 `...?`:
```dart
const Object i = 3; // Where i is a const Object with an int value...
const list = [i as int]; // Use a typecast.
const map = {if (i is int) i: 'int'}; // Use is and collection if.
const set = {if (list is List<int>) ...list}; // ...and a spread.

可以查阅 Lists、Maps 和 Classes 获取更多关于使用 const 创建常量值的信息。


内置类型

Dart 语言支持下列内容:

Numbers (int, double) Strings (String) Booleans (bool) Lists (也被称为 arrays)

Sets (Set) Maps (Map) Runes (常用于在 Characters API 中进行字符替换)

Symbols (Symbol) The value null (Null)

使用字面量来创建对象也受到支持。例如 'This is a string' 是一个字符串字面量,true 是一个布尔字面量。

由于 Dart 中每个变量引用都指向一个对象(一个 类 的实例),通常也可以使用 构造器 来初始化变量。 一些内置的类型有它们自己的构造器。例如你可以使用 Map() 来创建一个 map 对象。

num

https://api.dart.dev/stable/dart-core/num-class.html https://dart.cn/guides/language/numbers Dart 支持两种 Number 类型:

int 和 double 都是 num 的子类。 num 中定义了一些基本的运算符 比如 +-*/ 等, 还定义了 abs()ceil()floor() 等方法(位运算符,比如 >> 定义在 int 中)。 如果 num 及其子类不满足你的要求,可以查看 dart:math 库中的 API。

num x = 1; // x can have both int and double values
x += 2.5;

int

整数值;长度不超过 64 位,具体取值范围 依赖于不同的平台。 在 DartVM 上其取值位于 -263 至 263 - 1 之间。 在 Web 上,整型数值代表着 JavaScript 的数字(64 位无小数浮点型),其允许的取值范围在 -253 至 253 - 1 之间。

整数是不带小数点的数字,下面是一些定义整数字面量的例子:

var x = 1;
var hex = 0xDEADBEEF;
var exponent = 8e5;

在 Dart 2.1 之前,在浮点数上下文中使用整数字面量是错误的。

整型字面量将会在必要的时候自动转换成浮点数字面量:
double z = 1; // Equivalent to double z = 1.0.
整型支持传统的位移操作

比如移位<<、>>、按位与&、按位或|,例如:

assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 >> 1) == 1); // 0011 >> 1 == 0001
assert((3 | 4) == 7); // 0011 | 0100 == 0111

double

64 位的双精度浮点数字,且符合 IEEE 754 标准。

如果一个数字包含了小数点,那么它就是浮点型的。下面是一些定义浮点数字面量的例子:

var y = 1.1;
var exponents = 1.42e5;

字符串和数字之间转换的方式:

// 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');

数字字面量为编译时常量

很多算术表达式只要其操作数是常量,则表达式结果也是编译时常量。

const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;

字符串 String

Dart 字符串(String 对象)包含了 UTF-16 编码的字符序列。 可以使用单引号或者双引号来创建字符串:

var s1 = '使用单引号创建字符串字面量。';
var s2 = "双引号也可以用于创建字符串字面量。";
var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:\'。';
var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'";

${表达式}

在字符串中,请以 ${表达式} 的形式使用表达式, 如果表达式是一个标识符,可以省略掉 {}。 如果表达式的结果为一个对象,则 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!');

>`==` 运算符负责判断两个对象的内容是否一样,如果两个字符串包含一样的字符编码序列,则表示相等。

>可以使用 `+` 运算符或并列放置多个字符串来连接字符串:

#### 多行字符串
>使用三个单引号或者三个双引号也能创建多行字符串:
```dart
var s1 = '''
你可以像这样创建多行字符串。
''';

var s2 = """这也是一个多行字符串。""";

“raw” 字符串

https://dart.cn/guides/language/language-tour#characters 在字符串前加上 r 作为前缀创建 “raw” 字符串(即不会被做任何处理(比如转义)的字符串):


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

// 代码中文解释 var s = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';


#### 字符串字面量的插值表达式
>https://dart.cn/guides/libraries/library-tour#strings-and-regular-expressions
>字符串字面量是一个编译时常量,只要是编译时常量都可以作为字符串字面量的插值表达式:
```dart
// 可以将下面三个常量作为字符串插值拼接到字符串字面量中。(These work in a const string.)
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// 而下面三个常量不能作为字符串插值拼接到字符串字面量。
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

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

bool

Dart 使用 bool 关键字表示布尔类型 布尔类型只有两个对象 true 和 false,两者都是编译时常量。

应该总是显示地检查布尔值

Dart 的类型安全不允许你使用类似 if (nonbooleanValue)或者 assert (nonbooleanValue)这样的代码检查布尔值。 相反,你应该总是显示地检查布尔值,比如像下面的代码这样:


// 检查是否为空字符串 (Check for an empty string).
var fullName = '';
assert(fullName.isEmpty);

// 检查是否小于等于零。 var hitPoints = 0; assert(hitPoints <= 0);

// 检查是否为 null。 var unicorn; assert(unicorn == null);

// 检查是否为 NaN。 var iMeantToDoThis = 0 / 0; assert(iMeantToDoThis.isNaN);


### List
>数组 (Array) 是几乎所有编程语言中最常见的集合类型,在 Dart 中数组由 List 对象表示。通常称之为 List。

>Dart 中 List 字面量看起来与 JavaScript 中数组字面量一样。
>下面是一个 Dart List 的示例:
```dart
var list = [1, 2, 3];

这里 Dart 推断出 list 的类型为 List<int>,如果往该数组中添加一个非 int 类型的对象则会报错。你可以阅读 类型推断 获取更多相关信息。

可以在 Dart 的集合类型的最后一个项目后添加逗号

这个尾随逗号并不会影响集合,但它能有效避免「复制粘贴」的错误。

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

List 的下标索引从 0 开始

第一个元素的下标为 0,最后一个元素的下标为 list.length - 1。 可以像 JavaScript 中的用法那样获取 Dart 中 List 的长度以及元素:


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

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

#### 在 List 字面量前添加 const 关键字会创建一个编译时常量:
```dart
var constantList = const [1, 2, 3];
// constantList[1] = 1; // This line will cause an error.

扩展操作符...和 空感知扩展操作符...?

https://github.com/dart-lang/language/blob/master/accepted/2.3/spread-collections/feature-specification.md Dart 在 2.3 引入了 扩展操作符...和 空感知扩展操作符...?,它们提供了一种将多个元素插入集合的简洁方法。

可以使用扩展操作符...将一个 List 中的所有元素插入到另一个 List 中:
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
如果扩展操作符右边可能为 null ,你可以使用 null-aware 扩展操作符...?来避免产生异常:
var list;
var list2 = [0, ...?list];
assert(list2.length == 1);

集合中的 if 和 集合中的 for 操作

https://github.com/dart-lang/language/blob/master/accepted/2.3/control-flow-collections/feature-specification.md 在构建集合时,可以使用条件判断 if 和循环 for

使用 集合中的 if 来创建一个 List 的示例,它可能包含 3 个或 4 个元素:
var nav = [
'Home',
'Furniture',
'Plants',
if (promoActive) 'Outlet'
];
使用 集合中的 for 将列表中的元素修改后添加到另一个列表中的示例:
var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');

Set

https://api.dart.dev/stable/dart-core/Set-class.html 在 Dart 中,set 是一组特定元素的无序集合。 Dart 支持的集合由集合的字面量和 Set 类提供。 使用 Set 字面量来创建一个 Set 集合的方法:

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

Dart 推断 halogens 变量是一个 Set<String> 类型的集合,如果往该 Set 中添加类型不正确的对象则会报错

可以使用在 {} 前加上类型参数的方式创建一个空的 Set,或者将{}赋值给一个 Set 类型的变量:

var names = <String>{}; // 类型+{}的形式创建Set。
// Set<String> names = {}; // 声明类型变量的形式创建 Set (This works, too).
// var names = {}; // 这样的形式将创建一个 Map 而不是 Set (Creates a map, not a set.)

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 字面量前添加 const 关键字创建一个 Set 编译时常量:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // This line will cause an error.

从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符......?以及 Collection if 和 for 操作

Map

通常来说,Map 是用来关联 keys 和 values 的对象。 其中键和值都可以是任何类型的对象。 每个 键 只能出现一次但是 值 可以重复出现多次。 Dart 中 Map 提供了 Map 字面量以及 Map 类型两种形式的 Map。

一对使用 Map 字面量创建 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 的构造器创建 Map:
```dart
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 中添加键值对与 JavaScript 的操作类似:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加键值对 (Add a key-value pair)
从一个 Map 中获取一个值的操作也与 JavaScript 类似。

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

如果检索的 Key 不存在于 Map 中则会返回一个 null:

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

使用 .length 可以获取 Map 中键值对的数量:

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

在一个 Map 字面量前添加 const 关键字可以创建一个 Map 编译时常量:

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

// constantMap[2] = 'Helium'; // This line will cause an error.

Map 可以像 List 一样支持使用扩展操作符......?以及集合的 if 和 for 操作。

runes 与 grapheme clusters

https://api.dart.dev/stable/dart-core/Runes-class.html https://pub.flutter-io.cn/packages/characters 在 Dart 中,runes 公开了字符串的 Unicode 码位。使用 characters 包 来访问或者操作用户感知的字符,也被称为 Unicode (扩展) grapheme clusters。

Unicode 编码为每一个字母、数字和符号都定义了一个唯一的数值。因为 Dart 中的字符串是一个 UTF-16 的字符序列,所以如果想要表示 32 位的 Unicode 数值则需要一种特殊的语法。

表示 Unicode 字符的常见方式是使用 \uXXXX,其中 XXXX 是一个四位数的 16 进制数字。例如心形字符的 Unicode 为 \u2665。对于不是四位数的 16 进制数字,需要使用大括号将其括起来。例如大笑的 emoji 表情😆的 Unicode 为 \u{1f600}

如果你需要读写单个 Unicode 字符,可以使用 characters 包中定义的 characters getter。它将返回 Characters 对象作为一系列 grapheme clusters 的字符串。 下面是使用 characters API 的样例:

import 'package:characters/characters.dart';
...
var hi = 'Hi 🇩🇰';
print(hi);
print('The end of the string: ${hi.substring(hi.length - 1)}');
print('The last character: ${hi.characters.last}\n');

在使用 List 操作 Rune 的时候需要小心,根据所操作的语种、字符集等不同可能会导致字符串出现问题,具体可参考 Stack Overflow 中的提问: [我如何在 Dart 中反转一个字符串?][How do I reverse a String in Dart?]。

Symbol

https://api.dart.dev/stable/dart-core/Symbol-class.html Symbol 表示 Dart 中声明的操作符或者标识符。你几乎不会需要 Symbol,但是它们对于那些通过名称引用标识符的 API 很有用,因为代码压缩后,尽管标识符的名称会改变,但是它们的 Symbol 会保持不变。

Symbol 字面量是编译时常量。

在标识符前加 # 前缀来获取 Symbol

#radix
#bar

函数 Function

https://api.dart.dev/stable/dart-core/Function-class.html Dart 是一种真正面向对象的语言,所以即便函数也是对象并且类型为 Function,这意味着函数可以被赋值给变量或者作为其它函数的参数。你也可以像调用函数一样调用 Dart 类的实例

定义一个函数

bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}

虽然高效 Dart 指南建议在公开的 API 上定义返回类型,不过即便不定义,该函数也依然有效:

isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}

如果函数体内只包含一个表达式,你可以使用简写语法:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

语法 => 表达式 是 { return 表达式; } 的简写, => 有时也称之为 箭头 函数。 在 =>;之间的只能是 表达式 而非 语句。比如你不能将一个 if语句 放在其中,但是可以放置 条件表达式。

参数 Parameter

函数可以有两种形式的参数:必要参数 和 可选参数。 必要参数定义在参数列表前面,可选参数则定义在必要参数后面。 可选参数可以是 命名的 或 位置的。 某些 API(特别是 Flutter 控件的构造器)只使用命名参数,即便参数是强制性的。

向函数传入参数或者定义函数参数时,可以使用 [尾随逗号][trailing comma]。

命名参数

命名参数默认为可选参数,除非他们被特别标记为 required

当你调用函数时,可以使用 参数名: 参数值 的形式来指定命名参数。

enableFlags(bold: true, hidden: false);

定义函数时,使用 {param1, param2, …}来指定命名参数:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool? bold, bool? hidden}) {...}

虽然命名参数是可选参数的一种类型,但是你仍然可以使用 required 来标识一个命名参数是必须的参数,此时调用者必须为该参数提供一个值。

const Scrollbar({Key? key, required Widget child})

如果调用者想要通过 Scrollbar 的构造函数构造一个 Scrollbar 对象而不提供 child 参数,则会导致编译错误。

可选的位置参数

使用 [] 将一系列参数包裹起来作为位置参数:

String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}

不使用可选参数调用上述函数的示例:

assert(say('Bob', 'Howdy') == 'Bob says Howdy');

使用可选参数调用上述函数的示例:

assert(say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal');

默认参数值

可以用 = 为函数的命名参数和位置参数定义默认值, 默认值必须为编译时常量, 没有指定默认值的情况下默认值为 null。

设置可选参数默认值示例:
/// 设置 [bold] 和 [hidden] 标识……
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold 的值将为 true;而 hidden 将为 false。
enableFlags(bold: true);

在老版本的 Dart 代码中会使用冒号:而不是 = 来设置命名参数的默认值。 原因在于刚开始的时候命名参数只支持 :。 不过现在这个支持已经过时,所以我们建议你现在仅 使用 = 来指定默认值。

为位置参数设置默认值:
String say(String from, String msg,
    [String device = 'carrier pigeon']) {
  var result = '$from says $msg with a $device';
  return result;
}

assert(say('Bob', 'Howdy') ==
    'Bob says Howdy with a carrier pigeon');
List 或 Map 同样也可以作为默认值

下面的示例定义了一个名为 doStuff() 的函数,并为其名为 list 和 gifts 的参数指定了一个 List 类型的值和 Map 类型的值。

void doStuff(
{List<int> list = const [1, 2, 3],
Map<String, String> gifts = const {
'first': 'paper',
'second': 'cotton',
'third': 'leather'
}}) {
print('list:  $list');
print('gifts: $gifts');
}

main() 函数

每个 Dart 程序都必须有一个 main() 顶级函数作为程序的入口, main() 函数返回值为 void 并且有一个 List<String> 类型的可选参数。

下面是一个简单 main() 函数:

void main() {
  print('Hello, World!');
}

使用命令行访问带参数的 main() 函数示例:

// 使用命令 dart args.dart 1 test 运行该应用
// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

可以通过使用 参数库 来定义和解析命令行参数 https://pub.flutter-io.cn/packages/args

函数是一级对象

可以将函数作为参数传递给另一个函数

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// 将 printElement 函数作为参数传递。
list.forEach(printElement);

可以将函数赋值给一个变量

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

该示例中使用了匿名函数

匿名函数

大多数方法都是有名字的,比如 main() 或 printElement()。 可以创建一个没有名字的方法,称之为 匿名函数、 Lambda 表达式 或 Closure 闭包。 可以将匿名方法赋值给一个变量然后使用它,比如将该变量添加到集合或从中删除。

匿名方法看起来与命名方法类似,在括号之间可以定义参数,参数之间用逗号分割。

后面大括号中的内容则为函数体:

([[类型] 参数[, …]]) {
函数体;
};

下面代码定义了只有一个参数 item 且没有参数类型的匿名方法。 List 中的每个元素都会调用这个函数,打印元素位置和值的字符串:

const list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});

如果函数体内只有一行返回语句,你可以使用胖箭头缩写法。

list.forEach(
    (item) => print('${list.indexOf(item)}: $item'));

词法作用域

Dart 是词法有作用域语言,变量的作用域在写代码的时候就确定了, 大括号内定义的变量只能在大括号内访问,与 Java 类似。

一个嵌套函数中变量在多个作用域中的示例:


bool topLevel = true;

void main() { var insideMain = true;

void myFunction() { var insideFunction = true;

void nestedFunction() {
  var insideNestedFunction = true;

  assert(topLevel);
  assert(insideMain);
  assert(insideFunction);
  assert(insideNestedFunction);
}

} }

>注意 `nestedFunction()` 函数可以访问包括顶层变量在内的所有的变量。

### 词法闭包
>闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。

>函数可以封闭定义到它作用域内的变量。

>接下来的示例中,函数 makeAdder() 捕获了变量 addBy。
>无论函数在什么时候返回,它都可以使用捕获的 addBy 变量。
```dart
/// 返回一个将 [addBy] 添加到该函数参数的函数。
/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // 生成加 2 的函数。
  var add2 = makeAdder(2);

  // 生成加 4 的函数。
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

测试函数是否相等

顶级函数,静态方法和示例方法相等性的测试示例:


void foo() {} // 定义顶层函数 (A top-level function)

class A { static void bar() {} // 定义静态方法 void baz() {} // 定义实例方法 }

void main() { Function x;

// 比较顶层函数是否相等。 x = foo; assert(foo == x);

// 比较静态方法是否相等。 x = A.bar; assert(A.bar == x);

// 比较实例方法是否相等。 var v = A(); // A 的实例 #1 var w = A(); // A 的实例 #2 var y = w; x = w.baz;

// 这两个闭包引用了相同的实例对象,因此它们相等。 assert(y.baz == x);

// 这两个闭包引用了不同的实例对象,因此它们不相等。 assert(v.baz != w.baz); }


### 返回值
>所有的函数都有返回值。
>没有显示返回语句的函数最后一行默认为执行 `return null;`。
```dart
foo() {}
assert(foo() == null);

运算符 Operator

Dart 支持下表的操作符。你可以将这些运算符实现为 一个类的成员。

描述 运算符
一元后缀 表达式++ 表达式-- () [] . ?.
一元前缀 -表达式 !表达式 ~表达式 ++表达式 --表达式
乘除法 * / % ~/
加减法 + -
位运算 << >> >>>
二进制与 &
二进制异或 ^
二进制或 \
关系和类型测试 >= > <= < as is is!
相等判断 == !=
逻辑与 &&
逻辑或 |\
空判断 ??
条件表达式 表达式 1 ? 表达式 2 : 表达式 3
级联 ..    ?.... ?..
赋值 = *= /= += -= &= ^= 等等……

一旦你使用了运算符,就创建了表达式。 一些运算符表达式的示例:

a++
a + b
a = b
a == b
c ? a : b
a is T

在 运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。


// 括号提高了可读性。
// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// 难以理解,但是与上面的代码效果一样。 if (n % i == 0 && d % i == 0) ...


>对于有两个操作数的运算符,左边的操作数决定了运算符的功能。
>比如对于一个 Vector 对象和一个 Point 对象,表达式` aVector + aPoint` 中所使用的是 Vector 对象中定义的相加运算符 `+`。

### 算术运算符
>Dart 支持常用的算术运算符:

运算符 | 描述
-- | --
+ | 加
– | 减
-表达式 | 一元负, 也可以作为反转(反转表达式的符号)
* | 乘
/ | 除
~/ | 除并取整
% | 取模

>示例:
```dart
assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // 结果是一个浮点数
assert(5 ~/ 2 == 2); // 结果是一个整数
assert(5 % 2 == 1); // 取余

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 还支持自增自减操作。

++var var = var + 1 (表达式的值为 var + 1)
var++ var = var + 1 (表达式的值为 var)
--var var = var – 1 (表达式的值为 var – 1)
var-- var = var – 1 (表达式的值为 var)

示例:

var a, b;

a = 0; b = ++a; // 在 b 赋值前将 a 增加 1。 assert(a == b); // 1 == 1

a = 0; b = a++; // 在 b 赋值后将 a 增加 1。 assert(a != b); // 1 != 0

a = 0; b = --a; // 在 b 赋值前将 a 减少 1。 assert(a == b); // -1 == -1

a = 0; b = a--; // 在 b 赋值后将 a 减少 1。 assert(a != b); // -1 != 0


### 关系运算符

== | 相等
-- | --
!= | 不等
> | 大于
< | 小于
>= | 大于等于
<= | 小于等于

>要判断两个对象 x 和 y 是否表示相同的事物使用 `==` 即可。(在极少数情况下,可能需要使用 identical() 函数来确定两个对象是否完全相同。)。

####  `== `运算符的一些规则:

>假设有变量 x 和 y,且 x 和 y 至少有一个为 null,则当且仅当 x 和 y 均为 null 时 `x == y` 才会返回 true,否则只有一个为 null 则返回 false。

>`x.==(y)` 将会返回值,这里不管有没有 y,即 y 是可选的。也就是说 == 其实是 x 中的一个方法,并且可以被重写。详情请查阅重写运算符。

### 类型判断运算符
>as、is、is! 运算符是在运行时判断对象类型的运算符。

Operator | Meaning
-- | --
as | 类型转换(也用作指定 类前缀))
is | 如果对象是指定类型则返回 true
is! | 如果对象是指定类型则返回 false

>当且仅当 obj 实现了 T 的接口,`obj is T` 才是 true。例如 `obj is Object` 总为 true,因为所有类都是 Object 的子类。

>仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型。例如:
```dart
(employee as Person).firstName = 'Bob';

如果你不确定这个对象类型是不是 T,请在转型前使用 is T 检查类型。

if (employee is Person) {
// Type check
employee.firstName = 'Bob';
}

你可以使用 as 运算符进行缩写:

(emp as Person).firstName = 'Bob';

上述两种方式是有区别的:如果 employee 为 null 或者不为 Person 类型,则第一种方式将会抛出异常,而第二种不会。

赋值运算符

可以使用 = 来赋值,同时也可以使用 ??= 来为值为 null 的变量赋值。

// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

+=这样的赋值运算符将算数运算符和赋值运算符组合在了一起。

= –= /= %= >>= ^=
+= *= ~/= <<= &= |=

复合运算符的原理

场景 复合运算 等效表达式
假设有运算符 op: a op= b a = a op b
示例: a += b a = a + b

使用赋值以及复合赋值运算符:

var a = 2; // 使用 = 赋值 (Assign using =)
a *= 3; // 赋值并做乘法运算 Assign and multiply: a = a * 3
assert(a == 6);

逻辑运算符

使用逻辑运算符你可以反转或组合布尔表达式

运算符 描述
!表达式 对表达式结果取反(即将 true 变为 false,false 变为 true)
|| 逻辑或
&& 逻辑与

使用逻辑表达式

if (!done && (col == 0 || col == 3)) {
// ...Do something...
}

按位和移位运算符

在 Dart 中,二进制位运算符可以操作二进制的某一位,但仅适用于整数。

运算符 描述
& 按位与
| 按位或
^ 按位异或
~表达式 按位取反(即将 “0” 变为 “1”,“1” 变为 “0”)
<< 位左移
>> 位右移

使用按位和移位运算符


final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // 按位与 (AND) assert((value & ~bitmask) == 0x20); // 取反后按位与 (AND NOT) assert((value | bitmask) == 0x2f); // 按位或 (OR) assert((value ^ bitmask) == 0x2d); // 按位异或 (XOR) assert((value << 4) == 0x220); // 位左移 (Shift left) assert((value >> 4) == 0x02); // 位右移 (Shift right)


### 条件表达式
>Dart 有两个特殊的运算符可以用来替代 `if-else` 语句:
```condition ? expr1 : expr2```

>`条件 ? 表达式 1 : 表达式 2` :如果条件为 true,执行表达式 1并返回执行结果,否则执行表达式 2 并返回执行结果。

>`expr1 ?? expr2`
>`表达式 1 ?? 表达式 2`:如果表达式 1 为非 null 则返回其值,否则执行表达式 2 并返回其值。

>根据布尔表达式确定赋值时,请考虑使用 `?` 和` :`。
```dart
var visibility = isPublic ? 'public' : 'private';

如果赋值是根据判定是否为 null 则考虑使用 ??

String playerName(String? name) => name ?? 'Guest';

上述示例还可以写成至少下面两种不同的形式,只是不够简洁:


// Slightly longer version uses ?: operator.
String playerName(String? name) => name != null ? name : 'Guest';

// Very long version uses if-else statement. String playerName(String? name) { if (name != null) { return name; } else { return 'Guest'; } }


### 级联运算符
>级联运算符 `..`, `?..` 可以让你在同一个对象上连续调用多个对象的变量或方法。
```dart
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

等同于

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

如果级联操作的对象可以为空,则在第一次操作中使用空级联?..。从?..开始保证该空对象上不尝试任何级联操作。

querySelector('#confirm') // Get an object.
?..text = 'Confirm' // Use its members.
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));

?..运行在 2.12 和以上的 版本 中可用。 上面的代码相当于:

var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));

级联运算符可以嵌套

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在返回对象的函数中谨慎使用级联操作符。

例如,下面的代码是错误的:

var sb = StringBuffer();
sb.write('foo')
..write('bar'); // 出错:void 对象中没有方法 write (Error: method 'write' isn't defined for 'void').

上述代码中的 sb.write() 方法返回的是 void,返回值为 void 的方法则不能使用级联运算符。

严格来说 .. 级联操作并非一个运算符而是 Dart 的特殊语法。

其他运算符

运算符 名字 描述
() 使用方法 代表调用一个方法
[] 访问 List 访问 List 中特定位置的元素
. 访问成员 成员访问符
?. 条件访问成员 与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar

流程控制语句

可以使用下面的语句来控制 Dart 代码的执行流程:

if 和 else

for 循环

while 和 do-while 循环

break 和 continue

switch 和 case

assert

使用 try-catch 和 throw 也能影响控制流,详情参考异常部分

If 和 Else

Dart 支持 if - else 语句,其中 else 是可选的,比如下面的例子。你也可以参考条件表达式。

if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}

不同于 JavaScript,Dart 的 if 语句中的条件必须是布尔值而不能为其它类型

For 循环

可以使用标准的 for 循环进行迭代

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
message.write('!');
}

在 Dart 语言中,for 循环中的闭包会自动捕获循环的 索引值 以避免 JavaScript 中一些常见的陷阱。

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

上述代码执行后会输出 0 和 1,但是如果在 JavaScript 中执行同样的代码则会输出两个 2。

如果要遍历的对象是一个可迭代对象(例如 List 或 Set),并且你不需要知道当前的遍历索引,则可以使用 for-in 方法进行 遍历:

for (var candidate in candidates) {
  candidate.interview();
}

可迭代对象同时可以使用 forEach() 方法作为另一种选择:

var collection = [1, 2, 3];
collection.forEach(print); // 1 2 3

While 和 Do-While

while 循环会在执行循环体前先判断条件:

while (!isDone()) {
  doSomething();
}

do-while 循环则会 先执行一遍循环体 再判断条件:

do {
  printLine();
} while (!atEndOfPage());

Break 和 Continue

使用 break 可以中断循环:

while (true) {
if (shutDownRequested()) break;
processIncomingRequests();
}

使用 continue 可以跳过本次循环直接进入下一次循环:

for (int i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.yearsExperience < 5) {
continue;
}
candidate.interview();
}

如果你正在使用诸如 List 或 Set 之类的 Iterable 对象,你可以用以下方式重写上述例子:

candidates
.where((c) => c.yearsExperience >= 5)
.forEach((c) => c.interview());

Switch 和 Case

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量, 比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。 枚举类型非常适合在 Switch 语句中使用。 Dart 中的 Switch 语句仅适用于有限的情况,比如使用解释器和扫描器的场景。

每一个非空的 case 子句都必须有一个 break 语句,也可以通过 continue、throw 或者 return 来结束非空 case 语句。 每个 case 子句都可以有局部变量且仅在该 case 语句内可见。

不匹配任何 case 语句的情况下,会执行 default 子句中的代码:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

忽略了 case 子句的 break 语句,会产生错误:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // 错误: 没有 break

  case 'CLOSED':
    executeClosed();
    break;
}

但是,Dart 支持空的 case 语句,允许其以 fall-through 的形式执行。

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // case 语句为空时的 fall-through 形式。
  case 'NOW_CLOSED':
    // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
    executeNowClosed();
    break;
}

在非空 case 语句中想要实现 fall-through 的形式,可以使用 continue 语句配合 label 的方式实现:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // 继续执行标签为 nowClosed 的 case 子句。

  nowClosed:
  case 'NOW_CLOSED':
    // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
    executeNowClosed();
    break;
}

断言 assert

在开发过程中,可以在条件表达式为 false 时使用 assert(条件, 可选信息); 语句来打断代码的执行。

assert 的第一个参数可以是值为布尔值的任何表达式。 如果表达式的值为 true,则断言成功,继续执行。 如果表达式的值为 false,则断言失败,抛出一个 AssertionError 异常。

assert 是否生效依赖开发工具和使用的框架:

Flutter 在调试模式时生效。 一些开发工具比如 dartdevc 通常情况下是默认生效的。 其他一些工具,比如 dart 以及 dart2js 通过在运行 Dart 程序时添加命令行参数 --enable-asserts 使 assert 生效。 在生产环境代码中,断言会被忽略,与此同时传入 assert 的参数不被判断。

// 确保变量值不为 null (Make sure the variable has a non-null value)
assert(text != null);

// 确保变量值小于 100。
assert(number < 100);

// 确保这是一个 https 地址。
assert(urlString.startsWith('https'));

assert 的第二个参数可以为其添加一个字符串消息。

assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

异常 Exception

https://dart.cn/guides/libraries/library-tour#exceptions Dart 代码可以抛出和捕获异常。 异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。

与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不必声明会抛出哪些异常,并且你也不必捕获任何异常。

Dart 提供了 Exception 和 Error 两种类型的异常以及它们一系列的子类,你也可以定义自己的异常类型。 但是在 Dart 中可以将任何非 null 对象作为异常抛出而不局限于 Exception 或 Error 类型。

抛出异常

下面是关于抛出或者 引发 异常的示例:

throw FormatException('Expected at least 1 section');

你也可以抛出任意的对象:

throw 'Out of llamas!';

优秀的代码通常会抛出 Error 或 Exception 类型的异常

因为抛出异常是一个表达式,所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:

void distanceTo(Point other) => throw UnimplementedError();

捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。 捕获一个异常可以给你处理它的机会:

try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}

对于可以抛出多种异常类型的代码,也可以指定多个 catch 语句,每个语句分别对应一个异常类型, 如果 catch 语句没有指定异常类型则表示可以捕获任意异常类型: 可以使用 on 或 catch 来捕获异常,使用 on 来指定异常类型,使用 catch 来捕获异常对象,两者可同时使用。

try {
breedMoreLlamas();
} on OutOfLlamasException {
// 指定异常
buyMoreLlamas();
} on Exception catch (e) {
// 其它类型的异常
print('Unknown exception: $e');
} catch (e) {
// // 不指定类型,处理其它全部
print('Something really unknown: $e');
}

你可以为 catch 方法指定两个参数,第一个参数为抛出的异常对象,第二个参数为栈信息 StackTrace 对象:

try {
// ···
} on Exception catch (e) {
print('Exception details:\n $e');
} catch (e, s) {
print('Exception details:\n $e');
print('Stack trace:\n $s');
}

关键字 rethrow 可以将捕获的异常再次抛出:


void misbehave() {
try {
dynamic foo = true;
print(foo++); // 运行时错误
} catch (e) {
print('misbehave() partially handled ${e.runtimeType}.');
rethrow; // 允许调用者查看异常。
}
}

void main() { try { misbehave(); } catch (e) { print('main() finished handling ${e.runtimeType}.'); } }


### Finally
>无论是否抛出异常,finally 语句始终执行,
>如果没有指定 catch 语句来捕获异常,则异常会在执行完 finally 语句后抛出:
```dart
try {
  breedMoreLlamas();
} finally {
  // 总是清理,即便抛出了异常。
  cleanLlamaStalls();
}

finally 语句会在任何匹配的 catch 语句后执行:

try {
breedMoreLlamas();
} catch (e) {
print('Error: $e'); // 先处理异常。
} finally {
cleanLlamaStalls(); // 然后清理。
}

类 class

Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null 以外的所有的类都继承自 Object 类。 基于 mixin 的继承 意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。

使用类的成员

对象的 成员 由函数和数据(即 方法 和 实例变量)组成。 方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。

使用.来访问对象的实例变量或方法:


var p = Point(2, 2);

// 获取 y 值 assert(p.y == 2);

// 调用变量 p 的 distanceTo() 方法。 double distance = p.distanceTo(Point(4, 4));


>使用 `?.` 代替 `.` 可以避免因为左边表达式为 null 而导致的问题:
```dart
// If p is non-null, set a variable equal to its y value.
var a = p?.y;

使用构造函数 constructor

可以使用 构造函数 来创建一个对象。 构造函数的命名方式可以为 类名类名 . 标识符 的形式。

例如下述代码分别使用 Point()Point.fromJson()两种构造器创建了 Point 对象:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

以下代码具有相同的效果,但是构造函数名前面的的 new 关键字是可选的:

var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 const 关键字,来创建编译时常量时:

var p = const ImmutablePoint(2, 2);

两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:


var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // 它们是同一个实例 (They are the same instance!)


>在 常量上下文 场景中,你可以省略掉构造函数或字面量前的 const 关键字。
>例如下面的例子中我们创建了一个常量 Map:
```dart
// Lots of const keywords here.
// 这里有很多 const 关键字
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

根据上下文,你可以只保留第一个 const 关键字,其余的全部省略:

// Only one const, which establishes the constant context.
// 只需要一个 const 关键字,其它的则会隐式地根据上下文进行关联。
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

但是如果无法根据上下文判断是否可以省略 const,则不能省略掉 const 关键字,否则将会创建一个 非常量对象 例如:


var a = const ImmutablePoint(1, 1); // 创建一个常量 (Creates a constant)
var b = ImmutablePoint(1, 1); // 不会创建一个常量 (Does NOT create a constant)

assert(!identical(a, b)); // 这两变量并不相同 (NOT the same instance!)


### 获取对象的类型
>可以使用 Object 对象的 runtimeType 属性在运行时获取一个对象的类型,该对象类型是 Type 的实例。
```dart
print('The type of a is ${a.runtimeType}');

到目前为止,已经解了如何 使用 类。其余部分将向你介绍如何 实现 一个类。

实例变量

声明实例变量的示例:

class Point {
double? x; // Declare instance variable x, initially null.
double? y; // Declare y, initially null.
double z = 0; // Declare z, initially 0.
}

所有未初始化的实例变量其值均为 null。

所有实例变量均会隐式地声明一个 Getter 方法。 非终值的实例变量和 late final 声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。


class Point {
double? x; // Declare instance variable x, initially null.
double? y; // Declare y, initially null.
}

void main() { var point = Point(); point.x = 4; // 使用 x 的 Setter 方法。 assert(point.x == 4); // 使用 x 的 Getter 方法。 assert(point.y == null); // 默认值为 null。 }


>实例变量可能是final,在这种情况下,必须精确设置一次。
>使用构造器参数或使用构造器的初始化列表
```dart
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

构造函数

声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数 还可以添加额外的标识符)。 大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:


class Point {
double x = 0;
double y = 0;

Point(double x, double y) { // 还会有更好的方式来实现此逻辑,敬请期待。 this.x = x; this.y = y; } }

>使用 this 关键字引用当前实例。
>当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。

>对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤:
```dart
class Point {
  double x = 0;
  double y = 0;

  // 在构造函数体执行前用于设置 x 和 y 的语法糖。
  Point(this.x, this.y);
}

默认构造函数

如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。

构造函数不被继承

子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。

构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数, 如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。

命名式构造函数

可以为一个类声明多个命名式构造函数来表达更明确的意图:

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  double x = 0;
  double y = 0;

  Point(this.x, this.y);

  // 命名式构造函数
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

调用父类非默认构造函数

默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前, 如果子类构造函数还有一个 初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行,

总的来说,这三者的调用顺序如下:

初始化列表 父类的无参数构造函数 当前类的构造函数

如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用:指定。

下面的示例中,Employee 类的构造函数调用了父类 Person 的命名构造函数。


class Person {
String? firstName;

Person.fromJson(Map data) { print('in Person'); } }

class Employee extends Person { // Person does not have a default constructor; // you must call super.fromJson(data). Employee.fromJson(Map data) : super.fromJson(data) { print('in Employee'); } }

void main() { var employee = Employee.fromJson({}); print(employee); // Prints: // in Person // in Employee // Instance of 'Employee' }


>因为参数会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:
```dart
class Employee extends Person {
  Employee() : super.fromJson(fetchDefaultData());
  // ···
}

传递给父类构造函数的参数不能使用 this 关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。

初始化列表

除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。 每个实例变量之间使用逗号分隔。

// Initializer list sets instance variables before
// the constructor body runs.
// 使用初始化列表在构造函数体执行前设置实例变量。
Point.fromJson(Map<String, double> json)
: x = json['x']!,
y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}

初始化列表表达式 = 右边的语句不能使用 this 关键字。

在开发模式下,你可以在初始化列表中使用 assert 来验证输入数据:

Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}

使用初始化列表设置 final 字段非常方便 下面的示例中就使用初始化列表来设置了三个 final 变量的值


import 'dart:math';

class Point { final double x; final double y; final double distanceFromOrigin;

Point(double x, double y) : x = x, y = y, distanceFromOrigin = sqrt(x x + y y); }

void main() { var p = Point(2, 3); print(p.distanceFromOrigin); }


#### 重定向构造函数
>有时候类中的构造函数仅用于调用类中其它的构造函数,
>此时该构造函数没有函数体,只需在函数签名后使用`:`指定需要重定向到的其它构造函数:
```dart
class Point {
  double x, y;

  // 该类的主构造函数。
  Point(this.x, this.y);

  // 委托实现给主构造函数。
  Point.alongXAxis(double x) : this(x, 0);
}

常量构造函数

如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。 你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。


class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

const ImmutablePoint(this.x, this.y); }

>常量构造函数创建的实例并不总是常量,具体可以参考 https://dart.cn/guides/language/language-tour#using-constructors

#### 工厂构造函数
>使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。
>例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。
>在工厂构造函数中无法访问 this。

>在如下的示例中,
> Logger 的工厂构造函数从缓存中返回对象, 
>Logger.fromJson 工厂构造函数从 JSON 对象中初始化一个最终变量。
```dart
class Logger {
  final String name;
  bool mute = false;

  // _cache 变量是库私有的,因为在其名字前面有下划线。
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

工厂构造函数的调用方式与其他构造函数一样:


var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'}; var loggerJson = Logger.fromJson(logMap);


### 方法
>方法是为对象提供行为的函数。

#### 实例方法
>对象的实例方法可以访问实例变量和 this。
>下面的 distanceTo() 方法就是一个实例方法的例子:
```dart
import 'dart:math';

class Point {
  double x = 0;
  double y = 0;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

操作符

运算符是有着特殊名称的实例方法。 Dart 允许您使用以下名称定义运算符:

< + \ []
> / ^ []=
<= ~/ & ~
>= * << ==
% >>  

你可能注意到有一些 操作符 没有出现在列表中,例如 !=。 因为它们仅仅是语法糖。表达式 e1 != e2 仅仅是 !(e1 == e2)的一个语法糖。

为了表示重写操作符,使用 operator 标识来进行标记。

下面是重写 +- 操作符的例子:


class Vector {
final int x, y;

Vector(this.x, this.y);

Vector operator +(Vector v) => Vector(x + v.x, y + v.y); Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

// Operator == and hashCode not shown. // ··· }

void main() { final v = Vector(2, 3); final w = Vector(2, 2);

assert(v + w == Vector(4, 5)); assert(v - w == Vector(0, 1)); }

#### Getter 和 Setter
>Getter 和 Setter 是一对用来读写对象属性的特殊方法,
>上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,
>如果为非 final 属性的话还会有一个 Setter 方法,
>可以使用 get 和 set 关键字为额外的属性添加 Getter 和 Setter 方法:
```dart
class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // 定义两个计算产生的属性:right 和 bottom。
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。

像自增++这样的操作符不管是否定义了 Getter 方法都会正确地执行。 为了避免一些不必要的异常情况,运算符只会调用 Getter 一次,然后将其值存储在一个临时变量中。

抽象方法

实例方法、Getter 方法以及 Setter 方法都可以是抽象的, 定义一个接口方法而不去做具体的实现让实现它的类去实现该方法, 抽象方法只能存在于 抽象类中。

直接使用分号;替代方法体即可声明一个抽象方法:


abstract class Doer {
// 定义实例变量和方法等等……

void doSomething(); // 定义一个抽象方法。 }

class EffectiveDoer extends Doer { void doSomething() { // 提供一个实现,所以在这里该方法不再是抽象的…… } }


### 抽象类 abstract
>使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。
>抽象类常用于声明接口方法、有时也会有具体的方法实现。
>如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数

#### 抽象类常常会包含 抽象方法。
>一个声明具有抽象方法的抽象类示例:
```dart
// This class is declared abstract and thus
// can't be instantiated.
// 该类被声明为抽象的,因此它不能被实例化。
abstract class AbstractContainer {
  // 定义构造函数、字段、方法等……

  void updateChildren(); // 抽象方法。
}

隐式接口 implements

每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。 如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。

一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:


// A person. The implicit interface contains greet().
// Person 类的隐式接口中包含 greet() 方法。
class Person {
// In the interface, but visible only in this library.
final String _name;

// 构造函数不在接口中。 Person(this._name);

// greet() 方法在接口中。 String greet(String who) => '你好,$who。我是$_name。'; }

// Person 接口的一个实现。 class Impostor implements Person { String get _name => '';

String greet(String who) => '你好$who。你知道我是谁吗?'; }

String greetBob(Person person) => person.greet('小芳');

void main() { print(greetBob(Person('小芸'))); print(greetBob(Impostor())); }


>如果需要实现多个类接口,可以使用逗号分割每个接口类:
```dart
class Point implements Comparable, Location {...}

扩展一个类 extends

使用 extends 关键字来创建一个子类,并可使用 super 关键字引用一个父类:


class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}

class SmartTelevision extends Television { void turnOn() { super.turnOn(); _bootNetworkInterface(); _initializeMemory(); _upgradeApps(); } // ··· }


#### 重写类成员 @override
>子类可以重写父类的实例方法(包括 操作符)、 Getter 以及 Setter 方法。
>可以使用 `@override` 注解来表示你重写了一个成员:
```dart
class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}
covariant

你可以使用 covariant 关键字 来缩小代码中那些符合 类型安全 的方法参数或实例变量的类型。


class Animal {
void chase(Animal x) { ... }
}

class Mouse extends Animal { ... }

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


##### 如果重写 == 操作符,必须同时重写对象 hashCode 的 Getter 方法。

#### noSuchMethod 方法
>如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod 方法,
>可以重写 noSuchMethod 方法来追踪和记录这一行为:
```dart
class A {
  // 除非你重写 noSuchMethod,否则调用一个不存在的成员会导致 NoSuchMethodError。
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: '
        '${invocation.memberName}');
  }
}
只有下面其中一个条件成立时,你才能调用一个未实现的方法:

接收方是静态的 dynamic 类型。

接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了 noSuchMethod 方法且具体的实现与 Object 中的不同。

https://github.com/dart-lang/sdk/blob/master/docs/language/informal/nosuchmethod-forwarding.md

扩展方法

https://dart.cn/guides/language/extension-methods 扩展方法是向现有库添加功能的一种方式。 你可能已经在不知道它是扩展方法的情况下使用了它。 例如,当您在 IDE 中使用代码完成功能时,它建议将扩展方法与常规方法一起使用。

这里是一个在 String 中使用扩展方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:


import 'string_apis.dart';

print('42'.padLeft(5)); // Use a String method. print('42'.parseInt()); // Use an extension method.


### 枚举类型
>枚举类型是一种特殊的类型,也称为 enumerations 或 enums,用于定义一些固定数量的常量值。

### 使用枚举 enum
>使用关键字 enum 来定义枚举类型:
```dart
enum Color { red, green, blue }

你可以在声明枚举类型时使用 尾随逗号。

每一个枚举值都有一个名为 index 成员变量的 Getter 方法,该方法将会返回以 0 为基准索引的位置值。 例如,第一个枚举值的索引是 0 ,第二个枚举值的索引是 1。以此类推。

assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

想要获得全部的枚举值,使用枚举类的 values 方法获取包含它们的列表:

List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

可以在 Switch 语句中使用枚举, 但是需要注意的是必须处理枚举值的每一种情况,即每一个枚举值都必须成为一个 case 子句,不然会出现警告:


var aColor = Color.blue;

switch (aColor) { case Color.red: print('红如玫瑰!'); break; case Color.green: print('绿如草原!'); break; default: // 没有该语句会出现警告。 print(aColor); // 'Color.blue' }


>枚举类型有如下两个限制:

>>枚举不能成为子类,也不可以 mix in,你也不可以实现一个枚举。

>>不能显式地实例化一个枚举类。

### 使用 mixin 为类添加功能
>mixin 是一种在多重继承中复用某个类中代码的方法模式。

#### 使用 with 关键字并在其后跟上 mixin 类的名字来使用 mixin 模式:
```dart
class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

实现一个 mixin

想要实现一个 mixin,请创建一个继承自 Object 且未声明构造函数的类。 除非你想让该类与普通的类一样可以被正常地使用,否则请使用关键字 mixin 替代 class。 例如:


mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;

void entertainMe() { if (canPlayPiano) { print('Playing piano'); } else if (canConduct) { print('Waving hands'); } else { print('Humming to self'); } } }


#### 可以使用关键字 on 来指定哪些类可以使用该 Mixin 类,
>比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:
```dart
class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

mixin 关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用 abstract class 代替。

类变量和方法 static

使用关键字 static 可以声明类变量或类方法。

静态变量

静态变量(即类变量)常用于声明类范围内所属的状态变量和常量:


class Queue {
static const initialCapacity = 16;
// ···
}

void main() { assert(Queue.initialCapacity == 16); }

>静态变量在其首次被使用的时候才被初始化。

#### 静态方法
>静态方法(即类方法)不能对实例进行操作,因此不能使用 this。
>但是他们可以访问静态变量。
>如下面的例子所示,你可以在一个类上直接调用静态方法:
```dart
import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。

可以将静态方法作为编译时常量。例如,你可以将静态方法作为一个参数传递给一个常量构造函数。


泛型 Generic

如果你查看数组的 API 文档,你会发现数组 List 的实际类型为 List<E><…>符号表示数组是一个 泛型(或 参数化类型) 通常 使用一个字母来代表类型参数,比如 E、T、S、K 和 V 等等

为什么使用泛型?

泛型常用于需要要求类型安全的情况,但是它也会对代码运行有好处:

适当地指定泛型可以更好地帮助代码生成。 使用泛型可以减少代码重复。

比如你想声明一个只能包含 String 类型的数组,你可以将该数组声明为 List<String>(读作“字符串类型的 list”),这样的话就可以很容易避免因为在该数组放入非 String 类变量而导致的诸多问题,同时编译器以及其他阅读代码的人都可以很容易地发现并定位问题:

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);
}

不久后你可能又会想专门为 String 类对象做一个缓存,于是又有了专门为 String 做缓存的类:

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 字面量也可以是参数化的。 定义参数化的 List 只需在中括号前添加 <type>; 定义参数化的 Map 只需要在大括号前添加 <keyType, valueType>

var names = <String>['小芸', '小芳', '小民'];
var uniqueNames = <String>{'小芸', '小芳', '小民'};
var pages = <String, String>{
'index.html': '主页',
'robots.txt': '网页机器人提示',
'humans.txt': '我们是人类,不是机器'
};

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

在调用构造方法时也可以使用泛型,只需在类名后用尖括号<...>将一个或多个类型包裹即可:

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

下面代码创建了一个键为 Int 类型,值为 View 类型的 Map 对象:

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

泛型集合以及它们所包含的类型

Dart的泛型类型是 固化的,这意味着即便在运行时也会保持类型信息:

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

与 Java 不同的是,Java 中的泛型是类型 擦除 的,这意味着泛型类型会在运行时被移除。 在 Java 中你可以判断对象是否为 List 但不可以判断对象是否为 List

限制参数化类型 <T extends SomeBaseClass>

有时使用泛型的时候可能会想限制泛型的类型范围,这时候可以使用 extends 关键字:


class Foo<T extends SomeBaseClass> {
// 具体实现……
String toString() => "'Foo<$T>' 的实例";
}

class Extender extends SomeBaseClass {...}


>这时候就可以使用 SomeBaseClass 或者它的子类来作为泛型参数:
```dart
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

这时候也可以指定无参数的泛型,这时无参数泛型的类型则为 Foo<SomeBaseClass>

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

将非 SomeBaseClass 的类型作为泛型参数则会导致编译错误:

var foo = Foo<Object>();

使用泛型方法 function<T>

https://github.com/dart-lang/sdk/blob/master/pkg/dev_compiler/doc/GENERIC_METHODS.md 起初 Dart 只支持在类的声明时指定泛型,现在同样也可以在方法上使用泛型,称之为 泛型方法:

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

方法 first 的泛型 T 可以在如下地方使用:

函数的返回值类型 T

参数的类型 List<T>

局部变量的类型 T tmp

库和可见性

import 和 library 关键字可以帮助你创建一个模块化和可共享的代码库。 代码库不仅只是提供 API 而且还起到了封装的作用:以下划线_开头的成员仅在代码库中可见。 每个 Dart 程序都是一个库,即便没有使用关键字 library 指定。

Dart 的库可以使用 包工具 来发布和部署。 https://dart.cn/guides/packages

如果你对 Dart 为何使用下划线而不使用 public 或 private 作为可访问性关键字,可以查看 SDK issue 33383 https://github.com/dart-lang/sdk/issues/33383

使用库 import

使用 import 来指定命名空间以便其它库可以访问。

比如你可以导入代码库 dart:html 来使用 Dart Web 中相关 API:

import 'dart:html';

import 的唯一参数是用于指定代码库的 URI, 对于 Dart 内置的库,使用 dart:xxxxxx 的形式。 而对于其它的库,你可以使用一个文件系统路径或者以 package:xxxxxx 的形式。 package:xxxxxx 指定的库通过包管理器(比如 pub 工具)来提供:

import 'package:test/test.dart';

URI 代表统一资源标识符。

URL(统一资源定位符)是一种常见的 URI。

指定库前缀 as

如果导入的两个代码库有冲突的标识符,你可以为其中一个指定前缀。 比如如果 library1 和 library2 都有 Element 类,那么可以这么处理:


import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// 使用 lib1 的 Element 类。 Element element1 = Element();

// 使用 lib2 的 Element 类。 lib2.Element element2 = lib2.Element();


#### 导入库的一部分 show hide
>如果你只想使用代码库中的一部分,你可以有选择地导入代码库。例如:
```dart
// 只导入 lib1 中的 foo。(Import only foo).
import 'package:lib1/lib1.dart' show foo;

// 导入 lib2 中除了 foo 外的所有。
import 'package:lib2/lib2.dart' hide foo;

延迟加载库 deferred as

延迟加载(也常称为 懒加载)允许应用在需要时再去加载代码库,

可能使用到延迟加载的场景:

为了减少应用的初始化时间。

处理 A/B 测试,比如测试各种算法的不同实现。

加载很少会使用到的功能,比如可选的屏幕和对话框。

目前只有 dart2js 支持延迟加载 Flutter、Dart VM 以及 DartDevc 目前都不支持延迟加载。

用 deferred as 关键字来标识需要延时加载的代码库:

import 'package:greetings/hello.dart' deferred as hello;

当实际需要使用到库中 API 时先调用 loadLibrary 函数加载库:

Future<void> greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

在前面的代码,使用 await 关键字暂停代码执行直到库加载完成。

loadLibrary 函数可以调用多次也没关系,代码库只会被加载一次。

当你使用延迟加载的时候需要牢记以下几点:

延迟加载的代码库中的常量需要在代码库被加载的时候才会导入,未加载时是不会导入的。

导入文件的时候无法使用延迟加载库中的类型。如果你需要使用类型,则考虑把接口类型转移到另一个库中然后让两个库都分别导入这个接口库。

Dart会隐式地将 loadLibrary() 导入到使用了 deferred as 命名空间 的类中。 loadLibrary() 函数返回的是一个 Future。

实现库

https://dart.cn/guides/libraries/create-library-packages 查阅 创建依赖库包 可以获取有关如何实现库包的建议,包括:

如何组织库的源文件。

如何使用 export 命令。

何时使用 part 命令。

何时使用 library 命令。

如何使用倒入和导出命令实现多平台的库支持。


异步支持

https://dart.cn/guides/libraries/library-tour#dartasync---asynchronous-programming Dart 代码库中有大量返回 Future 或 Stream 对象的函数,这些函数都是 异步 的,它们会在耗时操作(比如I/O)执行完毕前直接返回而不会等待耗时操作执行完毕。

async 和 await 关键字用于实现异步编程,并且让你的代码看起来就像是同步的一样。

处理 Future

可以通过下面两种方式,获得 Future 执行完成的结果:

使用 async 和 await;

使用 Future API,具体描述参考 https://dart.cn/guides/libraries/library-tour#future

使用 async 和 await 的代码是异步的,但是看起来有点像同步代码。

例如,下面的代码使用 await 等待异步函数的执行结果。

await lookUpVersion();

必须在带有 async 关键字的 异步函数 中使用 await:

Future<void> checkVersion() async {
var version = await lookUpVersion();
// 使用 version 继续处理逻辑
}

尽管异步函数可以处理耗时操作,但是它并不会等待这些耗时操作完成,异步函数执行时会在其遇到第一个 await 表达式(代码行)时返回一个 Future 对象,然后等待 await 表达式执行完毕后继续执行。

使用 try、catch 以及 finally 来处理使用 await 导致的异常:

try {
version = await lookUpVersion();
} catch (e) {
// 无法找到版本时做出的反应
}

你可以在异步函数中多次使用 await 关键字。 例如,下面代码中等待了三次函数结果:

var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await 表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future 对象里。 Future 对象代表一个“承诺”,await 表达式会阻塞直到需要的对象返回。

如果在使用 await 时导致编译错误,请确保 await 在一个异步函数中使用。 例如,如果想在 main() 函数中使用 await,那么 main() 函数就必须使用 async 关键字标识。

声明异步函数 async

异步函数 是函数体由 async 关键字标记的函数。

将关键字 async 添加到函数并让其返回一个 Future 对象。 假设有如下返回 String 对象的方法:

String lookUpVersion() => '1.0.0';

将其改为异步函数,返回值是 Future:

Future<String> lookUpVersion() async => '1.0.0';

注意,函数体不需要使用 Future API。如有必要,Dart 会创建 Future 对象。

如果函数没有返回有效值,需要设置其返回类型为 Future

处理 Stream

如果想从 Stream 中获取值,可以有两种选择:

使用 async 关键字和一个 异步循环(使用 await for 关键字标识)。

使用 Stream API。详情参考 https://dart.cn/guides/libraries/library-tour#stream

在使用 await for 关键字前,确保其可以令代码逻辑更加清晰并且是真的需要等待所有的结果执行完毕。 例如,通常不应该在 UI 事件监听器上使用 await for 关键字,因为 UI 框架发出的事件流是无穷尽的。

使用 await for 定义异步循环看起来是这样的:

await for (varOrType identifier in expression) {
  // 每当 Stream 发出一个值时会执行
}

表达式 的类型必须是 Stream。执行流程如下:

等待直到 Stream 返回一个数据。

使用 1 中 Stream 返回的数据执行循环体。

重复 1、2 过程直到 Stream 数据返回完毕。

使用 break 和 return 语句可以停止接收 Stream 数据,这样就跳出了循环并取消注册监听 Stream。

如果在实现异步 for 循环时遇到编译时错误,请检查确保 await for 处于异步函数中。

例如,要在应用程序的 main() 函数中使用异步 for 循环,main()函数体必须标记为 async:

Future<void> main() async {
// ...
await for (var request in requestServer) {
handleRequest(request);
}
// ...
}

生成器 generator

当你需要延迟地生成一连串的值时,可以考虑使用 生成器函数。

Dart 内置支持两种形式的生成器方法:

同步 生成器:返回一个 Iterable 对象。

异步 生成器:返回一个 Stream 对象。

通过在函数上加 sync* 关键字并将返回值类型设置为 Iterable 来实现一个 同步 生成器函数,在函数中使用 yield 语句来传递值:

Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}

实现 异步 生成器函数与同步类似,只不过关键字为 async* 并且返回值为 Stream:

Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}

如果生成器是递归调用的,可是使用 yield* 语句提升执行性能:

Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}

可调用类 call

通过实现类的 call() 方法,允许使用类似函数调用的方式来使用该类的实例。

在下面的示例中,WannabeFunction 类定义了一个 call()函数,函数接受三个字符串参数, 函数体将三个字符串拼接,字符串间用空格分割,并在结尾附加了一个感叹号。


class WannabeFunction {
String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction(); var out = wf('你好', ',使用 Dart 的', '朋友');

void main() => print(out);


***

## 隔离区
>大多数计算机中,甚至在移动平台上,都在使用多核 CPU。
>为了有效利用多核性能,开发者一般使用共享内存的方式让线程并发地运行。
>然而,多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。

>为了解决多线程带来的并发问题,Dart 使用 isolate 替代线程,
>所有的 Dart 代码均运行在一个 isolate 中。
>每一个 isolate 有它自己的堆内存以确保其状态不被其它 isolate 访问。

>可以查阅下面的文档获取更多相关信息:

>[Dart 异步编程:隔离区和事件循环](https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a)

>[dart:isolate API 参考](https://api.dart.dev/stable/dart-isolate) 介绍了 Isolate.spawn() 和 TransferableTypedData 的用法

>[Background parsing cookbook on the Flutter site](https://flutter.cn/docs/cookbook/networking/background-parsing)
>Flutter 网站上关于[后台解析](https://flutter.cn/docs/cookbook/networking/background-parsing)的 Cookbook

***

## 类型别名 typedef
>类型别名(通常称为typedef ,因为它使用关键字typedef定义词进行声明)是指类型类型的简明方法。
>下面是一个声明和使用名为 IntList 的类型别名的示例:
```dart
typedef IntList = List<int>;
IntList il = [1, 2, 3];

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

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

在大多数情况下,建议使用内联功能类型而不是类型别名。但是,函数类型别名仍可能很有用:


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

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

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


***

## 元数据
>使用元数据可以为代码增加一些额外的信息。
>元数据注解以 `@` 开头,其后紧跟一个编译时常量(比如 deprecated)或者调用一个常量构造函数。

>Dart 中有两个注解是所有代码都可以使用的:`@deprecated` 和 `@override`。
>你可以查阅 [扩展一个类](https://dart.cn/guides/language/language-tour#extending-a-class) 获取有关 @override 的使用示例。

>元数据可以在 library、class、typedef、type parameter、 constructor、factory、function、field、parameter 或者 variable 声明之前使用,
>也可以在 import 或 export 之前使用。
>可使用反射在运行时获取元数据信息。

>下面是使用 `@deprecated` 的示例:
```dart
class Television {
  /// _弃用: 使用 [turnOn] 替代_
  @deprecated
  void activate() {
    turnOn();
  }

  /// 打开 TV 的电源。
  void turnOn() {...}
}

自定义元数据注解

下面的示例定义了一个带有两个参数的 @todo 注解:


library todo;

class Todo { final String who; final String what;

const Todo(this.who, this.what); }

>使用 `@Todo` 注解的示例:
```dart
import 'todo.dart';

@Todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}

注释

Dart 支持单行注释、多行注释和文档注释。

单行注释

单行注释以 // 开始。所有在 // 和该行结尾之间的内容均被编译器忽略。

void main() {
// TODO: refactor into an AbstractLlamaGreetingFactory?
print('Welcome to my Llama farm!');
}

多行注释

多行注释以 /* 开始,以 */ 结尾。所有在 /**/ 之间的内容均被编译器忽略(不会忽略文档注释), 多行注释可以嵌套。


void main() {
/*
* This is a lot of work. Consider raising chickens.

Llama larry = Llama(); larry.feed(); larry.exercise(); larry.clean(); */ }


### 文档注释
>文档注释可以是多行注释,也可以是单行注释,文档注释以 `///` 或者` /**` 开始。
>在连续行上使用 `///` 与多行文档注释具有相同的效果。

>在文档注释中,除非用中括号括起来,否则分析器会忽略所有文本。
>使用中括号可以引用类、方法、字段、顶级变量、函数和参数。
>括号中的符号会在已记录的程序元素的词法域中进行解析。

>一个引用其他类和成员的文档注释:
```dart
/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
///
/// Just like any other animal, llamas need to eat,
/// so don't forget to [feed] them some [Food].
class Llama {
  String? name;

  /// Feeds your llama [food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

在生成的文档中,[feed] 会成为一个链接,指向 feed 方法的文档, [Food] 会成为一个链接,指向 Food 类的 API 文档。

解析 Dart 代码并生成 HTML 文档,可以使用 Dart 的 文档生成工具。 关于生成文档的示例,请参考 Dart API documentation 查看关于文档结构的建议,请参考文档: Guidelines for Dart Doc Comments.


Dart 编程语言规范

https://dart.cn/guides/language/spec

WangShuXian6 commented 3 years ago

简介

Dart是一 门通用编程语言,是有着类似 C 语言的语法的新语言,其目的是让绝大多数程序员快速上手。 下面是约定俗成的 “Hello World"示例,

main() {
print('hello world');
}

Dart是纯面向对象、基于类、使用可选类型、支持混入式继承及Actor模式的并发编程语言。

动机

Dart立志成为 一 个精心设计的开发平台,为当下的开发者要编写的各种应用提供支持。 它力图屏蔽底层平台的各种问题与实现的细节,让开发者能方便地使用这些新兴平台所提供的强大功能。

设计准则

万物皆对象

Dart是一门纯面向对象的编程语言,这意味着 Dart 程序在运行时所处理的值都是对象,甚至包括数字、布尔值等基本数据,无 一 例外。 Dart坚持对所有数据统一处理,这方便了所有与语言相关的人员:语言的设计者、实现者,以及最重要的使用者。 举个例子,集合类能够操作任意类型的数据,使用者无须关注自动转箱、拆箱的问题。 类似的底层细节与开发者要解决的实际问题是无关的,编程语言的 一 个关键任务是把开发者从各种可认知的负担中解放出来。 采用统 一 的对象模型的同时,简化了系统实现者的任务

面向接口编程, 而非面向实现

关注对象的行为而非它的内部实现,这是面向对象编程语言的核心原则

Dart却力图通过以下几种方式来维护这个原则

• Dart 的类型基于接口,而不是类。作为一项原则,任意类都隐含了一个接口,能够被其他类实现,不管其他类是否使用了同样的底层实现(部分core type 例外,比如数字、布尔值与字符串)。 • Dart 没有final 方法,允许重写几乎所有方法(同样,部分内置的操作符例外)。 • Dart 把对象进行了抽象封装,确保所有外部操作都通过存取方法来改变对象的状 态。 • Dart 的构造函数允许对对象进行缓存,或者从子类型创建实例,因此使用构造函数并不意味着绑定了一个具体的实现。

类型是为开发者服务的

静态类型信息可以为用户与计算机提供富有价值的文档。如果使用得当,则这些信息能使代码更易读,特别是涉及多个库时,也使自动化工具更容易协助开发者。 类型简化了各种分析任务,特别是可以帮助编译器提升程序的性能。 它还有助千检测编程错误。

Dart 是一门类型可选的语言,

它的具体定义是: • 类型在语法层面上来说是可选的; • 类型对运行时语义没有影响。

类型变为可选,照顾了那些不愿意与类型系统打交道的开发者。 因此而选择Dart 的开发者,完全可以把Dart 当成一门动态类型语言。 虽然类型可选,但只要代码中有类型注解,就意味着代码有了额外的文档,所有的编码人员都会从中受益。 类型注解同时让工具能更好地配合开发者的开发工作。 对于可能存在的类型不一致和遗漏, Dart 会给出警告,不会报错。这些警告的程度与性质都经过校准,不会铺天盖地地出现,确保真正对开发者有益。 同时, Dart 编译器不会拒绝一段缺少类型或类型不一致的程序。因此,使用类型不会限制开发者的工作流程。类型缺失或不完整的代码,仍然可被用来测试和实验。 静态的正确性与灵活性之间的平衡使得类型服务千开发者。

限制

Dart 必须在当下的浏览器上高效运行。 Dart 必须让当下的开发者能够快速上手, 这决定了它的语法必须是近似C 语言的,也决定了Dart 的语义选择不能偏离主流开发者的期望。

Dart 的语义已经受到了上述限制的影响时,我们应该注意为此所做的设计决策而引入的利弊权衡,包括字符串、数字的处理,以及返回语句等。

概述

Dart 的一个简单表达式:

3

毋庸置疑,这个表达式的值是整数3 。

稍微复杂的表达式:

3 + 4
(3+4}*6
1 + 2 * 2
1234567890987654321 * 1234567890987654321

它们的值分别是7 、42 、5 及 I 524 157 877 457 704 723 228 166 437 789 971 041 。

Dart 中的整数很像数学里的数字,它们没有32 位或64 位可代表的最大值的限制,其大小的唯一限制是可用内存。

Dart 不仅支持整数,也支持浮点数、字符串、布尔值等。许多内置类型都有简便的语法:

3.14159 //一个浮点数
'a string'
"another string - both double quoted and single quoted forms are supported"
'Hello World' //你已经见过它
true
false// 所有的布尔值都在这里
[] //一个空的列表
(0, 1.0, false, 'a', [2, 2.0, true, \b"]] //有5 个元素的列表,最后一个也是列表

Dart 支持标准的单行注释,即在II之后的内容将被忽略,直到行末。 最后两行是用字面量表示的列表,第1 个列表为空,第2 个列表的长度是5, 且最后一个元素是另一个长度为4 的字面量列表(注:这里的列表相当千其他语言的数组,因为Dart 是用List 来表示数组的,所以还是翻译为列表)。

[1, 2, 3] [1]

上面例子的值是2 。列表的第1 个元素的索引是0 ,第2 个是1 ,以此类推。 列表有length与isEmpty 两个属性(它还有更多的属性)。

[1, 2, 3].length; // 3

某些Dart 的实现可能并不符合这一原则。在Dart 被编译为JavaScript 时,所有数字都变成了JavaScript的数字,而JavaScript 只有数字类型且没有整数类型,即要用浮点数来表示整数,并有大小限制。所以,大千2^53^ 的整数可能就不那么容易获得了。

[].length; // 0
[].isEmpty; // true
['a'].isEmpty; // false

定义函数 Dart 函数main()

main() {
print ('Hello World');
}

一个Dart 程序的执行总是开始千对main()函数的调用。 每个函数都由函数头与函数体组成。 函数的头部定义了函数的名称与参数(我们的示例函数没有参数)。 main()方法的函数体中只有一条语句,即调用了接收一个参数的print()函数。 这里传递的参数是一个字面量的字符串'Hello World' 。 程序运行的效果是打印出“Hello World" 。

另一个函数:

twice(x) => x * 2;

声明了一个名为twice 的函数,并有一个名为x 的参数。该函数返回x 乘以2 的结果。可以这样写来执行它:

twice(2)

以上函数调用的结果是4 。这个twice 函数有两部分,一部分是由函数名与形式参数组成的函数签名,另一部分就是跟在=>后面的只包含了一个表达式的函数体。另一种更传统的书写方式是:

twice(x) {
return x * 2;
}

以上两个例子是完全相等的,但在第2 个例子中,函数体可以包含零个或多个语句。它只是调用了一个retum 语句,使函数计算出x*2 的结果并将其返回给调用者。

另一个例子如下:

max(x, y) {
if (x > y)
return x;
else
return y;
}

它将返回两个参数中更大的那一个。我们同样可以把它缩写成这样:

max(x, y) => (x > y) ? x : y;

第1 种形式使用了if 语句,这在其他编程语言中是很常见的;第2 种形式使用了条件表达式,同样,这在类C 的编程语言中也是很常见的。使用表达式能让我们使用缩写方式来定义函数。

一个更加复杂的函数:

maxElement(a) {
var currentMax =
a.isEmpty ? throw 'Maximal element undefined for empty array' : a[0];
for (var i = 0; i < a.length; i++) {
currentMax = max(a[i], currentMax);
return currentMax;
}
}

这个名叫maxElement 的函数,接收一个列表a 并返回其中的最大值。在这里,我们真正需要使用常规方式来定义函数,因为这里的计算有很多步骤,会产生一连串的语句

函数体的第1 行声明了一个名为currentMax 的变量,并对其进行初始化。Dart 程序中的每个变量都必须显式声明。变量currentMax 代表我们目前数组中的最大值。

在许多编程语言中,开发者可能会选择把currentMax 初始化为一个值,这个值代表可能的最小整数,它的典型名称是MIN_INT 。在数学上, “可能的最小整数”的说法是很荒谬的。但是,在数字大小有限制的编程语言里,这个说法是有意义的。前面介绍过, Dart的整数是没有大小限制的,所以我们直接把currentMax 初始化为列表的第1 个元素。如果列表为空,则我们不能那样做,那也意味着传入的参数a 是无效的,因为你无法获取一个空列表的最大值。因此,我们检测了a 是否为空。如果为空,我们就会抛出一个异常,否则我们就用列表的第1 个值来初始化currentMax 。 异常是使用throw 语句抛出的。throw 关键字后面跟着一个定义抛出内容的表达式。在Dart 中,任何类型值都可以被抛出,不要求它们是特定的Exception 类型。在这里,我们抛出了一个描述问题的字符串。在下一行的开头,循环语句for 遍历了整个列表1, 使用前面定义好的max 函数,把每个元素依次与currentMax 进行比较。如果当前元素大千currentMax, 则我们把它作为新的最大值赋给currentMax 。循环结束后,我们就可以断定currentMax 是列表中最大的元素并把它返回。

Dart 允许你在类外部定义函数(比如前面的twice 、max 与maxElement) 与变量。虽然如此, Dart 是一门纯面向对象语言。我们 前面看过的所有值,包括数字、字符串、布尔值、列表甚至函数本身都是Dart 中的对象。所有这些对象都是某个类的实例。像length 、isEmpty 这些操作,甚至[]索引操作符,都是对象的方法。

编写一个类。 Point 类,它代表了直角坐标系的点:

class Point {
var x, y;
Point(a, b) {
x = a;
y = b;
}
}

Point 类有两个实例变量(或者字段) x 与y。要创建一个Point 类的实例,我们可以用new 表达式来调用它的构造函数:

var origin= new Point(0, 0);
var aPoint = new Point(3, 4);
var anotherPoint = new Point(3, 4);

以上3 行创建了3 个新的不一样的Point 实例。特别是, aPoint 与anotherPoint 是两个不同的对象。每个对象都有唯一标识,它们通过这个标识来区分彼此。 每个Point 的实例都有各自的x 和y 变量的副本,可以通过点符号来访问它们:

origin.x // 0
origin.y // 0
aPoint.x // 3
aPoint.y // 4

变量x 与y 的值是由构造函数的实际参数来设置的,而构造函数是由new 调用的。将构造函数的形式参数直接赋值给同名的字段,这种方式非常普遍,所以Dart 为此提供了一 种特殊的语法糖:

class Point {
var x, y;
Point(this.x, this.y);
}

新版的Point 与原版完全相同,但更简洁。我们再给它添加一些行为:


class Point {
var x, y;

Point(this.x, this.y); scale(factor) => new Point(x factor, y factor); }

>这个版本多了一个scale 方法,它接收一个定义比例系数的参数factor, 并返回一个新的点。新的点的坐标是根据当前点的坐标按照factor 缩放而得来的。
```dart
aPoint.scale(2).x // 6
anotherPoint.scale(10).y // 40

另一个有趣的操作是对点进行加法操作:

class Point {
var x, y;
Point(this.x, this.y);
scale(factor) => new Point(x * factor, y * factor);
operator +(p) => new Point(x + p.x, y + p.y);
}

现在我们可以写这样的表达式了:

var temp = (aPoint + anotherPoint).y; // 8

点的操作符的行为就像是一个实例方法,而事实上,它们就是一个有着怪异名称和调用语法的实例方法。

Dart 也支持静态成员。我们可以给Point 添加一个计算两点间距离的静态方法:


import 'dart:math';

class A { static distance(pl, p2) { var dx = pl.x - p2.x; var dy = pl.y - p2.y; return sqrt(dx dx + dy dy); } }

>static 修饰符表明此方法不针对某个特定实例,它无法访问实例变量x 和y, 因为实例变量对千每个Point 实例都是不同的。这个方法使用了一个库函数sqrt()来计算平方根。

>你可能会问, sqrt()从何而来?要理解这一点,我们需要先解释Dart 的模块化理念。
>Dart 代码用库作为模块化的基本单元。每个库都定义了一个的命名空间,这个命名空间包含所有在库中声明的实体的名称,其他库的实体也能被导入进来。Dart 核心库中声明的实体,都会被隐含地导入到所有的Dart 库中。然而, sqrt()并不在核心库中。它位于一个名为dart:math 的库中,如果你要使用它,就必须先显式地导入这个库。

>下面是一个导入的库的完整例子,它包含了Point 类:
```dart
library points;

import 'dart:math';

class Point {
  var x, y;
  Point(this.x, this.y);
  scale(factor) => new Point(x * factor, y * factor);
  operator +(p) => new Point(x + p.x, y + p.y);
  static distance(pl, p2) {
    var dx = pl.x - p2.x;
    var dy = pl.y - p2.y;
    return sqrt(dx * dx + dy * dy);
  }
}

我们声明了一个名为points 的库并同时导入了dart:math 库。 这个导入的库使得points库可以访问sqrt 函数。现在,任意其他库都可以导入points 库来使用我们的Point 类。 需要注意的一个关键细节是import 后面跟着的是一个字符串'dart:math' 。 一般情况下,导入指向的都是用字符串表示的统一资源标志符(UR1) 。编译器通过这些URI 指定的位置去寻找对应的库。 Dart 的内置标准库都使用'dart:c'这种方式的UR1 ,其中c代表某一特定库。

相关语言及其对Dart 的影响

Dart 的设计深受早期编程语言的影响,特别是Smalltalkw 、Java 和JavaScript。Dart 的语法贴近C 语言、Java 和JavaScript。 从某些角度看,特别是在坚待纯对象模型这点上,Dart 的语义贴近Smalltalk 。

不过, Dart 有很多至关重要的不同点。Dart 引入了自己的基于库的封装模型,这不同于上面提到的所有语言。 Smalltalk 支待对字段进行基千对象的封装,方法和类则是全局的。 Java 使用基于类的封装和包级别的访问控制, JavaScript 则完全依赖闭包的封装。 与Smalltalk 和Java 相似, Dart 基于类,支持单继承,但增加了基千混入的继承,这种实现方式首次出现千Smalltalk 的方言Strongtalk中。 Smalltalk 的类方法是实例方法,而Dart 的类方法更像Java 风格的静态方法,所以,它们不完全一样。 Dart 的构造函数在语法上与Java 非常相似,实际上却有很大不同。 Dart 的类型检查也非常像Strongtalk。 Dart 的并发机制接近最早的Actor 模式(尽管势在必行),同样,这与上面提及的语言都不一样。 Erlang 的成功是促使Dart 选用Actor 模式的关键因素,但不同的是, Dart 采用了非阻塞的并发模型。 Dart 还内置了对异步编程的支待,这方面受到了C#的巨大影响。

WangShuXian6 commented 3 years ago

对象、接口、类与mixin

在Dart 中,一切皆对象,这甚至包括了最简单的数据如数字或布尔值true 和false 等。 一个对象由(可能为空的)一组字段提供状态,由一组方法提供行为。 对象的状态可以是可变或不变的。对象的方法永不为空,因为所有的Dart 对象都具备一定的行为。 对象从它们的类中获得行为。每个对象都有一个类,我们将之表述为对象是类的一个实例。 因为每个对象都有一个决定其行为的类,所以Dart 是一门基于类的语言。

Point 类:


import 'dart:math';

class Point { var x, y; Point(this.x, this.y); scale(factor) => new Point(x factor, y factor); operator +(p) => new Point(x + p.x, y + p.y); static distance(pl, p2) { var dx = pl.x - p2.x; var dy = pl.y - p2.y; return sqrt(dx dx + dy dy); } }

>Point 类的实例各自都有两个字段x 和y 构成对象的状态。
>它们也拥有几个提供有用行为的方法,这些方法包括scale 和+,但实际上它们还拥有其他方法。
>这些额外的方法不是由Point 类自身定义的,而是从其父类继承而来的。
>除了内置在每个Dart 实现中的Object类,每个类都有一个父类。
>一个类可以明确地列出它的父类,但那不是必需的。如果一个类没有列出父类,那么其父类就是Object 。Point 类就是这种情况

>可以明确地指定Object 作为Point 的父类,明确指定Object 类与不指定Object 类这两种定义方式是完全相同的。
```dart
class Point extends Object {...其余定义未变}

定义对象行为的方法通常被称为对象的实例方法。注意方法distance()不属于实例行为。它是一个静态方法,不是实例方法。


accessor

accessor 是为方便访问值所提供的特殊方法。

们再次重温Point类,并考虑如何修改以让它使用极坐标的表示方式。 把字段x 和y 替换为新的字段rho 和theta 。 可能仍有部分客户端需要访问直角坐标,所以可以选择使用存储的极坐标来计算直角坐标。由此产生的类如下:

class Point {
  var rho, theta;
  Point(this.rho, this.theta);
  x() => rho * cos(theta);
  y() => rho * sin(theta);
  scale(factor) => new Point(rho * factor, theta);
  operator +(p) => new Point(x() + p.x(), y() + p.y());
  static distance(pl, p2) {
    var dx = pl.x() - p2.x();
    var dy = pl.y() - p2.y();
    return sqrt(dx * dx + dy * dy);
  }
}

该代码实际是有错的,因为在+方法内使用了直角坐标来调用构造函数,但它实际要求的是极坐标。稍后会处理它。

我们将忽略的另一个问题是数值精度;所有这些转换都可能得不出精确的结果。

由于已经将字段x 和y 替换为计算相应值的同名方法。所有的客户端不得不将它们对x 和y 的引用修改为对方法的调用。 例如,如果客户端有一段这样的代码:

print(myPoint.x);

则需要将它改为:

print(myPoint.x());

唯一区别就是跟在x 后面的空参数列表,这个变化虽小,但终归是一个变化。 虽然现代开发工具能帮助开发者自动重构,但在不知道有哪些客户端的情况下,修改一个使用广泛的API,迫使它们做出修改,就算是有方便的工具进行协助,也是不可接受的。

getter 方法

Dart 提供的getter 方法是一种更好的解决方案,它们通常被称为getter 。 getter 是一个不带参数的特殊方法,可以在不提供参数列表的情况下直接调用。 getter 方法的引入是通过在方法名前添加前缀get 。 getter 方法不需要参数列表,甚至是空的参数列表。

class Point {
  var rho, theta;
  Point(this.rho, this.theta);
  get x => rho * cos(theta);
  get y => rho * sin(theta);
  scale(factor) => new Point(rho * factor, theta);
  operator +(p) => new Point(x + p.x, y + p.y);
  static distance(pl, p2) {
    var dx = pl.x - p2.x;
    var dy = pl.y - p2.y;
    return sqrt(dx * dx + dy * dy);
  }
}

现在,客户端都不需要修改代码了。getter 的调用语法与变量的访问没有区别。

Dart 编译器如何知道两者的区别?

答案是它不知道。 Dart中所有实例变量的访问,实际上都是调用getter 。 每个实例变量始终有一个与之关联的getter, 由Dart 编译器提供。

setter 方法

如果客户端给字段赋值,比如:

myPoint.y = myPoint.y * 2;

新版Point 类没有对应的可以被赋值的字段。客户端不清楚怎么修改并使其代码保待运行。 为了解决这个问题,我们使用setter 方法(简称为setter) 。

setter 方法名前面要添加前缀set, 并只接收一个参数。 setter 的调用语法与传统的变量赋值是一样的。 如果一个实例变量是可变的,则一个setter 将自动为它定义,所有实例变量的赋值实际上都是对setter 的调用。


class Point {
var rho, theta;
Point(this.rho, this.theta);

get x => rho cos(theta); set x(newX) { rho = sqrt(newX newX + y * y); theta = acos(newX / rho); }

set y(newY) { rho = sqrt(x x + newY newY); theta = asin(newY / rho); }

get y => rho * sin(theta);

scale(factor) => new Point(rho * factor, theta);

operator +(p) => new Point(x + p.x, y + p.y);

static distance(pl, p2) { var dx = pl.x() - p2.x(); var dy = pl.y() - p2.y(); return sqrt(dx dx + dy dy); } }


>问题还存在第3 个方面。客户端通过调用Point 类的构造函数来创建一个点,如下所示:
```dart
new Point(3,4);

新Point 类的构造函数接收两个参数,它们代表向量的长度和角度,而不是直角坐标。 正如前面提到的, Point 类内部也依赖这个API, 所以它们是无法正常工作的。 像往常一样,解决方法是保留现有的API, 同时根据需要改变表现方式。 因此,我们还是用极坐标来表示点,但为保持接口不变,我们保留已有的接收直角坐标参数的构造函数。

class Point {
  var rho, theta;
  Point(a, b) {
    rho = sqrt(a * a + b * b);
    theta = atan(a / b);
  }
//...剩余代码不变...
}

现在已经达到改变点的表示方法同时不对客户造成任何影响的目标,并且没有做任何前期规划。我们不需要提前决定是通过accessor 方法还是特定的属性声明来暴露点的坐标, Dart 帮我们做了,而且没有带来任何语法上的不便。 在任意主流编程语言中,我们都能修复这种构造函数的问题。然而,如果没有accessor, 则完全平滑过渡是不可能的。


实例变量

当一个类声明一个实例变量时,它会确保每个实例都有自己的唯一变量复制。 对象的实例变量需要占用内存,这块内存是在对象创建时分配的。重要的是,此内存在被访问之前,应该被设置为某些合理的值。在低级语言如C 语言中则并不如此,新分配的存储空间的内容可能是不明确的,通常就是内存在重新分配之前的值。这将会导致可靠性、安全性方面的问题。

Dart 会将每个新分配的变量(不只是实例变量,还包括局部变量、类变量和顶层变量)初始化为null 。 在Dart 中,与其他对象一样, null 也是一个对象。 不能把null 与其他对象混淆,如0 或false 。null 对象只是在Dart 核心库中定义的Null 类的唯一实例。 这种情况与其他语言如C 、C++ 、C# 、Java 或JavaScript 是不一样的,但这是一切皆对象的理念所带来的必然结果。

表征独立

声明实例或静态变量会自动引入一个getter。如果变量是可变的,则一个setter 也会被自动定义。 Dart 中的字段都不是直接访问的,所有对字段的引用都是对accessor方法的调用。 只有对象的accessor 才能直接访问它的状态,所有访问对象状态的代码都必须通过这些accessor 方法,这意味着每个类的底层表示都可以随时更改,且不需要客户端修改代码,甚至重新编译也不需要!这种属性被称为“表征独立“。 在上一节见识了”表征独立”的好处,展示了改变点的表示方式且无须修改任何使用了点的代码。

类变量 static

除了实例变量,类也可以定义类变量。 一个类只有一份类变量的副本,无论它有多少个实例。即使类没有实例,类变量也存在。 类变量的声明是在变量名前放置单词static 。

可以添加一个类变量来跟踪有多少个实例被创建。 在这里,每当类构造函数Box()运行时,它就会增加己创建箱子的数量。

class Box {
static var numberOfinstances = 0;
Box() {
numberOfinstances = numberOfinstances + 1;
}
}

像实例变量,类变量从不直接引用。所有对它们的访问都是通过accessor 。 在它的声明类中,类变量可以通过名称直接引用。 在类的外部,只能通过在变量名前加上类名来访问:

fn() {
Box.numberOfinstances == 0 ? print('No boxes yet') : print('We haveboxes!');
}

类变量通常也被称为静态变量,但“静态变量”这个术语也包括了类变量与顶层变量。 为了避免混淆,我们将坚持使用“类变量”这个术语。 我们也经常会用”字段”这个术语来统称实例和类变量。

类变量是延迟初始化的

在getter 第一次被调用时类变量才执行初始化,即第一次尝试读取它时。 与其他变量一样,如果一个类变量没有被初始化,则它会默认初始化为null 。

类变量的延迟初始化有助于避免一个典型的问题:过量的前期初始化导致应用程序启动缓慢。

然而,延迟初始化也会导致出乎意料的行为。

假设一个类变量在被赋值之前就被读取,就像下面的例子(注:例子中使用的是顶层变量而非类变量,但它们的行为是一 致的) : 此处, schrodingers 的初始化永远不会执行,并且对print()的调用也永远不会执行。 程序将抛出异常


class Cat {}

class DeadCat extends Cat {}

class LiveCat extends Cat { LiveCat() { print("I'm alive!"); } }

// 延迟初始化 var schrodingers = new LiveCat();

main() { schrodingers = new DeadCat(); }


>虽然以上情况看起来可能是非常明显的,但在更复杂的情况下就未必了。
>例如,在调试的过程中,开发者可能会检查变量的值,那样就会触发变量的初始化。
>开发者应该始终密切关注延迟初始化带来的影响。

***

## final 变量
>Dart 的变量可以用单词final 作为前缀,表明它们在初始化后不能再修改。
>final 字段有 getter 但是没有setter.
> final 类变量必须在声明时就进行初始化。
>final 实例变量必须在任意实例方法运行前进行初始化。
>实现这一点有几种方法。第一种是在声明变量时就进行初始化,例如:
```dart
class A {
  final origin = new Point(0, 0);
}

这种方式不一定总是很方便的。 在不同的构造函数中,设置这个变量的方式可能不一样。 例如,这个变量可能取决于构造函数的参数。 如果想把fmal 实例变量设置为构造函数参数的值,则可以使用普通的构造函数简写。 作为例子,考虑以下Point 类,它表示不可变的点:

class Point {
final x, y;
Point(this.x, this.y);
// Point 类的剩余代码...
}

不过,某些情况下这还不够。变量的值可能会取决于构造函数的参数,但又不完全相同, 也就是说,它们的值是基于构造函数参数计算得来的

试图给一个final 实例变量赋值通常会导致一个名为NoSuchMethodError 的错误,因为赋值操作只是调用setter 的语法糖,而final 实例变量所对应的setter 方法是未定义的。 单独声明一个对应的 setter 是可行的,它也会被调用。然而这对实例变量的值没有任何影响,在final 变量初始化之后,它的值就无法改变了。

大部分实例变量在声明时就被赋值并不再改变。你可能为此感到惊讶,但这已经被系统研究验证了。 因此,大部分实例变量最好都声明为final 。 有一种强烈的观点是final 应该被设置为默认,但是那样会违背已有的习惯,所以Dart在这里选择了传统的做法。


相同与相等

所有对象都支待相等操作符=。这个操作符是在Object 类中定义的,因此所有的类都继承了它,所以所有对象实例的行为都包含了它。考虑以下代码:

main() {
var aPoint = new Point(3, 4);
var anotherPoint = new Point(3, 4);
aPoint == anotherPoint; //值为false
}

Object 类的== 方法用于检测参数与接收者是否相同。 每个对象都有唯一标识,一个对象只与它自己相同。 从上面的例子可以看出,两个对象可以是同一个类的实例,并且有相同的状态,但它们仍不相同。

我们说两个对象不相等是因为相等被定义为相同,但是这回避了为什么要如此定义的问题。 ,决定实例如何才是有意义的相等,是定义类的开发者的责任。做到这一点的方法是重写== 。 在Point 类中,我们可以这样定义相等;

operator ==(p) => x == p.x && y == p.y;

某些情况下,开发者可能希望检查两个表达式是否代表相同的对象,但这比较少见。 开发者通常只检测对象是否相等。

dart:core 库中定义了一个identical()方法,开发者可以使用它来检查两个对象是否相同。

identical(origin, origin) 的值为true, identical(aPoint, aPoint) 与identical(anotherPoint, anotherPoint)的值也是一样的。 另一方面,identical(aPoint, another Point)的值为false 。

来定义Object 类的相等方法:

bool operator ==(other) => identical(this, other);

hashCode

所有Dart 对象都支持一个名为hashCode 的getter 方法。 对象的相等和hashCode 是相互关联的。如果两个对象相等,那么它们的哈希码也应该相等,代码的实现者必须小心维护这个属性。 在实践中,这意味着如果你选择重写上述两个方法中的一个,那么你也应该重写另一个。

相等具备自反性

实现自定义的相等必须要谨慎。 我们期望我们的相等具备:

自反性(a=a)、 可传递性((a = b) && (b = c) 意味着(a=c)、 互换性(a =b) 意味着(b=a)

其中,自反性是你可以在自己实现==方法时确保的,而其他属性在不断扩展的系统内是比较难以维护的。

开发者总是可以引入类似的代码:

class BadApple {
operator ==(x) => true;
}

以上代码会逐步破坏整个系统的相等属性。

类与父类

每个类都声明了一组实例成员,包括实例变量和各种实例方法。 每个类(Object类除外)继承了父类的实例成员。

单继承

由于除了Object 类外的所有类都只有一个父类,而Object类没有父类,所以Dart 类层次结构形成了一个以Object 类为根的树。这种结构叫作单继承, 如下图所示。这只是整个Dart 类层次的一个小片段。 image

重写

如果子类声明一个与父类的某个方法同名的实例方法,那么可以说成子类重写了父类的方法。

重写并不总是合法的。

你不能用一个普通方法重写getter, 反之亦然。这些情况会导致编译错误。


class S {
var v;
final f = 0;
get g => 42;
set s(x) => v = 2;
m(a, b) => 91;
}

class C extends S { V() => 1; // 非法:方法v( )重写隐含的getter 方法v f() => 2; // 非法:方法f ()重写隐含的ge 七ter 方法f g() => 100; // 非法:方法g( )重写隐含的getter 方法g }


#### 试图用方法或 getter 重写 setter ,或者用 setter 重写方法或 getter 在技术上都是不可行的。
>如果你尝试,则Dart 会警告你:
```dart
class D extends S {
  s(y) => 200; //警告: D 有方法s 和setters=
}

某些情况下,无意义的重写会导致警告而不是编译错误。

当一个重写方法比被重写的方法需要更多的参数时, Dart 编译器将产生一个警告,但是代码仍然可以编译。

class E extends S {
m(x, y, z) => 101; // 警告:重写方法参数个数不一致
}

在这种情况下,为什么Dart 只发出警告而不马上拒绝这样的代码呢? 因为Dart 极力避免给开发者强加工作流程。 各种不一致的情况都能在开发中出现,并且它们最终都应该被修正。然而,在不能取得任何进展前,强迫开发者马上处理这些情况,往往适得其反。 因此, Dart 确保开发者能够通过警告知道这些问题,但只在绝对必要的情况下才会中止编译。


抽象方法与抽象类

就运行时而言,抽象方法根本不存在。毕竟,它们没有实现,也无法运行。 调用抽象方法就与调用一个不存在的方法一样。 只包含抽象方法的类在定义接口时很有用

抽象方法

通常来说,简单地声明一个方法而不提供它的实现是有用的,这种方法被称为抽象方法。 任何种类的实例方法都可以是抽象的,不管它是getter 、setter、操作符或普通的方法。 声明一个抽象方法将告诉代码的阅读者(人或者电脑),这个方法只在代码运行时才可用。 这能帮助开发者理解代码,同时有利千对错误的处理。

抽象类 abstract

有一个抽象方法的类本身就是一个抽象类,抽象类的声明是通过在类名前加上前缀 abstract 。

一个抽象类,它不包含任何实现信息,纯粹作为一个接口,如下所示。

abstract class Pair {
get first;
get second;
}

类Pair 有两个抽象的 getter 方法 first 和 second 。 Pair 被显式声明为抽象类。 如果删掉 abstract 修饰符,那么Dart 解析器会发出这个类具有抽象方法的警告。 抽象类有抽象方法是完全正确的,但如果这个类是可实例化的类,那么显然是个问题。 abstract 修饰符能让我们宣告自己的意图,而Dart 解析器也会根据情况做出相应的改变。

AbstractClassinstantiationError 错误

抽象类不是被用来实例化的,毕竟,它缺失部分实现。 对它进行实例化会导致运行时错误,具体来说,会产生一个名为AbstractClassinstantiationError 的错误。Dart 解析器也会 对此发出警告。

new Pair();
//静态警告:试图实例化一个抽象类
//抛出AbstractClassinstantiationError 错误

接口

每个类都隐含地定义了一个接口,此接口描述了类的实例拥有哪些方法。 很多编程语言都有正式的接口声明,但在Dart 中没有。这是不必要的,因为我们始终可以定义一个抽象类来描述所需的接口。


abstract class CartesianPoint {
get x;
get y;
}

abstract class PolarPoint { get rho; get theta; }


### implements
>尽管没有了接口声明,类也能使它的实例实现特定的接口:
```dart
class Point implements CartesianPoint, PolarPoint {
// 这里是Point 类的实现代码
}

以上Point 类不是CartesianPoint 的子类, 它没有继承CartesianPoint (或PolarPoint) 的任何成员。 implements 的目的是在接口间建立预期的关联,而不是共享实现。

在运行时检查对象是否符合某个接口是可行的:is

main() {
  5 is int; // true
  'x' is String; // true
  [] is Point; // false
  aPoint.toString() is String; // true
  new Point(0, 0) is String; // false
  aPoint is CartesianPoint; // true
}

请注意, is 不检查对象是否为某个类或其子类的实例。 相反, is 检查对象的类是否明确地实现了某个接口(直接或间接)。 换句话说,我们并不关心对象是如何实现的,我们只在意它支持哪些接口。 这是与其他有类似构造的语言的关键区别。 如果一个类希望模拟另一个类的接口,则它并不局限千已有的实现。这样的模拟对客户端而言应该是不可区分 的(除非使用反射)

接口的继承类似于类

类的隐含接口会继承父类的隐含接口,同时会继承父类实现的接口。 同类一样,接口可以重写父接口的实例方法; 另外,某些重写可能是非法的, 例如重写方法与被重写方法的参数不一致,或者试图用普通方法重写getter 或setter, 反之亦然。

一个接口有多个父接口,不同的父接口之间可能会产生冲突

假设一个同名的方法在多个父接口中出现,而且它们的参数不一致,则在这种情况下,互相冲突的方法没有一个会被继承 如果一个父类定义了一个getter,而另一个父类也定义了同名的普通方法,那么结果也是一样的。 这些情况也会引起各种警告。


对象的创建

Dart 中的计算都是围绕对象展开的。因为Dart 是纯面向对象的语言,所以即使是最微不足道的Dart 程序也会涉及对象的创建。 举个例子,一个字符串对象的创建。

某些对象,比如字符串'Hello World' 、布尔量true 或数字91 都是字面量。

构造函数

大多数对象都是由实例创建表达式创建的 比如new Point(O, 1)。 这种表达式调用了一个构造函数,这里是Point()。 每个类至少有一个构造函数,构造函数的名称总是从我们想创建实例的类名开始的。

构造函数可以由开发者明确地声明,或者也可以隐含地产生。 在没有明确的构造函数被声明时,隐含的构造函数将被创建,它们没有参数和函数体。例如:

class Box {
  var contents;
}

等同于:

class Box {
var contents;
Box();
}

以上类又等同千:

class Box {
var contents;
Box() {}
}

最简单的一个构造函数:

Point(a, b) {
    x = a;
    y = b;
  }

评估一个实例创建表达式的第1·步是评估构造函数的参数。 在new Point(0,1)中,参数是字面量的整数0 和1 。 同其他函数调用一样,形式参数被设置为实际参数的值,所以a设置为0 且b 设置为1 。 现在我们能够分配一个新的Point 类的实例,它存储了两个字段x和y。 起初,这两个字段将被系统设置为null, 这确保了用户的代码永远不会遇到未初始化的内存。 现在可以执行构造函数的函数体了,其中涉及两个变量即x 和y 的赋值操作。 这些赋值操作其实是调用setter 方法。 字段x 和y 被赋值为0 和1 ,其实是隐含定义的setter 执行的。 此时,构造函数返回一个新创建对象的引用,这也是new 表达式的最终结果。

super

考虑一个类,它代表三维空间的点。这个类有x 、y 和z 三个坐标,并且很自然地被声明为Point 类的子类。


class Point {
final x, y;
Point(this.x, this.y);
}

class Point3D extends Point { var z; Point3D(a, b, c) : super(a, b) { z = c; } }

>一个Point3D 的实例可以通过new Point3D(1, 2, 3)来创建。

>Point3D 类有三个字段: 
>>Point 类继承的字段x 和y, 在Point3D 中声明的字段z
>所有这三个字段将再次被设置为null 。
>但是,在执行Point3D 的构造函数的函数体之前,我们将不得不执行Point 的构造函数的函数体,否则字段x 和y 将不会被正确初始化。

>一般来说, Dart 编译器不能确定应该传递怎样的参数给父构造函数
>最简单的情况下(父构造函数没有参数),我们需要调用一个明确的父构造函数来指引我们,在上面的例子中就是super(a, b) 。
>在父构造函数调用中, super 代表了父类的名称,也就是Point3D(x1, y1, z1)在执行自身的构造函数的函数体之前调用了Point(x1, y1)的构造函数的函数体。

#### 初始化列表
>其目的是在普通代码运行前对实例变量进行初始化。
>假定点是不可变的,但会使用极坐标的表示方式。
>它执行了两个初始化操作:一个是rho, 一个是theta。
```dart
import 'dart:math';

class Point {
  final rho, theta;
  Point(a, b)
      : rho = sqrt(a * a + b * b), //初始化列表
        theta = atan(a / b); //初始化列表
  get x => rho * cos(theta);
  get y => rho * sin(theta);
  scale(factor) => new Point(rho * factor, theta);
  operator +(p) => new Point(x + p.x, y + p.y);
  static distance(p1, p2) {
    var dx = p1.x - p2.x;
    var dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
  }
}

这里的初始化列表是从Point(a, b)后的冒号开始直到该行结尾的分号: rho= sqrt(a * a + b * b), theta= atan(a/b)

初始化操作用逗号分隔,并从左到右执行。 除了初始化实例变量外,初始化列表也可以包含一个父构造函数的调用。在Point3D()中看过了这一点,即它使用了这个初始化列表: super(a,b)。 如果初始化列表中没有调用父构造函数,那么一个隐含的父构造函数super()将会被添加到初始化列表的尾部。

初始化实例变量的方式

在实例变量声明时进行初始化;
class Point {
  var x = 0, y = 0;
}
使用构造函数初始化方式:
class Point {
  var x, y;
  Point(this.x, this.y);
}
通过初始化列表:
class Point {
  var x, y;
  Point(a, b)
      : x = a,
        y = b;
}
在构造函数中初始化:

该方式不适用千final 实例变量,因为它使用了final 变量所没有的setter 方法。final 实例变量只能初始化一次

class Point {
var x, y;
Point(a, b) {
x = a;
y = b;
}
}

对于一个普通的实例变量,可以选择以上任意一种或多种方式,或者都不使用 当对象被实例化时,各种初始化构建操作将按照上面列出的顺序执行。

对象的创建过程

假设Point3D 还是按照前面的定义,而Point 类有如下构造函数:

Point(a, b)
: x = a,
y = b;

class Point {
var x, y;
Point(a, b)
: x = a,
y = b;
}

class Point3D extends Point { var z; Point3D(a, b, c) : super(a, b) { z = c; } }

>则这个实例的创建过程如下图所示,执行顺序依照箭头的方向:
![image](https://user-images.githubusercontent.com/30850497/122889509-70adba00-d375-11eb-971f-48ec74ba475f.png)

##### 左侧
>通过new Point3D(7, 8, 9)创建一个Point3D 的实例,从计算实际参数开始,这里的参数是7 、8 和9, 然后构造函数Point3D()被调用。
>下一步是分配一个新的Point3D 实例。所有实例变量被设置为null, 
>然后,继续执行Point3D 的初始化列表。这导致父类执行初始化,进而导致Point 的初始化列表开始执行,并将使实例变量x 和y 的值被设置,
>然后执行被隐含添加在Point 类初始化列表尾部的父类初始化操作。这将调用Object 的初始化列表,它什么也没有做(它最后甚至没有调用父类进行初始化)。
>所有这些步骤都显示在上图的左侧。

##### 右侧
>在遍历执行完所有父类链的初始化列表后,下一步就是执行构造函数的函数体。
>在该图中,这对应箭头的掉头。
>一个构造函数的函数体在开始前总是隐含地运行父类构造函数的函数体。
>传递给父构造函数的参数跟初始化列表中调用父构造函数的参数相同,它们不会重新计算。
>因此,在我们的例子中,我们开始运行Point3D(),它又开始运行Point(),进而导致运行什么都没有做的Object()。
>因为Point()没有函数体,所以我们返回到Point3D()并初始化变量Z, 
>完成后我们返回新分配的对象。
>以上处理过程对应上图的右侧。

### 重定向构造函数
>Point()现在是一个重定向构造函数。
>重定向构造函数的目的是把执行重定向到另一个构造函数,在这里是Point.polar()。
>在重定向构造函数中,参数列表跟在一个冒号后面,并以this.id(..)的形式指定重定向到哪个构造函数。
```dart
import 'dart:math';

class Point {
  var rho, theta;
  Point.polar(this.rho, this.theta);
  Point(a, b) : this.polar(sqrt(a * a + b * b), atan(a / b));

  get x => rho * cos(theta);
  set x(newX) {
    rho = sqrt(newX * newX + y * y);
    theta = acos(newX / rho);
  }

  set y(newY) {
    rho = sqrt(x * x + newY * newY);
    theta = asin(newY / rho);
  }

  get y => rho * sin(theta);

  scale(factor) => new Point.polar(rho * factor, theta);

  operator +(p) => new Point(x + p.x, y + p.y);

  static distance(pl, p2) {
    var dx = pl.x - p2.x;
    var dy = pl.y - p2.y;
    return sqrt(dx * dx + dy * dy);
  }
}

工厂构造函数 factory

缓存需求

假设想要避免分配过多的点。我们想保留点的一份缓存,而不是每次请求都生成一个新的点。 当有人尝试分配一个点时,我们就检查缓存中是否存在相等的点,如果有,就返回那一个点。 一般来说,构造函数使上述设想比较难以实现 在大多数编程语言中,构造函数总是会分配一份新的实例。 如果你想使用缓存,那么你必须提前考虑好,并确保你的点是通过一个方法调用来分配的,而这个方法通常叫作工厂方法。

工厂方法

在Dart 中,任意构造函数都可以被替换为工厂方法,并且对客户是完全透明的。我们通过工厂构造函数来做到这一点。

工厂构造函数由factory 前缀开头。 看起来像普通的构造函数,但可能没有初始化列表或初始化形式参数。 相反,它们必须有一个返回一个对象的函数体。 工厂构造函数可以从缓存中返回对象,或选择分配一个新的实例。 它甚至可以创建一个不同类的实例(或者从缓存或其他数据结构中查找它们)。

只要生成的对象符合当前类的接口,则一切都会按预期执行。

通过这种方式, Dart 解决了传统构造函数的一些典型缺点。


noSuchMethod()

Dart 中的计算都是围绕对象方法的调用。如果调用了一个不存在的方法,则默认的行为是抛出NoSuchMetbodError 错误。但是,并非总是如此。 当调用一个实例中不存在的方法时, Dart 运行时会调用当前对象的noSuchMethod()方法。 因为Object 类noSuchMethod()方法的实现就是抛出NoSuchMethodError 错误,所以我们通常都会看到这个熟悉的行为。 这个方案的优点在于noSuchMethod()能够被重写。 例如,如果你要实现另一个对象的代理,那么你可以定义代理的noSuchMethod()方法,并把所有的调用都转发给代理的目标。

class Proxy {
final forwardee;
Proxy(this.forwardee);
noSuchMethod(inv) { return runMethod(forwardee, inv);}
}

noSuchMethod()的参数是Invocation 类的一个实例,它是定义在核心库中的一种特殊类型,用千描述方法的调用。 一个Invocation 反映了原始的调用,描述了我们试图调用方法的名称、传递的参数及其他一些细节。 为了真正把每个调用转发给forwardee, 我们在noSuchMethod()的实现中使用了一个辅助函数runMethod(),它接收一个接收对象和一个invocation, 并使用提供的参数调用接收对象上对应的方法。 在讨论反射的时候,会介绍如何实现runMethod()方法。 健壮的代理实现比上面的代码要复杂一些的。一个细微之处是Proxy 不会转发在Object类中定义的方法,因为这些方法是继承的,并不会导致noSuchMethod()被调用。Object 的接口被设计得很小,可以手动拦截处理。 即使有这些复杂性, Proxy 的完整实现还是很简单的。 编写忽略目标类型的通用代理的能力,是Dart 这种可选类型语言的灵活性的完美体现。这种灵活性是强制类型的语言无法提供的。


常量对象与字段

有些对象是在编译时就可以计算的常量。 其中许多是显而易见的:

字面量,如3.14159; 字符串,如“Hello World" 等。

用户定义的常量对象

常量对象的创建是使用const 而不是new 。

同new 表达式一样, const 表达式也是调用构造函数,但该构造函数必须是常量构造函数,而且它的参数必须是常量。

Dart 要求常量构造函数的参数必须是数字、布尔量或者字符串。 幸好,数字字面量如0 等始终是常量。

一个常量构造函数不能有函数体。它可以有一个初始化列表,前提是只计算常量(假设参数是已知的常量)。

并不是总需要创建常量。仍然可以使用new 调用常量构造函数。如果那样做,则我们传递的参数不再受限制,但结果不再是常量。

常量的值可以提前计算,只需一次,无须重新计算。Dart 程序中的常量是规范化的,一个给定的值只会产生一份常散。

把点变成常量

class Point {
  final x, y;
  const Point(this.x, this.y);
}

const origin = const Point(0, 0);

在适当的条件下,我们可以生成一个代表原点的Point 常量对象。 origin 变量被声明为常量。我们只能把一个常量赋给它。

将Point 的构造函数声明为常量。这会给类及构造函数强加一些非常严格的限制。我们需要一个状态不可变的类。 幸运的是,不可变的点是非常自然的。

不能将类似Point.polar()的构造函数定义为常量,因为它使用了像sqrt()这样的结果不是常量的函数。Point 有这样一个构造函数是没有问题的,只是不能用const 调用它而已。因此,任意使用这个构造函数创建的点都不是常量。


类方法

类方法是不依赖于个体实例的方法 通过类变量而引入的accessor 都是类方法,我们可以称它们为类getter 和setter

除了自动引入的类accessor, 开发者也能显式地定义这些accessor。

把普通方法定义为类方法也是可以的。 例:在Point 类中有:

static distance(pl, p2) {
var dx = pl.x - p2.x;
var dy = pl.y - p2.y;
return sqrt(dx * dx + dy * dy);
}

就像类变量accessor 一样,用户定义的类方法在声明它们的类中是可用的, 在类外只能将它们所在的类作为前缀才能访问,例如Point.distance(aPoint, anotberPoint)

在类方法中使用this 将导致编译错误。 因为一个类方法不特定于任意实例,所以this在其内部是未定义的。

如果你尝试调用一个不存在的类方法,那么你会得到一个运行时错误:

Point.distant(aPoint, anotherPoint); // NoSuchMethodError!

因为这是一个静态调用,所以Dart 编译器在编译时能检测到Point 没有distant()方法

类方法永远不会被继承

因为类方法永远不会被继承,所以声明一个抽象的类方法就没有意义了。 如果尝试,则你将得不到任何进展,因为这在语法上是非法的。

class ExtendedPoint extends Point {
var origin = new Point(0,0);
get distanceFromOrigin => distance(origin, this);
// 对不起, NoSuchMethodError!
}

getter 方法distanceFromOrigin 不能使用distance 方法。 我们本可以把distanceFromOrigin定义在Point 中(假设我们同时定义了origin) ,但是小stance 方法在Point 的子类中都不可 见。

为了使这段代码工作,必须这样写:

get distanceFromOrigin => Point.distance(origin, this); // ok

实例及其类与元类

每个对象都是一个类的实例。 类也是对象 类是对象,它们本身也是某个类的实例。类的类通常被称为元类。

Dart 语言指定类的类型为Type,但没有指明它们属于哪个类。

在一个典型的实现方式中,类可能是一个私有类_Type 的实例。 通常来说_Type 自身的类就是_Type, 也就是说,_Type 是自身的一个实例。 这解决了对类型可能进行的无穷无尽的追溯。

对象aC 是类C 的一个实例,类C 又是_Type 的一个实例,_Type 是它自身的一个实例 把Type 用下画线开头,表明它是一个私有类 image

反射是唯一可靠的发现对象所属类的方式

对象都支待一个名为runtimeType 的getter, 它默认返回对象的所属类。但是,子类可以随意重写runtimeType 。

类方法调用很容易与类型字面量的实例方法混淆:

  Type.runtimeType; // NoSuchMethodError
  (Type).runtimeType; //正常工作

第1 行调用了一个不存在的类方法,类Type 没有名为runtimeType 的类方法。

第2 行调用了类型字面量Type 的所有对象中都定义了的runtimeType 方法。

假如我们把类方法看作类对应Type 对象的实例方法,则可以解决这个问题,但此时的情况不是这样的。


Object 与其方法

Object 类的接口

所有对象共享的接口只由5 个方法组成

操作符方法==

getter 方法

hashCode

runtime Type

toString()

toString(),这个方法返回一个对象的字符串表示。它的默认版本通常会打印出类似“An Instance of C "的字符串,其中C 是对象的类名。我们通常都会重写这个方法以使它更有意义。

Object 类的轮廓类似于这样:

class Object {
bool operator ==(other) => identical(this, other);
external int get hashCode;
external String toString();
external noSuchMethod(Invocation im);
external Type get runtimeType;
}

除操作符方法==之外的所有方法都由Dart 底层实现,并不直接使用Dart 代码实现。

它们被标记为external, 表明它们的实现是在其他地方。

external机制用于声明代码的实现来自于外部。 这些外部代码可以有多种提供方式:

通过作为底层实现基础的外部函数接口(这里就是这样), 或者甚至可能动态地生成实现。

对大多数开发者而言,第1 种情况是最有可能的。


mixin

单继承有很大的局限性

每个Dart 类都有一个mixin。 这个mixin 是在类主体中定义的特定功能

可以把 mixin 看作一个函数,它接收一个父类S 并返回一个新的拥有特定主体的 S子类

//非法代码:只是作为演示
mixinCollection (S) {
return class Collection extends S {
forEach (f);
where (f);
}}

每次用某个特定的类调用这个函数,都会产生一个新的子类。 因此,我们把mixin 应用看作用一个mixin 与一个父类来派生一个新类。 我们把M 与父类S 的mixin 操作写成S with M S 必须指定一个类 如何指定mixin? 通过指定一个类,每个类都通过它的主体隐含定义了一个mixin, 就是使用它来对S 执行mixin 操作。

class CompoundWidget extends Widget with Collection{
//
}

作为mixin 的类不能有显式声明的构造函数,违反这个限制将会导致编译错误。此限制在未来可能会放松。

mixin 例子:表达式问题

考虑一个被称为表达式问题的经典设计挑战,同时看一下mixin 是如何帮助我们完成一个优雅的解决方案的

表达式语言:

Expression - Expression + Expression | Expression - ExpressionlNumber

这个语言可能有如下形式的AST (译注:抽象语法树)


class Expression {}

class Addition extends Expression { var operandl, operand2; }

class Subtraction extends Expression { var operandl, operand2; }

class Number extends Expression { int val; }


#### 假设你要为这个语言定义一个求值器,
>可以依照经典的面向对象风格来定义
>即给以上每个子类添加一个eval 方法:
```dart
get eval => operandl.eval + operand2.eval; //在Addi巨on 类中定义
get eval => operandl.eval - operand2.eval; //在Subtraction 类中定义
get eval => val; //在Number 类中定义

问题

以上实现方式是有问题的。 当你想把这些表达式转换为字符串时,你就需要添加另一个方法到原先的层次结构中。 类似功能的函数可能有无数个,你的类很快就会变得难以维护与使用。

还有一个问题是,并不是所有想添加新功能的人都可以访问到原始源代码。

另一种实现方式是把求值器定义为AST 类外部的一个函数。 但这样的话,你不得不对传递过来的表达式进行类型检查,然后执行相应的动作。 这样做烦琐且效率低下, 所以一般会使用访问者模式来替代。 无论如何,这样的代码结构有个双重问题:尽管添加功能很容易,但是添加新类型很难。

下面的表格展示了目前的困难局面。

Class\Function eval toString()
Addition operandl.eval + operand2.eval '$operand! + $operand2'
Subtraction operandl.eval - operand2.eval '$operand! - $operand2'
Number val '$val'
Multiplication operandl.eval * operand2 '$operandl * $operand2'

类对应表格中的行,功能对应列。 面向对象的风格可以很容易地添加行,添加列却是侵入式的。 函数式风格的解决方案则正好相反:添加列很容易,添加行却是侵入式的。

我们真正需要的是一种把各个条目独立地填入表格的方法。使用mixin 可以很好地解决这个问题

使用mixin

定义类型的结构及对应的构造函数。

从三个初始数据类型开始,只是不把它们定义为抽象类,因为它们并不是我们最终要实例化的数据类型。 abstractExpressions.dart


library abstract_expressions;

abstract class AbstractExpression {}

abstract class AbstractAddition { var operandl, operand2; AbstractAddition(this.operandl, this.operand2); }

abstract class AbstractSubtraction { var operandl, operand2; AbstractSubtraction(this.operandl, this.operand2); }

abstract class AbstractNumber { var val; AbstractNumber(this.val); }


##### 定义第1 个功能:求值器
>通过一组mixin 类来做到这一点。
>求值器完全独立于类型层次结构,没有导入哪怕一个依赖。
>evaluator.dart
```dart
library evaluator;

abstract class ExpressionWithEval {
  get eval;
}

abstract class AdditionWithEval {
  get operandl;
  get operand2;
  get eval => operandl.eval + operand2.eval;
}

abstract class SubtractionWithEval {
  get operandl;
  get operand2;
  get eval => operandl.eval - operand2.eval;
}

abstract class NumberWithEval {
  get val;
  get eval => val;
}
客户端应该使用的实际类型是单独定义的:

每个具体的AST 类型都被定义成一个mixin 应用,即用相应的求值器mixin 来扩展对应的抽象数据类型。 expressions.dart


library expressions;

import 'abstractExpressions.dart'; import 'evaluator.dart';

abstract class Expression = AbstractExpression with ExpressionWithEval;

class Addition = AbstractAddition with AdditionWithEval implements Expression;

class Subtraction = AbstractSubtraction with SubtractionWithEval implements Expression;

class Number = AbstractNumber with NumberWithEval implements Expression;


##### 一个简单的表达式树
>可以给 expressions 库添加一个main()函数,用来构建一个简单的表达式树。
>这是可能的,因为各个AST 节点类的父类构造函数都隐含地为它们定义了合成的构造函数。
```dart
main() {
  var e = new Addition(new Addition(new Number(4), new Number(2)),
      new Subtraction(new Number(10), new Number(7)));
}
为什么将抽象类型和实际类型分离?

expressions 库即实际类型的作用是通过mixin 应用连接各个组件来定义我们的整个系统。

abstractExpressions 库即抽象类型的作用是定义我们的AST 节点的形式。

保持它们的独立,使我们在扩展系统时只需对expressions 库做修改,无须触碰我们的数据类型的表现形式。

一般的模式是,每个具体类都基于扩展一个定义其数据表示的抽象类, 同时用一系列的mixin 来代表该数据类型所具备的功能。

这种方法之所以有效,是因为我们为每个类型和功能的组合单独定义了一个mixin 。 例如,上面的每个eval 方法都是在各自的mixin 类中定义的。

增加一种类型

如果我们想增加一种类型,那么我们可以独立地添加。 下面我们将增加乘法的 AST 节点: 这个添加操作同样完全独立千原先的类层次结构和已有的功能。 multiplication.dart

library multiplication;

abstract class AbstractMultiplication { var operand1, operand2; AbstractMultiplication(this.operand1, this.operand2); }


>定义乘法是如何求值的。
>我们可以单独定义一个库:
>multiplicationEvaluator.dart
```dart
library multiplication_evaluator;

abstract class MultiplicationWithEval {
  get operand1;
  get operand2;
  get eval => operand1.eval * operand2.eval;
}

在expressions 库中创建相应的具体类。 它遵循与其他所有类型一样的模式


import 'multiplication.dart';
import 'multiplicationEvaluator.dart';

class Multiplication = AbstractMultiplication with MultiplicationWithEval implements Expression;


##### 将我们在main()方法中构建的树打印出来
>可以通过修改main()来做到这一点,
>也可以通过修改我们的代码以使用新添加的类型。
```dart
main() {
  var e = new Addition(new Addition(new Number(4), new Number(2)),
      new Subtraction(new Number(10), new Number(7)));

  print('$e = ${e.eval}'); // Instance of 'Addition' = 9
}

所打印内容的信息量比我们想象的要少,因为e 的打印使用的是从Object 继承来的默认toStringQ实现。 为了解决这个问题,我们可以把一个专门的toString()实现添加到我们的类层次结构中。

string_converter.dart


library string_converter;

abstract class ExpressionWithStringConversion { toString(); }

abstract class AdditionWithStringConversion { get operand1; get operand2; toString() => '($operand1 + $operand2)'; }

abstract class SubtractionWithStringConversion { get operand1; get operand2; toString() => '($operand1 - $operand2)'; }

abstract class NumberWithStringConversion { get val; toString() => '$val'; }

abstract class MultiplicationWithStringConversion { get operand1; get operand2; toString() => '($operand1 * $operand2)'; }

>再次,我们按照每一个功能、类型的组合定义一种mixin 的方式

>要改进expressions 库来整合新的功能
>expressions.dart
```dart
library expressions;

import 'abstractExpressions.dart';
import 'evaluator.dart';
import 'multiplication.dart';
import 'multiplicationEvaluator.dart';
import 'stringConverter.dart';

abstract class Expression = AbstractExpression
    with ExpressionWithEval, ExpressionWithStringConversion;

class Addition = AbstractAddition
    with AdditionWithEval, AdditionWithStringConversion
    implements Expression;

class Subtraction = AbstractSubtraction
    with SubtractionWithEval, SubtractionWithStringConversion
    implements Expression;

class Number = AbstractNumber
    with NumberWithEval, NumberWithStringConversion
    implements Expression;

class Multiplication = AbstractMultiplication
    with MultiplicationWithEval, MultiplicationWithStringConversion
    implements Expression;

main() {
  var e = new Addition(new Addition(new Number(4), new Number(2)),
      new Subtraction(new Number(10), new Number(7)));

  print('$e = ${e.eval}'); // ((4 + 2) + (10 - 7)) = 9

}

我们的main 函数仍然不变,但是它会打印一个描述更好的树:((4 + 2) + (10 - 7)) = 9

我们可以根据需要把扩展过程继续下去。 只要你想,你就可以添加多种类型、功能,只要像上面一样,把要使用的类型的最终形式定义成mixin 应用。 每个类型所对应的每个功能,都通过一个独立的mixin 类来定义。 添加新功能确实需要修改这些mixin 应用,但是这看起来更像调整你的make 文件以包括新加的功能或类型(译注: make 是C 语言开发中常用的一种构建工具)。 如果新类型和功能都是独立定义的,那么我们始终可以定义一个mixin 把新功能独立地添加到新类型上,并让它们良好地混合在一起。 从前面那个表格来看,每个mixin 表示一个单独的条目,而每个mixin 应用构成该表的一行。


相关语言

Dart 的对象模型在很大程度上受到了Smalltalk的影响。 Smalltallc 是第1 个纯面向对象的编程语言,这里的许多想法都跟它直接相关。 虽然Dart的元类层次结构比Smalltalk 更简单,但最终指向自己的元类层次结构的概念是始于Smalltalk 的。 与Smalltallc 不同, Dart 的实例由构造函数而不是方法进行创建,遵循的是C++所建立的传统。 但是,我们解决了经典构造函数的主要缺陷,即不能生产不是由类新分配的对象。

noSuchMethod()的使用是以Smalltalk 的doesNotUnderstand 为蓝本的。

表征独立的概念可以追溯到Self 语言。然而, Dart 并不坚待统一引用的原则,因为它还受到了JavaScript 和C 风格语法的影响。

Mixin 起源于某种Lisp 方言的惯用语法。这里使用的mixin 语言模型是由William Cook和吉拉德·布拉查于1990 年提出,并在Strongtalk 中首次实施的。 Strongtalk 是一个创新的Smalltalk 系统,它对Dart 有非常大的影响。 类似的结构也存在于其他语言中,例如Newspeak 、Scala (它们被称为trait) 和Ruby 等。

”表达式问题”是因Philip Wadler 而得名的。


总结

Dart 是一种纯面向对象的基于类的编程语言,这意味着所有运行时值都是对象,且每个对象都是某个类的实例。

对象有状态和行为。状态只能通过特殊的accessor 方法访问: getter 跟setter。 这确保了Dart 对象上的所有计算都是通过程序接口完成的。

类在运行时才具体化,对象本身也必须如此。因此,每个类都是类型为Type 的元类的一个实例。

每个类都至少有一个构造函数,构造函数用于创建对象。

某些对象是常量,这意味着它们在编译时就可以预先计算。

每个Dart 类都有唯一的父类,除了类层次结构的根,即Object。所有Dart 对象都从Object 继承了共同的行为。

Dart 支持基于mixin 的继承:每个类都引入了一个mixin, 它捕获了类本身对类层次结构所做的独特贡献。mixin 使类的代码以模块化方式重用,而不依赖于它在类层次结构中的位置。

WangShuXian6 commented 3 years ago

Dart 程序是由被称为库的模块化单元组成的

一个“Hello World" 程序。虽然它可能是最简单的,但它终究是一个库。

main() {
print('hello world');
}

与我们在points 库中看到的不一样,这里没有一个显示的库声明。 大多数库都有这样的一个声明

对于快速、简单的任务,能够只写一个函数并运行是非常方便的

顶层

在上面的例子中,库由一个顶层函数main()构成。 通常来说,一个库由多个顶层声明组成,这些声明可能定义了函数、变量及类型。

-个实现了栈的库

library stack1;

final _contents = [];
get isEmpty => _contents.isEmpty;

get top => isEmpty ? throw 'Cannot get top of empty stack' : _contents.last;
get pop => isEmpty ? throw 'Cannot pop empty stack' : _contents.removeLast();
push(e) {
  _contents.add(e);
  return e;
}

我们有一个顶层变量_contents, 它被初始化为一个空列表。 与实例变量和类变量一样,顶层变量引入了隐含的accessor。 同样,用户的代码不会直接访问变量。 顶层变量是延迟初始化的,与类变量一样,在它们的getter 第1 次被调用时才执行初始化。 在stackl1中,_contents在某个访问它的方法被调用时才被设置为[]。

顶层变量和类变量一起被称为静态变量。它们的区别在于作用域,即在什么范围内能够通过名称对它们进行访问。 类变量的作用域被限制在声明它们的类中(甚至子类也无法访问它们) 顶层变量(也被称为库变量)的作用域覆盖了声明它们的整个库。 库作用域通常由多个类与函数构成。

与类变量一样,顶层变量也可以声明为final, 在这种情况下,它们没有定义setter 且必须在声明时就初始化。 也可以把静态变量(可以是类或库变量)声明为常量,那样的话,它们只能被赋予一个编译时常量且自身被视为不可变。

顶层函数(常被称为库方法)的作用域规则与顶层变量一样,在整个库中都是可用的,它可以是普通函数、getter 和setter。 在各种情况下,函数的函数体都可以使用=>加一个表达式的简写方式(如例子中的isEmpty 、top 、pop) ,或者使用由大括号包裹一系列语句(如例子中的push()) 。

除了顶层函数与变量,我们也可以声明顶层类。 在Dart 中类声明都是顶层的,因为Dart 不支待嵌套类。


脚本

"Hello World" 程序同时是一个脚本示例,是一个可以直接执行的Dart 库。 脚本从main()函数开始执行。 如果一个库没有main()函数,那么根据定义,它就不是一个脚本,自身也不能够被执行。

脚本还有另一个特性。脚本的第1 行可以是以字符开头的一行纯文本。 在某些环境下,这使得各种解释器能够根据文件头部以开头的指令来运行这个脚本。


隐私

库是Dart 的基础封装单元。以下画线_开头的成员都是库私有的。 我们在上面见过一个_contents 库变量。使_contents 私有有助于维护stack1 所引入堆栈抽象的完整性。 因为只有在stack1 内部才能访问_contents, 所以我们确信没有其他代码会改动堆栈的底层实现。

CachingClass 所在库之外的代码都无法访问_cache 字段。

class CachingClass {
var _cache;
operator [](i) {
if (_cache[i] == null) {
_cache[i] = complicatedFunction(i);
}
return _cache[i];
}
}

这种方案使你(和编译器及其他工具)不必查看某个变量的声明就可以识别它是否私有。

隐私不应与安全混淆。 Dart 的隐私是为了支待软件工程的需要,而不是安全需求。 唯-安全的边界在isolate 之间,在同一个isolate 中,代码的安全性并没有得到保障。


导入

例如,我们想在一个应用中使用 stack1, 可能会这样写:


import 'stack1.dart';

main() { push('gently'); push('harder'); print(pop); print(top); }

>这使得我们的main()函数能够访问stack1 的push()和pop 。
>这个脚本将先打印出harder,然后打印出gently。
>这段代码能够工作的前提是,将库stack1 保存到一个名为 ```stack1.dart``` 的文件中,并与我们的脚本放在同一个文件夹下。
>但是,如果将stackl 保存在别处,例如```http://staxRUs/stackl.dart``` 呢?
>我们可以把导入语句改为:```import'http://staxRUs/stackl.dart';```

>Dart 的导入语句适用于任意URI (Universal Resource Indicators) 。
>尽管如此,我们仍不推荐使用上面的几种URI, 因为只要导入的库的位置发生变化,就会影响到你的代码。
>这些URI 适用千追求速度且不求完美的实验性任务
>而真正严谨的代码需要更多的规则。

### package:
>通常情况下,我们会这样写:```import 'package:stack1.dart';```
>这种```package:```的导入方式会执行一个常驻的封装了代码位置信息的包管理器。Dart 环境通常都自带包管理器

### dart:
>对于Dart 平台自身的库没有必要使用```package:```方式,这些库都是通过```dart:```来访问的,
>例如:```import 'dart:io';```
>其他的例子包括```dart:html, dart:json``` 等。

>无论你使用哪种方式, URI 最好都指向一个真正的库,否则可能产生编译错误。
>该URI也必须是一个不可变的字符串字面量,并且没有使用字符串插值。

### 命名空间

#### 库内可用的对象
>库内可用的对象包含了库本身所声明的对象及通过导入语句从其他库导入的对象。
>```dart:core``` 中定义的对象是隐含导入的。

#### 命名空间:提供给客户的对象
>库内的可用对象与它提供给客户的对象是不同的。
>首先,库从外部导入的对象对库的用户是不可用的。
>此外,库的私有成员对库的调用者是不可用的。
>因此,通常也把库对外可用的对象称为库导出的命名空间。

#### 导入有冲突的变量
>stack2
>设想我们有另一个实现了栈的库stack2 。
>我们可以编写一些代码来测试这两种实现。
>我们可能会这样开始构造脚本:
```dart
import 'package:stack1.dart';
import 'package:stack2.dart';

main() {}

到现在为止都不错。你可以编译这段代码,没有任何问题。

但是,当我们尝试在main()方法中使用导入的代码时,我们会遇到麻烦。


import 'package:stack1.dart';
import 'package:stack2.dart';

main() { //测试stack1 push('gently'); //静态警告 push('harder'); //静态警告 print(pop); //静态警告 print(top); //静态警告

//测试stack2 push('gently');

///静态警告 push('harder'); //静态警告 print(pop); //静态警告 print(top); //静态警告 }

>你能说出使用stack1 与使用stack2 的代码的区别吗?当然不能,编译器也不能。
>stack2中的方法与stack1中的方法同名了,而把它们导入到同一个作用域造成了无法挽回的歧义。

>Dart 编译器对每个歧义对象的使用都会给出一个警告。
>导入有冲突的变量本身不会出现任何警告,只有在你尝试使用这些歧义对象时才出现警告,
>这与Dart 避免打扰开发者的理念是一致的。
>这也带来另一个好处,即当某个人在你导入的库中新增一个顶层变量时,你的代码不会被轻易破坏。

>如果我们忽略警告并尝试去运行该代码,那么第1 个pushO调用将会导致运行时错误。
>具体地说,是一个NoSuchMethodError 错误被抛出,因为push()没有被明确定义。

#### 为导入提供不同的前缀来进行区分
>克服以上歧义的一种好方法是,为这两个导入提供不同的前缀来进行区分。
```dart
//import 'package:stack1.dart' as stack1;
//import 'package:stack2.dart' as stack2;

import 'stack1.dart' as stack1;
import 'stack2.dart' as stack2;

main() {
  //测试stack1
  stack1.push('gently'); //静态警告
  stack1.push('harder'); //静态警告
  print(stack1.pop); //静态警告
  print(stack1.top); //静态警告

//测试stack2
  stack2.push('gently');

  ///静态警告
  stack2.push('harder'); //静态警告
  print(stack2.pop); //静态警告
  print(stack2.top); //静态警告
}

Dart 库内声明的对象优先级高于任何导入的对象

导入前缀不得与本库内其他顶层变量声明发生冲突。 编译器会将这些冲突标记为错误。 因为本地变量的优先级高千所有导入的对象,所以导入前缀会稷盖任意不是通过前缀导入所导入的同名变量。

对多个导入使用同样的前缀是允许的。 如果导入语句引入的前缀名称发生冲突,那么在前面看到的变量名冲突规则同样适用千此:只有真正使用了名称冲突的对象时才会导致在编译时产生警告,在执行时也因为报出NoSuchMethod 错误而导致运行失败。

仅仅在指向不同的对象声明时, Dart 才会认为变量冲突。 如果从同一个库中导入两次push(),那么在编译与运行时都不会有问题。

命名空间组合器show 和hide

命名空间是名称到声明的映射。 例如,考虑stackl 所声明的命名空间,它包含了_contents 、isEmpty 、top 、pop 和push。 当我们导入stack1 时,因为_contents 是私有的,所以我们看不到它,它不在stack1所导出的命名空间中。 在通常情况下,导入语句将库导出的命名空间提供给导入者访问。 导入前缀和命名空间组合器使我们能够操纵被导入的命名空间。

hide 组合器

hide 组合器接收一个命名空间和一个标识符列表,并将标识符列表中的对象从命名空间中丢弃,然后产生一个新的命名空间。

如果我们在一个库中使用以下导入语句:

library lib1;
import 'stack1.dart' hide isEmpty, top;

那么只有pop 和push 在lib1中可用,因为这里导入的命名空间不再是stack1 完整导出的命名空间。 hide 操作符被应用到导出的命名空间中,将isEmpty 和top 移除了。 实际提供给导入者作用域的是hide 操作符应用之后的结果。

show 组合器

只有在标识符列表中出现的对象会被保留在命名空间中。 可以使用show 来获得与前面例子同样的效果:

library lib1;
import 'stack1.dart' show pop, push;

何时使用show, hide

如果你要导入一个大型的库,而你只想使用其中的少数成员,那么你会发现show 更方便。

相反,如果你试图解决库之间的一两个冲突,那么你可能选择使用hide, 但更好的方式是通过as 引入前缀来避免冲突。

尽管如此,我们将看到hide 的更多用途。

如果真的想使自己的库健壮,那么你应该组合使用前缀和show, 只导入你实际使用的元素:

library lib1;
impert 'stack1.dart' as stack1 show pop, push;

通过这种方式,你只要看一眼库的顶部就知道你所依赖的成员。 大多数开发者可能觉得这种约束有一些乏味,但好的开发工具应该能自动为你维护这些导入。

有人可能会觉得通过前缀访问会比较麻烦。 一种替代方案是坚持使用show, 而不使用前缀。 使用show 将防止导入的新成员有意外冲突。 然而采用这种方式时,可能还是需要解决不同导入的成员之间的冲突,并提防导入成员和继承成员之间的冲突。


将库拆分成part

有时,一个库可能太大,不能方便地保存在一个文件中。 Dart 允许你把库拆分成较小的被称为part 的组件

假设有一个库实现了一个Dart 的集成开发环境(IDE)

这个 IDE 包含了类浏览器、对象观察器、调试器及对单元测试、包管理和版本控制的集成支持。 这是一个大程序,我们绝对不想用一个庞大的文件来保存这一切。 同时,因为紧密集成,我们可能希望IDE 是一个库,其自身的私有状态在各子系统间共享。 我们可能有如下结构:


library ide;

import 'dart:io' as io; import 'dart:rnirrors' as mirrors; import 'dart:async' as async; //还有更多:如UI 等. part 'browsing.dart'; part 'inspecting.dart'; part 'debugging.dart'; part 'unitTestintegra巨on.dart'; part 'packages.dart'; part 'vcs.dart';

>每个子系统都存放在各自的文件中,而库通过使用part 指令来引用它们。
>每个 part 指令都给定了一个指向对应part 所在位置的URI。这些URI 与导入语句遵循同样的规则。
>所有part 都共享同一个作用域,即引用它们的库的内部命名空间,而且包含所有导入。

### part 是结构化的,每个part 都必须以一个part 头来指定它属于哪一个库。
>part 指令看起来可能类似C 语言```#include``` 指令,但情况并非如此。
>例如,'brwosing.dart'的开头可能是这样的:
```dart
part of ide;
//顶层声明
class ClassBrowser{

}

part 的头部使用库名称来指明它所在的库。 不是所有的库都有名称, 但如果使用part来构建库,那么库本身必须要命名。 各个part 应该有很好的结构,并且按照逻辑分组,而不是纯粹地堆积代码。

如果库通过URI 引用某个part, 而URI 的内容并不是一个part, 那么这就是一个编译错误, 而如果part 本身不指向同一个库,那么会产生一个警告。与导入一样(导出也是一样的)


导出

IDE 例子

集成开发环境不一定具有庞大的百万行级别的代码,但它可能会达到那样的规模。 我们的项目可能不断发展并超出我们最疯狂的期望, 而我们真的需要把它拆分成多个库。 同时,我们可能也想给第三方提供IDE 的API 。 当然,第三方可以根据需要导入IDE 的不同组件库。 但是,这些API 可能是庞大而复杂的,而我们可能希望提供一个更好管理的API 子集。 在我们避免让我们的用户导入大量的子库时,这种方式也很有用。 此外,我们也不想让我们项目的内部库暴露在外。

为一系列的库构建一个可管理的API


library ideAPI;

export 'browsing.dart' show AbstractBrowser, LibraryBrowser, ClassBrowser; export 'inspecting.dart' show Objectlnspector; export 'debugging.dart' show ThreadBrowser, ActivationBrowser; export 'unitTestintegration.dart' show TestBrowser, TestSuite; export 'packages.dart' show PackageBrowser; export 'vcs.dart' show RepositoryBrowser;


### 导出指令 export
>导出指令允许一个库使用来自其他命名空间的对象来扩充自己的导出命名空间。

>在ideAPI 中,库的全部用途就是以一种便于使用的方式来聚集和包装来自不同库的特性。
>它通过使用show 和hide 对几个库的导出命名空间进行了合理过滤,从而构建出一整套API 。
>虽然我们的例子只使用了show,但在我们导出一个大型的API 而又不想与其他导出的库发生冲突时, hide 就非常有用了。

>使用hide 有一个缺点:如果导出的库添加了新成员,则可能会发生冲突。
>hide 的优势是,在我们的客户需要访问我们导出的库的新成员时,我们不需要显式地更新我们的导出语句。

### 导出与导入完全独立
>ideAPI 没有任何导入。
>你可以导出一个库,即使你从没使用过这个库。
>正是因为如此,才使得ideAPI 这样的库能够聚合(拆分)多个API 。

### 导出的规则与导入、part 的规则相同
>导出使用的URI 必须指向一个库,而且必须是不可变的没有使用插值的字符串字面量。
>如果违反了这些要求,那么编译器会报错。
>如果一个库使用多个导出语句来导出同一个对象,那么也是一个编译错误。这种情况在本质上是含糊不清的,我们实际导出的应该是哪个实体呢?

***

## 钻石导入
>从多个聚合性API 导入时,可能会从不同的路径导入相同的对象。这种情况被称为钻石导入

***

## 延迟加载
>推迟库的加载的原因:

>>为了使应用快速启动且保待初始下载量尽可能小

>>在拥有诸多功能的大型应用中,因为某些特性不被所有用户使用,所以实现相应功能的库也不总是需要的。
>>不加载不会使用到的库有助千减少内存的使用。

### 延迟加载 deferred
```dart
import 'rarelyUsed.dart' deferred as rarelyUsed;

在开发时编译器仍然会把导入的对象引入到当前的作用域中, 但在运行时尝试访问这对象则会导致动态错误,除非它们被显示地加载。

延迟加载的导入必须提供前缀,且前缀不能被库中的其他导入使用。 违反这些规则将会导致编译错误。

显示地加载 loadLibrary()

当真正需要使用延迟加载的库时(例如,当用户选择了需要相应功能的菜单项时), 我们可以调用rarelyUsed 的loadLibrary()方法。

rarelyUsed.loadLibrary().then(onLoad);

onLoad 的定义如下:

onLoad(loadSucceeded) => loadSucceeded ? doStuff() : makeExcuses();

对loadLibrary()的调用是异步的

它启动了库的加载,但会立即返回而不等待库加载完成。 loadL如ary()的结果是一个future, 它是某个值的占位符且该值在一段时间后才可用。 future 支待一个then()方法,该方法的参数是一个用于接收值的回调函数。 当future 代表的值最终可用时,该回调函数将被调用,并传入真正的值。

loadLibrary()返回的future 代表一个表示加载是否成功的布尔量

在加载处理完成后(成功或失败), onLoad 函数将被调用。 它的参数loadSucceeded, 即上述布尔量,会告诉我们库是否真正被载入。 然后我们会选择合适的行动方针。

例如, doStufti()可能会这样写:

doStuff() { rarelyUsed.respondToMenuRequest();}

在确定库加载完成之前,我们要避免访问rarelyUsed。 如果在库加载完成之前就访问它,则我们会得到一个运行时错误。 可以选择只在doStufti()中引用rarelyUsed, 从而降低过早访问的风险。 在rarelyUsed 加载之前,我们仍然要面对doStuff()可能在其他地方被调用的风险。


相关语言

Dart 的导入是遵循传统的,除了依赖千URI 而不像大多数传统语言一样使用内置的标识符。 传统导入的优缺点都被带入到Dart 中。 例如,不可能将同一库的多个副本绑定到同一地址空间中的不同依赖项。 Dart 对isolate 的支持可能会缓解这一问题。 另一方面,这种导入形式是切合实际的,并被大部分开发者认可。

钻石导入与钻石继承间题是有关联的,但它们还是不太相同。

Dart 对隐私的处理方式是原创的。 它偏离了历史悠久的基于类的隐私模型, 例如在C++、Java 及C#中所见。 它也不同千Smalltalk 的基于对象的隐私模型,以及在CLU 、Ada 、Modula 和Oberon 等语言中的基于抽象数据类型(ADT) 的隐私模型。 这种方式是务实的。它很好地处理了多个类之间的交互,而不依赖千类型系统。 在缺少强制静态类型系统的情况下,基于类的封装需要昂贵的动态检测,因此动态类型的面向对象语言都倾向千使用基千对象的封装,或是把所有都设为公开。 前一种选择被认为是过于严格的一项规则,而后者则不适用于为严谨软件工程而设计的编程语言。 命名空间组合器show 和hide 的由来有着悠久的历史。具体例子包括模块组合器、trait的操作、Racket中对单元的操作及其他等。


总结

Dart 程序由库组成。库聚合类(类型)、函数和变量。 Dart 的库是针对隐私而不是安全性的封装单元。 Dart 程序的执行总是从脚本的main()函数开始的。 一个库能被拆分成多个part 。 Dart 库通过导入来接入它们自身的依赖,并能通过执行命名空间组合器来选择性地导入其他库的对象,也能够通过添加前缀来区分各个导入。 库通过命名空间组合器也能将其他库或自身的部分内容重新导出。 库可以延迟到运行时才加载,以改善启动时间及(或者)减少不必要的使用。

WangShuXian6 commented 3 years ago

函数

函数是Dart 的主力。 所有计算都由函数来执行。 我们将所有语法形式的函数与方法笼统地称为函数,不管它们的表现是否像严格意义上的函数。 函数是Dart 中的一等值,它们能被保存在变量中,能作为参数传递及作为函数的返回值。 与所有Dart 运行时的值一样,函数同样是对象。

多种函数:

顶层函数 实例与类方法(它们可能是getter、setter、运算符或普通方法) 构造函数。 本地函数和函数字面量。

它们都有着共同的结构


参数

函数总是有一个形式参数列表 虽然这个参数列表可能为空,并且getter 方法是没有参数列表的。

参数要么是位置型的,要么是命名型的。

位置参数

位置参数可以是必填的或可选的。

必填参数

下面是一些拥有必填参数的函数:


zero() => 0; //没有参数的函数;从技术角度上讲,它有0 个参数

get zero => 0; //上面函数的另一版本

id(x) => x; // identity 函数

identity(x) { return x; } //一个更烦琐的过entity 函数

add(a, b) => a + b; //有两个必填参数的函数


#### 可选参数
>可选参数必须排列在一起放置在参数列表尾部并用方括号包裹。
>任意必填参数都必须出现在可选参数前面。
>可选参数可以指定默认值但必须是编译时常量。
```dart
increment(x, [step = 1]) => x + step; // step 是可选的,默认值是1

以用一个或两个参数来调用increment()

main() {
increment(1); //值为2
increment(2, 4); //值为6
increment(); //运行时错误
increment(2, 3, 4); //运行时错误
}

命名参数

命名参数要在位置参数之后声明并用大括号包裹。

一个完全依赖千命名参数的例子:

addressLetter()可以使用任意命名参数的组合来调用,包括全部或没有参数的情况。


class Address {
var street, number, city, zip, country;
}

addressLetter({name: '', street: '', number, city, zip, country}) { var addr = new Address(); addr.street = street; addr.number = number; addr.city = city; addr.zip = zip; addr.country = country; return addr; }

main() { addressLetter(street: "Downing", number: 10); addressLetter(street: "Wall", city: "New York", country: "USA"); addressLetter(city: "Paris", country: "France"); addressLetter(name: "Alice", country: "Wonderland"); addressLetter(); addressLetter(name: "room", number: 101, country: "Oceania"); }


#### 必填参数与命名参数混合的情况:
```dart
var map = new Map();
fail() => throw ('Key not found');
lookup(key, {ifMissing: fail}) {
  var result = map[key];
  if (result == null) {
    return ifMissing();
  }
  return result;
}

通常,作为错误处理程序的回调函数会通过命名参数来指定,其他形式参数则通过位置参数给出。

lookup("anything"); //抛出错误
lookup("any七hing", ifMissing: () => map["anything"] = 42); //返回值为42

如果想给出更好的错误信息,则我们可能希望这样写: 如果可选参数没有指定默认值,则它的值默认是null,

var map = new Map();
lookup(key, {ifMissing}) {
var result = map[key];
if (result != null) {
return result;
}
return ifMissing == null ? throw "$key notfound" : ifMissing();
}

命名参数始终是可选的

换而言之,对参数的分类如必填或可选,与参数的位置或命名的分类是没有关联的。 不能混合使用可选位置参数与命名参数,你只能使用其中一种。

形式参数不是final 变量

与其他变量一样,它们能够被修改,但这不是一种好风格,应该避免。 应该尽量减少使用可变的变量,可变将导致代码难千理解与推理。


函数体

函数体包含了函数在执行时需要计算的代码。 函数体跟在函数签名之后,而且有两种形式

(1) 大括号括起来的语句列表(可能为空)。 (2) =>符号后跟着一个表达式。

在第1 种形式中,函数体从第1 条语句开始执行,直到以下任意一种情况发生:

函数最后一条语句被成功执行, 一个return 语句被执行, 或是抛出了一个没有被捕获的异常。

Dart 中的每个函数要么返回一个值,要么抛出一个异常。 如果我们完成了最后一条语句,而且它不是return, 则我们将返回null 。

构造函数

构造函数是用来创建类的实例的特殊函数。 构造函数包括了工厂构造函数与生产构造函数, 其中工厂构造函数是具备特殊功能的普通函数。 生产构造函数与工厂构造函数的区别在于,它始终返回一个新的实例或者抛出一个异常。所以就算是没有显式地使用return 语句,生产构造函数也不会返回null 。 实际上,生产构造函数不能返回任何表达式,它只可能包含一个没有关联表达式的return 语句。


函数声明

大多数函数都是通过函数声明来进行介绍的,而构造函数、getter 和setter 例外。 函数声明有一个函数名称,后面跟着参数列表和函数体。

抽象方法有函数签名但是没有函数体。抽象方法从技术上说并不是函数声明。 把它们作为声明只是为了辅助静态检查器。

函数声明可以出现在顶层(例如, main()) 或是作为方法存在,

局部函数

函数也可以是局部函数。局部函数就是定义在其他函数内部的函数。

为计算斐波那契数列定义一个内部的辅助函数:


fib(n) {
lastTwo(n) {
if (n < 1) {
return [0, 1];
} else {
var p = lastTwo(n - 1);
return [p[1], p[0] + p[1]];
}
}

return lastTwo(n)[1]; }

>这并不是计算第n 个斐波那契数的最佳方式,但是它避免了幼稚的递归计算版本所带来的浪费。
>由于lastTwo()只是fib()的一个实现细节,所以最好是把lastTwo()嵌入fib()中,
>以避免引入一个额外的函数名而使外层命名空间受到污染。

***

## 闭包
>函数可以定义在表达式的内部。它们被称为函数字面量,或者被更笼统地称为闭包。
>与函数声明不同,闭包没有名称。
>与其他函数一样,它们也有参数列表与函数体。
```dart
  (x) => x; //另一个identity 函数
  (x) { return x;} //又一个
  (x, [step = 1]) => x + step; //有一个可选参数的闭包
  (a, b) => a + b; //有两个必填参数的闭包

它们看起来就像是没有名称的函数声明。 真正的好处则来自于将它们作为大型表达式的一部分。

reduce()

虑列表元素求和的例子 可以写一个for 循环来做到这一点,但那是相当原始的方法。 一个有经验的Dart 开发者会这样写:

sum(nums) => nums.reduce((a, b) => a + b);

Dart 中的列表及其他很多类型都定义了reduce()方法。 任意实现了lterable 接口的类型应该都有一个可运行的reduce()方法。 这个方法接收一个被我们称为combiner 的二元函数作为参数。 当reduce 被调用时,它会遍历当前的对象。 处理开始时,前两个元素将传递给combiner 并执行。 在后续的每个迭代中, combiner 被重新执行,上一次combiner 的执行结果会作为第1 个参数,而下一个元素将作为第2 个参数。>如果combiner()把它的参数相加,则最终效果就是把当前对象的前两个元素相,然后加上第3 个,以此类推,得到总和。

where()

country.cities.where((city) =>city.population> 1000000);

这个函数大致找出了所有country 中人口数量大千一百万的城市。 这段代码使用了where()方法,它也来自于lterable, 接收一个函数作为参数。 在这种情况下, where()的参数必须是一个一元断言函数,而它将返回那些断言为真的元素。


调用方法与函数

函数可以通过标准的方式来调用,即在函数值表达式后面加上一个括号参数列表, 例如:

print('Hello, oh brave new world that has such people in it' )
```。

有些函数被称为getter, 可以不使用参数列表来调用。
```dart
true.runtimeType; // bool

对getter 和setter 的统一使用为我们提供了表征独立的宝贵财富。 但需要注意,不像支持表征独立的其他几种语言, Dart 并不完全遵循统一引用的原则。 在Dart 中,方法跟字段不是通过相同的语法来访问的。 尽管getter 和字段没有区别对待,保证了表征独立,但getter 与方法是通过不同的语法访问的。 因此, Dart 开发者需要注意一个函数是被声明为getter 还是一个无参数的方法。这种情况同样适用千setter 。

级联

除了平常使用点运算符来执行成员选择, Dart 也支持使用双点运算符进行方法级联。 当我们需要对一个对象执行一系列的操作时,级联是非常有用的。

级联的求值过程就像普通的方法调用,只是它的值不是方法调用的返回值,而是当前对象。

"Hello".length.toString(); // 值为'5'
"Hello"..length; //值为 Hello 对象
"Hello"..length.toString(); //值为“Hello"

因此,我们并不这样写:

var address= new Address.of("Freddy Krueger");
address.setStreet ("Elm", "13a");
address.city = "Carthage";
address.state = "Eurasia";
address.zipCode(66666, extend: 66666);
address;

我们可以写成一个单独的表达式:

new Address.of("Freddy Krueger")
..setStreet("Elm", "13a")
..city = "Carthage"
..state = "Eurasia"
..zipCode(66666, extend: 66666);

级联不仅仅节省了几个按键,还使得在没有预先进行规划的情况下,也能创造出流畅的API 。 没有级联时,为了将调用链接起来,所有方法必须设计为总是返回当前对象。 有了级联时,我们可以达成同样的链接效果,而且不用理会API 中各个方法的返回值。

将级联代码格式化以使它具有良好的可读性是很重要的。 我们不应该滥用级联,例如把一系列的级联放在同一行或是使用过多层次的级联。

级联对构建所谓建造者模式API 是非常有用的,即一个对象描述符被分步创建,在经历一系列级联后,对象在构建结束时才被创建。

另一种能用到级联的情况是当我们执行某个对象的方法时,我们需要的返回值是对象本身,但方法返回的却是其他值。 如果这样写:

var sortedColors =
['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3).sort();

则我们将发现sortedColors 是null, 因为sort()方法的返回值是void 。 我们可以重新调整代码:

var colors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3);
colors.sort();

但是直接使用级联是更好的办法:

var sortedColors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3)
..sort();

赋值

Dart 中的赋值通常都是函数调用,因为对字段的赋值只是setter 方法的语法糖而已。

V = e 这样的赋值操作,其确切含义取决于v 的声明。

如果v 是局部变量或参数,那么这就只是一个传统的赋值。 否则,这个赋值只是对调用名为v=的setter 的语法糖而已。

赋值是否有效取决于 setter v 是否被定义,或变量v 是否为final 。 Final 变批不能重复赋值且不会导致对应的setter 被执行。

复合赋值如 i+= 2 被定义为普通的赋值

使用运算符

Dart 支待用户自定义的运算符,比如我们为Point 定义的运算符。 Dart 中用户自定义的运算符实际上是有着特殊名称及特殊语法的实例方法,这些方法必须使用内置标识符 operator 作为前缀。

除了语法,所有实例方法涉及的规则都适用于运算符。

允许自定义的运算符有: <、>、<=、>=、=、一、十、I 、~/、*、%、|、^、&、<<、>>、[J=、[]、~

此外,还有一些固定的运算符不允许开发者自定义。它们是&&||及自增和自减运算符++.-- (包括作为前缀与后缀)。

赋值并不认为是一个运算符,虽然复合赋值的语义会依赖于运算符。

相同也不是Dart 的运算符。相反, Dart 提供了预定义的identical()函数。

运算符的优先级规则是固定且遵循惯例的,它们的元数也是一样的。

大多数运算符都是二元的。值得注意的例外是负号和 []=,后者是用于给类似数组和map (或任意有序集合)赋值的情况,并且需要两个参数:一个索引和一个新的值。 减号的情况是特殊的,因为我们支持二元的减法和一元的负号操作。


Function 类

Function 是代表所有函数的公共顶层接口的抽象类。Function 没有声明任何实例方法。 然而它声明了类方法apply(),此方法接收一个函数和一个参数列表,并使用提供的参数列表去调用传入的函数。

apply()的签名是:

static apply(Function function,List positionalArguments,[Map<Symbol, dynamic>namedArguments]) 

apply()的形式参数是带有类型注解的。它需要一个被调用的函数和一个位置参数的列表(可能为空)。 命名参数可以通过一个名称与实际参数组成的map 来提供,且实际参数可以是任意类型的对象。 最后一个参数是可选的。 大部分函数不需要任何命名参数,所以只在需要时才提供它们是比较方便的。 apply()方法提供了一种使用动态确定的参数列表来调用函数的机制。通过它,我们可以处理在编译时参数列表数量不确定的情况。

模拟函数

面向对象编程的关键原则是对象的行为而不是对象的实现。 理想情况下,任意对象都应该能模仿其他对象。 例如, Proxy 的实例被设计为模仿任意对象的行为。

而函数是对象,我们应该也能够模仿它们的行为 函数最常见且重要的行为是被调用时所执行的操作,但函数调用是一个内置的操作 用Proxy 来模仿函数

// 存疑
var p = new Proxy ((x) => x*2);
p (1); //我们希望得到2

原来函数的执行会转换成调用一个名为call()的特殊方法。 所有真正的函数都隐含支持一个签名跟函数本身一致的call()方法,它的作用是执行当前的函数。

在上面的例子中, p(1)实际上是p.call(1) 。当然,我们不能把p.call(1)看作p.call.call(1) ,那将造成无限递归。 因为Proxy 没有call 方法,所以noSuchMethod()被执行,将调用发送到代理目标。 此代理目标是一个函数,它有自己的call()方法。 任何声明了call()方法的类都被认为隐含实现了Function 类。 注意Function 没有声明call()方法。原因是没有特定的函数签名来声明: call()可以有不同个数的参数,而且可能会有或者没有拥有不同默认值的可选参数(位置参数或命名参数)。 所以, Fuction 真的没有通用的call()可以声明。也因为这样, Dart 在语言层面对call()方法进行了特殊处理。


函数作为对象

Dart 是一门纯面向对象的语言,所以Dart 中所有运行时的值都是对象,包括函数

函数也支待所有在Object 中声明的方法。

在多数情况下,函数继承Object 的方法并保持不变。 至于函数的toStringO方法,它的实现有一定的自由度。它们通常会产生一个对当前函数的合理描述,其中可能会包含函数的签名。

因为==hashCode 的实现通常都是继承而来的,所以两个函数只有在相同的情况下才会相等。 否则,这将难以做到,因为在一般概念下,函数的语义相等是不可判定的。

一个函数表达式每被计算一次,就可能分配一个新的函数对象。

同样,局部函数的声明每次被新的动态作用域包含时,就会引入一个新的对象

局部函数increment

makeCounter() {
var counter = 0;
increment() => ++counter;
return increment;
}

每次对makeCounter()的调用都返回一个不同的increment 函数。 在这种情况下,很明显,我们不需要相同的函数对象,因为它们每个都要绑定到一个不同的计数器变量。

顶层或静态的函数声明可以被命名,且它们的名称始终表示同一个对象。

然而,我们可以在某些情况下做得更好。 Dart 对通过对象属性获取的闭包给予了特殊对待。 如果(且仅当!)两个表达式o1 和o2 计算得到的是同一个对象(即ol 和o2 相同), 那么Dart 会确保对于给定的标识符m, 如果ol.m 是合法的,则ol.m==o2.m 。

不同函数的运行时类型

noSuchMethod()的实现继承自Object, runt皿eType 同样如此。 Dart 运行时的各种类都可以用来表示函数。 所有这些类都将实现Function, 但对于不同函数的运行时类型,我们不能指望在它们之间执行相等与相同检测。

相同的函数具有相同的运行时类型。


生成器函数

Dart 支待生成器,它是用来产生集合值的函数。 生成器可以是同步或异步的。 同步的生成器为迭代器生成提供语法糖 而异步的生成器则为流的生成提供语法糖。

迭代器与可迭代对象

迭代器是允许对集合内容按顺序进行迭代的对象。 在我们想简单生成集合内容时,迭代器特别方便。

支持通过迭代器进行迭代的集合被称为可迭代对象,可迭代对象必须有一个名为iterator 的用于返回迭代器的getter 。

for-in循环可以操作任意可迭代对象。 迭代器与可迭代对象的接口分别被类Iterator 和Iterable 实现。 迭代器的生成非常公式化。我们需要定义一个可迭代的集合类且必须为它定义一个返回(明显地) iterator 的getter。 自然,你将需要定义一个具有moveNextO方法的迭代器类。

作为例子,这里有一段使用令人沮丧的方式来打印到20 为止的自然数的代码: 示例,不能运行


class Naturalsiterable {
var n;
Naturalsiterable.to(this.n);
get iterator => new Naturallterator(n);
}

class Naturallterator { var n; var current = -1; Naturallterator(this.n); moveNext() { if (current < n) { current++; return true; } return false; } }

naturalsTo(n) => new Naturalsiterable.to(n); main() { for (var i in naturalsTo(20)) { print(i); } }

>实际上,一个实现了完整Iterable 接口的典型例子将会更长。

### 同步生成器
>为了减少迭代器而导致的重复代码, Dart 支持同步生成器函数。
>使用同步生成器,让我们省去了即使是实现最基本的迭代器也需要定义两个类的麻烦。
>我们可以给函数体使用```sync*```修饰符来定义生成器函数:
```dart
naturalsTo(n) sync* {
  var k = 0;
  while (k < n) yield k++;
}

被调用时,此函数将立即返回一个可迭代对象i, 该对象又包含了迭代器j。 在迭代器j的moveNext()第1 次被调用时,此函数才开始执行。 在进入循环后, yield 语句被执行,导致k 被加1 ,而上一次k 的值被追加到i 同时naturalsTo()的执行将暂停。 在下一次moveNext()被调用时,暂停yield 的naturalsTo()将继续执行同时循环将重复。

有更好的方式来实现这个特殊的例子。 当序列中的元素是基千自然数计算得来时,有一个方便的构造函数, lterable.generate()接收一个整数n 和一个函数f, 它将产生一个代表f(0)... f(n-1) 的可迭代对象:

naturalsTo (n) => new Iterable. generate (n, (x) => x);

虽然如此,但我们的例子是为了展示在通常情况下定义迭代器所需的必备要素,以及sync*是如何简化这些操作的。

生成器函数的函数体是在函数返回一个结果给调用者之后才开始执行的

生成器内return会直接终止生成器 在某些情况下, finally 分句可能会改变控制流程,进而导致return 不会终止生成器。

在生成器中我们不能使用return 语句来返回值,这样的语句将被编译器标记为错误。 允许这样的语句是没有任何意义的,因为调用者已经获得了返回值,且调用者已完成处理并从调用堆栈上消失。

虽然生成器在返回结果给调用者之后才运行,但它的函数体与结果仍有关联且会进行交互。 在同步生成器中,返回的结果始终是一个可迭代对象。 生成器始终跟它生成的可迭代对象及迭代器相关联。 同步生成器中的Yield 语句将对象追加到与之关联的可迭代对象,并暂停函数体的执行,如上所示。 只有通过调用与之关联的迭代器的moveNext()方法才能让函数体再次执行。 当yield 暂停执行函数体时, moveNext()返回true 给调用者。当生成器终止时, moveNext()返回false 。


相关语言

函数作为一等值在编程语言中有着庞大而辉煌的历史。 函数式编程语言使用函数值作为基础构建模块

将函数作为对象的概念可以追溯到Smalltalk,它的block 是Dart 闭包的前身。 Smalltalk初始的block 存在较多的限制,它们在各种现代方言中都已被去除。

Dart 的函数与它在Smalltalk 语言中的祖先间的关键差异是闭包中return 的行为。 Smalltalk 支待非本地返回,这意味着在闭包中执行的return 将使外层方法退出并将结果返回给调用者。其结果是,使用接收函数的库函数来定义控制结构成为可能。 而这在Dart 中是不行的。不支持非本地返回这一决定,是底层Web 平台的实现限制所造成的。

Scala也支待非本地返回。


总结

与其他所有运行时的值一样, Dart 函数是对象。 Dart 中的函数可以声明为接收位置或命名参数。 位置参数可以是必填或可选的,命名参数始终是可选的。

函数始终遵循词法作用域且对周边环境是封闭的。 然而,因为return 的语义, Dart 的函数不太适合用来实现用户自定义的控制结构。

Dart 函数既能作为类的方法也能作为独立的结构。方法可以与实例(实例方法)或类(类方法)关联。 独立的函数可以在库级别进行声明(顶层函数),也可以通过函数声明或字面表达式成为其他函数内的本地函数。

所有内置的操作符也都是函数,且它们大部分都被定义为实例方法,可以被开发者重写。 用户定义的类可以通过实现特殊的call 方法来模拟内置函数类型。 所有Dart 函数都被认为是Function 类型的成员。

WangShuXian6 commented 3 years ago

类型

Dart 的类型是可选的

可选类型

一门语言之所以被称为类型可选,仅当以下条件成立时: • 类型在语法层面是可选的; • 类型对运行时语义没有影响。

后者比前者更加重要。 习惯了传统静态类型语言的开发者起初可能为此感到不安。 这一点虽然不起眼却是至关重要的,它是Dart 语言设计的基石。

Dart 坚持类型注解不影响语义这一原则

首先,它是Dart 能作为动态语言使用的关键因素;任意用动态语言编写的程序都应当可以用Dart 实现。我们预想代码是会演变的,会随着时间推移而获得类型注解。 如果类型注解会改变Dart 程序的行为,则意味着把类型注解添加到程序中,很有可能会使正常运行的程序停止工作。 这将使类型注解的使用得不到推广,因为开发者们害怕正常工作的代码会因此出现故障。

此外, Dart 程序往往会同时包含使用了类型和没有使用类型的代码。 这意味着开发者不能假定类型安全,而且无法假定某个类型注解的正确性。 在这种情况下,允许类型注解来承担语义可能会造成混乱及不稳定。

基于类型的重载即使在全静态语言中也是一个存在问题的功能。 因为类型不影响语义,所以Dart 不支持基于类型的重载。 因此,即使我们给前面列出的所有例子都添加类型注解,它们在生产环境下的表现也不会发生改变。


类型之旅

Dart 的变量可以与类型关联。 类型也可以用来指示方法的返回类型

int sum(int a, int b) => a + b;
void main() {
print(sum(3, 4));
}

阅读代码的人能够受益于类型注解提供的文档, 但Dart 运行时对此并不关心。

开发工具如集成开发环境(IDE) 可以通过不同的方式来利用类型注解: 它们可以对可能存在的类型不一致发出警告,可以通过多种方式来帮助开发者, 例如为当前表达式提供适用的方法菜单(自动补全),或者提供基千类型信息的代码重构等。

Object 类型

Object 是所有不同类型的公共父类型,所以把参数标记为Object 类型看起来是合理的。但是事实并非如此。 显式使用类型Object, 意味着我们真正期望此变量中的每个对象都必须是 有效的值。这两种情况看起来类似,但当我们对Object 类型的表达式进行操作时,如果尝 试使用不被所有对象支持的方法,则我们会得到警告

dynamic 类型

如果程序中的变量没有显式地给予类型,则它的类型就是dynamic 。 dynamic 类型是一种特殊的类型,它告知类型检查器不要对变量本身的操作或给变量赋值等行为发出警告。 使用类型dynamic 能有效地使静态类型检查安静下来。它告诉类型检查器,我们明确知道自己在做什么。 在很难(或者无法)找到确切的类型来描述代码逻辑的情况下,这是非常有用的。

原则上来说,我们可以显示地使用dynamic 作为类型注解。

dynamic sum(dynamic a, dynamic b) => a + b; //永远不要这样做!

这是毫无意义的,而且是一种糟糕的风格。 它没有传达任何信息给类型检查器或读者。 不添加类型注解可以达到同样的效果,而且不会带来不必要的混乱。

Point

import 'dart:math';

class Point {
  num x, y;
  Point(this.x, this.y);
  Point scale(num factor) => new Point(x * factor, y * factor);
  Point operator +(Point p) => new Point(x + p.x, y + p.y);
  static num distance(Point p1, Point p2) {
    num dx = p1.x - p2.x;
    num dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
  }
}

选择num 是因为它是整数与浮点数的公共父类型

在使用了初始化形式参数的构造函数简写中,并没有用到类型注解。 参数的类型可以由实例变量的声明处获得,因此没有必要重复。


接口类型

Dart 的类型是接口类型。 它们给对象定义了一组可用的方法。 一般来说,它们不会告诉我们对象的具体实现。 这与关注对象的行为而非实现的基本原则是一致的。

Dart 并没有声明接口的语法。接口是通过类声明引入的。 每个类都引入了一个隐性接口,其签名是基于类的成员。 对于传统接口声明的需求,我们定义一个纯抽象类就可以轻松解决。

任何类都可以实现一个接口,即使该接口与类完全没有关联。 这是Dart 不需要接口声明语法的原因。

implements

Pair:

abstract class Pair {
get first;
get second;
}

以声明一个类,它实现了Pair 所定义的接口:

class ArrayPair implements Pair {
var _rep;
ArrayPair(a, b) {
_rep = [a, b];
}
get first => _rep[0];
get second => _rep[1];
}

类ArrayPair 实现了Pair, 而不是继承它。implements 子句后跟着一个或多个类想要实现的接口。

implements 子句所做的,是使类与它所列出的接口建立明确的子类关系。 这种关系会影响Dart 类型检查器及运行时的行为。

一个操作Pair 类对象的函数:

Pair reversePair(Pair p) => new ArrayPair(p.second, p.first);

使用reversePair:

reversePair(new ArrayPair(3, 4)); //一个新的Pair对象,first = 4 且second = 3

当对象从一个变量传递到另一个变量时,会触发类型检查。这样的值传递发生千: • 执行赋值操作; • 传递实际参数给函数; • 函数返回结果。

implements 子句中列出的每个接口都被认为是当前类的直接父接口。

类的父类也被认为是类的直接父接口之一。

如果将类型的所有父接口看作一个集合,那么我们可以这样来计算它:先取得类型的所有直接父接口,然后递归计算它们的直接父接口。 计算一直进行,直到没有元素可以被添加到集合中。

严格地说,在值传递的过程中,类型检查器并不强制要求变量之间有父接口关系。 它检查的是可赋值性。可赋值性比继承关系更宽松。 只要两个类型之间存在父子关系, Dart就认为它们可以相互赋值。 也就是说,不仅ArrayPair 可以赋给Pair (因为ArrayPair 是Pair的子类型), Pair也可以赋给ArrayPair。

Dart 的可赋值性规则支待类型隐性向下转换。

Dart 选择非严格的类型检查。

着Dart 的类型准则是弱类型的。我们不能保证通过类型检查的Dart 程序在运行时不会出错。 这种保证在现实中没有哪种静态类型系统可以做到。

通常来说,类型系统能够强制的属性都会被定义为类型准则的一部分,而对于不能强制的属性就只能有不同程度的降级处理。>一个典型的例子是函数式语言中的模式匹配,它所引入某些属性是不属于静态类型系统范畴的。


泛型

Dart 的类可以是泛化的,也就是说,它们能通过类型进行参数设置。

参数化类型

泛型类可以指定实际的类型参数:

List<String> L = [];
Map<String, int> m = {};

给泛型类提供类型参数并不是必需的

如果我们选择使用泛型类且不提供类型参数,则类型dynamic 将会被隐性使用,代替所有缺失的类型参数

在某些情况下显式使用dynamic 类型是可取的

  Map<String, dynamic> mm = {};

函数类型


类型具体化

虽然类型注解不会出现在运行时,但类型的其他方面会。

每个对象都承载了自身的运行时类型,而且可以通过由Object 继承而来的runtimeType 方法进行访问。 用户可以自由地重写runtimeType, 这也意味着在一般情况下,对象的实现类型并不能通过调用nmtimeType 来获得。

每个类型声明都会引入一个代表它自身的类型为Type 的编译时常量对象。 这些对象在运行时也是可见的。 可以通过动态类型检测和强制类型转换来测试对象是否为某个类型的成员

类型检测

类型检测是用来测试对象是否属千某个类型的表达式:

var v = [1, 2, 3];
v is List; // true
v is Map; // false
v is Object; //无意义的:始终为true

类型检测的一般形式是e is T, 其中e 是一个表达式而T 是一个类型。 类型检测会对e求值并将结果的动态类型与类型T 做对比测试。

上述代码的最后一行是永远不能出现在正常的Dart 程序中的。它的值始终是true, 因为Dart 中所有的值都是对象。

强制类型转换

强制类型转换同样是对一个表达式求值并测试结果对象是否属于某个类型, 不同的是,它们的结果并不明确。 相反,如果测试失败,则强制类型转换会抛出一个CastError, 否则它将返回未改动的被检测的对象。

Object o = [3, 4, 5];
o as List; //有点昂贵的空操作
o as Map; //抛出异常

强制类型转换大致上可以看作以下方式的简写:

  var t = e;
  t is T ? t : throw new CastError();

强制类型转换的典型用法是数据校验

  List L = readNextVal() as List;
//我确信我从readNextVal( )中得到的是一个列表
//如果不是,事情搞砸了,那么我应该失败
//接下来,对列表进行处理

应该警惕对强制类型转换的滥用

Object o = [5, 6, 7];
// 大量的中间逻辑
o.length;//正常工作,但会产生类型警告:因为不是所有对象都有length 属性

许多开发者可能会试图将以上代码改写为;

Object o = [5, 6, 7];
//相同的中间逻辑
(o as List).length; //糟糕!避免警告的错误方式

强制类型转换是在运行时执行的,因此会带来运行时消耗。 如果你的目的只是让类型检查器安静,那么只需要一个赋值语句: 存疑

Object O = [5, 6, 7];
//相同的中间逻样
List L = O;
L.length;

检查模式

在开发过程中,对变量的类型进行校验是非常有用的。 例如,我们想确保输入的参数或返回的对象符合我们的预期。 Dart 为此提供了检查模式。 在检查模式中,每次发生值传递都会触发动态检查。这意味着,在每次的参数传递中,函数或方法返回结果以赋值操作时, Dart 都自动执行一次动态类型测试。 检查模式确保了赋给变量的动态值是变量的静态类型的成员。 同样,实际参数的动态类型也会与形式参数的静态类型进行对比检测, 而函数结果的动态类型则会与函数声明的返回类型进行对比检测。

num n = 3.0;
int i = n; //梒查模式下将触发动态错误;,生产模式下正常工作
num x = i; //始终正常工作
int j = null;

检查模式的行为不同于静态类型检查规则。 当赋值被执行静态检查时,我们使用可指派性规则,即只要双方存在子类或父类关系就允许赋值。 而在检查模式所实现的动态检查中,赋值操作中的值的真实类型必须是变量的静态类型的子类,或者是null 。

在没有检查模式的情况下,开发者可以为代码添加类型转换,但那样会令人不快。 类型转换不仅烦琐,还会引入运行时开销,在生产环境下全面使用类型转换的代价是很高的, 所以只有真正需要的时候才使用类型转换。

在检查模式下,类型注解是会影响程序的行为,但是检查模式也可看作一个完全受开发者控制的为验证类型注解正确性的工具。 在检查模式下,类型注解非常类似千断言。检查模式同时会激活程序中所有的assert 语句

具体化泛型

在运行时,类型参数是具体化的。 当泛型类被实例化时,在运行时传递与储存的都是实际的类型参数。 因此, new List<String>()创建的实例的类实际上是不同于newList<Object>()所创建实例的类的。 我们可以编写如下代码进行测试:

  var L = new List<String>();
  L is List<String>; // true
  L is List<int>; // false

Dart 中的这种测试是存在局限性的。因为类型系统是不严格的,所以我们不能保证某个对象一定符合它的声明, 比如一个List<int>里面只包含整数。我们可以确信,该对象是一个列表,并且它被创建为整数列表。但是,任何类型的对象随后都可以被插入到这个列表中。

但在检查模式下,我们可以更加自信。通过插入不合适的对象从而试图破坏该列表的行为都将被检查模式阻止。 检查模式会将对象的实际类型与变量声明的类型和函数结果的类型进行比较和测试: 而在泛型类型中,泛型的实际类型参数将被类型声明中的类型变量所替代。

var L = new List<String>();
L[0] = 'abc'; //始终ok
L[1] = 42; //检查模式下无法运行- 42 是整型,不是String 的子类型

检查模式将确保泛型类的实例的使用不会被破坏。

具体化和可选类型

虽然可选类型应该不会影响运行时语义,但泛型的具体化肯定会影响运行时语义。 虽然如此,具体化也只在程序试图观察或确定运行时类型结构时才会影响程序的行为。 这些情况包括: • 使用类型测试,强制转换或调用runtimeType 来查询对象的类型; • 给泛型类型的构造函数传递实际类型参数以设置对象的运行时类型; • 使用反射检查或设置变量或函数的类型; • 使用检查模式; • 通过给函数对象的签名添加类型注解来确定其具体化类型。

最后三点都有可能受到类型注解的影响,但一般不会产生语义效果。 其中,最后一点可能是最微妙的。如果类型测试的逻辑牵涉函数,则它会被类型注解影响。 这个函数可以是一个明确定义的闭包或被提取为对象属性

下面的代码演示了该问题:


typedef int IntFunction(int);

observeAnnotations(f) { return f is IntFunction; }

String idl(String x) => x;

id2(x) => x;

int id3(int x) => x;

main() { observeAnnotations(idl); // false observeAnnotations(id2); // true observeAnnotations(id3); // true }


>除了类型注解外,三个函数idl 、id2 、id3 是一模一样的,也因为使用了不同的类型注解,所以导致它们在类型测试中的表现不一致。
>当然,我们这里明确地执行了一个类型测试,所以这种情形下的类型影响行为的表现并不会让我们感到惊讶。

### 类型和代理
>
WangShuXian6 commented 3 years ago

表达式和语句

WangShuXian6 commented 3 years ago

反射

WangShuXian6 commented 3 years ago

异步和isolate

WangShuXian6 commented 3 years ago

结论

WangShuXian6 commented 3 years ago

WangShuXian6 commented 3 years ago

Dart 语言简介

Dart 是谷歌开发的计算机编程语言,后来被ECMA CECMA-408) 认定为标准。 它被用于Web 、服务器移动应用和物联网等领域的开发。 它是宽松开源许可证(修改的BSD 证书)下的开源软件。 Dart 有以下三个方向的用途,每一个方向,都有相应的SDK 。 Dart 语言可以创建移动应用、Web 应用,以及Command-line 应用等

移动端开发

Dart 在移动端上的应用离不开Flutter 技术

Flutter 采用Dart 的原因很多.单纯从技术层面分析如下:

Dart 是AOT(Ahead Of Time) 编译的,可编译成快速、可预测的本地代码, Flutter几乎可以使用Dart 编写; Dart 也可以JIT(J ust In Time) 编译,开发周期快; Dart 可以更轻松地创建以60fps 运行的流畅动画和转场; Dart 使Flutter 不需要单独的声明式布局语言; Dart 容易学习,具有静态和动态语言用户都熟悉的特性。 Dart 最初设计是为了取代JavaScript 成为Web 开发的首选语言,最后的结果可想而知,因此到Dart 2 发布时,巳专注于改善构建客户端应用程序的体验,可以乔出Dart 定位的转变。用过Java 、Kotlm 的人,可以很快地上手Dart 。


Web 开发

Dart 是经过关键性Web 应用程序验证的平台。它拥有为Web 蚊身打造的库,如dart:html,以及完整的基于Dart 的Web 框架。使用Dart 进行Web 开发的团队会对速度的提 高感到非常激动。选择Dart 是因为其高性能、可预测性和易学性、完善的类型系统,以及完美地支持Web 和移动应用。


服务端开发

Dart 的服务端开发与其他的语言类似,有完整的库,可以帮助开发者快速开发服务端代码。

WangShuXian6 commented 3 years ago

Dart 语法基础

关键字

Dart 的关键字

abstract2 dynamic2 implements2 show1
as2 else import2 static2
assert enum in super
async1 export2 in2 super
await3 extends is sync1
break external2 library2 this
case factory2 mixin2 throw
catch false new true
class final null try
const finally on1 typedef2
continue for operator2 var
covariant2 Function2 part2 void
default get2 rethrow while
deferred2 hide1 return with
do if set2 yield3