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

前言

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

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

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

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

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

规则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;
  }
}

代码又臭又长,对吧?正确的做法是用 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));

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

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);
  
  // ...
}

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,548评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,069评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,985评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,305评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,324评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,030评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,639评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,552评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,081评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,194评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,327评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,004评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,688评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,188评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,307评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,667评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,337评论 2 358

推荐阅读更多精彩内容

  • 因为工作需要,公司组里要求考阿里巴巴编程规范,于是我花了一天的时间看了一遍,然后刷了一些题,终于在第三次的时候考过...
    添砖java的啾阅读 5,866评论 0 2
  • 如何阅读指南 DO 应始终遵循的准则 DON'T 不应该这么使用的准则 PREFER 应该遵循的准则,但是在某些情...
    _白羊阅读 3,073评论 0 3
  • 这一秒不放弃,下一秒有奇迹 代码规范整理 命名风格 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元...
    来晚了各位阅读 1,288评论 0 1
  • Java代码规范整理 对于一个整体的软件系统而言,既需要宏观上的架构决策,设计与指导原则,也必须重视微观上的代码细...
    jeffrey_hjf阅读 5,227评论 0 1
  • Java代码规范整理 对于一个整体的软件系统而言,既需要宏观上的架构决策,设计与指导原则,也必须重视微观上的代码细...
    亚武de小文阅读 2,746评论 8 47