@[toc]
Dart是谷歌开发并在2011年亮相,2015推出了基于Dart语言的移动应用程序的开发框架 Sky,后更名为 Flutter。经过多年的发展和完善,Flutter 逐渐成为公司开发应用程序的新宠儿。
Dart是面向对象的、类定义的、单继承的语言。它的语法类似C语言,可以转译为JavaScript,支持接口(interfaces)、混入(mixins)、抽象类(abstract classes)、具体化泛型(reified generics)、可选类型(optional typing)和sound type system。
体验 Dart 程序
// 定义一个函数
printInteger(int aNumber) {
print('The number is $aNumber.'); // 打印输出到控制台。
}
// Dart 程序从 main() 函数开始执行。
main() {
var number = 42; // 声明并初始化一个变量。
printInteger(number); // 调用一个函数。
}
Dart 应用程序总是会从顶级函数 main() 开始执行;
Dart 语言需要 ;
符号作为每一条语句的结束符号;
$variableName
(或 ${expression}
) 表示字符串插值。
简介
Dart 在语法上和一些热门的语言非常相似,例如C、JavaScript、Python、Swift。语法的设计上有着非常多的相似之处,如果你使用或者了解多门语言,你能很轻松的掌握 Dart 语言。
学习不同的编程语言,通过相互之间的类比,是了解和掌握语言之间的差异性必要条件,笔者这里只记录一些值得玩味的点,让你能快速的一名 Dart 语言的开发者。想要更深入的了解 Dart 语言特性,请查阅 官方开发文档 以及相关的社区。
变量
String name = 'Bob'; // 指定类型,不可接收其他类型
var name = 'Bob'; // 推断出类型,不可接收其他类型
Object name = 'Bob'; // 可接收其他类型,只能使用Object中定义的方法和变量,类似NSObject
dynamic name = 'Bob'; // 可接收其他类型,可以使用所有可能的方法和变量,类似id
final 和 const
如果你从未打算更改一个变量,那么使用 final
或 const
,而不是 var
或者某一指定类型。 final
和 cons
修饰的变量只能被设置一次,区别在于, final
变量在第一次使用时被初始化,const
变量是一个编译时期的常量。
//可以省略String这个类型声明
final str = "hi world";
//final String str = "hi world";
const str1 = "hi world";
//const String str1 = "hi world";
内置类型
Dart 语言内置如下的类型:numbers、strings、booleans、list(数组)、sets(集合)maps(字典)、runes(Unicode字符)、symbols。
Dart 所有变量引用的都是对象类型,每个对象都是一个类型的实例。数字、函数或者 null 都是对象。所有的类都继承自 Object 类。
-
numbers
Dart 有两种类型的 number:int 和 double,两个都是 num 的子类。
var x = 1; var hex = 0xDEADBEEF; var y = 1.1; double z = 1; // double z = 1.0.
Dart 尽管是强类型语言,但是在声明变量的时候,类型是可选的,因此你可以指定变量类型,如 double,也可以使用 var 声明,由 Dart 自动进行类型推断。
-
Strings
var s1 = '使用单引号创建字符串字面量。'; var s2 = "双引号也可以用于创建字符串字面量。"; var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:\'。'; var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'"; var s5 = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。'; var s6 = '可以拼接' '字符串' "即便它们不在同一行。"; var s7 = '使用加号+运算符' + '也可以达到相同的效果。'; var s8 = """ 你可以像这样创建多行字符串 """; var s9 = '字符串插值'; print ('Dart 有$s9,使用起来非常方便'); print ('使用${s9.substring(3,5)}表达式也非常方便');
-
Booleans
布尔类型只有两个对象 true 和 false,两者都是编译时常量。Dart 需要显示地检查布尔值,不允许使用其他类型作为条件,例如 int 类型。
var fullName = ''; assert(fullName.isEmpty); bool hited = false; assert(hited);
-
Lists
Dart 中的数组类型的字面量和 JavaScript 中的数组字面量一样。
var list1 = [1,2,3]; var count = list1.length; // 3 // 替换 list1[0] = 0; var list2 = [4,5,6]; // 拼接 var list3 = list1 + list2; // [0, 2, 3, 4, 5, 6] // 扩展符号 ... 和 ...? var list4 = [1, ...list2]; // [1, 4, 5, 6] var list5; // null var list6 = [1,...?list5]; // [1],数组可能为空,使用 ...? 避免异常 // 2.3版本的 Collection If bool promoActive = true; var list7 = [ 'Home', 'Furniture', 'Plants', if (promoActive) 'OutLet' ]; // 2.3版本的 Collection For var listOfInts = [1,2,3]; var list8 = [ '0', for (var i in listOfInts) '$i' ]; var list9 = [ for (var i =1; i<4; i++) i, for (var i in [4,5,6]) i ];
-
Sets
一组特定元素的无序集合。
// Dart 2.2 版本之后才加入 Set 字面量的创建方式 var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'}; // 创建空 set 类型,添加非指定类型时异常,不指定类型的{}是空Map类型 var names = <String>{}; // 添加一个或一组 names.add('luo'); names.addAll(['liang','guo']);
从 Dart2.3版本开始,Set 和 List 一样支持使用扩展操作符 ... 和 ...? 以及 Collection If 和 Collection For 的方式创建实例。
-
Maps
Maps是一组 key 和 value 的集合对象,在 OC 中,我们称之为字典类型。Dart 中的 key 和 value 可以是任何类型的对象。
// 字面量 var fruits = { "first":"Apple", "second":"pear" }; print(fruits); // 构造函数 var nobleGases = Map(); nobleGases['1'] = 'helium'; nobleGases['2'] = 'neon'; // 所有的key var keys = nobleGases.keys; // 所有的值 var values = nobleGases.values;
从 Dart2.3版本开始,Map 和 List 一样支持使用扩展操作符 ... 和 ...? 以及 Collection If 和 Collection For 的方式创建实例。
-
Runes
Unicode 编码为每一个字母、数字和符号都定义了一个唯一的数值。因为 Dart 中的字符串是一个 UTF-16 的字符序列,所以如果想要表示 32 位的 Unicode 数值则需要一种特殊的语法。
通常使用 \uXXXX 来表示 Unicode 字符, XXXX 是一个四位数的 16 进制数字。例如心形字符(♥)的 Unicode 为 \u2665。对于不是四位数的 16 进制数字,需要使用大括号将其括起来。例如大笑的 emoji 表情(😆)的 Unicode 为 \u{1f600}。
import 'package:characters/characters.dart'; ... var hi = 'Hi 🇩🇰'; print(hi); print('The last character: ${hi.characters.last}\n');
函数
Dart 中函数也是一种对象类型:Function,这就意味着函数可以作为变量、参数和返回值使用。
// 返回类型可选,可不写
返回值类型 函数名(参数列表) {
函数体;
};
例如:
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != nil;
}
当函数体只包含一个表达式,可以简写成下面这样,你可以经常看到这种写法:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != nil;
可选参数
函数的参数有两种:必须要参数和可选参数。必要参数定义在最前面,可选参数接在后面,可选参数又分为两种:命名参数和位置参数。这两种选择其中之一,不能同时出现。
必要参数没什么可说的,我们来了解一下可选参数。
命名参数
通过名称来传递参数,名称的位置可变动。
通过 {parm1, param2, ...}
的形式来传递参数。
// 定义可选参数
void enableFlags(String str, {bool blod, bool hidden}) {
print("$str, $blod,$hidden");
}
// 调用
void main() {
enableFlags("xiaoming",hidden: false, blod: true);
}
位置参数
使用 []
将一系列参数包裹起来作为位置参数。
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
提供默认参数值
你可以为可选参数提供默认值,默认值必须为编译时常量,没有指定默认值的情况下值为 null。
// 提供默认参数
void enableFlags({bool bold = false, bool hidden = false}) {...}
String say(String from, String msg,
[String device = 'carrier pigeon', String mood]) {...}
函数对象
函数可以看作是 Function 类,可以赋值给变量,也可以作为参数使用。
void main() {
var method = printElement; // 赋值给变量
[1,2,3].forEach(method); // 作为参数
}
printElement(int e) => print(e);
匿名函数
匿名函数是编程语言中关于函数的一种重要的变体:没有名字的函数,也叫 Lambda 表达式 或者 Closure 闭包。你可以在各种编程语言中找到它们,可以将匿名函数直接赋值给变量去使用,或者直接放在参数列表中使用(通常是最后一个参数,部分语言称之为尾随闭包,有其他变体)。
(参数列表) {
函数体;
};
// 使用匿名函数实现上个列子中的效果
[1,2,3].forEach((e) {
print(e);
});
// 或者
[1,2,3].forEach( (e) => print(e) );
注意事项
- 同类的同一个方法在不同实例对象中不相等。
- 所有函数都有返回值,即使没有显示的返回语句(返回nil)。
运算符
运算符在编程语言中同样占据着重要的地位,Dart 中同样拥有并遵循的绝大部分的运算符效果,例如算术运算符、关系运算符、类型判断运算符、逻辑运算符等等。我们主要关注一些特别的运算符的定义和使用场景。
-
算术运算符
~/
: 除并且取整5 ~/ 2
的结果为 2。 -
类型判断运算符
Dart 作为一门面向对象语言,类型判断运算符必不可免。常见的
as
类型转换和is
类型判断。Dart 中有一个和is
相反的类型判断运算符is!
,当对象是指定类型则返回 false。 -
赋值运算符
=、+=、-=
等等这样的复合赋值运算符在编程语言中非常常见,Dart 中有??=
这样的复合运算符来为 null 的变量进行默认值的赋值。a ??= "xiaoming"; // a = a ?? "xiaoming";
-
级联运算符
..
可以让你在同一个对象上连续调用多个对象的变量或者方法。querySelector('#confirm') // 获取对象 (Get an object). ..text = 'Confirm' // 使用对象的成员 (Use its members). ..classes.add('important') ..onClick.listen((e) => window.alert('Confirmed!'));
-
访问运算符
.
和?.
,Dart 通过点语法访问成员变量或者方法,?.
则会加上一层判断,如果操作对象为 null,则停止访问并返回 null。
异常
Dart中处理异常没有太多的差别,只是抛出的对象可以是任何非 null 对象而不局限于 Exception 或 Error 类型。
- 抛出异常
throw FormatException('Expected at least 1 section');
- 捕获异常
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 指定异常
buyMoreLlamas();
} on Exception catch (e) {
// 其它类型的异常
print('Unknown exception: $e');
} catch (e) {
// 不指定类型,处理其它全部
print('Something really unknown: $e');
} finally {
// 总是清理,即便抛出了异常。
cleanLlamaStalls();
}
类
Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,所有的类都继承自 Object 类。基于 mixin 的继承意味着除 Object 类之外都只能单继承。Extension
方法是一种在不更改类型或创建子类的情况下向类添加功能的方法,类似于 Swift 中的 Extension
,OC 中的分类。
构造函数
声明一个与类名一样的函数即可声明一个构造函数。
class Point {
double x, y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
// 或者使用 Dart 提供的语法糖
Point(this.x, this.y);
}
- 如果你没有声明构造函数,那么Dart 会自动生成一个无参的构造函数并且调用父类的无参构造方法。
- 构造函数不被继承,子类不继承父类的构造函数,如果子类不声明自己的构造函数,那么只会有一个默认无参的构造函数。
调用父类非默认构造函数
默认情况下,本类的构造函数会调用父类的无参构造方法,并且先于本类的构造函数之前,如果本类构造函数存在一个初始化列表,那么调用父类的无参构造方法会在该列表的初始化之后,本类的构造函数之前,即下面的顺序。
- 本类的初始化列表
- 父类的无参构造函数
- 本类的构造函数
如果父类没有匿名无参构造函数,那么子类就必须在函数体前使用 :
指定调用父类其中一个构造函数。
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// 指定构造函数
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// in Person
// in Employee
}
初始化列表
在构造函数体之前,我们还可以初始化实例变量。
Point.fromJson(Map<String, double> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
重定向构造函数
类似于便利构造器,我们可以将提供一些默认值后,调用类的其他构造器。
class Point {
double x, y;
// 该类的主构造函数。
Point(this.x, this.y);
// 委托实现给主构造函数。
Point.alongXAxis(double x) : this(x, 0);
}
Getter 和 Setter
Dart 中实例对象的每个属性都有一个隐式的 Getter 方法,如果非 final 属性的话还会有一个 Setter 方法,你可以使用 get 和 set 关键字为额外的属性添加 Getter 和 Setter 方法。
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;
}
抽象类
使用关键字 abstract
标识符可以让该类成为抽象类,抽象类一般无法被实例化,常用于声明接口方法,有时也会有具体的方法实现。
abstract class AbstractContainer {
// 定义构造函数、字段、方法等……
void updateChildren(); // 抽象方法。
}
隐式接口
每一个类都隐式地定义并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所有实现的其他接口。如果你想要创建一个 A 类,它支持所有 B 类中的 API,但是不想继承 B,那么就可以通过关键字 implements
实现 B 类中的隐式接口。
class Person {
// _name 变量同样包含在接口中,但它只是库内可见的。
final _name;
// 构造函数不在接口中。
Person(this._name);
// greet() 方法在接口中。
String greet(String who) => '你好,$who。我是$_name。';
}
// Person 接口的一个实现。
class Impostor implements Person {
get _name => '';
String greet(String who) => '你好$who。你知道我是谁吗?';
}
如果需要实现多个类接口,可以使用逗号分割每个接口类:
class Point implements Comparable, Location {...}
创建子类
使用 extends
关键字来创建一个子类,并可使用 super
关键字引用一个父类:
class Television {
void turnOn() {
...
}
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
...
}
}
重写
你可以使用 @override
注解来表示你重写来一个成员。
class SmartTelevision extends Television {
@override
void turnOn() {...}
}
noSuchMethod()
如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod
方法,可以通过重写该方法来追踪和记录这一行为。
class A {
// 除非你重写 noSuchMethod,否则调用一个不存在的成员会导致 NoSuchMethodError。
@override
void noSuchMethod(Invocation invocation) {
print('你尝试使用一个不存在的成员:' +
'${invocation.memberName}');
}
}
你不能调用一个未实现的方法除非下面其中的一个条件成立:
- 接收方是静态的 dynamic 类型
- 接收方具有静态类型,定义了未实现的方法/抽象方法,并且实现了 noSuchMethod 方法且实现与 Object 中的不同
Extension 扩展类
extension 是 Dart2.7 引入的,向现有库添加功能的一种方式。
// 给 String 添加转换为 Int 的功能
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
main() {
// 使用新的方法
print('123'.parseInt());
}
枚举类型
一些固定数量的常量值,Dart 的枚举功能没有特别之处,我们看下常用的方法。
// 定义枚举
enum Color { red, green, blue }
// 获取索引
var idx = Color.red.index;
// 获取所有的类型
List<Color> colors = Color.values;
Dart 中的枚举类型还有两个限制:
- 枚举不能继承,也不可以实现一个枚举。
- 不能显示地实例化一个枚举类。
Mixin 模式
Mixin 是一种在多重继承中复用某个类中代码的方法模式。Dart 通过关键字 with
来使用 Mixin 模式。
mixin Athlete {
run() {
print("run.");
}
}
mixin Singer {
sing() {
print("Sing.");
}
}
class A with Athlete, Singer {
// ...
}
void main() {
var a = A();
a.run();
a.sing();
}
- Minix 类:定一个类,继承自 Object 但不为该类定义构造函数
- 单纯的 Mixin 类:可以使用关键字 mixin 代替 class
类变量和类方法
类变量和类方法也成为静态变量和静态方法,它们属于类而不是某个实例变量。
在变量和方法前通过关键字 static
可以声明类变量和类方法。
类方法不能被实例访问,因此内部也不能使用 this。
泛型
泛型存在于很多编程语言中,例如 Python,Swfit,OC 等,Dart 语言同样不例外,在之前 List<E> 这样的声明,使用到的就是泛型。<...> 符号表示一个泛型类,通常使用一个字母来表示类型参数,例如E、T、S、K 和 V 等。
泛型常用于需要类型安全的情况,有了它可以更好地生成代码,减少代码量。拿 List 来举例,List<String>
让数组中的元素都必须是字符串类型,当你放入非字符串类就会提前告知你错误,另外,通过泛型,我们可以只编写一份通用的 List 操作就可以适用于多种类型的操作。
类似的,Set 和 Map 同样可以使用泛型来约束元素内容。
var names = <String>['小芸', '小芳', '小民'];
var uniqueNames = <String>{'小芸', '小芳', '小民'};
var pages = <String, String>{
'index.html': '主页',
'robots.txt': '网页机器人提示',
'humans.txt': '我们是人类,不是机器'
};
var nameSet = Set<String>.from(names);
var views = Map<int, View>();
var names = List<String>();
names.addAll(['小芸', '小芳', '小民']);
限制参数化类型
有时想要泛型只支持一定的类型时,可以通过关键字 extends
来限制泛型的范围。
// 支持泛型,但是有约束范围
class Foo<T extends SomeBaseClass> {...}
// 某类的子类
class Extender extends SomeBaseClass {...}
这样你在使用 Foo 类时,就可以使用 SomeBaseClass 或者其子类作为泛型参数。
var a = Foo<SomeBaseClass>();
var b = Foo<Extender>();
var c = Foo<Object>(); // 非SomeBaseClass类型时,将发生错误
泛型方法
Dart 中的泛型还可以用在方法中,我们称使用泛型的方法为泛型方法。
T first<T>(List<T> ts) {
// 处理一些初始化工作或错误检测……
T tmp = ts[0];
// 处理一些额外的检查……
return tmp;
}
- 方法的返回值为 T 类型
- 参数 ts 为 T 类型的数组
- 临时变量的类型为 T 类型
库和可见性
代码库不仅只是提供 API 而且还起到了封装的作用,库中的私有成员通过 _
开头声明。每个 Dart 程序都是一个库。
导入库
对于 Dart 内置库,使用 dart:xxx
的形式。
import 'dart:html';
而对于其他的库,使用系统路径或者以 package:xxx
的形式 package:xxx 指定的库通过包管理器,如pub工具,来提供。
import 'package:test/test.dart';
指定前缀
如果导入的库有冲突,可以为其中一个指定前缀。
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// 使用 lib1 的 Element 类。
Element element1 = Element();
// 使用 lib2 的 Element 类。
lib2.Element element2 = lib2.Element();
部分导入
// 导入 foo
import 'package:lib1/lib1.dart' show foo;
// 导入 除了 foo 外的所有
import 'package:lib2/lib2.dart' hide foo;
延迟加载库
允许应用在需要的时候才去加载库代码,延迟加载有几个好处:
- 减少应用的初始化时间
- 处理 A/B 测试,比如测试各种算法的不同实现
- 加载很少会使用到的功能
使用 deferred as
关键字来标识需要延时加载的代码库。
import 'package:greetings/hello.dart' deferred as hello;
当实际需要使用到库中 API 时先调用 loadLibrary
函数加载库。
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
- 延迟加载的代码库中的常量需要在代码库被加载的时候才会导入,未加载时候是不会导入的。
- 导入文件的时候无法使用延迟加载库中的类型。
- Dart会隐式地将
loadLibrary
方法导入到使用了deferred as
命名空间 的类中。loadLibrary
函数返回的是一个Future
类型。
异步
Dart 通过 async
和 await
关键字实现异步编程,使用了这些关键字的函数会返回 Future
和 Stream
对象。
- Future 对象
必须在带有 async 关键字的异步函数中使用 await。
// 异步函数
Future checkVersion() async {
var version = await lookUpVersion(); // 耗时操作
// 使用 version 继续处理
}
你可以在异步函数中多次使用 await 关键字。
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
await 表达式的返回值通常是一个 Future 对象,如果不是 Dart 也会自动将其包裹在一个 Future 对象里。Future 对象代表一个“承诺”,await 表达式会阻塞到需要的对象返回。
- Stream 对象
想要从 Stream 中获取值,需要使用 async
关键字或一个异步循环 await for
。
Future method() async {
await for (var request in requestList) {
// 返回类型为 Stream
handleRequest(request);
}
}
生成器
Dart 中的生成器和迭代器和 Python 中的类似,当你需要延迟地生成一连串的值时,可以考虑使用生成器函数。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++;
}
总结
- 所有变量引用都是对象,是类的实例,包括数字、函数以及 null。
- Dart 是强类型语言,变量在声明是可以指定类型,也可以由Dart自行推断出类型。
- Dart 支持泛型,List<int> 表示一组由 int 对象组成的数组。
- Dart 支持顶级函数,类方法,类变量等,你还可以进行函数嵌套和局部函数。
- Dart 的私有属性通过
_
开头表示 - Dart 可以显示警告和错误两种类型问题。