likes
comments
collection
share

你知道吗?Dart 不支持方法重载!—— 关于Dart 的成员属性和方法的编码8条建议

作者站长头像
站长
· 阅读数 12

本篇内容整理自:Dart 高效编码:Members部分

前言

这里要讲到的成员(Members)包括类的属性(Properties)和方法(Methods)。养成良好的类成员定义和使用习惯,不仅可以使代码更清晰,也能够提高程序的健壮性。本篇介绍如何正确地使用类成员。

建议1:优先将字段或顶级变量定义为 final

将字段或顶级变量定义为 final 可以表明它们在运行过程中是不可变的。类或库应该将可变的范围缩减到最低,从而提高代码的可维护性。当然,这并不是说不需要可变的数据,而是说非必要不要定义可变的字段或变量,尽可能地地使用 final 定义,哪怕是回头发现需要改变的时候再改也会比一开始定义为可变的要好。 如果一个成员属性在一开始的时候没法初始化,但是初始化之后就没法再更改,那么可以使用 late final 定义:

class Student {
  late final name;
  int age;
  
  Student({required this.name, required this.age});
}

建议2:使用 getter 作为在概念上是访问类属性的操作

我们获取类的成员既可以使用 getter,也可以使用一个方法。两种方式看似没有多大区别,但是如何选择对于 API 设计来说却十分重要。很多其他编程语言如果考虑封装,需要使用 getXX()方法来获取对象的属性值。只对那些定义为公共成员的才可以使用 .XX 访问。 比如 Java 代码:

public class Circle {
    public Circle(double radius) {
        this.radius = radius;
    }
    private double radius;

    public double getRadius() {
        return radius;
    }
}

Dart 并不是这样,所有使用.XX 的方式都可以看作是成员访问,包括计算成员。而类的字段是特殊的,这是因为 Dart语言本身为他们实现了 getter 访问。而且,使用 getter 替换一个方法来访问属性给调用者一个信号,那就暗示这是对象的字段形式的操作,这个操作告诉调用者如下信息:

  • 这个操作不会携带任何参数,并且返回一个结果;
  • 调用者只需要关心结果,而无需关心具体如何实现,或者说基本不用关心。而使用动词+名词的组合形式更多地是描述调用者在做一件工作。当然这并不意味着 getter 方式运行得更快,实际上 IterableBase.length 的计算复杂度是 O(n)(需要依次遍历才能计算长度)。对于一些重要的计算,使用 getter 也没关系。但是如果说 getter 需要做大量的工作的,这就更像是一项工作了,这个时候使用方法会更准确地表达意图。例如下面的方式就是 getter 使用的反例:
// 错误示例
connection.nextIncomingMessage; //  需要处理网络 I/O
expression.normalForm; //  可能需要进行指数计算
  • getter 操作不会产生用户可见的副作用。这个可能有点难理解,实际就是说调用者使用 getter 访问对象属性的时候,这个对象或程序状态并不会因此发生变化。比如在 getter 操作里不会产生输出,写入文件,更改对象其他属性等等。当然,如果 getter 里的操作不影响用户可见的部分吗,那么做一些其他操作也是可以的,比如延迟计算和存储结果,写入缓存等等 —— 前提是调用者不关系这些影响。
// 错误示例
stdout.newline;   //产生了输出
list.clear;			  //修改的对象
  • 操作是幂等的。“幂等”的意思是,在当前上下文情况下,除非对象状态在调用期间被改变过,否则不论调用多少次返回的结果应该是一致的。比如说我们获取 list.length,只要没有对 list进行过增、改、删,那么每次返回的列表长度就应该是一样的。比如下面的方式,因为每次获取的时间戳都是不同的,因此就不适合使用 getter。
// 错误示例
DateTime.now;
  • getter 操作返回的对象不应该暴露整个对象。如果要暴露整个对象,那就类似对象的转换了,应该使用 toX() 或 asX() 方法,例如 toString()toJson()asMap()等。

如果我们要返回的结果符合上面的几点,那么就可以使用 getter。看起来似乎很少能够满足,但是实际上会有很多情况都可以这么做。下面是一些典型的例子:

rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

建议3:使用 setter 作为在概念上是更改类属性的操作

与 getter类似,如何从 settter 还是函数中选择,需要看一下操作是否是类似字段的操作。如果满足下面三个特性,那么就应该使用 setter:

  • 操作只有一个参数,且没有返回值;
  • 操作会更改对象的状态数据;
  • 操作是幂等的。这里的幂等的意思是,如果使用相同的参数调用 setter 两次,那么第二次应该看起来什么也没做。在 setter 内部,当然可能会有缓存失效设置或做日志,但是从调用者来看,第二次相当于是什么也没发生。

下面是两个典型的示例:

rectangle.width = 3;
button.visible = false;

建议4:如果没有相应的 getter,那么也不应该定义 setter

通常来说 getter 和 setter 是表示对象的可见属性。而如果给一个属性只设置了 setter 而没有 getter 的话会很奇怪,而且使用起来也很别扭。比如一个没有 getter 的属性,可以使用赋值操作符“=”,但不能使用“+=”,因为后者需要使用 getter 访问。 这头条建议不是说让你为了适应 setter 而加上 getter 访问,实际上如果只有 setter 而没有 getter 的话,更合适的方式是写一个方法来更改对象属性。

建议5:不要使用运行时类型检测来实现“伪”重载

注意在Dart 语言不支持方法重载(overload)。很多面向对象的语言支持相同方法名,但参数列表不同的形式实现方法重载。而 Dart 是不支持重载的,例如下面的代码是会报错的。 你知道吗?Dart 不支持方法重载!—— 关于Dart 的成员属性和方法的编码8条建议 当然,你可以定义一个参数是 dynamic类型的方法,然后在方法内部使用“is xx”来实现“伪重载”,比如下面这样:

// 错误示例
void fakeOverload([dynamic type]) {
  if (type is String) {
    // 处理 字符串
  } else if (type is num) {
    // 处理数值
  } else {
    //...
  }
}

如果代码层明确知道那个类型的参数该如何处理,那么应该单独定义各自的方法,这样业务代码会更清晰,而且通过静态类型检查可以避免代码的错误。 这条建议的核心是不要使用运行时类型用于方法重载。假设有些接口并不知道传入的类型是什么,然后希望接口内部通过 is 判断来进行对应的操作,那么这种操作是可以的,比如我们在 Redux 中处理 Action 响应的 Reducer 就是这么做的。

CounterState counterReducer(CounterState state, dynamic action) {
  if (action is CounterAddAction) {
    return CounterState(state.count + 1);
  }

  if (action is CounterSubAction) {
    return CounterState(state.count - 1);
  }

  return state;
}

建议6:避免在没有初始化的情况下定义 public late final 字段

这条其实显而易见,如果你定义了一个没有初始化的public late final 字段,因为 setter 是公开的,意味着这个字段的初始化可能由外部完成。那么什么时候能用这个字段就存在很大的不确定性了 —— bug随时都可能出现。因此,如果要定义 late final 字段,应该遵循下面的几条:

  • 不适用 late
  • 使用 late 的时候同时初始化该字段
  • 使用 late,但设置为私有字段,对外提供 public getter 访问。

建议7:避免返回可能为空的 Future,Stream 或集合

当一个接口返回一个容器类型对象,可以有两种方式指明没有数据:返回空的容器或返回 null。通常我们会更喜欢是返回空容器来表明没有数据。这个时候,我们可以在返回值上调用方法或访问类似 isEmpty的属性。 因此,当确实没有数据可提供时,建议返回一个空的集合,一个非空的 Future(可以包裹一个为空的数据类型),或者一个不发送任何值的 stream

建议8:不要为了进行链式操作返回 this

Dart 使用..操作符完成链式操作,因此无需在手动返回 this来支持链式操作了。下面是两个对比示例。

// 正确示例
var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');

// 错误示例
var buffer = StringBuffer()
    .write('one')
    .write('two')
    .write('three');

总结

Dart 语言和其他语言相比,还是有些细微的差别。这些差别可能我们大部分时候体会不出来,但是一旦用到的时候,还是需要根据 Dart 自身特性来,而不是沿用其他语言的习惯。比如本篇介绍的下面4点就需要特别注意:

  • getter 和 setter 需要支持幂等,也就是在没有对对象进行其他更改操作的情况下,对该对象的属性的多次getter 操作的返回值要一致;同时,setter 操作时,相同参数的情况先,多次setter 操作后面的操作都可以当做是无效的。
  • 不应该只提供 setter,不提供 getter,如果有这种情况,应该不使用 setter,而是单独写一个修改属性的方法。
  • Dart 不支持方法重载,因此不要使用动态类型来伪造重载,而是针对具体的类型编写对应的处理方法。
  • 链式调用的时候,不要通过返回 this 实现,而是使用..操作符。