0x0 前言

因学习Flutter需要故把Dart基础知识也看了遍,前端开发学习不算是太吃力,和JavaScript语法差不多,值得注意的是Dart是强类型编程语言,很多地方需要强制规范。学习之前需要注意下面的坑:

// 定义一个函数
printInteger(int aNumber) {
  print('The number is $aNumber.'); // 打印到控制台。
}
 
// 应用从这里开始执行。
void main() {
  var number = 42; // 声明并初始化一个变量。
  printInteger(number); // 调用函数。
}
  • 任何保存在变量中的都是一个 对象 , 并且所有的对象都是对应一个 的实例。 无论是数字,函数和 null 都是对象。所有对象继承自 Object 类。
  • 尽管 Dart 是强类型的,但是 Dart 可以推断类型,所以类型注释是可选的。 在上面的代码中, number 被推断为 int 类型。 如果要明确说明不需要任何类型, 需要使用特殊类型 dynamic
  • Dart 支持泛型,如 List <int> (整数列表)或 List <dynamic> (任何类型的对象列表)。
  • Dart 支持顶级函数(例如 main() ), 同样函数绑定在类或对象上(分别是 静态函数实例函数 )。 以及支持函数内创建函数 ( 嵌套局部函数 ) 。
  • 类似地, Dart 支持顶级 变量 , 同样变量绑定在类或对象上(静态变量和实例变量)。 实例变量有时称为字段或属性。
  • 与 Java 不同,Dart 没有关键字 “public” , “protected” 和 “private” 。 如果标识符以下划线(_)开头,则它相对于库是私有的。 有关更多信息,参考 库和可见性
  • 标识符 以字母或下划线(_)开头,后跟任意字母和数字组合。
  • Dart 语法中包含 表达式( expressions )(有运行时值)和 语句( statements )(没有运行时值)。 例如,条件表达式 condition ? expr1 : expr2 的值可能是 expr1expr2 。 将其与 if-else 语句 相比较,if-else 语句没有值。 一条语句通常包含一个或多个表达式,相反表达式不能直接包含语句。
  • Dart 工具提示两种类型问题:警告_和_错误。 警告只是表明代码可能无法正常工作,但不会阻止程序的执行。 错误可能是编译时错误或者运行时错误。 编译时错误会阻止代码的执行; 运行时错误会导致代码在执行过程中引发 [异常](#exception)。

0x1 关键词

abstract 2 dynamic 2 implements 2 show 1
as 2 else import 2 static 2
assert enum in super
async 1 export 2 interface 2 switch
await 3 extends is sync 1
break external 2 library 2 this
case factory 2 mixin 2 throw
catch false new true
class final null try
const finally on 1 typedef 2
continue for operator 2 var
covariant 2 Function 2 part 2 void
default get 2 rethrow while
deferred 2 hide 1 return with
do if set 2 yield 3

避免使用这些单词作为标识符。 但是,如有必要,标有上标的关键字可以用作标识符:

  • 带有 1 上标的单词为 上下文关键字, 仅在特定位置具有含义。 他们在任何地方都是有效的标识符。
  • 带有 2 上标的单词为 内置标识符, 为了简化将 JavaScript 代码移植到 Dart 的工作, 这些关键字在大多数地方都是有效的标识符, 但它们不能用作类或类型名称,也不能用作 import 前缀。
  • 带有 3 上标的单词是与 Dart 1.0 发布后添加的异步支持相关的更新,作为限制类保留字。
    不能在标记为 asyncasync*sync* 的任何函数体中使用 awaityield 作为标识符。

关键字表中的剩余单词都是保留字。 不能将保留字用作标识符。

0x2 变量

创建一个变量并进行初始化:

var name = 'Bob';

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

name 变量的类型被推断为 String 。 但是也可以通过指定类型的方式,来改变变量类型。 如果对象不限定为单个类型,可以指定为 对象类型动态类型, 参考 设计指南

dynamic name = 'Bob';

另一种方式是显式声明可以推断出的类型:

String name = 'Bob';

如果声明变量没有赋值,那么默认值则是null,即使变量是数字 类型默认值也是null,因为在Dart中一切都是对象,数字类型也不例外。

0x3 常量

如果需要声明一个在程序运行中用不被修改的变量,可以使用finalconst声明:

final name = 'Bob';
final String nickname = 'Bobby';
const bar = 1000000; // 压力单位 (dynes/cm2)
const double atm = 1.01325 * bar; // 标准气压

俩者区别:constfinal更加严格。final只是要求变量在初始化后值不变,但通过final,无法在**编译时(运行之前)**知道这个变量的值;而const所修饰的是编译时常量,我们在编译时就已经知道了它的值,显然,它的值也是不可改变的。

0x4 内建类型

Dart 语言支持以下内建类型:

  • Number
  • String
  • Boolean
  • List (也被称为 Array)
  • Map
  • Set
  • Rune (用于在字符串中表示 Unicode 字符)
  • Symbol

set:

在 Dart 中 Set 是一个元素唯一且无需的集合:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
 
var names = <String>{}; // 创建空集

更多关于 Set 的内容,参阅 GenericSet

map:

通常来说, Map 是用来关联 keys 和 values 的对象。 keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。 但是 value 可以出现多次。 Dart 中 Map 通过 Map 字面量 和 Map 类型来实现:

var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};
 
var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

更名多关于 Map 的内容,参考 Generics and Maps

是 Set 还是 Map ? Map 字面量语法同 Set 字面量语法非常相似。 因为先有的 Map 字母量语法,所以 {} 默认是 Map 类型。 如果忘记在 {} 上注释类型或赋值到一个未声明类型的变量上, 那么 Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

Rune:

在 Dart 中, Rune 用来表示字符串中的 UTF-32 编码字符。例如emoji表情,特殊图形字体等等可以用该类型表示。

Symbol:

一个 Symbol 对象表示 Dart 程序中声明的运算符或者标识符。 你也许永远都不需要使用 Symbol ,但要按名称引用标识符的 API 时, Symbol 就非常有用了。 因为代码压缩后会改变标识符的名称,但不会改变标识符的符号。 通过字面量 Symbol ,也就是标识符前面添加一个 # 号,来获取标识符的 Symbol 。

0x5 函数

Dart 是一门真正面向对象的语言, 甚至其中的函数也是对象,并且有它的类型 Function 。 这也意味着函数可以被赋值给变量或者作为参数传递给其他函数。 也可以把 Dart 类的实例当做方法来调用。

标准写法:

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

省略了类型声明,函数依旧是可以正常使用的:

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

简写函数很像javascript中的 function 函数。

可选参数:

可选参数可以是命名参数或者位置参数,但一个参数只能选择其中一种方式修饰。

调用函数时,可以使用指定命名参数 *paramName*: *value*。 例如:

enableFlags(bold: true, hidden: false);

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

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

函数有两种参数类型: required 和 optional。 required 类型参数在参数最前面, 随后是 optional 类型参数。 命名的可选参数也可以标记为 “@ required” 。

Flutter 创建实例的表达式可能很复杂, 因此窗口小部件构造函数仅使用命名参数。 这样创建实例的表达式更易于阅读。

使用 @required 注释表示参数是 required 性质的命名参数, 该方式可以在任何 Dart 代码中使用(不仅仅是Flutter)。

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

此时 Scrollbar 是一个构造函数, 当 child 参数缺少时,分析器会提示错误。

Required 被定义在 meta package。 无论是直接引入(import) package:meta/meta.dart ,或者引入了其他 package,而这个 package 输出(export)了 meta,比如 Flutter 的 package:flutter/material.dart

位置可选参数:

将参数放到 [] 中来标记参数是可选的:

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] 标志 ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

下面示例演示了如何为位置参数设置默认值:

String say(String from, String msg,
    [String device = 'carrier pigeon', String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}
 
assert(say('Bob', 'Howdy') ==
    'Bob says Howdy with a carrier pigeon');

list 或 map 可以作为默认值传递。 下面的示例定义了一个方法 doStuff(), 并分别指定参数 listgifts 的默认值。

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

0x6 函数是一等对象

一个函数可以作为另一个函数的参数。 例如:

void printElement(int element) {
  print(element);
}
 
var list = [1, 2, 3];
 
// 将 printElement 函数作为参数传递。
list.forEach(printElement);

0x7 运算符

这里基础内容不再详述。

级联运算符 (..):

级联运算符 (..) 可以实现对同一个对像进行一系列的操作。 除了调用函数, 还可以访问同一对象上的字段属性。 这通常可以节省创建临时变量的步骤, 同时编写出更流畅的代码:

querySelector('#confirm') // 获取对象。
  ..text = 'Confirm' // 调用成员变量。
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

第一句调用函数 querySelector() , 返回获取到的对象。 获取的对象依次执行级联运算符后面的代码, 代码执行后的返回值会被忽略。

上面的代码等价于:

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

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

0x8 控制流程语句

  • if and else
  • for loops
  • while and do-while loops
  • break and continue
  • switch and case
  • assert

如果 assert 语句中的布尔条件为 false , 那么正常的程序执行流程会被中断。 在本章中包含部分 assert 的使用, 下面是一些示例:

// 确认变量值不为空。
assert(text != null);
 
// 确认变量值小于100。
assert(number < 100);
 
// 确认 URL 是否是 https 类型。
assert(urlString.startsWith('https'));

提示: assert 语句只在开发环境中有效, 在生产环境是无效的; Flutter 中的 assert 只在 debug 模式 中有效。 开发用的工具,例如 dartdevc 默认是开启 assert 功能。 其他的一些工具, 例如 dartdart2js, 支持通过命令行开启 assert : --enable-asserts

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

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

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

0x9 类

Dart 是一种基于类和 mixin 继承机制的面向对象的语言。 每个对象都是一个类的实例,所有的类都继承于 Object. 。 基于 Mixin 继承意味着每个类(除 Object 外) 都只有一个超类, 一个类中的代码可以在其他多个继承类中重复使用。

0x10 使用类的成员变量

对象的由函数和数据(即方法和实例变量)组成。 方法的调用要通过对象来完成: 调用的方法可以访问其对象的其他函数和数据。

使用 (.) 来引用实例对象的变量和方法:

var p = Point(2, 2);
 
// 为实例的变量 y 设置值。
p.y = 3;
 
// 获取变量 y 的值。
assert(p.y == 3);
 
// 调用 p 的 distanceTo() 方法。
num distance = p.distanceTo(Point(4, 4));

使用 ?. 来代替 . , 可以避免因为左边对象可能为 null , 导致的异常:

// 如果 p 为 non-null,设置它变量 y 的值为 4。
p?.y = 4;

0x11 泛型

在 API 文档中你会发现基础数组类型 List 的实际类型是 List<E> 。 <…> 符号将 List 标记为 泛型 (或 参数化) 类型。 这种类型具有形式化的参数。 通常情况下,使用一个字母来代表类型参数, 例如 E, T, S, K, 和 V 等。

在类型安全上通常需要泛型支持, 它的好处不仅仅是保证代码的正常运行:

  • 正确指定泛型类型可以提高代码质量。
  • 使用泛型可以减少重复的代码。

如果想让 List 仅仅支持字符串类型, 可以将其声明为 List<String> (读作“字符串类型的 list ”)。 那么,当一个非字符串被赋值给了这个 list 时,开发工具就能够检测到这样的做法可能存在错误。另外一个使用泛型的原因是减少重复的代码。 泛型可以在多种类型之间定义同一个实现, 同时还可以继续使用检查模式和静态分析工具提供的代码分析功能。 例如,假设你创建了一个用于缓存对象的接口:

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

后来发现需要一个相同功能的字符串类型接口,因此又创建了另一个接口:

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

后来,又发现需要一个相同功能的数字类型接口 … 这里你应该明白了。

泛型可以省去创建所有这些接口的麻烦。 通过创建一个带有泛型参数的接口,来代替上述接口:

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

在上面的代码中,T 是一个备用类型。 这是一个类型占位符,在开发者调用该接口的时候会指定具体类型。