Dart基础语法之基础表达式

225 阅读17分钟

当前版本:3.5.0 (stable)

一、基础表达式

1.1 变量

下面是创建并初始化变量的例子:

变量会保存引用。name 变量包含一个值为 "dart" 的 String 对象的引用。

var name = 'dart';

变量 name 的类型被推断为 String,但你可以通过指定类型来更改它。如果一个对象不受限于单一类型,可以指定为 Object 类型(或在必要时使用 dynamic)。

Object name = 'dart';
// 另一种选择是显式声明将要被推断的类型
String nameStr = 'dart';
空安全

空安全能够防止意外访问 null 的变量而导致的错误。这样的错误也被称为空解引用错误。访问一个求值为 null 的表达式的属性或调用方法时,会发生空解引用错误。但是对于 toString() 方法和 hashCode 属性,空安全会体现出例外情况。 Dart 编译器可以在空安全的基础上在编译期检测到这些潜在的错误。

空安全引入了三个关键更改:

  1. 当你为变量、参数或另一个相关组件指定类型时,可以控制该类型是否允许 null 。要让一个变量可以为空,你可以在类型声明的末尾添加 ?
String name   //可空类型。可以是“null”或字符串。
String name    //不可为null的类型。不能为“null”,但可以为字符串。

2. 你必须在使用变量之前对其进行初始化。可空变量是默认初始化为 null 的。 Dart 不会为非可空类型设置初始值,它强制要求你设置初始值。 Dart 不允许你观察未初始化的变量。这可以防止你在接收者类型可以为 nullnull 不支持的相关方法或属性的情况下使用它。 3. 你不能在可空类型的表达式上访问属性或调用方法。同样的例外情况适用于 null 支持的属性或方法,例如 hashCodetoString()

空安全将潜在的 运行时错误 转变为 编译时 分析错误。当非空变量处于以下任一状态时,空安全会识别该变量:

  • 未使用非空值进行初始化。
  • 赋值为 null

此检查允许你在部署应用程序之前修复这些错误。

默认值

具有可空类型的未初始化变量的初始值为 null 。即使是具有数值类型的变量,初始值也为空,因为数字(就像 Dart 中的其他所有东西一样)都是对象。

int? lineCount;
assert(lineCount == null);
// tips 当你在生产环境中运行代码时,assert() 调用会被忽略。另外在开发过程中,assert(condition) 如果其 条件 为 false,会抛出一个异常。

对于空安全,你必须在使用非空变量之前初始化它们的值:

int lineCount = 0;

你不必在声明变量时初始化变量,但在使用之前需要为其赋值。例如以下代码是合法的,因为 Dart 可以检测到 lineCount 在传递给 print() 时是非空的:

int lineCount;

if (weLikeToCount) {
  lineCount = countLines();
} else {
  lineCount = 0;
}

print(lineCount);
延迟初始化变量

late 修饰符有两种用法:

  • 声明一个非空变量,但不在声明时初始化。
  • 延迟初始化一个变量。

通常 Dart 的语义分析可以检测非空变量在使用之前是否被赋值,但有时会分析失败。常见的两种情况是在分析顶级变量和实例变量时,Dart 通常无法确定它们是否已设值,因此不会尝试分析。

如果你确定变量在使用之前已设置,但 Dart 推断错误的话,可以将变量标记为 late 来解决这个问题:

late String description;

void main() {
  description = 'hellodart!';
  print(description);
}
//如果你没有初始化一个 late 变量,那么当变量被使用时会发生运行时错误。

当一个 late 修饰的变量在声明时就指定了初始化方法,那么内容会在第一次使用变量时运行初始化。这种延迟初始化在以下情况很方便:

  • (Dart 推断)可能不需要该变量,并且初始化它的开销很高。
  • 你正在初始化一个实例变量,它的初始化方法需要调用 this

在下面的例子中,如果 temperature 变量从未被使用,则 readThermometer() 这个开销较大的函数也永远不会被调用:

late String temperature = readThermometer();
终值 (final) 和常量 (const)

如果你不打算更改一个变量,可以使用 finalconst 修饰它,而不是使用 var 或作为类型附加。一个 final 变量只能设置一次,const 变量是编译时常量。(const 常量隐式包含了 final。)

实例变量可以是final但不能是const。

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

你不能修改 final 变量的值:

name = 'Alice'; // 会报错,final变量只能设置一次。

请使用const 修饰 编译时常量 的变量。如果const 变量位于类级别,请将其标记为static const(静态常量)。在声明变量的位置,将其值设置为编译时常量,比如数字、字符串、const 常量或在常量数字上进行的算术运算的结果:

const bar = 1000000; 
const double atm = 1.01325 * bar;

const 关键字不仅仅可用于声明常量,你还可以使用它来创建常量 值(values) ,以及声明 创建(create) 常量值的构造函数。任何变量都可以拥有常量值。

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`

你可以省略以 const 声明中的值的 const 修饰,就像上面的 baz 一样。

如果变量的值没有被 final 或者 const 修饰,即使它以前被 const 修饰,你也可以修改这个变量:

foo = [1, 2, 3]; 

虽然 final 对象不能被修改,但它的字段可能可以被更改。相比之下,const 对象及其字段不能被更改:它们是 不可变的

1.2操作符

Dart 支持下表所示的运算符。该表从高到低显示了 Dart 的运算符关联性和运算符优先级

使用运算符时,可以创建表达式。以下是一些运算符表达式的示例:

a++
a + b
a = b
a == b
c ? a : b
a is T
运算符优先级示例

在上图运算符表中,每个运算符的优先级都高于其后一行中的运算符。例如,乘法运算符的%优先级高于等式运算符(因此先于等式运算符执行)==,而等式运算符的优先级又高于逻辑与运算符&&。该优先级意味着以下两行代码的执行方式相同:

if ((n % i == 0) && (d % i == 0)) ...

if (n % i == 0 && d % i == 0) ...

对于采用两个操作数的运算符,最左边的操作数决定使用哪种方法。例如,如果您有一个Vector对象和一个Point对象,则aVector + aPoint使用Vector加法 ( +)。

算术运算符

Dart 支持常见的算术运算符,如下表所示。

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 还支持前缀和后缀的增量和减量运算符。

int a;
int b;

a = 0;
b = ++a; // 在给b赋值之前递增 a。
assert(a == b); // 1 == 1

a = 0;
b = a++; // 现将a的初始值赋值给b后,将 a 递增。
assert(a != b); // 1 != 0

a = 0;
b = --a; // 在给b赋值之前递减 a。
assert(a == b); // -1 == -1

a = 0;
b = a--; // 现将a的初始值赋值给b后,将 a 递减。
assert(a != b); // -1 != 0
相等和关系运算符

下表列出了相等和关系运算符的含义。

要测试两个对象 x 和 y 是否表示同一事物,请使用 == 运算符。 (在极少数情况下,您需要知道两个对象是否是完全相同的对象,请使用 identical() 函数。)以下是 == 运算符的工作原理:

  1. 如果xy为空,则如果两者都为空则返回 true,如果只有一个为空则返回 false。
  2. 返回使用参数y在x==上调用方法的结果。==

以下是使用每个相等和关系运算符的示例:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);
类型测试运算符

asis和运算符is!对于在运行时检查类型非常方便。


obj is T如果obj实现了指定的接口,则的结果为trueT。例如,obj is Object?始终为true。

as当且仅当您确定对象属于该类型时,才使用该运算符将对象转换为特定类型。例如:

(employee as Person).firstName = 'Bob';

如果不确定该对象是否属于类型T,请is T在使用该对象之前检查其类型。

if (employee is Person) {
  // 类型检查
  employee.firstName = 'Bob';
}

//代码并不等效。如果employee为 null 或不为Person,则第一个例子会引发异常;第二个示例不执行任何操作。
赋值运算符

可以使用=运算符分配值。如果仅当赋值变量为空时才分配,请使用运算??=符。

a = value;
// 如果 b 为 null,则将值赋给 b;否则,b 保持不变
b ??= value;

复合赋值运算符,例如+=将运算与赋值相结合

复合赋值运算符的工作原理如下:

下面的示例使用赋值和复合赋值运算符:

var a = 2; 
a *= 3; // 赋值和乘法: a = a * 3
assert(a == 6);
逻辑运算符

您可以使用逻辑运算符反转或组合布尔表达式。

以下是使用逻辑运算符的示例:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}
按位运算符和移位运算符

您可以在 Dart 中操作数字的各个位。通常,您会将这些按位运算符和移位运算符与整数一起使用。

具有大操作数或负操作数的位运算行为在不同平台之间可能有所不同。要了解更多信息,请查看 位运算平台差异

以下是使用按位和移位运算符的示例:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR

assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right

// Shift right example that results in different behavior on web
// because the operand value changes when masked to 32 bits:
assert((-value >> 4) == -0x03);

assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >>> 4) > 0); // Unsigned shift right
条件表达式

Dart 有两种运算符,可以替代if-else语句的表达式:
condition `` ? `` expr1 `` : ``expr2

如果条件为真,则计算expr1 的值(并返回其值);否则,计算并返回 expr2的值。

expr1 `` ?? ``expr2

如果expr1非空,则返回其值;否则,计算并返回expr2的值。

当您需要根据布尔表达式分配值时,请考虑使用条件运算符?:

var visibility = isPublic ? 'public' : 'private';

如果布尔表达式测试是否为空,请考虑使用运算符?? (也称为空合并运算符)。

String playerName(String? name) => name ?? 'Guest';
级联表示法

级联(..?..)允许您对同一对象进行一系列操作。除了访问实例成员之外,您还可以在同一对象上调用实例方法。这通常可以为您省去创建临时变量的步骤,并允许您编写更流畅的代码。

考虑以下代码:

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

构造函数Paint()返回一个Paint对象。级联表示法后面的代码会对该对象进行操作,忽略可能返回的任何值。

前面的示例等效于此代码:

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

如果级联操作的对象可以为空,则对第一个操作使用空短级联 (?.. )。以 开头可确保不会对该空对象尝试任何级联操作。

querySelector('#confirm') // Get an object.
  ?..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'))
  ..scrollIntoView();

上述代码等效于下面的代码:

var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();

您还可以嵌套级联。例如:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在返回实际对象的函数上构造级联时要小心。例如,以下代码会失败:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // Error: method 'write' isn't defined for 'void'.

sb.write()调用返回 void,我们无法在void上构建级联。

扩展运算符

用于将一个集合(如List、Set、Map)的所有元素扩展到另一个集合中的一种语法糖

扩展运算符实际上并不是运算符表达式。语法是集合字面量本身的一部分。

因为它不是运算符,所以语法没有任何“运算符优先级”。实际上,它具有最低的“优先级”——任何类型的表达式都可以作为扩展目标,例如:

  var list1 = [1, 2, 3];
  var list2 = [4, 5, 6];
  // 使用扩展运算符将list1的元素添加到list3中
  var list3 = [...list1, ...list2, 7, 8];
  print(list3); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]
其他运算符

1.3注释

Dart 支持单行注释、多行注释和文档注释。

单行注释

单行注释以 开头////Dart 编译器会忽略 和 行末之间的所有内容。

void main() {
  // TODO: 这是单行注释
  print('Welcome to my Llama farm!');
}
多行注释

多行注释以 开头/*并以 结尾*/。 Dart 编译器会忽略/*和之间的所有内容*/。多行注释可以嵌套。

void main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}
文档注释

文档注释是多行或单行注释,以///或开头/**。在连续的行上使用///具有与多行文档注释相同的效果。在文档注释中,分析器会忽略所有文本,除非文本括在括号中。使用括号,您可以引用类、方法、字段、顶级变量、函数和参数。括号中的名称在记录的程序元素的词法范围内解析。

以下是引用其他类和参数的文档注释的示例:

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
///
/// Just like any other animal, llamas need to eat,
/// so don't forget to [feed] them some [Food].
class Llama {
  String? name;

  /// Feeds your llama [food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

1.4 元数据

使用元数据提供有关代码的其他信息。元数据注释以字符 开头@,后跟对编译时常量的引用(例如deprecated)或对常量构造函数的调用。

所有 Dart 代码都有四个注释: @Deprecated@deprecated@override@pragma

class Television {
  /// 使用 [turnOn] to turn the power on instead.
  @Deprecated('Use turnOn instead')
  void activate() {
    turnOn();
  }

  /// 打开电视电源。
  void turnOn() {...}
  // ···
}

@deprecated 被弃用的

含义:若某类或某方法加上该注解之后,表示此方法或类不再建议使用,调用时也会出现删除线,但并不代表不能用,只是说,不推荐使用,因为还有更好的方法可以调用。

@override重写

帮助自己检查是否正确的复写了父类中已有的方法;告诉读代码的人,这是一个复写的方法

@pragma

是一个用于向 Dart 编译器提供特殊指令的注解。它通常用于优化性能、控制生成代码的行为,或者是告诉编译器忽略某些特定的警告。@pragma 注解可以用于函数、类、字段等多种场景。

元数据可以出现在库、类、typedef、类型参数、构造函数、工厂、函数、字段、参数或变量声明之前以及导入或导出指令之前

1.5 库和导入

指令 import 和 library 可以帮你创建一个模块化和可共享的代码库。库不仅提供 API,也是一个隐私单位:以下划线 (_) 开头的标识符只在库中可见。”每个 Dart 应用都是一个库“,即使它没有使用 library 指令。

使用库

使用 import 指令来指定一个库在其他库的作用域内如何被使用。

例如,Dart Web 应用程序通常使用dart:html 库,可以像这样导入:

import 'dart:html';

指令 import 唯一需要的参数是指定了这个库的 URI。对于内置库,URI 有特殊的 dart: 格式。对于其他更多的库,你可以使用一个文件系统路径或者 package: 格式。package: 格式指定由包管理器比如 pub 工具提供的库。比如

import 'package:test/test.dart';
指定库前缀

如果您导入两个具有冲突标识符的库,则可以为其中一个或两个库指定前缀。例如,如果 library1 和 library2 都具有 Element 类,则您可能会有如下代码:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();
仅导入库的一部分

如果只想使用库的一部分,则可以选择性地导入该库。例如:

// 仅导入 foo.
import 'package:lib1/lib1.dart' show foo;

// 导入所有除了 foo.
import 'package:lib2/lib2.dart' hide foo;
延迟加载库

延迟加载(也称为惰性加载)允许 Web 应用在需要库时按需加载库。当您想要满足以下一个或多个需求时,请使用延迟加载。

  • 减少 Web 应用程序的初始启动时间。
  • 执行 A/B 测试——例如,尝试算法的替代实现。
  • 加载很少使用的功能,例如可选的屏幕和对话框

仅 dart2js 支持懒加载。Fultter 和 Dart VM,还有 dartdevc 不支持懒加载

要懒加载一个库,你必须先使用 deferred as 导入它:

import 'package:greetings/hello.dart' deferred as hello;

当你需要这个库时,使用这个库的标识符调用 loadLibrary() 。

Future<void> greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

在上述代码中,await关键字暂停执行,直到库加载完成。

您可以loadLibrary()多次调用一个库而不会出现问题。该库仅加载一次。

使用延迟加载时请注意以下几点:

  • 延迟库的常量不是导入文件中的常量。请记住,这些常量在延迟库加载之前不存在。
  • 您不能在导入文件中使用延迟库中的类型。相反,请考虑将接口类型移至延迟库和导入文件都导入的库中。
  • Dart 隐式地插入loadLibrary()到您使用 定义的命名空间中。该函数返回一个。deferred as namespace``loadLibrary()Future
library指令

要指定库级文档注释元数据注释,请将它们附加到library文件开头的声明中。

/// A really great test library.
@TestOn('browser')
library;
实现库

要获取关于如何实现一个库包的建议,包括:

  • 如果组织库中的源代码。
  • 如果使用 export 指令。
  • 何时使用 part 指令。
  • 如何使用条件导入和导出来实现支持多平台的库。

1.6 关键词

下表列出了 Dart 语言为自己保留的单词。除非另有说明,否则这些单词不能用作标识符。即使允许,使用关键字作为标识符也会使阅读代码的其他开发人员感到困惑,应避免这样做。要了解有关标识符用法的更多信息,请单击术语

  • 带角标 1 的是 上下文关键词,它们只在特定的地方有有意义。除此之外他们在所有地方都是合法的关键词。
  • 带角标 2 的是 内置标识符。为了简化将JavaScript代码移植到Dart的任务,这些关键字在大多数地方都是有效的标识符,但它们不能用作类或类型名称,也不能用作导入前缀
  • 带角标 **3该关键字可以无限制地用作标识符