面向 Java 开发者的 Dart 简介

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。

文章

资源

网站