跳到主要内容

Dart 的函数、类与运算符

本文将介绍 Dart 编程中的函数、类与运算符的基本概念和用法,帮助零基础的读者快速入门 Dart 语言。

函数

函数是一段用于独立完成某个功能的代码。在 Dart 中,所有类型都是对象,包括函数。函数的类型叫作 Function,这意味着函数也可以被定义为变量,甚至可以作为参数传递给另一个函数。

函数的定义和使用

以下是一个简单的函数示例:

bool isZero(int number) {
return number == 0;
}

void printInfo(int number, Function check) {
print("$number is Zero: ${check(number)}");
}

Function f = isZero;
int x = 10;
int y = 0;
printInfo(x, f); // 输出 10 is Zero: false
printInfo(y, f); // 输出 0 is Zero: true

可以使用箭头函数来简化单行函数的定义:

bool isZero(int number) => number == 0;

可选参数和命名参数

Dart 不支持函数重载,但提供了可选命名参数和可选参数的方式,使函数的参数声明更加优雅和可维护。

  • 可选命名参数使用 {} 包裹,可选命名参数使用 {} 包裹。调用函数时,可以通过 paramName: value 的方式来传递参数。未传递的参数将为 null。:
void enableFlags({bool bold, bool hidden}) {
print("$bold, $hidden");
}

enableFlags(bold: true, hidden: false); // 输出 true, false
enableFlags(bold: true); // 输出 true, null
  • 命名参数的默认值 在定义函数时,可以为可选命名参数提供默认值,这样即使调用时未传递该参数,也会使用默认值:
void enableFlags({bool bold = true, bool hidden = false}) {
print("$bold, $hidden");
}

enableFlags(); // 输出 true, false
enableFlags(bold: false); // 输出 false, false
enableFlags(hidden: true); // 输出 true, true
  • 可选参数使用 []包裹,调用函数时,可以省略这些参数。未传递的参数将为 null。:
void enableFlags(bool bold, [bool hidden]) {
print("$bold, $hidden");
}

enableFlags(true, false); // 输出 true, false
enableFlags(true); // 输出 true, null
  • 可选参数的默认值 在定义函数时,可以为可选参数提供默认值,这样即使调用时未传递该参数,也会使用默认值:
void enableFlags(bool bold, [bool hidden = false]) {
print("$bold, $hidden");
}

enableFlags(true, false); // 输出 true, false
enableFlags(true); // 输出 true, false

可选命名参数与可选参数的区别

可选命名参数 使用 {} 包裹,调用时必须使用 paramName: value 的方式,参数顺序可以不固定,且可读性高。

可选参数 使用 [] 包裹,调用时按顺序传递参数,省略的参数将为 null,相比之下可读性较低,但更简洁。

综合示例:

void describePerson(String name, {int age = 30, String gender = 'unknown'}) {
print("Name: $name, Age: $age, Gender: $gender");
}

describePerson('Alice'); // 输出 Name: Alice, Age: 30, Gender: unknown
describePerson('Bob', age: 25); // 输出 Name: Bob, Age: 25, Gender: unknown
describePerson('Charlie', gender: 'male'); // 输出 Name: Charlie, Age: 30, Gender: male

void enableFeatures(String feature, [bool isEnabled = true, String level = 'basic']) {
print("Feature: $feature, Enabled: $isEnabled, Level: $level");
}

enableFeatures('Dark Mode'); // 输出 Feature: Dark Mode, Enabled: true, Level: basic
enableFeatures('Notifications', false); // 输出 Feature: Notifications, Enabled: false, Level: basic
enableFeatures('Security', true, 'advanced'); // 输出 Feature: Security, Enabled: true, Level: advanced

类是特定类型的数据和方法的集合,也是创建对象的模板。Dart 是面向对象的语言,每个对象都是一个类的实例,都继承自顶层类型 Object。

类的定义及初始化

以下是一个类的定义示例:

class Point {
num x, y; // 成员变量
static num factor = 0; // 静态变量
// 构造函数
Point(this.x, this.y);
// 实例方法
void printInfo() => print('($x, $y)');
// 静态方法
static void printZValue() => print('$factor');
}
// 创建类的实例
var p = Point(100, 200);
p.printInfo(); // 输出 (100, 200)
// 访问和修改静态变量
Point.factor = 10;
Point.printZValue(); // 输出 10

详解:

  • 成员变量:xy 是实例变量,每个实例都有独立的这些变量。
  • 静态变量:factor 是静态变量,属于类本身,而不是某个具体的实例。
  • 构造函数:Point(this.x, this.y) 是类的构造函数,用于初始化成员变量 xythis.x 表示当前实例的 x 变量。
  • 实例方法:printInfo() 是实例方法,用于打印实例的 xy 值。
  • 静态方法:printZValue() 是静态方法,只能访问静态变量 factor

命名构造函数与初始化列表

在 Dart 中,类的构造函数不仅用于创建对象,还可以用来初始化对象的成员变量。Dart 提供了命名构造函数和初始化列表来实现灵活的对象初始化。

  • 命名构造函数

    命名构造函数:允许为类定义多个构造函数,以不同的方式初始化对象。命名构造函数通过类名后面的点号和构造函数名称来定义。

  • 初始化列表

    初始化列表用于在构造函数体执行之前初始化成员变量。初始化列表在构造函数参数列表之后、构造函数体之前,用冒号 : 分隔。

class Point {
num x, y, z;
// 默认构造器 使用初始化列表初始化变量 z
Point(this.x, this.y) : z = 0;
// 命名构造函数
Point.bottom(num x) : this(x, 0);
// 实例方法
void printInfo() => print('($x, $y, $z)');
}
// 使用命名构造函数创建实例
var p = Point.bottom(100);
p.printInfo(); // 输出 (100, 0, 0)

详解:

  • 初始化列表:在默认构造函数 Point(this.x, this.y) 后面使用 : z = 0 初始化成员变量 z
    Point(this.x, this.y) : z = 0;
  • 命名构造函数:Point.bottom(num x) 是命名构造函数,它调用默认构造函数 Point(x, 0) 来初始化对象,将 x 初始化为传入的参数 x,y 初始化为 0。
    Point.bottom(num x) : this(x, 0);

类的复用

在面向对象的编程中,类的复用是通过继承、接口实现和混入 (Mixin) 来实现的。Dart 提供了这些机制来促进代码的复用和组织。

继承

继承是复用代码的最常见方式,子类从父类继承成员变量和方法。子类可以重写父类的方法,也可以添加自己的新功能。

class Point {
num x = 0, y = 0;

void printInfo() => print('($x, $y)');
}

// Vector 继承自 Point
class Vector extends Point {
num z = 0;

@override
void printInfo() => print('($x, $y, $z)');
}

var v = Vector();
v.x = 3;
v.y = 4;
v.z = 5;
v.printInfo(); // 输出 (3, 4, 5)

详解:

  • 父类 (Point):定义了成员变量 xy,以及实例方法 printInfo
  • 子类 (Vector):继承自 Point,添加了新的成员变量 z,并重写了 printInfo 方法。

接口实现

Dart 中的接口是隐式的,任何类都可以作为接口实现。实现接口的类必须提供接口中定义的所有方法。

class Point {
num x = 0, y = 0;

void printInfo() => print('($x, $y)');
}

// Coordinate 实现了 Point 接口
class Coordinate implements Point {
num x = 0, y = 0;

@override
void printInfo() => print('($x, $y)');
}

var c = Coordinate();
c.x = 5;
c.y = 10;
c.printInfo(); // 输出 (5, 10)

解释:

  • 接口 (Point):定义了成员变量 xy,以及实例方法 printInfo
  • 实现类 (Coordinate):实现了 Point 接口,必须定义接口中的所有成员。

混入 (Mixin)

混入是 Dart 提供的一种机制,用于在类之间共享行为,而不需要继承。混入使用 with 关键字,将一个类的功能添加到另一个类中。

class Point {
num x = 0, y = 0;

void printInfo() => print('($x, $y)');
}

mixin Z {
num z = 0;

void printZInfo() => print('z = $z');
}

// 使用混入
class Vector with Point, Z {
@override
void printInfo() => print('($x, $y, $z)');
}

var v = Vector();
v.x = 3;
v.y = 4;
v.z = 5;
v.printInfo(); // 输出 (3, 4, 5)
v.printZInfo(); // 输出 z = 5

解释:

  • 父类 (Point):定义了成员变量 xy,以及实例方法 printInfo
  • 混入 (Mixin Z):定义了成员变量 z 和方法 printZInfo
  • 子类 (Vector):通过 with 关键字混入了 PointZ 的行为,重写了 printInfo方法。

复用机制的选择

  • 继承:用于需要复用父类方法实现的场景。子类会继承父类的所有成员,并可以重写父类的方法。
  • 接口实现:用于需要定义接口并强制实现其所有方法的场景。实现类只需要提供接口的方法实现。
  • 混入 (Mixin):用于在类之间共享行为而不需要继承的场景。混入可以避免多重继承的复杂性,提供灵活的代码复用方式。

运算符

Dart 提供了与其他编程语言类似的运算符,并增加了几个用于简化处理变量实例缺失的运算符:

  • ?. 运算符:用于在调用对象的成员方法时避免空指针异常。
Point p;
p?.printInfo(); // 如果 p 为 null,则跳过调用
  • ??= 运算符:用于在变量为 null 时赋值。
var a;
a ??= 10; // 如果 a 为 null,则赋值 10
  • ?? 运算符:用于在变量为 null 时返回另一个值。
var a;
var b = a ?? 10; // 如果 a 为 null,则 b 为 10

运算符重载

Dart 允许用户自定义运算符的实现。以下是一个自定义 + 运算符和重载 == 运算符的示例:

class Vector {
num x, y;
Vector(this.x, this.y);

Vector operator +(Vector v) => Vector(x + v.x, y + v.y);

bool operator ==(dynamic v) => x == v.x && y == v.y;
}

final x = Vector(3, 3);
final y = Vector(2, 2);
final z = Vector(1, 1);
print(x == (y + z)); // 输出 true

构造函数执行顺序

当一个类继承自另一个类时,构造函数的执行顺序是先调用父类的构造函数,然后再调用子类的构造函数。这保证了父类的初始化逻辑先于子类执行.

如果父类和子类有多个构造函数,可以通过在子类构造函数中显式调用父类的指定构造函数来确保正确的初始化顺序。

class Parent {
String name;

// 默认构造函数
Parent(this.name) {
print('Parent constructor: $name');
}

// 命名构造函数
Parent.withoutName() : name = 'No Name' {
print('Parent constructor without name');
}
}

class Child extends Parent {
int age;

// 子类构造函数调用父类默认构造函数
Child(String name, this.age) : super(name) {
print('Child constructor: $name, $age');
}

// 子类构造函数调用父类命名构造函数
Child.withoutParentName(this.age) : super.withoutName() {
print('Child constructor without parent name, age: $age');
}
}

void main() {
var c1 = Child('Alice', 10);
// 输出:
// Parent constructor: Alice
// Child constructor: Alice, 10

var c2 = Child.withoutParentName(15);
// 输出:
// Parent constructor without name
// Child constructor without parent name, age: 15
}

解释:

  • 父类构造函数:Parent 类有一个默认构造函数 Parent(this.name) 和一个命名构造函数 Parent.withoutName()
  • 子类构造函数:Child 类有一个默认构造函数 Child(String name, this.age),通过 super(name) 调用父类默认构造函数;还有一个命名构造函数 Child.withoutParentName(this.age),通过 super.withoutName() 调用父类的命名构造函数。