面向 Java 开发者的 Dart 简介

面向 Java 开发者的 Dart 简介

关于此 Codelab

subject上次更新时间:11月 3, 2021
account_circlekathyw 编写

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 并将其输出到控制台。

99c813a1913dcc42.png c97a12197358d545.png

启动 DartPad

此 Codelab 为每组练习提供了一个新的 DartPad 实例。下方的链接将打开一个新的实例,其中包含默认的“Hello”示例。在整个 Codelab 中,您可以全程使用同一 DartPad,但如果您点击 Reset,DartPad 会恢复到默认示例,您的修改将会丢失。

b2f84ff91b0e1396.png 打开 DartPad

定义 Bicycle 类

b2f84ff91b0e1396.pngmain() 函数上方,添加一个包含三个实例变量的 Bicycle 类。同时从 main() 中移除内容,如以下代码段所示:

class Bicycle {
 
int cadence;
 
int speed;
 
int gear;
}

void main() {
}

cf1e10b838bf60ee.png 观察

  • 在本例中,Dart 分析器会生成错误,告知您必须初始化变量,因为它们不可为 null。您将在接下来的部分中解决该问题。
  • Dart 的主方法名为 main()。如果您需要访问命令行参数,可以添加这些参数:main(List<String> args)
  • main() 方法位于顶层。在 Dart 中,您可以在类之外定义代码。变量、函数、getter 和 setter 都可以独立于类而存在。
  • 原始 Java 示例使用 private 标记声明专用实例变量,而 Dart 不会使用该标记。稍后,您将在“添加只读变量”中详细了解隐私权方面的做法。
  • main()Bicycle 均未声明为 public,因为默认情况下,所有标识符都是公开的。Dart 没有针对 publicprivateprotected 的关键字。
  • 按照惯例,Dart 使用双字符缩进,而不是四字符缩进。由于有名为 dart 格式的便捷工具,您不必费心考虑 Dart 的空格规范。正如 Dart 代码规范(高效 Dart)中所指出,“Dart 的官方空格处理规则涵盖 Dart 格式生成的任何规则。”

定义 Bicycle 构造函数

b2f84ff91b0e1396.png 将以下构造函数添加到 Bicycle 类中:

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

cf1e10b838bf60ee.png 观察

  • 此构造函数没有正文,这在 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 中且两端对齐处于关闭状态时,调整格式特别有用。

b2f84ff91b0e1396.png 点击格式

实例化并输出 Bicycle 实例

b2f84ff91b0e1396.png 将以下代码添加到 main() 函数中:

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

b2f84ff91b0e1396.png 移除可选的 new 关键字:

var bike = Bicycle(2, 0, 1);

cf1e10b838bf60ee.png 观察

  • new 关键字在 Dart 2 中是可选元素。
  • 如果您知道某个变量的值不会更改,则可以使用 final 代替 var
  • print() 函数可以接受任何对象(而不仅仅是字符串)。它使用对象的 toString() 方法将其转换为 String

运行示例

b2f84ff91b0e1396.png 点击 DartPad 窗口顶部的 Run 以执行示例。如果未启用 Run,请参阅本页面下文中的问题部分。

您应该会看到以下输出内容:

Instance of 'Bicycle'

cf1e10b838bf60ee.png 观察

  • 应该不会显示任何错误或警告,这表明类型推断有效,并且分析器会推断以 var bike = 开头的语句定义了一个 Bicycle 实例。

改进输出

虽然输出“Instance of ‘Bicycle'”没有错,但它不够详细。所有 Dart 类都有一个 toString() 方法,您可以通过替换该方法提供更有用的输出。

b2f84ff91b0e1396.pngBicycle 类中的任意位置添加下方的 toString() 方法:

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

cf1e10b838bf60ee.png 观察

  • @override 注解可告知分析器您想要替换某个成员。如果未能正确执行替换,分析器会抛出错误。
  • 指定字符串时,Dart 支持单引号或双引号。
  • 使用字符串插值将表达式的值放在字符串字面量中:${expression}。如果表达式是标识符,您可以跳过大括号:$variableName
  • 使用粗箭头 (=>) 表示法缩短单行函数或方法。

运行示例

b2f84ff91b0e1396.png 点击 Run

您应该会看到以下输出内容:

Bicycle: 0 mph

有问题?检查您的代码

添加只读变量

原始 Java 示例将 speed 定义为只读变量:将其声明为专用变量,并且仅提供一个 getter。接下来,您将在 Dart 示例中提供相同的功能。

b2f84ff91b0e1396.png 在 DartPad 中打开 bicycle.dart(或继续使用您的副本)。

如需将 Dart 标识符标记为仅供其库专用,请让其名称以下划线开头 (_)。您可以通过更改 speed 的名称并添加 getter,将其转换为只读状态。

将 speed 设置为专用的只读实例变量

b2f84ff91b0e1396.pngBicycle 构造函数中,移除 speed 参数:

Bicycle(this.cadence, this.gear);

b2f84ff91b0e1396.pngmain() 中,从对 Bicycle 构造函数的调用中移除第二个 (speed) 参数:

var bike = Bicycle(2, 1);

b2f84ff91b0e1396.png 将剩余几处 speed 更改为 _speed。(两处)

b2f84ff91b0e1396.png_speed 初始化为 0:

int _speed = 0;

b2f84ff91b0e1396.png 将以下 getter 添加到 Bicycle 类中:

int get speed => _speed;

cf1e10b838bf60ee.png 观察

  • 每个变量(即使是数值)都必须进行初始化,或通过将 ? 添加到其类型声明中,声明可为 null。
  • Dart 编译器会针对带有下划线前缀的任何标识符强制执行库隐私设置。“库隐私”通常意味着标识符仅在定义该标识符的文件(不仅仅是类)内可见。
  • 默认情况下,Dart 为所有公开实例变量提供隐式 getter 和 setter。除非您需要强制执行只读或只写变量、计算或验证值或更新其他位置的值,否则无需定义自己的 getter 或 setter。
  • 原始 Java 示例为 cadencegear 提供了 getter 和 setter。Dart 示例不需要为这两个参数使用显式 getter 和 setter,因此它仅使用实例变量。
  • 您可以先从一个简单的字段(例如 bike.cadence)入手,然后对其进行重构,以使用 getter 和 setter。API 保持不变。换句话说,在 Dart 中,从字段转换为 getter 和 setter 并不是破坏性更改。

完成将 speed 实现为只读实例变量的过程

b2f84ff91b0e1396.png 将以下方法添加到 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 不支持重载构造函数,而且会以不同的方式处理这种情况,详见本部分的介绍。

b2f84ff91b0e1396.png 在 DartPad 中打开 Rectangle 示例

添加 Rectangle 构造函数

b2f84ff91b0e1396.png 添加一个空的构造函数,用其替换 Java 示例中的四个构造函数:

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

此构造函数使用可选命名参数

cf1e10b838bf60ee.png 观察

  • this.originthis.widththis.height 使用简写形式在构造函数的声明中分配实例变量。
  • this.originthis.widththis.height 是可选命名参数。命名参数由花括号 ({}) 括起。
  • this.origin = const Point(0, 0) 语法为 origin 实例变量指定默认值 Point(0,0)。指定的默认值必须是编译时常量。该构造函数为所有三个实例变量提供默认值。

改进输出

b2f84ff91b0e1396.png 将以下 toString() 函数添加到 Rectangle 类中:

@override
String toString() =>
      'Origin: (${origin.x}, ${origin.y}), width: $width, height: $height';

使用构造函数

b2f84ff91b0e1396.pngmain() 替换为以下代码,以验证您是否可以仅使用所需的参数来实例化 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());
}

cf1e10b838bf60ee.png 观察

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

b2f84ff91b0e1396.png 在 DartPad 中打开 Shapes 示例

在控制台区域中,您应该会看到圆和正方形的计算面积:

12.566370614359172
4

cf1e10b838bf60ee.png 观察

  • Dart 支持抽象类。
  • 您可以在一个文件中定义多个类。
  • dart:math 是 Dart 的核心库之一。其他核心库包括 dart:coredart:asyncdart:convertdart:collection
  • 按照惯例,Dart 库常量为 lowerCamelCase(例如,使用 pi,而非 PI))。如果您想了解原因,请参阅样式指南:常量名称优先使用 lowerCamelCase
  • 以下代码显示了两个计算值的 getter:num get area => pi * pow(radius, 2); // Circle num get area => pow(side, 2); // Square

方法 1:创建顶层函数

b2f84ff91b0e1396.png 在最高层级添加以下函数(任意类之外),将工厂实现为顶层函数:

Shape shapeFactory(String type) {
  if (type == 'circle') return Circle(2);
  if (type == 'square') return Square(2);
  throw 'Can\'t create $type.';
}

b2f84ff91b0e1396.png 替换 main() 方法的前两行即可调用该工厂函数:

  final circle = shapeFactory('circle');
 
final square = shapeFactory('square');

运行示例

输出内容应当与先前一样。

cf1e10b838bf60ee.png 观察

  • 如果使用 'circle''square' 以外的任何字符串调用该函数,则会抛出异常。
  • Dart SDK 针对许多常见异常定义了多个类。此外,您可以实现 Exception 类来创建更具体的异常,或者如在本例中一样,您也可以抛出一个字符串来描述所遇到的问题。
  • 出现异常时,DartPad 会报告 Uncaught。如想看到更有用的信息,请将代码封装到 try-catch 语句中并输出异常。您可以查看此 DartPad 示例(可选练习)。
  • 如要在字符串内使用英文单引号,请使用斜杠 ('Can\'t create $type.') 对嵌入的引号进行转义,或使用英文双引号 ("Can't create $type.") 指定字符串。

有问题?检查您的代码

方法 2:创建工厂构造函数

使用 Dart 的 factory 关键字创建工厂构造函数。

b2f84ff91b0e1396.png 将工厂构造函数添加到抽象 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;
}

b2f84ff91b0e1396.pngmain() 的前两行替换为用来实例化形状的以下代码:

  final circle = Shape('circle');
 
final square = Shape('square');

b2f84ff91b0e1396.png 删除您之前添加的 shapeFactory() 函数。

cf1e10b838bf60ee.png 观察

  • 工厂构造函数中的代码与 shapeFactory() 函数中使用的代码相同。

有问题?检查您的代码

5. 实现接口

Dart 语言不包含 interface 关键字,因为每个类都定义一个接口

b2f84ff91b0e1396.png 在 DartPad 中打开 Shapes 示例(或继续使用您的副本)。

b2f84ff91b0e1396.png 添加一个实现 Circle 接口的 CircleMock 类:

class CircleMock implements Circle {}

b2f84ff91b0e1396.png 您应该会看到“Missing concrete implementations”(缺少具体实现)错误,因为 CircleMock 不会继承 Circle 的实现,而是仅使用其接口。您可以通过定义 arearadius 实例变量来修复此错误:

class CircleMock implements Circle {
  num area = 0;
  num radius = 0;
}

cf1e10b838bf60ee.png 观察

  • 即使 CircleMock 类不定义任何行为,它在 Dart 中也是有效的,分析器不会抛出任何错误。
  • CircleMockarea 实例变量会实现 Circlearea 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));
  }
}

b2f84ff91b0e1396.png 在 DartPad 中打开 Scream 示例

输出应如下所示:

Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!

cf1e10b838bf60ee.png 观察

  • 使用字符串插值时,字符串 ${'a' * length} 的计算结果为“'a' 字符重复 length 次”。

将命令式代码转换为函数式代码

b2f84ff91b0e1396.png 移除 main() 中的命令式 for() {...} 循环,并将其替换为使用方法链的一行代码:

  values.map(scream).forEach(print);

运行示例

此函数式方法与命令式示例一样,会输出六次 scream。

有问题?检查您的代码

使用更多 Iterable 功能

核心 ListIterable 类支持 fold()where()join()skip() 等。Dart 还内置了针对映射和集的支持。

b2f84ff91b0e1396.pngmain() 中的 values.map() 行替换为以下内容:

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

运行示例

输出应如下所示:

Aaah!
Aaaah!
Aaaaaah!

cf1e10b838bf60ee.png 观察

  • 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 未涵盖以下内容:

如果您想了解 Dart 技术的实际应用,不妨试试 Flutter Codelab

了解详情

您可以通过以下文章、资源和网站详细了解 Dart。

文章

资源

网站