前言
函数是我们用得最多的了,通过函数可以将复杂业务拆分为简短精悍的函数,从而分解大业务,提高可维护性和代码复用性。在设计函数的时候,接收什么样的参数最为关键,参数定义了函数的输入。本篇来介绍 Dart 函数的合理使用。
Dart 函数参数简介
在 Dart 中,将参数传递给函数的方式有两种,包括占位方式和命名方式,再加上是否可为空组合起来有4种,如下所示。
// 占位参数形式,第二个参数需要传
void someFunction1(int a, String? b) {
// ...
}
// 占位参数形式,第二个参数可不传
void someFunction2(int a, [String? b]) {
// ...
}
// 命名参数形式,p 为必传参数
void someFunction4(int a, {required Person p, Student? s} ) {
// ...
}
// 命名参数形式,p 和 s 为可选参数
void someFunction4(int a, {Person? p, Student? s} ) {
// ...
}
其中可选占位参数使用[...]形式,表示其中的参数可传可不传。命名参数采用{...}形式,如果参数必传则需要声明为 required
。注意,可选占位参数和命名参数不可同时使用,例如下面的形式会报错。
// 错误!可选占位参数和命名参数不可共用
void someFunction4(int a, [String? b], {Person? p, Student? s} ) {
// ...
}
由于 Dart 存在这两种形式,按说是用哪种形式都行,但是实际应用中还是需要遵循一定的规范。
规范1:避免对布尔参数使用占位形式传递
由于布尔值在字面上很难知晓具体的意思,因此,布尔参数若作为占位形式参数传递的话,会降低可读性。比如下面的例子,如果不去看函数定义、文档或内部代码,很难知道布尔值的意义。
// 错误示例
// 实际是创建单次任务
Task(true);
// 循环任务
Task(false);
// 呃,看不懂这是啥?
ListBox(false, true, true);
// 禁用状态的 button
Button(false);
这种情况,对于构造函数,可以使用命名构造函数或命名参数的形式,语义上会清晰很多。下面的代码,即便没有注释也能够看得懂。
// 正确示例
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
当然,对于 setter 而言,因为有上下文,使用布尔值是没问题的。
// 正确示例
listBox.canScroll = true;
button.isEnabled = false;
规范2:对于可选占位参数,按频次设置合理的次序
可选占位参数意味着可以不传,如果不怎么用的参数放在前面的话,会导致如果需要传后面的参数就必须给该参数赋值,而如果没有值就得传 null
,这样的代码会很难看。
// 错误示例
void someFunction(int a, [int? b, int? c, int d = 0]) {
//...
}
// 调用示例
someFunction(1, null, null, 12);
上面的例子中,d 可能是经常要传的参数,结果放到了最后,导致调用者很郁闷,每次传 d 的时候都得给 b 和 c 传 null
。正确的示例如下:
// 正确示例
void someFunction(int a, [int d = 0, int? b, int? c]) {
//...
}
// 调用示例
someFunction(1, 12);
当然,这种情况,使用命名参数更合理。
// 正确示例
void someFunction(int a, {int d = 0, int? b, int? c}) {
//...
}
// 调用示例
someFunction(1, d: 12);
规范3:避免要求传一些无意义的参数
如果我们需要省略参数传递,那么应该就直接省略,而不是传没有意义的参数,例如 null、空字符串等等。省略参数看起来更简洁,也能够避免空值导致的错误 —— 这会让函数以为传入了一个有意义的参数。
// 正确示例
var rest = string.substring(start);
// 错误示例
var rest = string.substring(start, null);
规范4:对于范围参数,遵循起始参数是闭区间,结束参数应该是开区间的约定
我们在获取范围值的时候,会需要传入起止位置,通常是整数下标值,一个起始下标表示从第几个元素开始,一个结束下标表示到哪里为止。由于这类参数通常是占位参数而非命名参数,因此建议是和 Dart的核心库的用法保持一致 —— 起始位置包括在内,而结束位置不包括在内,用数学区间表示就是 [s, e)
形式。下面是 Dart 核心库的示例:
// 正确示例
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc
规范5:对于可选参数,尽量设置默认值
可选参数如果没有默认值,在不传递的时候,会默认为 null
。如果不小心使用的话,可能会导致出现 bug。因此,假设编码的时候知道默认状态,那么请尽量设置默认值,例如下面的 DateTime
和 Duration
的例子:
//正确示例
DateTime(int year,
[int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0]);
Duration(
{int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0});
总结
本篇介绍了 Dart 的函数参数传递形式以及5条规范。个人来说,更倾向于使用命名参数,因为在调用函数的时候会明确参数语义 —— 尤其是参数很多的时候。此外,建议明确参数是否是 required
,从而约束调用者的参数传递行为。对于知道默认值的可选参数 ,推荐设置默认值。这样我们可以省略很多 null
判断。
网友评论