Dart 之方法、操作符与可调用对象

前言

同一张公交卡,既可以在闸机上"刷一下",也可以在手机应用里查看余额。卡片本身保存着余额,闸机负责读取它,应用则可能提供统一的查询入口。看起来都是"调用",实际是在问不同的对象做不同的事。

Dart 里也有几种容易混在一起的写法:普通方法由某个对象完成,静态成员属于整个类,操作符让两个对象可以用 + 或 == 表达关系,可调用对象则让一个对象像函数一样被调用。它们都能减少重复代码,但负责的问题不同。

如果没有分清这些关系,就容易把不属于某个对象的功能写成实例方法,把两个对象的比较写成难懂的普通方法,或者看到括号就以为一定是在调用函数。把"谁在做事、数据属于谁、调用表达什么"分清,代码才会自然。

一、先分清方法属于谁

方法是写在类中的函数,用来提供对象的行为。 普通实例方法通过对象调用,可以读取这个对象的实例字段,也可以调用它的其他实例成员。

dart 复制代码
class Counter {
  Counter(this.value);

  int value;

  void increase() {
    value++;
  }
}

void main() {
  final counter = Counter(1);
  counter.increase();
  print(counter.value);
}

输出 2。调用 counter.increase() 时,方法操作的是 counter 的 value;换成另一个 Counter 对象,操作的就是另一个对象的 value。

这里的关键不是"方法写在类里",而是实例方法的工作对象由调用它的实例决定。 同一份方法代码,可以服务许多对象,但每次使用的实例数据不同。

二、静态成员属于整个类

静态成员属于类本身,不属于某一个实例。 在声明前加 static,可以定义类级别的字段或方法,通过类名调用它们。

dart 复制代码
class Temperature {
  static const freezingPoint = 0;
  static int createdCount = 0;

  Temperature(this.celsius) {
    createdCount++;
  }

  final double celsius;

  static bool isFreezing(double value) {
    return value <= freezingPoint;
  }
}

void main() {
  final first = Temperature(18);
  final second = Temperature(-2);

  print(Temperature.createdCount);
  print(Temperature.isFreezing(second.celsius));
  print(first.celsius);
}

输出依次为 2、true 和 18.0。createdCount 记录所有 Temperature 对象的数量,不属于 first 或 second 中的某一个;isFreezing 只需要传入一个温度,也不需要知道某个 Temperature 对象是谁,所以从类名调用更合适。

2.1 静态方法为什么不能直接使用 this

this 表示当前实例。静态方法是通过类调用的,没有一个"当前实例",所以不能在静态方法中直接读取实例字段或使用 this。

dart 复制代码
class User {
  User(this.name);

  final String name;

  static String greeting(String name) {
    return '你好,$name';
  }
}

void main() {
  final user = User('小林');
  print(User.greeting(user.name));
}

greeting 使用的是参数 name,不是某个隐含的用户对象。若一个方法必须直接读取当前用户的 name,就应写成实例方法,再通过 user.greeting() 调用。

静态方法可以访问同一个类的静态成员,但不能把静态成员当成实例成员来使用。静态字段适合保存所有实例共享的状态或常量;如果状态应该分别属于每个对象,就不要放到 static 中。Dart 类变量和静态方法

2.2 静态方法和顶层函数怎么选

一个工具函数不一定要放进类里。若它不需要类名来表达归属,也不需要访问类的静态状态,顶层函数通常更直接。

dart 复制代码
double celsiusToFahrenheit(double celsius) {
  return celsius * 9 / 5 + 32;
}

void main() {
  print(celsiusToFahrenheit(20));
}

如果函数和某个类型的概念紧密相关,使用 Temperature.isFreezing() 可以让调用处看出它处理的是温度。若它是多个功能都会使用的通用转换,也可以保持为顶层函数。static 不是为了把所有函数都收进类里,而是为了表达"这个能力属于这个类的整体"。

三、操作符是有特殊写法的方法

Dart 中多数操作符实际上对应对象的方法,只是调用时使用了更自然的符号。 例如 a + b 可以理解为调用 a 的加法操作,让 a 根据 b 计算结果。

下面用二维点表示位移:

dart 复制代码
class Point {
  const Point(this.x, this.y);

  final int x;
  final int y;

  Point operator +(Point other) {
    return Point(x + other.x, y + other.y);
  }
}

void main() {
  const start = Point(2, 3);
  const move = Point(4, -1);
  final result = start + move;

  print('${result.x}, ${result.y}');
}

输出 6, 2。这里的加号不是把两个 Point 强行变成数字相加,而是由 Point 规定"两个点相加"代表坐标分别相加。

可重载的操作符必须使用 Dart 允许的名称,例如 +、-、*、/、%、<、>、<=、>=、==、[] 和 []= 等。声明时写 operator 加操作符名称。不是所有看起来像操作符的符号都能这样声明,例如 != 的行为由语言结合 == 处理,不能把它当作普通的自定义操作符来声明。Dart 操作符

四、操作符要表达清楚的对象关系

操作符会让代码更像数学表达式,但也更容易把含义藏起来。适合重载的情况通常是读者看到符号就能猜到含义,例如点的位移、金额的加法或集合的包含关系。

如果一个操作需要复杂业务流程,就应该使用有名字的方法。例如"计算订单最终价格"涉及优惠券、税费和库存,写成 order + coupon 往往比 order.calculateTotal(coupon) 更难读。

4.1 重写 == 时还要处理 hashCode

如果两个对象只要坐标相同就算相等,需要同时提供 hashCode,让它们在 Set 或 Map 中遵守相同的判断规则。

dart 复制代码
class Coordinate {
  const Coordinate(this.x, this.y);

  final int x;
  final int y;

  @override
  bool operator ==(Object other) {
    return other is Coordinate && other.x == x && other.y == y;
  }

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  const first = Coordinate(1, 2);
  const second = Coordinate(1, 2);

  print(first == second);
  final coordinates = <Coordinate>{};
  coordinates.add(first);
  coordinates.add(second);
  print(coordinates.length);
}

输出 true 和 1。相等的两个对象必须拥有相同的 hashCode,否则集合可能无法按预期识别重复值。反过来,hashCode 相同不代表两个对象一定相等,因为不同对象可能发生哈希碰撞。

操作符只应根据对象真正表示的数据计算结果。不要让 == 有时按编号比较、有时按显示文字比较,也不要让同一个对象的 hashCode 在放入集合后随可变字段改变。Object 相等与哈希约定

五、下标操作符让对象像容器一样访问

[] 和 []= 也是可以声明的操作符。它们适合表达"根据键读取或写入对象中的数据",但并不会自动让对象变成 List 或 Map。

dart 复制代码
class ScoreBoard {
  ScoreBoard(this._scores);

  final List<int> _scores;

  int operator [](int index) => _scores[index];

  void operator []=(int index, int score) {
    _scores[index] = score;
  }
}

void main() {
  final board = ScoreBoard([80, 90]);
  board[0] = 85;
  print(board[0]);
}

输出 85。调用 board[0] = 85 时,实际使用的是 []=;读取 board[0] 时,使用的是 []。边界检查仍由类自己负责,这里因为内部 List 会在下标超出范围时抛出错误。

如果对象不是容器,或者访问动作不能自然解释为"按键取值",就不要为了少写几个字符而定义下标操作符。清楚的方法名比看似简短的语法更重要。

六、让对象可以像函数一样调用

只要类定义了 call() 方法,它的实例就可以使用函数调用语法。 这不会把对象变成真正的顶层函数,而是让括号调用转交给这个对象的 call() 方法。

dart 复制代码
class AddTax {
  AddTax(this.rate);

  final double rate;

  double call(double price) {
    return (price * (1 + rate) * 100).round() / 100;
  }
}

void main() {
  final addTax = AddTax(0.13);
  print(addTax(100));
}

输出 113.0。addTax 是一个对象,它还保存了 rate;addTax(100) 看起来像调用函数,实际执行的是 addTax.call(100)。

可调用对象适合"既要保存配置,又要反复执行同一种操作"的场景,例如带固定规则的转换器、校验器或过滤器。若对象没有需要保存的状态,直接写普通函数通常更容易读。

call() 和普通函数一样,可以有参数、可选参数、命名参数和返回值。调用者只看到括号时,不一定知道对象内部保存了哪些配置,所以类名和变量名应表达清楚用途,避免把任意对象都包装成看不出含义的调用形式。Dart 可调用对象

七、四种写法怎样选择

可以先问三个问题:行为需要哪个对象的数据,状态是所有实例共享还是各自独立,调用处是否有一种自然且不会误解的表达。

需求 合适的写法 调用形式
操作某个对象自己的数据 实例方法 account.deposit(100)
不依赖具体实例,属于类型整体 静态成员或顶层函数 Temperature.isFreezing(0)
两个值之间有清楚、直观的符号关系 操作符 first + second
对象保存配置,之后反复执行同一动作 可调用对象 validator(input)

同一功能有时有不止一种写法。例如温度转换可以是顶层函数,也可以是静态方法;选择时看调用处是否需要突出类型归属。操作符和可调用对象都能让代码变短,但如果读者必须跳回类定义才能猜到含义,就应该改用有名字的方法。

八、常见误区

8.1 把共享状态误放进实例

如果想统计所有对象的数量,应该使用静态字段;如果想记录每个对象自己的状态,应该使用实例字段。把实例状态放进 static,会让所有对象互相覆盖;把共享计数放进实例,则每个对象都会得到一份独立数字。

8.2 以为 static 方法能自动知道当前对象

static 方法没有 this。需要哪个对象,就把它作为参数传入;如果方法总是围绕一个对象工作,就改成实例方法。

8.3 为了炫技重载操作符

操作符的优势是表达熟悉的关系,不是隐藏业务流程。一个符号如果有多种解释,或者操作会产生明显副作用,就用普通方法写出动作名称。

8.4 把可调用对象当成普通函数的替代品

可调用对象的价值在于保存状态和配置。没有状态时,普通函数通常更容易发现、测试和复用;有状态时,也要让类名说明这个对象会执行什么。

九、总结

实例方法操作调用它的对象,能够使用当前实例的数据;静态成员属于类整体,没有当前实例,适合共享状态、常量和不依赖实例的能力。与类无紧密归属的通用功能,可以写成顶层函数。

操作符是特殊命名的方法,只有在符号关系自然、含义稳定时才适合使用。重写 == 时同步处理 hashCode,使用 [] 和 []= 时明确对象确实像容器一样工作。

定义 call() 后,对象可以像函数一样调用。它适合保存规则后反复执行;没有需要保存的状态时,普通函数往往更直接。最终选择的标准是:读者能否从调用处理解行为属于谁、数据从哪里来、结果代表什么。

十、测试题

  1. counter.increase() 修改的是哪一个 Counter 对象的数据?同一个方法为什么可以服务多个 Counter 实例?
  2. Temperature.createdCount 为什么通过类名访问?如果把它改成实例字段,会发生什么?
  3. 静态方法中为什么不能直接使用 this?如果一个方法需要读取当前用户的 name,应该改成什么形式?
  4. start + move 在 Point 示例中表达了什么?什么样的业务操作不适合重载成操作符?
  5. 重写 == 后为什么通常还要重写 hashCode?如果两个对象 hashCode 相同,是否一定相等?
  6. board[0] = 85 和 board[0] 分别会调用哪个操作符?对象定义 [] 就自动拥有 List 的所有能力吗?
  7. addTax(100) 实际调用了什么?可调用对象和普通函数各适合什么情况?
  8. "统计所有实例数量""读取某个账户余额""把两个点相加""按固定规则校验输入"分别更适合静态成员、实例方法、操作符还是可调用对象?说明理由。
相关推荐
恋猫de小郭27 分钟前
Flutter Impeller Vulkan 内存持续上涨,问题没想到是文字阴影
android·前端·flutter
张小姐的猫11 小时前
【MySQL】访问 & 数据流动原理
android·数据库·mysql·adb·1024程序员节
朱涛的自习室14 小时前
212 个需求、22 万行代码:Agent 接管开源项目的 19 天
android·前端·人工智能
传奇开心果19 小时前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
android·flutter
哲鲤19 小时前
AOSP思绎|Android 多媒体:一段视频为什么需要这么多服务
android
阿杰m19 小时前
Android源码第三章:Handler、Looper 与 MessageQueue——一条消息如何入队、唤醒并执行
android
海雅达手持终端PDA1 天前
巡线、深井、爆管抢修与极寒水网的硬件矩阵怎么配?海雅达水务巡检终端PDA选型指南
android·物联网·5g·集成测试·能源·政务
行者-全栈开发1 天前
Flutter 插件 device_calendar_plus 开源鸿蒙适配实战:从权限弹窗失灵到循环日程跑通
flutter·跨平台·harmonyos·arkts·日历·适配·开源鸿蒙
恋猫de小郭1 天前
Flutter 开始支持 Jetpack Compose Platform View
android·前端·flutter