关于此 Codelab
1. 简介
Dart 是 Flutter 的编程语言,而 Flutter 是 Google 的界面工具包,用于通过单一代码库构建经过原生编译的精美移动应用、Web 应用和桌面应用。
此 Codelab 向您介绍 Dart,重点介绍 Java 开发者可能没有想到的一些功能。1 分钟就能编写出 Dart 函数,5 分钟就能编写出脚本,10 分钟就能编写出应用!
学习内容
- 如何创建构造函数
- 指定参数的不同方式
- 何时以及如何创建 getter 和 setter
- Dart 如何处理隐私权事宜
- 如何创建工厂
- Dart 中函数式编程的工作原理
- Dart 的其他核心概念
所需条件
要完成此 Codelab,您只需要一个浏览器!
您将使用 DartPad 编写和运行所有示例。DartPad 是一款基于浏览器的交互式工具,让您可以运用 Dart 语言的功能和核心库。如果您愿意,也可以使用 IDE,例如 WebStorm、配备 Dart 插件的 IntelliJ 或配备 Dart Code 扩展程序的 Visual Studio Code。
您想通过此 Codelab 学习哪些内容?
2. 创建简单的 Dart 类
首先,构建一个与 Java 教程中的 Bicycle
类具有相同功能的简单 Dart 类。Bicycle
类包含一些带有 getter 和 setter 的专用实例变量。main()
方法会实例化 Bicycle
并将其输出到控制台。
启动 DartPad
此 Codelab 为每组练习提供了一个新的 DartPad 实例。下方的链接将打开一个新的实例,其中包含默认的“Hello”示例。在整个 Codelab 中,您可以全程使用同一 DartPad,但如果您点击 Reset,DartPad 会恢复到默认示例,您的修改将会丢失。
定义 Bicycle 类
在
main()
函数上方,添加一个包含三个实例变量的 Bicycle
类。同时从 main()
中移除内容,如以下代码段所示:
class Bicycle {
int cadence;
int speed;
int gear;
}
void main() {
}
观察
- 在本例中,Dart 分析器会生成错误,告知您必须初始化变量,因为它们不可为 null。您将在接下来的部分中解决该问题。
- Dart 的主方法名为
main()
。如果您需要访问命令行参数,可以添加这些参数:main(List<String> args)
。 main()
方法位于顶层。在 Dart 中,您可以在类之外定义代码。变量、函数、getter 和 setter 都可以独立于类而存在。- 原始 Java 示例使用
private
标记声明专用实例变量,而 Dart 不会使用该标记。稍后,您将在“添加只读变量”中详细了解隐私权方面的做法。 main()
和Bicycle
均未声明为public
,因为默认情况下,所有标识符都是公开的。Dart 没有针对public
、private
或protected
的关键字。- 按照惯例,Dart 使用双字符缩进,而不是四字符缩进。由于有名为 dart 格式的便捷工具,您不必费心考虑 Dart 的空格规范。正如 Dart 代码规范(高效 Dart)中所指出,“Dart 的官方空格处理规则涵盖 Dart 格式生成的任何规则。”
定义 Bicycle 构造函数
将以下构造函数添加到
Bicycle
类中:
Bicycle(this.cadence, this.speed, this.gear);
观察
- 此构造函数没有正文,这在 Dart 中是可行的。
- 如果您忘记了无正文构造函数末尾的分号 (
;
),则 DartPad 会显示以下错误:“A function body must be provided”(必须提供函数正文)。 - 在构造函数的参数列表中使用
this
是为实例变量赋值的便捷方法。 - 上述代码等同于以下代码,它使用了初始化程序列表:
Bicycle(int cadence, int speed, int gear)
: this.cadence = cadence,
this.speed = speed,
this.gear = gear;
设置代码格式
点击 DartPad 界面顶部的 Format,即可随时调整 Dart 代码的格式。当您将代码粘贴到 DartPad 中且两端对齐处于关闭状态时,调整格式特别有用。
点击格式。
实例化并输出 Bicycle 实例
将以下代码添加到
main()
函数中:
void main() {
var bike = new Bicycle(2, 0, 1);
print(bike);
}
移除可选的
new
关键字:
var bike = Bicycle(2, 0, 1);
观察
new
关键字在 Dart 2 中是可选元素。- 如果您知道某个变量的值不会更改,则可以使用
final
代替var
。 print()
函数可以接受任何对象(而不仅仅是字符串)。它使用对象的toString()
方法将其转换为String
。
运行示例
点击 DartPad 窗口顶部的 Run 以执行示例。如果未启用 Run,请参阅本页面下文中的问题部分。
您应该会看到以下输出内容:
Instance of 'Bicycle'
观察
- 应该不会显示任何错误或警告,这表明类型推断有效,并且分析器会推断以
var bike =
开头的语句定义了一个 Bicycle 实例。
改进输出
虽然输出“Instance of ‘Bicycle'”没有错,但它不够详细。所有 Dart 类都有一个 toString()
方法,您可以通过替换该方法提供更有用的输出。
在
Bicycle
类中的任意位置添加下方的 toString()
方法:
@override
String toString() => 'Bicycle: $speed mph';
观察
@override
注解可告知分析器您想要替换某个成员。如果未能正确执行替换,分析器会抛出错误。- 指定字符串时,Dart 支持单引号或双引号。
- 使用字符串插值将表达式的值放在字符串字面量中:
${expression}
。如果表达式是标识符,您可以跳过大括号:$variableName
。 - 使用粗箭头 (
=>
) 表示法缩短单行函数或方法。
运行示例
点击 Run。
您应该会看到以下输出内容:
Bicycle: 0 mph
有问题?请检查您的代码。
添加只读变量
原始 Java 示例将 speed
定义为只读变量:将其声明为专用变量,并且仅提供一个 getter。接下来,您将在 Dart 示例中提供相同的功能。
在 DartPad 中打开
bicycle.dart
(或继续使用您的副本)。
如需将 Dart 标识符标记为仅供其库专用,请让其名称以下划线开头 (_
)。您可以通过更改 speed
的名称并添加 getter,将其转换为只读状态。
将 speed 设置为专用的只读实例变量
在
Bicycle
构造函数中,移除 speed
参数:
Bicycle(this.cadence, this.gear);
在
main()
中,从对 Bicycle
构造函数的调用中移除第二个 (speed
) 参数:
var bike = Bicycle(2, 1);
将剩余几处
speed
更改为 _speed
。(两处)
将
_speed
初始化为 0:
int _speed = 0;
将以下 getter 添加到
Bicycle
类中:
int get speed => _speed;
观察
- 每个变量(即使是数值)都必须进行初始化,或通过将
?
添加到其类型声明中,声明可为 null。 - Dart 编译器会针对带有下划线前缀的任何标识符强制执行库隐私设置。“库隐私”通常意味着标识符仅在定义该标识符的文件(不仅仅是类)内可见。
- 默认情况下,Dart 为所有公开实例变量提供隐式 getter 和 setter。除非您需要强制执行只读或只写变量、计算或验证值或更新其他位置的值,否则无需定义自己的 getter 或 setter。
- 原始 Java 示例为
cadence
和gear
提供了 getter 和 setter。Dart 示例不需要为这两个参数使用显式 getter 和 setter,因此它仅使用实例变量。 - 您可以先从一个简单的字段(例如
bike.cadence
)入手,然后对其进行重构,以使用 getter 和 setter。API 保持不变。换句话说,在 Dart 中,从字段转换为 getter 和 setter 并不是破坏性更改。
完成将 speed 实现为只读实例变量的过程
将以下方法添加到
Bicycle
类中:
void applyBrake(int decrement) {
_speed -= decrement;
}
void speedUp(int increment) {
_speed += increment;
}
最后一个 Dart 示例看起来与原始 Java 类似,但更为精简的是 23 行,而不是 40 行:
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. 使用可选参数(而不是重载)
下个练习定义了 Rectangle
类,这是选自 Java 教程的另一个示例。
该 Java 代码展示了重载构造函数。重载是 Java 中的一种常见做法,即构造函数具有相同的名称,但参数的数量或类型有所不同。Dart 不支持重载构造函数,而且会以不同的方式处理这种情况,详见本部分的介绍。
添加 Rectangle 构造函数
添加一个空的构造函数,用其替换 Java 示例中的四个构造函数:
Rectangle({this.origin = const Point(0, 0), this.width = 0, this.height = 0});
此构造函数使用可选命名参数。
观察
this.origin
、this.width
和this.height
使用简写形式在构造函数的声明中分配实例变量。this.origin
、this.width
和this.height
是可选命名参数。命名参数由花括号 ({}
) 括起。this.origin = const Point(0, 0)
语法为origin
实例变量指定默认值Point(0,0)
。指定的默认值必须是编译时常量。该构造函数为所有三个实例变量提供默认值。
改进输出
将以下
toString()
函数添加到 Rectangle
类中:
@override
String toString() =>
'Origin: (${origin.x}, ${origin.y}), width: $width, height: $height';
使用构造函数
将
main()
替换为以下代码,以验证您是否可以仅使用所需的参数来实例化 Rectangle
:
main() {
print(Rectangle(origin: const Point(10, 20), width: 100, height: 200));
print(Rectangle(origin: const Point(10, 10)));
print(Rectangle(width: 200));
print(Rectangle());
}
观察
Rectangle
的 Dart 构造函数有 1 行代码,而 Java 版本的等效构造函数有 16 行代码。
运行示例
您应该会看到以下输出内容:
Origin: (10, 20), width: 100, height: 200
Origin: (10, 10), width: 0, height: 0
Origin: (0, 0), width: 200, height: 0
Origin: (0, 0), width: 0, height: 0
有问题?请检查您的代码。
4. 创建工厂
工厂是 Java 中常用的设计模式,与直接对象实例化相比具有诸多优势,例如隐藏实例化的详情、能够返回工厂的返回类型的子类型,以及可以选择返回现有对象,而非返回新对象。
此步骤演示了实现形状创建工厂的两种方法:
- 方法 1:创建顶层函数。
- 方法 2:创建工厂构造函数。
在本练习中,您将使用 Shape 示例,该类会实例化形状并输出其计算面积:
import 'dart:math';
abstract class Shape {
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);
}
在控制台区域中,您应该会看到圆和正方形的计算面积:
12.566370614359172
4
观察
- Dart 支持抽象类。
- 您可以在一个文件中定义多个类。
dart:math
是 Dart 的核心库之一。其他核心库包括dart:core
、dart:async
、dart:convert
和dart:collection
。- 按照惯例,Dart 库常量为
lowerCamelCase
(例如,使用pi
,而非PI)
)。如果您想了解原因,请参阅样式指南:常量名称优先使用 lowerCamelCase。 - 以下代码显示了两个计算值的 getter:
num get area => pi * pow(radius, 2); // Circle num get area => pow(side, 2); // Square
方法 1:创建顶层函数
在最高层级添加以下函数(任意类之外),将工厂实现为顶层函数:
Shape shapeFactory(String type) {
if (type == 'circle') return Circle(2);
if (type == 'square') return Square(2);
throw 'Can\'t create $type.';
}
替换
main()
方法的前两行即可调用该工厂函数:
final circle = shapeFactory('circle');
final square = shapeFactory('square');
运行示例
输出内容应当与先前一样。
观察
- 如果使用
'circle'
或'square'
以外的任何字符串调用该函数,则会抛出异常。 - Dart SDK 针对许多常见异常定义了多个类。此外,您可以实现
Exception
类来创建更具体的异常,或者如在本例中一样,您也可以抛出一个字符串来描述所遇到的问题。 - 出现异常时,DartPad 会报告
Uncaught
。如想看到更有用的信息,请将代码封装到try-catch
语句中并输出异常。您可以查看此 DartPad 示例(可选练习)。 - 如要在字符串内使用英文单引号,请使用斜杠 (
'Can\'t create $type.'
) 对嵌入的引号进行转义,或使用英文双引号 ("Can't create $type."
) 指定字符串。
有问题?请检查您的代码。
方法 2:创建工厂构造函数
使用 Dart 的 factory
关键字创建工厂构造函数。
将工厂构造函数添加到抽象
Shape
类中:
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()
的前两行替换为用来实例化形状的以下代码:
final circle = Shape('circle');
final square = Shape('square');
删除您之前添加的
shapeFactory()
函数。
观察
- 工厂构造函数中的代码与
shapeFactory()
函数中使用的代码相同。
有问题?请检查您的代码。
5. 实现接口
Dart 语言不包含 interface
关键字,因为每个类都定义一个接口。
在 DartPad 中打开 Shapes 示例(或继续使用您的副本)。
添加一个实现
Circle
接口的 CircleMock
类:
class CircleMock implements Circle {}
您应该会看到“Missing concrete implementations”(缺少具体实现)错误,因为
CircleMock
不会继承 Circle
的实现,而是仅使用其接口。您可以通过定义 area
和 radius
实例变量来修复此错误:
class CircleMock implements Circle {
num area = 0;
num radius = 0;
}
观察
- 即使
CircleMock
类不定义任何行为,它在 Dart 中也是有效的,分析器不会抛出任何错误。 CircleMock
的area
实例变量会实现Circle
的area
getter。
有问题?请检查您的代码。
6. 使用 Dart 进行函数式编程
在函数式编程中,您可以执行以下操作:
- 将函数作为参数传递。
- 为变量分配函数。
- 解构一个函数,将多个参数转换为一系列函数,每个函数接受一个参数(也称为柯里化)。
- 创建一个可用作常量值的无名函数(也称为 lambda 表达式 ;Java 的 JDK 8 版本中已添加 lambda 表达式)。
Dart 支持所有这些功能。在 Dart 中,甚至函数也属于对象,具有 Function
类型。这意味着可以将函数分配给变量,或作为参数传递给其他函数。您还可以将 Dart 类的实例作为函数进行调用(如此示例所示)。
下面的示例使用了命令式(非函数式)代码:
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));
}
}
输出应如下所示:
Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!
观察
- 使用字符串插值时,字符串
${'a' * length}
的计算结果为“'a'
字符重复length
次”。
将命令式代码转换为函数式代码
移除
main()
中的命令式 for() {...}
循环,并将其替换为使用方法链的一行代码:
values.map(scream).forEach(print);
运行示例
此函数式方法与命令式示例一样,会输出六次 scream。
有问题?请检查您的代码。
使用更多 Iterable 功能
核心 List
和 Iterable
类支持 fold()
、where()
、join()
和 skip()
等。Dart 还内置了针对映射和集的支持。
将
main()
中的 values.map()
行替换为以下内容:
values.skip(1).take(3).map(scream).forEach(print);
运行示例
输出应如下所示:
Aaah!
Aaaah!
Aaaaaah!
观察
skip(1)
会跳过values
列表字面量中的第一个值:1。take(3)
会获取values
列表字面量中接下来的 3 个值:2、3 和 5。- 跳过其余值。
有问题?请检查您的代码。
7. 恭喜!
完成本 Codelab 后,您已了解 Java 与 Dart 之间的一些差异。Dart 易于学习,并且其核心库和一套丰富的可用软件包可以提高您的工作效率。Dart 可针对大型应用轻松进行扩展。已有数百名 Google 工程师使用 Dart 编写为 Google 带来大量收入的任务关键型应用。
后续步骤
20 分钟的 Codelab 时长有限,无法展示 Java 与 Dart 之间的所有差异。例如,本 Codelab 未涵盖以下内容:
- async/await,让您可以编写异步代码,就像它是同步代码一样。您可以查看此 DartPad 示例,它以动画形式演示了 π 的小数点后 5 位的计算过程。
- 方法级联,其中每种方法都是构建器!
- null 感知型运算符
如果您想了解 Dart 技术的实际应用,不妨试试 Flutter Codelab。
了解详情
您可以通过以下文章、资源和网站详细了解 Dart。
文章
资源
网站