0%

Javaer入门Dart

如果你是一名Java开发人员,可以通过 codelabs 快速入门Dart。

1. 简介

Dart是一门用于Flutter的编程语言,而Flutter是Google开发的移动应用SDK。

阅读本文,你将学习到:

  • 如何编写构造方法
  • 为对象赋值的多种方式
  • 如何 & 何时创建getter和setter
  • 如何处理访问权限
  • 如何创建工厂模式
  • 函数式编程
  • 其他主要的概念

此外,codelabs提供了 DartPad 供大家编写Dart代码。

2. 创建一个简单的Dart类

我们以一个简单的Bicycle类来进行学习,Bicycle类包含一些私有的成员变量,以及对应的setter和getter。
我们尝试编写Bicycle类然后通过main()方法将Bicycle实例打印到控制台。

2.1 启动DartPad

现在可以打开 DartPad ,一边编写Dart代码。

2.2 定义Bicycle类

在DartPad定义一个Bicycle类,其中包含三个成员变量:cadencespeedgear

1
2
3
4
5
6
7
8
class Bicycle {
int cadence;
int speed;
int gear;
}

void main() {
}

在上面的代码中,我们定义了Bicycle类,以及一个main()方法,代码相对比较简单。其中有一些Dart和Java的区别需要留意:

区别 Dart Java
main()方法 1. 不带参数则定义为void main(),带参数定义为void main(List<String> args)
2. 必须在最外层,位于类外部
1. 定义为public static void main(String[] args)
2. 位于类内部
方法、变量定义 能够位于类外 面向对象语言,所有变量、方法必须位于类内部
访问类型 不支持private、public、protected修饰词,全部类、成员变量和方法默认定义为public 通过修饰词private、public、protected修饰
类成员私有化 成员命名以下划线开始(下一节将学习到) 通过private修饰
缩进 默认使用两个空格 默认使用四个空格

2.3 定义构造方法

1
Bicycle(this.cadence, this.speed, this.gear);

上面代码便是Dart的构造方法,初次阅读会觉得比较奇怪,但是在Dart:

  1. 构造方法没有函数体是允许的!
  2. 如果没函数体的构造方法行末尾漏了分号;,DartPad会提示:A function body must be provided.
  3. 在构造方法的参数列表中使用this,是Dart为实例变量赋值的一种快捷方式。也即是等同于以下代码,大家记住可以这么用就可以了。
    1
    2
    3
    4
    5
    Bicycle(int cadence, int speed, int gear) {
    this.cadence = cadence;
    this.speed = speed;
    this.gear = gear;
    }

2.4 实例化并打印

在我们的main()方法中添加实例化的代码:

1
2
3
4
5
6
7
8
void main() {
var bike1 = new Bicycle(2, 1, 0); // ①
Bicycle bike2 = new Bicycle(2, 0, 1); // ②
var bike3 = Bicycle(2, 0, 1); // ③
final bike4 = Bicycle(2, 0, 1); // ④

print(bike1)
}

以上四种实例化方式都是允许的,其中:第二种方式指明类型会加快编译速度,第一种并没有指明类型;在Dart 2之后new可以省略(也就是第三种方式);如果明确bike4是不变的则可以使用final代替var(第四种方式)。
print()就如同Java中的System.out.print(),这里打印结果是Instance of 'Bicycle',如果想要打印具体的信息,需要重写toString()方法(思想与Java一致):

1
2
@override
String toString() => 'Bicycle: $speed mph';

这里的重写代码也是有点独特:

  1. @overrid同Java一致,表明这是重写类方法,Java中字母o是大写
  2. 字符串支持单引号或双引号进行表示
  3. 如果要在字符串中插入表达式,则可以使用 ${表达式},如果是插入成员变量值,可以直接使用 $成员变量
  4. 上面包含了一个=>,该符号表示,当方法的实现只有一行代码的时候可以使用=>简写

2.5 添加只读成员变量

并不是所有类成员变量都是对外开放的,有时可能会需要一些私有的成员变量,例如将Bicyclespeed属性定义为私有的,在Java的实现方式便是:

  1. 将speed成员变量使用private修饰;
  2. 提供speed的getter方法;

而Dart并没有访问修饰词(默认都是public),那如何表示私有化呢?答案是命名使用下划线,如下:

1
2
3
int _speed = 0;
## getter
int get speed => _speed;

注意:

  1. 未初始化的成员变量默认值都为null,即使是表示数值的变量
  2. Dart编译器会把以下划线开头的成员变量认定为私有private
  3. 默认地,Dart为所有public的成员变量隐式提供了对应的setter和getter方法,我们不需要特地去编写对应的setter和getter,除非将变量强制为只读 or 只写,或者在getter or setter中添加额外的操作等。
  4. 因为默认成员变量为public,例如Bicycle类的cadencegear,所以直接通过bike.cadencebike.gear进行访问

最后,将speed定义为私有后的Bicycle类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Bicycle {
int cadence;
int _speed = 0;
int get speed => _speed;
int gear;

// 构造方法
Bicycle(this.cadence, this.gear);

// 刹车
void applyBrake(int decrement) {
_speed -= decrement;
}

// 加速
void speedUp(int increment) {
_speed += increment;
}

@override
String toString() => 'Bicycle: $_speed mph';
}

void main() {
var bike = Bicycle(2, 1);
print(bike);
}

3. 可选命名参数(替代重载)

在Java中,经常会重载方法:也就是编写具有相同函数名,但参数列表不同的多个方法。而Dart不允许重载,并通过可选命名参数代替了重载。
现在定义一个矩形类Rectangle,包含三个成员变量:originwidthheight

1
2
3
4
5
class Rectangle {
Point origin;
int width;
int height;
}

如何通过可选命名参数来代替重载,我们以构造方法为例:

1
Rectangle({this.origin = const Point(0, 0), this.width = 0, this.height = 0});

注意可选命名参数必须包含在花括号中{ },且指定的默认值必须是编译期常量(也就是编译期便可确定值)。又例如:

1
2
3
4
5
// 其中x、y为可选命名参数,r不是
void move(int r, {int x = 10, int y = 20}) {
this.width += x;
this.height += y;
}

使用可选命名参数的方法,在调用时可选命名参数必须指明赋值的局部变量,而非可选命名参数则不需要:

1
2
3
4
5
6
7
8
void main() {
// origin是可选命名参数,为其赋值必须在前面指明: origin
// width和height同理
print(Rectangle(origin: const Point(10, 20), width: 100, height: 200));
print(Rectangle(origin: const Point(10, 10)));
print(Rectangle(width: 200));
print(Rectangle());
}

4. 创建工厂模式

这一小节将介绍两种创建Shape的工厂,先介绍一下Shape类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'dart:math';
// 抽象类
abstract class Shape {
// num表示数据类型是数值,可以为int、float或short等
// 定义一个获取面积的getter方法
num get area;
}

// 圆
class Circle implements Shape {
final num radius;
Circle(this.radius);
num get area => pi * pow(radius, 2);
}

// 正方形
class Square implements Shape {
final num side;
Square(this.side);
num get area => pow(side, 2);
}

main() {
final circle = Circle(2);
final square = Square(2);
print(circle.area);
print(square.area);
}

上面的代码中:

  • Dart支持抽象类(abstract)
  • 在同一个文件中能够定义多个class
  • Dart的核心库包括了:dart.mathdart:coredart:asyncdart:convertdart:collection
  • 在Dart 1.x版本常量均为大写(例如PI),在Dart 2都改为小写(例如pi)

接下来介绍一下创建工厂模式两种方式。

4.1 顶层方法

所有类的外部创建一个工厂方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Shape shapeFactory(String type) {
if (type == 'circle') return Circle(2);
if (type == 'square') return Square(2);
// 如果参数type不是'circle'或'square'将会抛出异常
// 如果想在字符串中添加单引号,可以使用反斜杠\,或整个字符串使用双引号表示
throw 'Can\'t create $type.'; }

// 前面的main方法就可以改写为:
main() {
final circle = shapeFactory('circle');
final square = shapeFactory('square');
print(circle.area);
print(square.area);
}

说明一下:

  1. Dart同Java一样能够抛出异常,也定义了很多常用的异常类,我们能够继承、使用这些常用的异常类。
  2. Dart支持通过throw一个字符串表示遇到的异常信息:throw 'Occurred NPE'
  3. Dart同样使用try/catch来进行异常的捕获:
    1
    2
    3
    4
    5
    try {
    // exception occurred
    } catch(err) {
    print(err);
    }

4.2 工厂构造器

Dart提供了factory关键字用于创建工厂构造器。下面在Shape的抽象类中添加一个工厂构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Shape {
factory Shape(String type) {
if (type == 'circle') return Circle(2);
if (type == 'square') return Square(2);
throw 'Can\'t create $type.';
}
num get area;
}

main() {
// 调用工厂构造器,通过创建Shape对象即可
final circle = Shape('circle');
final square = Shape('square');
print(circle.area);
print(square.area);
}

5. 实现接口

Dart同样也有接口的思想,但Dart并没有interface的关键字,因为每个类都被定义为接口,也就是一个类,既能被当成父类进行继承extends,也能当做接口进行实现implements:

1
2
3
4
5
6
7
8
9
10
class A {
}

// 此时A作为父类被继承
class B extends A {
}

// 此时A作为接口被实现
class C implements A {
}

我们定义一个CircleMock实现我们上面的Circle类:

1
2
3
4
5
6
7
class Circle implements Shape {
final num radius;
Circle(this.radius);
num get area => pi * pow(radius, 2);
}

class CircleMock implements Circle {}

此时DartPad会提示编译错误:Error: The non-abstract class 'CircleSub' is missing implementations for these members: 'radius', 'area'.,编译出错可以这么理解:

1
2
3
4
5
6
7
8
9
10
11
12
// Circle被作为了接口实现
class Circle implements Shape {
// num radius、num get area被视为接口中的方法,需要被实现
final num radius;
num get area => pi * pow(radius, 2);
}

class CircleMock implements Circle {
// 可以通过定义相关的成员变量,编译器也不会报错(这里有点莫名其妙啊)
num area;
num radius;
}

6. 函数式编程

在函数式编程中,可以把方法作为为参数、将方法赋值给变量、支持lambda表达式。
Dart支持函数式编程,所以也支持以上提到的特点,此外,Dart中方法也是一个对象,且具有对应的类型Function,因此Dart中可以将方法赋值给变量或传递给参数。

6.1 类实例作为方法调用

Dart还可以将类实例作为方法一样调用(但该类必须包含call()方法),例如:

1
2
3
4
5
6
7
8
9
10
11
class WannabeFunction {
// call方法返回值为void,省略了void
call(String a, String b, String c) => '$a $b $c!';
}

main() {
var wf = new WannabeFunction();
// 将WannabeFunction类的实例wf作为方法一样调用,此时调用的是WannabeFunction类中的call方法
var out = wf("Hi","there,","gang");
print('$out');
}

6.2 命令式代码(非功能性)

1
2
3
4
5
6
7
8
String scream(int length) => "A${'a' * length}h!";

main() {
final values = [1, 2, 3, 5, 10, 50];
for (var length in values) {
print(scream(length));
}
}

打印的结果是:

1
2
3
4
5
6
Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!

上面使用了字符串插值器A${'a' * length}h!,表示将a字母重复length次。

6.3 将命令式代码转化为功能性代码

我们将上面的代码的for(){ }循环使用方法链的形式进行替换:

1
2
3
4
5
6
7
8
9
String scream(int length) => "A${'a' * length}h!";

main() {
final values = [1, 2, 3, 5, 10, 50];
// for (var length in values) {
// print(scream(length));
// }
values.map(scream).forEach(print); // 下面解释该行代码
}

上面的代码挺难理解的,可以这么理解:

  1. map可以理解为一个迭代器,values.map()便是对values中的数值进行迭代,迭代所做的操作就交由map()方法的参数,这里传入的就是scream,也就是scream()方法,用Java代码表示便是:
    1
    2
    3
    4
    5
    6
    // for相当于map,进行迭代
    for (int val : values) {
    // map()的参数便是对迭代进行的操作
    // 这里便是调用scream()方法,因为函数式编程中方法可以作为参数进行传递
    scream(val);
    }
  2. forEach()可以理解为对处理结果进行的最后操作,例如打印(传入print方法)。forEach()这个在Java 8也已支持。
  3. 综上,values.map(scream).forEach(print);这行代码意思便是,对values进行遍历,对其中每一个值进行scream()处理,然后调用print()进行处理后的最后操作。

6.4 集合的新功能点

Dart在dart:collection库中还包含了一些迭代的新功能点,例如foldwherejoinskip等:

1
2
// values.map(scream).forEach(print);
values.skip(1).take(3).map(scream).forEach(print);

打印的结果:

1
2
3
Aaah!
Aaaah!
Aaaaaah!

其中:

  • skip(1)表示遍历values时跳过第一个
  • take(3)表示遍历时,获取接下来的三个值,剩下的值忽略掉
  • map()forEach()如上面解释,进行遍历和处理

7. 写在最后

以上是关于Java和Dart的一些区别,只要会Java上手Dart应该还是比较简单的。但上面也只讲了一小部分内容,尚未包括异步/同步、方法级联、Null操作。
当然,笔者个人觉得,并不需要要完全学习完Dart才能上手Flutter,学习一下基本的Dart,在Flutter中一边摸索中一边学习Dart应该是最合适的。
codelabs上面还提供了一些学习的链接:
文章:

  • Why Flutter Uses Dart
  • Announcing Dart 2: Optimized for Client-Side Development
  • Why I moved from Java to Dart

资源:

  • A Tour of the Dart Language
  • A Tour of the Dart Libraries
  • Effective Dart

网页:

  • Dart Language: www.dartlang.org
  • Dart for the web, including AngularDart: webdev.dartlang.org
  • Flutter mobile app SDK: flutter.io