0x00 前言
Note: 本文内容基于Dart 2.10 语法
近日Google发布了Flutter2.0, 使用Flutter开发的App可以在不做修改的情况下发布到更多的主流平台;再加上早些时候Fuchsia也宣布将Dart作为主要的UI开发语言,如果未来你想构建跨平台应用,Dart可能会成为主要选项甚至必备技能。
Dart在语法上有很多Java以及Kotlin的影子,本教程通过与Java、Kotlin的对比学习Dart,有利于Android开发者更好地理解和掌握相关语法。
0x01 内置类型
数值
数值类型包括整形int
, BigInt
,以及浮点型double
,int
是64bit的,超出这个范围使用Bigint
。
数值类型的字面值在编译期可以确定,所以使用const
声明数值常量。
const A = 3;
const B = 9;
const C = A * B;
字符串
var str = "可以使用双引号字符串";
var str = '也可以向JS一样使用单引号字符串'
var str = "字符串可以" + "通过运算符链接";
var str = "像Kotlin一样 $exprName 使用字符串模板参数";
var str = """
像kotlin一样,
三个双引号或者单引号内的文字可以随意换行
也无需转义
""";
var str = '相邻字符串 '
'不使用加号'
" 也会连接成一个字符串";
布尔型
bool
表示布尔型,就像Java的Boolean
,但是如前所述bool
的默认值是null
,需在初始化时显式的设置初值
List
Dart中没有Array只有List,Java中经常会纠结于Array与List的选择,不让用户做选择才是好产品。
List的API与Java很相近,但是Dart允许List像Array或者Kotlin的listOf
一样使用字面值创建
// list的类型为List<Int>
final list = [1, 3, 5, 7, 9];
print(list[1]); // 3
Dart2.3开始,加入了spread
操作符进行list的展开,spread
借鉴了ES6的三点语法
final listA = [1, 2, 3, 4];
final listB = [...listA, 5, 6, 7, 8, 9];
print(listB); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Dart2.3还加入Collection-If
功能,在list内可以写条件表达式;
final list = [
if (true) 1,
if (false) 2,
3
];
print(list); // [1, 3]
以及Collection-For
功能,list内写循环表达式
final listA = [1, 2, 3, 4];
final listB = [
for (var a in listA) a * 2,
];
print(listB); // [2, 4, 6, 8]
Map
Dart的Map的API与Java的几乎一样,但是允许字面值创建Map
var map = {
1:"壹",
2:"贰", // 允许尾逗号的存在,据说Kotlin 1.4 中也准备支持尾逗号
};
print(map[1]); // "壹"
print(map[3]); // null
Set
API几乎与Java的一样,同样可以字面值创建
final s = {1, 2, 1, 2, 1, 2};
print(s); // {1, 2}
符号文字(Rune)
Rune是UTF-32编码的字符串。它可以通过文字转换成符号表情或者代表特定的文字。
var clapping = '\u{1f44f}';
print(clapping); // 👏
Java中没有专用的Rune类型,通过\uXXXX
的String表示Unicode字符,例如
String s = "\u00X1";
符号(Symbol)
Symbol可以看作C中的宏,表示编译时的一个常量,使用#
后跟一个标识符表示。
在Java和Kotlin中没有对应的类型,在ruby中倒是有Symbol类型的存在。
main() {
print(i);
switch(i){
case #A: ///#A是一个Symbol
print("A");
break;
case #B:
print("B");
break;
}
var b = new Symbol("b");
print(#b == b); ///#b是一个Symbol
}
Symbol
和Rune
虽然在Java和Kotlin中不存在对应的类型,但在使用中也比较少,所以不会成为学习Dart的障碍。
0x02 函数
函数签名
Dart采用与Java一样的函数签名写法
String greet(String name) {
return "Hello, $name";
}
Dart可以通过类型推导确定返回值类型,随意允许在签名中省略返回值类型:
greet(String name) {
return "Hello, $name";
}
个人不推荐省略返回值类型的写法,函数定义和函数调用变得不容易区分影响可读性。
函数是一等公民
Dart中万物皆对象,函数本质上也是Function
类型的实例,类似于Kotlin中也有对应的FunctionN
类型,都继承自kotlin.jvm.functions.Function
接口。 可以将函数像作为一个实例进行参数传递。
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// Pass printElement as a parameter.
list.forEach(printElement);
lambda传参
像Kotlin一样,可以将lambda赋值给函数类型参数
var l = [1,2,3];
l.forEach((i) { print(i); });
lambda内只有一行时,可以使用箭头函数:
l.forEach((i) => print(i));
() => exp
相当于 () { return exp; }
的语法糖,lambda表达式以外,在函数体只有一行的函数声明时也可以使用
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
可选参数(Optional parameter)
Dart的函数参数分为必选参数和可选参数,可选参数在函数调用时可省略传值,Java中没有相对应的概念,Kotlin的默认值参数可以实现同样功能。
- 可选参数分为
命名可选参数
与位置可选参数
- 定义参数列表时,必选参数需要放到可选参数前面
命名可选参数
使用{}
包裹;位置可选参数
需使用[]
包裹。
命名可选参数(Optional named parameter)
命名可选参数
传值时需要携带参数名称,如果不需要传值则不需要携带参数名称。
不传值的参数会使用函数定义时的默认值,如果函数定义中没有默认参数值
,则会传null
void enableFlags({bool bold, bool hidden}) {...}
// bold中传入了null、hidden中传入了true
enableFlags(hidden: true);
如果命名可选参数
添加了@required
注解,则此参数必须在在函数调用时指定,不可缺省。@required
的使用需要引入package:meta/meta.dart
// child中不传值调用会报错
const Scrollbar({Key key, @required Widget child})
位置可选参数(Optional positional parameter)
位置可选参数
中可选取任意位置,省略其后的所有传值,
若函数定义没有默认参数值
,则会被传入null
String say(String from, String msg,
[String device, 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');
// 函数调用无需携带参数名
// 也可以之省略第四个参数
assert(say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal');
默认参数值
函数定义时,可以指定默认参数值
。与Kotlin的默认值参数
区别如下:
- Kotlin
默认值参数
:有默认值的参数在函数调用时自动变为可选参数
,允许缺省传参。若位置参数
的方式传参(省略指定参数名称),则指定默认值的参数起,后面的所有参数都需要指定默认值:
//kotlin
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: String= "bbb") {
}
//位置参数:后面的参数可以都省略,但必须都有默认值
fun reformat("aaa")
//命名参数:通过参数名指定参数,不要求默认值参数后必须都指定默认值
fun reformat(str = "aaa", upperCaseFirstLetter = false)
- Dart
默认参数值
:可选参数不一定有默认参数值,如果不指定默认参数值,则缺省传参时会被传入null
因为Kotlin的默认值参数
自动变为可选参数,所以不用像Dart那样使用[]
、{}
单独定义可选参数,个人认为Kotlin的做法更优雅简洁。
扩展函数
Dart 2.6起,开始支持像Kotlin那样的扩展函数。但是写法没有Kotlin简洁
extension NumExtention<T extends num> on T {
T pow(int p) => math.pow(this, p) as T;
}
print(2.pow(5)); // 32
- 如上,扩展函数也可以使用泛型
- 在
extension
前添加下划线可以private
化 - 扩展函数只能在top-level声明,Kotlin可以在Class内部声明(但用处不大)。
扩展函数是Kotlin能提供强大DSL的基础,DSL非常适合声明式UI的书写,例如Kotlin的Anko比起Flutter写法上要优雅的多。个人认为扩展函数未来将会为Dart的生态带来很大变化。
0x03 运算符、操作符
?操作符
与Kotlin一样,使用?.在方法调用的同时进行判空
List<dynamic> list = null;
list?.add(3); // list为null则add不会调用
算数操作符
与Java大体相同,区别仅在于除法运算:
- Java没有专门的除法运算符,
/
表示取模运算 - Dart中使用
~/
表示取模,/
表示除法 ,返回double
型
int div = 4 ~/ 2; // 2
div = 4 / 2; // 返回double类型,编译异常
类型判断
与Kotlin相同,相当于Java的instanceof
bool isHoge = h is Hoge;
bool isNotHoge = h is! Hoge; // == !(h is Hoge)
类型转换
类似Kotlin的Smart Cast
if (p is Person) {
print(p.firstName);
}
Elvis操作符
类似Kotlin的 ?:
,Dart中是??
var b = a ?? "hoge";
若a为null则赋值”hoge“,Kotlin好像没有对应的功能。。。
a ??= "hoge";
级联操作符(..)
Strictly speaking, the “double dot” notation for cascades is not an
operator. It’s just part of the Dart syntax.
严格意义上 级联(..)
不能算操作符,是Dart的语法。
级联允许对同一个对象进行连续操作,相当于Kotlin作用域函数的apply
。
querySelector('#confirm') // Get an object.
..text = 'Confirm' // Use its members.
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
/*
上面代码相当于:
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();
0x04 控制流程、异常处理
流程控制
Control Flow与Java基本一样,甚至for-in
的使用也无差。
Iterable
实例可以像Kotlin一样使用foreach
,不需要获取当前i
时,可以使用
candidates.forEach((candidate) => candidate.interview());
Dart中switch-case
的用法上与Java有少许区别,需注意:
switch
后必须跟编译时常量,类型上无限制
(int
、string
、enum
或其他可以用常量构造函数
创建的实例,但要求此类型不能重写==
,因为switch基于==
进行比较,重写==
有可能会违反互换性原则,即 父类 == 子类
但 子类型 !== 父类型
)
case
之后必须要跟break
或者continue
default
可省略(与Java相同)contine label;
语句可以跳转到label:
的位置
(避免Java中漏写break进入下一个case的问题)
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed;
// Continues executing at the nowClosed label.
nowClosed:
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}
异常
与Kotlin相似,Dart中所有的异常都是未检查的异常,方法不声明它们可能抛出哪些异常,也不要强制捕获任何异常。
Dart可以抛出任何非空对象,不仅仅是Exception和Error
//抛出异常的例子:
throw FormatException('Expected at least 1 section');
//也可以抛出任意对象:
throw 'Out of llamas!';
throw语句是一个表达式,所以可以出现在=>中
void distanceTo(Point other) => throw UnimplementedError();
可以使用on
、catch
或者on...catch
捕捉异常:
try {
breedMoreLlamas();
} on OutOfLlamasException {
// A specific exception
buyMoreLlamas();
} on Exception catch (e) {
// Anything else that is an exception
print('Unknown exception: $e');
} catch (e) {
// No specified type, handles all
print('Something really unknown: $e');
}
要在捕获中处理异常,同时允许其继续传播,使用rethrow
关键字。
void misbehave() {
try {
dynamic foo = true;
print(foo++); // Runtime error
} catch (e) {
print('misbehave() partially handled ${e.runtimeType}.');
rethrow; // Allow callers to see the exception.
}
}
void main() {
try {
misbehave();
} catch (e) {
print('main() finished handling ${e.runtimeType}.');
}
}
finally
的使用与Java无差别
0x05 类、构造函数
类(Class)
与Java相同,Dart中所有的类都是Object
的子类。
与Java和Kotlin一样,this
关键字指向当前实例,Dart的代码风格建议省略this,仅在名称冲突等必要的时候使用。
Dart1的对象实例化与Java一样使用new
关键字,Dart2开始可以向Kotlin一样不使用new
创建实例
实例通过instance.runtimeType
可以获取一个类型对象,相当于Java的instance.class
构造函数
构造函数可以有两种形式
ClassName
:类名作为构造函数,像Java、Kotlin一样ClassName.identifier
: 命名的构造函数,类似工厂方法,可以让构造函数的作用更有针对性
var p1 = Point(2, 2);
//Named constructor
var p2 = Point.fromJson({'x': 1, 'y': 2});
当然,这里我们省略了new
关键字,Dart1必须像下面这样:
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
Dart的函数不支持重载,所以当需要定义多个构造函数时,有如下两个选择:
- 使用可选参数
- 增加多个命名构造函数
构造参数为成员赋值
Kotlin的构造参数添加var
/val
,可以声明为类的成员变量,并在调用构造函数时直接为其赋值。
Dart与Kotlin类似,通过添加this.
也可以直接为成员赋值,但是成员依然需要单独声明:
class Person {
// 成员变量声明
final String firstName;
final String lastName;
// 调用时直接为成员变量赋值
Person(this.firstName, this.lastName) {
...
}
}
构造函数的继承
像Java一样,没有声明构造函数的子类只有默认的构造函数(无参的类名构造函数),而不会自动继承父类的任何有参构造函数。所以 想调用父类特定构造函数时,需在子类中显示声明一个构造函数并通过super
关键字调用
class Mutant extends Person {
Mutant(String firstName, String lastName) : super(firstName, lastName);
Mutant.anonymous() : super.anonymous() {
print("anonymous mutant");
}
}
重定向构造函数
子类构造函数要么通过super
继承自父类的构造函数,要么通过this
重定向到另一个自己的构造函数,有点类似于类似与Kotlin的次级构造函数指向主构造函数。
class Point {
num x, y;
// The main constructor for this class.
Point(this.x, this.y);
// Delegates to the main constructor.
Point.alongXAxis(num x) : this(x, 0);
}
初始化列表
构造函数体执行之前,通过初始化列表可以进行成员初始化。 各参数的初始化用逗号分隔。
// 在构造函数体执行之前,
// 通过初始列表设置实例变量。
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
final
成员的初始化可以放到初始化列表进行,但是不能放到构造函数体内
class Hoge {
final String fuga;
Hoge(String moge)
: assert(moge != null),
fuga = moge.toUpperCase() {
print(fuga);
}
}
/*
👇放到函数体内的初始化,编译器会报错
class Dame {
final String fuga;
Hoge(String moge) {
fuga = moge.toUpperCase();
}
}
*/
常量构造函数
类成员变量全为final
、且前置了const
关键字的构造函数,我们称为常量构造函数
。常量构造函数
实例化的对象成为编译时常量。
class Point {
final int x;
final int y;
const Point(this.x, this.y);
}
const p = Point(1, 2);
工厂构造函数
前置factory
关键字后称为工厂构造函数
。工厂构造函数
与Java的工厂方法类似,需要返回一个类型实例,可以配合缓存实现单例模式。
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to
// the _ in front of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
0x06 类的成员
成员方法
与Java的成员方法无论在定义或是调用上没有区别。
惟一的区别是Dart的成员方法无法重载,但是有可选参数
的加持,也没有大问题
<br/>
getter/setter
Dart的getter/setter与Kotlin的类似,但是没有backing filed
的概念,所有没有filed
变量用来存储数据,需要自己声明一个私有变量来充当所谓的backing field
class Rect {
final Point point;
final Size size;
num get area => size.width * size.height;
// 定义私有变量用来充当backing filed
Color _color;
Color get color => _color;
set color(Color newColor) {
_color = newColor;
notifyColorChanged();
}
}
抽象方法
与Java的抽象方法相似:
- 抽象方法没有方法体
- 抽象方法只能存在于抽象类中,但抽象类中可以非抽象方法(有方法体的方法)
- 抽象方法前不需加
abstract
关键字,这点与Java和Kotlin不同
abstract class Animal {
void hello(); //抽象方法
}
class Cat extends Animal {
void hello() {
print("meow");
}
}
可调用类(Callable classes)
类中只有一个call()
方法,所以可以通过类直接调用,有点像Java8的SAM(Single Abstract Method)
接口。
class WannabeFunction {
call(String a, String b, String c) => '$a $b $c!';
}
main() {
var wf = new WannabeFunction();
var out = wf("Hi","there,","gang");
}
0x07 继承与Mixins
隐式接口
Java中我们总是把接口和抽象类放到一起理解:接口是极致抽象的抽象类。
Dart中接口和类是统一的,每个类同时也是一个接口:
- 没有
interface
关键字,声明class
的同时隐式地创建同名接口 - 类中的非私有的成员可以通过接口对外暴露,但需要被实现
- 与Java一样,使用
implements
关键字实现接口 - 抽象类同样具有隐式接口
// Person声明的同时,创建同名接口,内有方法greet()
class Person {
// 接口不含有private变量
final _name;
// 接口不包含构造函数
Person(this._name);
// 接口中没有方法实现
String greet(String who) => 'Hello, $who. I am $_name.';
}
// 实现Person接口
class Impostor implements Person {
get _name => '';
// 必须要实现greet方法
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
因为类同时也是接口,extends
和implements
关键字都是合法的,但意义完全不同,需要注意
继承
与Java的继承没有区别:
- 使用
extends
关键字 - 只允许单继承
方法重写
注意是重写不是重载,Dart没有方法重载
- 与Java一样,可通过
@Override
注解表示一个重写方法,当然可以省略 - 方法重写时可以通过
covariant
关键字缩小参数类型,这是Java或Kotlin中没有的
class Animal {
void chase(Animal x) { ... }
}
class Mouse extends Animal { ... }
class Cat extends Animal {
void chase(covariant Mouse x) { ... }
}
运算符重载
虽然正确的英文是Overridable operators
,但是类似功能总是被译成运算符重载
,whatever。
虽然Java中没有,Kotlin、C++等都有此功能。
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);
}
Java中我们要求重写equals
的同时必须重写hashcode
,Dart中也是一样的, 所以我们对==
进行运算符重载时,不要忘了hashcode
class Person {
final String firstName, lastName;
Person(this.firstName, this.lastName);
// Override hashCode using strategy from Effective Java,
// Chapter 11.
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
// You should generally implement operator == if you
// override hashCode.
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.firstName == firstName &&
person.lastName == lastName);
}
}
void main() {
var p1 = Person('Bob', 'Smith');
var p2 = Person('Bob', 'Smith');
var p3 = 'not a person';
assert(p1.hashCode == p2.hashCode);
assert(p1 == p2);
assert(p1 != p3);
}
noSuchMethod()
Java或Kotlin中没有类似功能,类似于Ruby的method_missing
。
因为Dart中存在dynamic
类型,无法保证类型安全的情况下有可能被调用一个不存在的方法。通过重写noSuchMethod()
方法,可以拦截这种非法调用。
只有当被调用对象满足以下三个条件其中之一时,noSuchMethod()
才有可能被调用:
- 被调用者是一个
dynamic
类型(此时若不重写noSuchMethod
,会调用Object#noSuchMethod
) - 被调用者是一个抽象类类型(此时若不重写
noSuchMethod
,会调用Object#noSuchMethod
) - 被调用者是一个普通类类型,此时如果重写了
noSuchMethod
,则允许存在不被实现的方法(有可能被错误调用)
class A {
@override
void noSuchMethod(Invocation invocation) => print("method ${invocation.memberName} is missing");
}
class B {
// 因为重写了noSuchMethod,所以hoge()可以没有实现
void hoge();
@override
void noSuchMethod(Invocation invocation) => print("method ${invocation.memberName} is missing");
}
void main() {
dynamic a = A();
a.hoge();
// hoge()没有实现也照样能实例化
B b = B();
b.hoge();
}
Mixins
mixin
是实现多继承的重要方式,Java和Kotlin中没有类似功能,Javascript和Python中有相似概念。
- 使用
with
关键字,可以同时“继承”多个mixin类
mixin类
:mixin
关键字替代class
关键字,无构造方法,也不需要调用super
。mixin
关键字是2.1之后引入的,2.1之前使用abstract class
,再阅读一些老项目代码时需要注意
mixin AndroidEngineer {
void implementAndroidApp() {
print("got it!");
}
}
mixin IOSEngineer {
void implementIOSApp() {
print("Jobs!!");
}
}
class Person {
}
class FlutterEngineer extends Person with AndroidEngineer, IOSEngineer {
}
void main() {
final e = FlutterEngineer();
e.implementAndroidApp();
e.implementIOSApp();
}
extends不可缺
with
的使用至少要伴随一个extends
,干爹可以有多个,但必须有且仅有一个亲爹。如果只想继承mixin类时,最不济可以这样写 :extends Object with 〜〜
菱形继承问题?
OOP之所以不允许多继承,主要是为了避免菱形继承问题
Derive
的fun1
究竟来自Base1
还是Base2
?
- 继承关系会按照如下优先级选取
fun1
:with > extend > implements
- 当with后有多个mixin类时,会按照从右往左的顺序选取
fun1
mixin类的继承
mixin类要么直接继承Object
,要么通过on
关键字继承其子类extends
的目标类。
因为mixin类需要使用on目标类的方法,所以要使用mixim类就必须继承on目标类,以提供mixin所需的方法
abstract class Super {
void method() {
print("Super");
}
}
class MySuper implements Super {
void method() {
print("MySuper");
}
}
mixin MyMixin on Super {
void method() {
super.method();//15行
print("Sub");
}
}
class Client extends MySuper with MyMixin {}
void main() {
Client().method();//23行
}
上面代码输出结果:
MySuper
Sub
- 23行会先查找
MyMixin
中有没有对应的方法 - 15行因为
MyMixin
限定子类必须继承或实现Super
,所以会调用MyMixin
子类所extend的父类的method - 虽然
MyMixin
继承的是Supert
,但是子类的直接父类是MySuper
,所以输出是MySuper
后跟Sub
0x08 静态变量、枚举
静态变量、方法
静态变量、方法的定义与使用与Java中完全相同,同样使用static
关键字,同样不用通过实例调用。
如果是一个广泛使用的静态方法,建议使用top-level方法替代静态方法,我们在Kotlin的编码规范中往往也是这样要求的。
一个疑问
Dart2官方有以下一段文字
You can use static methods as compile-time constants. For example, you
can pass a static method as a parameter to a constant constructor.
可以将静态方法当做编译期常量对待,甚至可以作为参数传给常量构造函数
。
其实我不太理解这段文字,如果可以当做编译期常量的话,下面代码应该work,但实际上会error
class Math {
static int plus(x, y) => x + y;
}
const sum = Math.plus(1,2); //Error: Method invocation is not a constant expression.
哪位大佬能答疑解惑的欢迎留言~
枚举类
与Java的enum
基本一样。
- enum中的item会被分配index,index从0开始按照被声明的顺序递增
enum Color { red, green, blue }
assert(Color.green.index == 1);
- enum可以用在switch-case中
- enum不能被
extends
或者mixin
、枚举不能创建实例,这与Java类似 - enum不允许被是实现,这与Java不同
0x09 泛型
整体上与Java的泛型相似,有少许区别
运行时持有类型信息
Java的泛型的类型信息在运行时会丢弃,Dart则一直保持到运行时,可以动态check泛型信息
final strList = <String>[
"hoge", "fuga", "moge"
];
assert(strList is List<String>);
泛型上界
- 跟Java一样,
<T extends S>
约定泛型T的上界S,但是不能通过super
关键字约束下届。 - Java可以使用
<T extends A & B & C>
约定多个上界,但在Dart中不允许。 - 定义上界时,调用时可以省略泛型书写,如下
class Foo<T extends SomeBaseClass> { }
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'
方法泛型
与Java的写法不同
// 泛型的定义在方法名和参数之间
// Java是写在返回值前面
T doHoge<T>(T value){ ... }
Dart2之前只有成员方法才能使用泛型,Dart2中普通的顶级函数也可以使用泛型
0x0A 库与导入(import)
Library的导入
import
关键字导入Library
import 'dart:html';
Library的URI
通过URI导入一个Library
根据library的种类不同,URI前缀scheme不同:
- Dart标准库…
dart:xxx
- 三方库(通过包管理器提供的库,如pub工具)…
package:xxx
- Flutter的Library中也有三方库
import 'package:test'
- Native实现库…
dart-ext: xxx
本地工程的源码还没有package化的,可添加相对路径源码依赖
import 'package:test/text.dart'
别名导入
与Kotlin的import相同
import "package:mylib/awesome_functions.dart" as awesome;
部分导入
当只想导入部分功能时
// Import only foo.
import 'package:lib1/lib1.dart' show foo;
// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;
懒加载库
库的懒加载(或者延迟加载),Java没有类似功能,可以类比Andoird中so的load。
懒加载适用于以下场景。
- 减少应用程序的初始启动时间
- 进行A/B测试时,可以根据需求加载不同的库
- 加载某些使用频次低的功能,懒加载可以提高性能,例如对话框等
延迟加载需要使用deferred as
关键字导入
import "package:rarely_used/lib.dart" deferred as rare;
当真正需要加在时,通过loadLibrary()
加载,返回一个Future
,所以必须使用await
同步。
Future doSomething() async {
await rare.loadLibrary();
rare.awesomeFunction();
}
使用懒加载有几个注意事项:
- 懒加载库中的const定义的常量,不能当常量使用,因为有可能还没有加载
- 相同的理由,不要使用懒加载库中的Class进行任何操作。可以让懒加载库作为某些公共接口的实现,通过接口的依赖倒置避免对懒加载库中Class的直接依赖
deferred as (namespace)
中的namespace
会被自动插入Future loadLibrary()
以作为执行加载的载体,所以注意避免在Library中出现同样签名的LoadLibrary()
方法
Library的创建
创建一个Library供外部使用。
需要创建pubspec.yaml
并进行若干配置。具体可以参考Create Library Packages,包括:
- 库的代码组织
export
指令的使用part
指令的使用library
指令的使用
Flutter工程中代码也是放在lib/
文件夹中,所以也可以看作是是一个Dart的Library。
0x0B 异步与并发
异步编程
Dart可以像C#或Javascript那样可以通过 async/await
实现异步逻辑
Future与async/await
与Js的async/awiat
是Promise
的语法糖一样,Dart的async/awiat
只不过是Future
的语法糖,可以帮助我们的用同步的代码处理Future提高可读性。脱离async/await
,Future本身的API仍然可以完成异步逻辑。
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
await
必须在async{...}
中使用。async{...}
中使用await可以等待异步完成,但是不会阻塞线程。
await expression
的expression通常是一个Future对象,如果不是则被自动包装成Future,这相对于Javascript更智能。
async
的返回值会被自动包装成Future,例如在main函数使用async,会返回Future<Void>
Future main() async {
checkVersion();
print('In main: version is ${await lookUpVersion()}');
}
lambda表达式中也可以出现async
Future<String> lookUpVersion() async => '1.0.0';
使用try...catch
处理Future中的异常
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
Stream与async/await
熟悉RxJava的人都知道Stream的概念(流式数据),Dart中的Stream类型也有同样功能:连续多个数据的异步处理。
与Future一样,我们可以通过Stream的API或者通过async/await
进行异步处理。
Stream使用类似循环语句的写法await for
:
Future main() async {
// ...
await for (var request in requestServer) {
handleRequest(request);
}
// ...
}
await for
同样必须出现在 async{...}
。
像循环语句一样,通过return
或者break
可以结束stream处理
await for
只能处理Cold Stream
,即Stream中的数据是有始有终的,对于像Event监听这种没有固定数量的数据(Hot Stream
)处理,无法使用,因为没有从循环外部取消订阅的API。
使用StreamAPI中的listene()
可以得到StreamSubscription
对象用来处理Hot Stream,它具有cancel
方法可以取消订阅。
Generators(生成器)
JavaScript中有类似概念:用来生成Iterable
或者 Stream
的函数。
Stream
相当于异步版本的Iterable
。
- 同步生成器:返回
Iterable
对象
// body前需要加 sync*
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
// yield的地方,暂停执行并通过Itertable返回当前值k
while (k < n) yield k++;
}
- 异步生成器:返回
Stream
对象
用法没有区别,只是sync*
换成 async*
,比Javascrip异步生成器方便得多
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);
}
}
并发(Isolate)
Dart没有Thread
,所有代码运行在Isolate
上。
Dart通过启动多Isolate
实现并发编程。
Isolate
相对于Thread
的主要不同在于,Isolate
拥有独立的heap,互相之间不共享资源,隔离程度更接近进程。Isolate
间通信需要通过开Port
,类似socket方式的IPC。
import "dart:isolate";
import "dart:async";
void w(sendPort) {
for(int i = 0; i< 100000; i ++) {
print("waiting: $i");
}
sendPort.send("finished!");
}
main() async {
final receivePort = ReceivePort();
final sendPort = receivePort.sendPort;
await Isolate.spawn(w, sendPort);
receivePort.listen((msg){
print("message from another Isolate: $msg");
});
}
这种IPC式的通信不像线程间通信那样方便,但是安全性更高。
通过Isolate.spawnUri()
可以将main()
中的其他文件在新的Isolate
中执行,Flutter的主进程也是以这种方式启动的。
0x0C 注释
Dart的注释用法与Java大体相同。
单行注释
final hoge = 0; // 到行末都是注释内容
多行注释
与Java一样/**/
void main() {
/*
* This is a lot of work. Consider raising chickens.
Llama larry = Llama();
larry.feed();
larry.exercise();
larry.clean();
*/
}
文档注释
使用dartdoc可以生成HTML的API文档。
dartdoc会在IDE智能提示时发挥作用,所以对外暴露的API建议添加文档注释。
Java的文档注释使用/**
开头,Dart中同样支持。另外,///
的连续多行注释也可以作为文档注释。
/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
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) {
// ...
}
}
像Java一样,文档注释中[]
会成为一个链接,例如[Food]
可以链接到Food类的定义位置。
0x0D Typedef、Metadata
Typedef
可以理解成Kotlin中typealias
的函数限定版,只能用来对函数定义别名。
typedef GestureTapDownCallback = void Function(TapDownDetails details);
定义还可以携带泛型
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // True!
}
注解 Metadata
注解在Dart中称作元数据(Metadata)。
Dart的注解是小写字母开头,Java中是大写。
Dart内置的注解参考annoations.dart , 其中最常用有@deprecated
与@override
等,例如:
class Television {
/// _Deprecated: Use [turnOn] instead._
@deprecated
void activate() {
turnOn();
}
/// Turns the TV's power on.
void turnOn() {...}
}
可以通过常量构造函数类
自定义注解
import 'dart:mirrors';
class Todo {
final String comment;
const Todo(this.comment);
}
@Todo("构造函数的参数")
class Hoge {}
main() {
final h = Hoge();
final mirror = reflectClass(Hoge);
final todoRef = mirror.metadata.first;
print(todoRef.reflectee.comment);
}
在反射中可以获取注解的变量。
0x0E 空安全NullSafty
从2.7之后,到目前最新的2.10,Dart在语法层面没有什么更新,最重要的变化就是空安全机制了(目前仍处于Beta测试阶段)。未来Dart可以像Kotlin一样实现编译期检查,并提供了?
、!
、 late
等各种关键字,处理空安全的相关case,基本是致敬Kotlin,熟悉Kotlin的同学应该不会陌生。 https://dart.cn/null-safety