面向 Java 开发者的 Dart 简介

Dart 是 Flutter 的编程语言,而 Flutter 是 Google 的界面工具包,可用于通过单一代码库来构建适用于移动端、网站和桌面的精致原生编译应用。

本 codelab 将向您介绍 Dart,并重点介绍 Java 开发者可能未预想的功能。借助 Dart,您可以在 1 分钟内编写函数、5 分钟内编写脚本,10 分钟内编写应用!

学习内容

  • 如何创建构造函数
  • 指定参数的不同方式
  • 何时以及如何创建 getter 和 setter
  • Dart 如何处理隐私权
  • 如何创建工厂
  • Dart 中函数式编程的工作原理
  • Dart 的其他核心概念

所需条件

完成本 Codelab 只需要一个浏览器即可!

您将使用 DartPad 编写和运行所有示例。DartPad 是一款基于浏览器的交互式工具,让您可以玩转 Dart 语言的各个功能和核心库。如果您愿意,也可以使用 IDE,例如 WebStorm 或 IntelliJ,并搭配 Dart 插件。或者您可以使用 Visual Studio Code,并搭配 Dart Code 扩展程序

您想通过本 Codelab 学习哪些内容?

我不熟悉这个主题,想大致了解一下。 我对这个主题有所了解,想回顾一下。 我在寻找示例代码以用于我的项目。 我在寻找有关特定内容的介绍。

首先,您需要构建一个简单的 Dart 类,其功能与 Java 教程中的 Bicycle相同。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 的主方法名为 main()。如果您需要访问命令行参数,可以添加这些参数:main(List<String> args)
  • main() 方法位于顶层。在 Dart 中,您可以在类外定义代码。变量、函数、getter 和 setter 都可以独立于类而存在。
  • 原始 Java 示例使用 private 标记声明专用实例变量,Dart 不这么做。稍后,您将在“添加只读变量”中详细了解库专用原则。
  • main()Bicycle 均未声明为 public,因为默认情况下,所有标识符都是公开的。Dart 没有针对 publicprivateprotected 的关键字。
  • 在本例中,Dart 分析器会生成错误,告知您必须初始化变量,因为它们不可为 null。您将在下一部分中解决该问题。
  • 按照惯例,Dart 使用双字符缩进(而不是四字符缩进)。多亏了名为 dartph 的便捷工具,您不必担心 Dart 的空格规范。正如 Dart 代码规范(高效 Dart)中所指出,“Dart 的官方空格处理规则是 whatever dartfmt produces(一切以 dartfmt 生成的结果为准)”。

定义 Bicycle 构造函数

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

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

cf1e10b838bf60ee.png 观察结果

  • 此构造函数不含主体,这么做在 Dart 中是有效的。
  • 如果您漏掉了无主体构造函数末尾的分号 (;),则 DartPad 会显示以下错误:“必须提供函数主体”。
  • 在构造函数的参数列表中使用 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

运行示例

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 标识符标记为仅供其库专用,请以下划线开头 (_) 为其命名。您可以通过更改其名称并添加 getter,将 speed 转换为只读变量。

将 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 保持不变。换句话说,从使用字段转换为使用 getter 和 setter 在 Dart 中不算破坏性更改。

完成将 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);
}

出现问题?请检查您的代码

下个练习定义了 Rectangle,这是来自 Java 教程中的另一个示例。

其 Java 代码中显示了重载构造函数,这是 Java 中的一种常见做法,即名称相同的多个构造函数使用不同数量或类型的参数。Dart 不支持重载构造函数,而且以不同的方式处理这种情况,参见本部分内容。

b2f84ff91b0e1396.png 在 DartPad 中打开 Rectangle 示例

添加 Rectangle 构造函数

b2f84ff91b0e1396.png 添加一个空的构造函数,替换 Java 示例中全部 4 个构造函数:

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)。指定的默认值必须是编译时常量。此构造函数为全部 3 个实例变量都提供了默认值。

改进输出

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

出现问题?请检查您的代码

工厂是 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 中打开 Shape 示例

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

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() 函数中使用的代码相同。

出现问题?请检查您的代码

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

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

b2f84ff91b0e1396.png 添加 CircleMock 类,以实现 Circle 接口:

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。

出现问题?请检查您的代码

在函数式编程中,您可以执行以下操作:

  • 将函数作为参数传递。
  • 为变量分配函数。
  • 解构一个函数,将多个参数传递到一系列函数,每个函数接收一个参数(也称为 currying)。
  • 创建一个可用作常量值的无名函数(也称为 lambda 表达式 ;lambda 表达式已添加到 Java 的 JDK 8 版本中)。

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

运行示例

函数式方法和命令式示例一样输出 6 个“尖叫声”。

出现问题?请检查您的代码

使用更多 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)。
  • 其余值将被跳过。

出现问题?请检查您的代码

完成本 Codelab 后,您已了解 Java 和 Dart 之间的一些差异。Dart 易于学习,此外,其核心库丰富的可用软件包可以提高您的工作效率。Dart 可针对大型应用轻松进行扩展。数百名 Google 工程师使用 Dart 编写为 Google 带来主要收入的任务关键型应用。

后续步骤

20 分钟的 Codelab 不足以向您展示 Java 和 Dart 之间的所有区别。例如,本 Codelab 尚未涵盖以下方面:

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

了解详情

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

文章

资源

网站