Dart 函数参数最佳实践指南

4,417 阅读5分钟

前言

函数是我们用得最多的了,通过函数可以将复杂业务拆分为简短精悍的函数,从而分解大业务,提高可维护性和代码复用性。在设计函数的时候,接收什么样的参数最为关键,参数定义了函数的输入。本篇来介绍 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。因此,假设编码的时候知道默认状态,那么请尽量设置默认值,例如下面的 DateTimeDuration 的例子:

//正确示例
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 判断。

我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章,提供体系化的 Flutter 学习文章。对应源码请看这里:Flutter 入门与实战专栏源码。如有问题可以加本人微信交流,微信号:island-coder

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!