Classes(类)
类(class)用于描述一类对象共同拥有的数据和行为,对象(object)则是类在运行时创建的实例。Dart 中能够赋给变量的值都是对象,每个对象都是某个类的实例;数字、字符串、函数以及 null 也都是对象。
学习 Dart 类时,重点需要掌握以下问题:
- 如何声明类并创建对象?
- 实例变量和实例方法分别负责什么?
- 构造函数如何初始化不可空字段?
this、getter、setter 和私有成员如何使用?- 实例成员和静态成员有什么区别?
- 两个内容相同的对象为什么不一定相等?
声明类并创建对象
使用 class 声明类。类中可以定义保存状态的实例变量,以及操作这些状态的实例方法:
class Rectangle {
double width;
double height;
Rectangle(this.width, this.height);
double area() {
return width * height;
}
}
void main() {
final rectangle = Rectangle(4, 3);
print(rectangle.width); // 4.0
print(rectangle.area()); // 12.0
}
上例中:
Rectangle是类名。width和height是每个实例各自保存的实例变量。Rectangle(this.width, this.height)是构造函数,负责初始化实例变量。area()是实例方法,可以直接访问当前对象的实例变量。Rectangle(4, 3)调用构造函数并创建一个Rectangle对象。
Dart 2 以后,调用构造函数时通常不需要写 new。即使写上 new Rectangle(4, 3) 仍是合法语法,实际代码一般使用更简洁的形式。
TypeScript 对比:两种语言都使用
class声明类,并通过点号访问实例成员。Dart 创建对象时直接调用类的构造函数,通常不写 TypeScript 必需的new。
实例变量
实例变量也称为字段(field)。每创建一个对象,都会得到一组属于该对象的实例变量:
class Counter {
int value = 0;
}
void main() {
final first = Counter();
final second = Counter();
first.value++;
print(first.value); // 1
print(second.value); // 0
}
修改 first.value 不会影响 second.value,因为两个对象保存的是不同状态。
初始化不可空字段
启用健全空安全后,不可空实例变量必须在对象创建完成前得到值。常用方式包括在声明处初始化,或通过构造函数初始化:
class User {
String name;
bool isActive = true;
User(this.name);
}
如果一个字段确实可以暂时没有值,应把类型声明为可空类型:
class User {
String name;
String? nickname;
User(this.name);
}
不要仅为了消除初始化错误就随意使用 late。late 表示开发者保证在读取前完成赋值;如果实际读取时尚未初始化,程序会在运行时抛出错误。
final 实例变量
对象创建后不应重新赋值的字段可以声明为 final,并由构造函数初始化:
class Article {
final String title;
final DateTime publishedAt;
Article(this.title, this.publishedAt);
}
final 只限制字段不能再次指向其他值,不会自动让字段引用的对象变成不可变对象。例如,final List<String> 仍然可以添加元素。
构造函数
构造函数与类同名,没有返回类型。前面的 Rectangle(this.width, this.height) 使用了初始化形式参数(initializing formal parameter),它会把传入的参数直接赋给同名实例变量。
普通写法与简写形式表达相同的初始化意图:
class Point {
final double x;
final double y;
Point(double x, double y)
: x = x,
y = y;
}
class Offset {
final double x;
final double y;
Offset(this.x, this.y);
}
当参数只用于初始化同名字段时,优先使用 this.x 这种简写。
命名构造函数
Dart 不支持按参数签名重载构造函数,但可以通过命名构造函数表达不同的创建方式:
class Point {
final double x;
final double y;
Point(this.x, this.y);
Point.origin()
: x = 0,
y = 0;
}
void main() {
final point = Point.origin();
print('(${point.x}, ${point.y})'); // (0.0, 0.0)
}
Point.origin() 的名称直接说明了创建意图。一个类可以声明多个命名构造函数,但每个名称必须唯一。
const 构造函数
当类的所有实例变量都是 final,并且构造过程可以在编译期完成时,可以声明 const 构造函数:
class Color {
final int red;
final int green;
final int blue;
const Color(this.red, this.green, this.blue);
}
void main() {
const first = Color(255, 0, 0);
const second = Color(255, 0, 0);
print(identical(first, second)); // true
}
在常量上下文中,参数完全相同的常量对象会被规范化为同一个实例。const 构造函数允许创建常量对象,但调用处是否产生编译时常量,还取决于调用是否处于常量上下文或是否显式使用 const。
实例方法与 this
实例方法在某个对象上调用,可以读取和修改该对象的实例变量:
class BankAccount {
double balance;
BankAccount(this.balance);
void deposit(double amount) {
if (amount <= 0) {
throw ArgumentError.value(amount, 'amount', 'must be positive');
}
balance += amount;
}
}
void main() {
final account = BankAccount(100);
account.deposit(50);
print(account.balance); // 150.0
}
在实例方法中,未加限定的 balance 会解析为当前对象的字段。this 表示当前对象;只有在需要消除名称歧义或明确传递当前对象时才需要显式写出:
class Profile {
String name;
Profile(this.name);
void rename(String name) {
this.name = name;
}
}
这里参数和字段都叫 name,因此使用 this.name 指向实例变量。
Getter 与 setter
Dart 会为普通实例变量隐式提供 getter,并为非 final 变量隐式提供 setter。也可以使用 get 和 set 定义计算属性或集中校验赋值:
class Temperature {
double _celsius;
Temperature(this._celsius);
double get celsius => _celsius;
double get fahrenheit => _celsius * 9 / 5 + 32;
set celsius(double value) {
if (value < -273.15) {
throw ArgumentError('Temperature is below absolute zero.');
}
_celsius = value;
}
}
void main() {
final temperature = Temperature(20);
print(temperature.fahrenheit); // 68.0
temperature.celsius = 25;
print(temperature.celsius); // 25.0
}
调用 getter 和 setter 时使用属性语法,不添加方法调用括号。getter 适合读取计算结果,setter 适合为赋值增加校验;包含明显动作或成本较高的操作通常应使用普通方法。
库级私有成员
Dart 没有 private 关键字。标识符以下划线 _ 开头时,它对当前库(library)私有:
class ApiToken {
final String _value;
ApiToken(this._value);
String get masked => '${_value.substring(0, 4)}****';
}
这里的 _value 只能在声明它的库中直接访问。私有边界是库,而不是类;同一个库中的其他声明仍然可以访问 _value。一个 Dart 文件通常就是一个库,但通过 part 组合的多个文件属于同一个库。
TypeScript 对比:TypeScript 可以使用
private或 JavaScript 的#field表示不同形式的私有成员。Dart 的_name是库级私有,没有完全等价的类级私有语法。
静态成员
使用 static 声明属于类本身的成员。静态成员通过类名访问,不依赖某个实例:
class Circle {
static const double pi = 3.141592653589793;
final double radius;
Circle(this.radius);
double area() => pi * radius * radius;
static bool isValidRadius(double radius) => radius > 0;
}
void main() {
print(Circle.pi);
print(Circle.isValidRadius(2)); // true
final circle = Circle(2);
print(circle.area());
}
实例方法可以访问静态成员,但静态方法没有当前实例,不能直接访问 radius 或使用 this。与对象状态无关、并且在概念上属于该类的常量或工具方法,才适合声明为静态成员。
对象的类型与成员访问
没有显式声明父类的自定义类会隐式继承 Object,其实例可以使用 Object 提供的成员,例如 runtimeType、toString() 和 hashCode:
class Course {
final String title;
Course(this.title);
}
void main() {
final course = Course('Dart classes');
print(course.runtimeType); // Course
print(course is Course); // true
print(course.toString()); // 默认输出类似 Instance of 'Course'
}
需要根据类型分支时使用 is;不要依赖 runtimeType.toString() 的文本结果实现业务逻辑。
可空对象的成员访问
可空对象可能为 null,因此不能直接访问实例成员。可以使用空判断操作符 ?.:
class User {
final String name;
User(this.name);
}
void main() {
User? currentUser;
print(currentUser?.name); // null
print(currentUser?.name ?? 'Guest'); // Guest
}
currentUser?.name 在对象非空时读取 name,为空时返回 null。只有在程序逻辑已经保证对象非空时才应使用非空断言 !。
对象相等性
默认情况下,自定义类的 == 判断两个引用是否指向同一个对象。字段内容相同的两个普通实例不一定相等:
class Point {
final int x;
final int y;
Point(this.x, this.y);
}
void main() {
final first = Point(1, 2);
final second = Point(1, 2);
print(first == second); // false
print(identical(first, second)); // false
}
如果业务语义要求按字段值比较,需要同时重写 == 和 hashCode:
class Point {
final int x;
final int y;
const Point(this.x, this.y);
bool operator ==(Object other) {
return identical(this, other) ||
other is Point && x == other.x && y == other.y;
}
int get hashCode => Object.hash(x, y);
}
void main() {
final first = Point(1, 2);
final second = Point(1, 2);
print(first == second); // true
}
相等的对象必须返回相同的 hashCode,否则它们在 Set 和 Map 键等哈希集合中的行为会不一致。类中包含可变字段时,应谨慎使用这些字段计算相等性和哈希值,因为对象加入哈希集合后再改变哈希相关字段会破坏查找结果。
常见误区
误区一:认为 final 对象不能修改
final 限制变量不能重新赋值,不会冻结对象:
class Counter {
int value = 0;
}
void main() {
final counter = Counter();
counter.value++; // 合法:修改对象内部状态
// counter = Counter(); // 编译错误:不能重新赋值 final 变量
}
误区二:让不可空字段保持未初始化
不可空字段必须在声明处、初始化列表或构造函数初始化形式参数中完成初始化。字段确实可缺失时使用 T?;能够保证稍后初始化时才考虑 late。
误区三:把 _name 理解为类级私有
以下划线开头的名称对库私有,同一个库中的其他类仍然可以访问它。需要设计封装边界时,应同时考虑库和文件的组织方式。
误区四:通过实例访问静态成员
静态成员属于类,应写成 Circle.pi,而不是通过 circle 实例访问。这样也能让调用处明确表达成员不依赖对象状态。
误区五:只重写 == 而忽略 hashCode
自定义值相等语义时必须同时维护 == 和 hashCode 的一致性。否则对象作为 Set 元素或 Map 键时可能出现难以理解的结果。
小结
- 类定义对象的数据和行为,对象是类在运行时创建的实例。
- 实例变量属于具体对象;不可空字段必须在对象创建完成前初始化。
- 构造函数与类同名,初始化形式参数可以简化字段赋值;命名构造函数用于表达不同创建方式。
const构造函数可以创建编译时常量对象,但要求实例状态满足常量约束。- 实例方法通过当前对象工作;
this只在消除歧义等场景下需要显式书写。 - getter 和 setter 使用属性访问语法;以下划线开头的名称对当前库私有。
- 静态成员属于类本身,静态方法不能直接访问实例状态。
- 默认相等性通常比较对象身份;按字段比较时必须同时重写
==和hashCode。