Dart 编码规范:合理使用变量和类成员

戳这里了解《Flutter入门与实战》专栏,持续更新、系统学习!

前言

我们写 Dart 代码的时候,变量和类成员天天用,但是用的方式一定对吗?恐怕未必,本篇我们来介绍变量和类成员该如何合理使用。

规则1:局部变量使用 var和 final 的方式要保持一致

对于大部分局部变量,无需指定类型,而应该是使用 varfinal,应该从下面的两条规则中选择一条:

  • 对于不会重复赋值的使用 final,其他的使用 var。这个其实看似很容易遵循,但是编写的时候很容易忽略。一个经验就是,优先使用 final,如果发现后面需要重新赋值的时候再使用 var
  • 对于局部变量,只使用 var,即便是那些不会重新赋值的局部变量,也就是对于局部变量不使用 final。注意,这里的局部变了指的是函数内部的局部变量,类的成员变量当然可以使用 final 修饰。这一条更容易遵循一些。

一旦你选择了上面中的一条,那么应该一直遵循下去,要不有强迫症的码农看到你的代码后会肯定会冒出一堆问题 —— “大佬,这里为什么用 final?这里为什么又不用 final?”估计最后尴尬的你只能“呃……”,然后找个理由搪塞过去了。
image.png

规则2:不要存储那些计算变量

计算变量是指可以通过别的类成员计算出来的属性。当你存储的时候,会导致很多问题,比如你可能需要在各个关联属性变更的地方埋点更新这个计算变量,一旦遗漏就会出现 bug。例如下面的例子就是一个典型的反面例子。

// 错误示例
class Circle {
  double radius;
  double area;
  double circumference;

  Circle(double radius)
      : radius = radius,
        area = pi * radius * radius,
        circumference = pi * 2.0 * radius;
}

面积和周长都是依赖于半径的。上面这种方式有两个缺陷:

  • 增加了两个成员属性来缓存面积和周长,浪费了存储空间;
  • 存在不同步的隐患,一旦半径更改了,如果不主动更新面积和周长,就会出现不一致的情况。要解决这个问题,需要在半径改变的时候更新面积和周长:
// 错误示例
class Circle {
  double _radius;
  double get radius => _radius;
  set radius(double value) {
    _radius = value;
    _recalculate();
  }

  double _area = 0.0;
  double get area => _area;

  double _circumference = 0.0;
  double get circumference => _circumference;

  Circle(this._radius) {
    _recalculate();
  }

  void _recalculate() {
    _area = pi * _radius * _radius;
    _circumference = pi * 2.0 * _radius;
  }
}

代码又臭又长,对吧?
image.png
正确的做法是用 get 来获取计算属性就可以了,像下面的代码是不是超级清爽?

// 正确示例
class Circle {
  double radius;

  Circle(this.radius);

  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

当然,这个规则也不是一成不变的,假设你的计算过程非常复杂,而且变量改变入口不多,那么声明一个成员属性来缓存计算结果会节省大量 CPU 的开销,也就是已空间换时间。这个时候是可以这么做的,具体怎么选择需要自己根据代价去判断。

规则3:如无必要,不要为类成员提供 getter 和 setter

在 Java 或 C#这类语言中,通常推荐是将所有成员属性隐藏,然后对外提供 getter 和 setter 来访问,这是因为这两门语言对 getter,setter 和直接访问属性的处理方式不同。而在 Dart 里面,通过成员访问和使用 getter 和 setter 是没有区别的。因此,如果一个成员对外完全可以访问(包括 getter 和 setter),那么就没必要使用 getter 和 setter。当然,如果一个成员对外只能进行 setter 或 getter,那么就需要单独提供对应的方法,而屏蔽另一个。

// 正确示例
class Box {
  Object? contents;
}

// 错误示例
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

规则4:对只读成员使用 final 修饰

如果你的成员属性对外部是只读的,那么应该使用 final 修饰,而不是提供 getter 访问。当然,这个前提是这个成员属性初始化之后不会再重新赋值。

// 正确示例
class Box {
  final contents = [];
}

// 错误示例
class Box {
  Object? _contents;
  Object? get contents => _contents;
}

规则5:对于简单的计算返回值,优先使用 => 表达式

这个其实是为了简化代码,提高可读性的一个指引。对于使用简单的表达式返回一个计算属性或调用方法时,使用=> 表达式更加简洁易懂。

// 正确示例
double get area => (right - left) * (bottom - top);

String capitalize(String name) =>
    '${name[0].toUpperCase()}${name.substring(1)}';

当然,假设计算返回值的代码有好几行,那么还是使用正常的函数形式更好。

// 正确示例
Treasure? openChest(Chest chest, Point where) {
  if (_opened.containsKey(chest)) return null;

  var treasure = Treasure(where);
  treasure.addAll(chest.contents);
  _opened[chest] = treasure;
  return treasure;
}

// 错误示例
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
    ? null
    : _opened[chest] = (Treasure(where)..addAll(chest.contents));

实际上,原则就是根据代码的可读性决定选择哪种方式,而不是生搬硬套。
image.png
对于成员属性来说也可以使用 => 形式,例如下面的情况会让代码更简洁。

num get x => center.x;
set x(num value) => center = Point(value, center.y);

除非要和同名参数做区分,否则不要使用 this.来访问成员

使用 this.访问类成员在很多语言中很常见,但在 Dart 中不推荐。 对于访问成员只有两种情况需要使用 this.

  • 构造方法中使用 this. 表名构造函数的参数是用于设置成员变量的;
  • 其他函数中的参数名和类成员属性同名,需要使用 this.来区分。
// 正确示例
class Box {
  Object? value;

  void clear() {
    update(null);
  }
  
  String toString() {
    return 'Box: $value';
  }

  void update(Object? value) {
    this.value = value;
  }
}

// 错误示例
class Box {
  Object? value;

  void clear() {
    this.update(null);
  }
  
  String toString() {
    return 'Box: ${this.value}';
  }

  void update(Object? value) {
    this.value = value;
  }
}

另外用到 this.的场合包括使用构造方法来完成构造命名构造器:

class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;
	
  // 使用构造方法
  ShadeOfGray.black() : this(0);

  // 使用命名构造方法
  ShadeOfGray.alsoBlack() : this.black();
}

尽可能在成员声明的时候初始化

如果成员属性不依赖于构造参数,那么可以在声明的时候进行初始化,这会使得代码量更少,而且避免了因为类有多个构造器导致重复代码出现的情况。

// 正确示例
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

// 错误示例
class ProfileMark {
  final String name;
  final DateTime start;

  ProfileMark(this.name) : start = DateTime.now();
  ProfileMark.unnamed()
      : name = '',
        start = DateTime.now();
}

有些成员没法在一开始初始化,是因为这些成员依赖于其他成员或需要调用方法来初始化。这个时候,应该用 late 来修饰,这个时候可以访问 this 来进行初始化。

class _AnimatedModelBarrierDemoState extends State<AnimatedModelBarrierDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller =
      AnimationController(duration: const Duration(seconds: 1), vsync: this);

  late Animation<Color?> _colorAnimation = ColorTween(
    begin: Colors.black.withAlpha(50),
    end: Colors.black.withAlpha(80),
  ).animate(_controller);
  
  // ...
}

总结

可以看到,实际上代码的写法有很多种,所谓“写法千万种,规范第一条;代码不规范,同事两行泪”。“码农何苦为难码农”呢?有了规范指引,才能够写出高质量代码。
image.png

猜你喜欢

转载自blog.csdn.net/shuijian00/article/details/124561004
今日推荐