「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」
前言
我们写 Dart 代码的时候,变量和类成员天天用,但是用的方式一定对吗?恐怕未必,本篇我们来介绍变量和类成员该如何合理使用。
规则1:局部变量使用 var和 final 的方式要保持一致
对于大部分局部变量,无需指定类型,而应该是使用 var
或 final
,应该从下面的两条规则中选择一条:
- 对于不会重复赋值的使用
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);
// ...
}
总结
可以看到,实际上代码的写法有很多种,所谓“写法千万种,规范第一条;代码不规范,同事两行泪”。“码农何苦为难码农”呢,有了规范指引,才能够写出高质量代码。
我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章,提供体系化的 Flutter 学习文章。对应源码请看这里:Flutter 入门与实战专栏源码。如有问题可以加本人微信交流,微信号:
island-coder
。👍🏻:觉得有收获请点个赞鼓励一下!
🌟:收藏文章,方便回看哦!
💬:评论交流,互相进步!
今天的文章Dart 编码规范:变量和类成员的合理使用分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17562.html