0%

Dart 语法原来这么好玩儿

说到到某个语言的语法可能大家会觉得很枯燥、乏味,而日常开发中我们往往更加注重的是业务逻辑和页面开发,语法的使用大多也停留在满足基本的需求。其实 Dart 语法有很多有意思的地方的,仔细探究一下你会发现,它的简洁清晰、灵活多样的语法会让人爱不释手。在本文中,我们将探索 Dart 语法的各种奇妙之处吧。

unwrap 操作

Flutter 中,unwrap 操作常常用于处理可能为空的数据,以便过滤掉空值并只保留非空值。其使用场景也相当广泛,例如 为 FutureStreams 添加 unwrap 来处理掉非空数据,或者从网络请求或其他异步操作中获取数据,并在数据流中处理结果等等,如下面这段代码:

1
2
3
4
5
6
7
extension Unwrap<T> on Future<T?> {
Future<T> unwrap() => then(
(value) => value != null
? Future<T>.value(value)
: Future.any([]),
);
}

unwrap 函数将可能为空的 Future 解包,如果 Future 返回的值不为 null,则将值包装在一个新的 Future 中返回,否则返回一个空的 Future。调用示例:

1
2
3
4
5
6
7
8
class ImagePickerHelper {
static final ImagePicker _imagePicker = ImagePicker();
static Future<File> pickImageFromGallery() => _imagePicker
.pickImage(source: ImageSource.gallery)
.unwrap()
.then((xFile) => xFile.path)
.then((filePath) => File(filePath));
}

这里用到图片选择器插件 image_picker,只有当返回的 xFile 不为空时才进行后续操作。如果不调用 unwrap 函数,此时这里返回的 xFileoptional 类型,要使用之前需要判断是否为 null。日常开发中这种情况还不少,给 Future 添加 Unwrap 函数之后这样非空判断集中在这一个函数里面处理。

unwrap 不仅在 Future 中使用,还可以为 Streams 添加 unwrap 操作,代码如下:

1
2
3
4
extension Unwrap<T> on Stream<T?> {
Stream<T> unwrap() => where((event) => event != null).cast();
}

unwrap 方法,通过 where 过滤掉了 null 的事件,并使用 cast() 方法将结果转换为 Stream<T> 类型,将可空的事件转换为非空的事件流,下面是调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
Stream<int?>.periodic(
const Duration(seconds: 1),
(value) => value % 2 == 0 ? value : null,
).unwrap().listen((evenValue) {
print(evenValue);
});
/* 输出结果
0
2
4
6
...
*/
}

通过 extensionFutureStreams 添加 unwrap 函数后让我们的代码看起来清晰简洁多了,有没有?

数组的展开、合并和过滤

下面代码为任意类型的可迭代对象(Iterable)添加名为 Flatten 的扩展。在这个扩展中,函数 flatten 使用了递归算法将多层嵌套的 Iterable 里面的所有元素扁平化为单层 Iterable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension Flatten<T extends Object> on Iterable<T> {
Iterable<T> flatten() {
Iterable<T> _flatten(Iterable<T> list) sync* {
for (final value in list) {
if (value is List<T>) {
yield* _flatten(value);
} else {
yield value;
}
}
}
return _flatten(this);
}
}

注意了上面代码中使用了 yield 关键字,在 Flutter 中,yield 关键字用于生成迭代器,通常与sync*async* 一起使用。它允许您在处理某些数据时逐步生成数据,而不是在内存中一次性处理所有数据。对于处理大量数据或执行长时间运行的操作非常有用,因为它可以节省内存并提高性能。

这个和 ES6 中使用 function* 语法和 yield 关键字来生成值一个东西,也是逐个生成值,而不需要一次性生成所有值。以下是 JS 写法:

1
2
3
4
5
6
7
8
9
10
function* generateNumbers(n) {
for (let i = 0; i < n; i++) {
yield i;
}
}

const numbers = generateNumbers(5);
for (const number of numbers) {
console.log(number);
}

我们来看看 Dart 中的 flatten() 函数的调用:

1
2
3
4
5
6
7
8
Future<void> main() async {
final flat = [
[[1, 2, 3], 4, 5],
[6, [7, [8, 9]], 10],
11,12
].flatten();
print(flat); // (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
}

嵌套的集合可能在数据处理、转换或展示中经常遇到,而将这些嵌套的集合扁平化可以简化数据处理过程,使代码更加简洁和易于理解。另外一点,递归展多维数组在面试中经常会出现,说不定哪天就用上了哈。

如果将两个数组合并成一个数组该怎么操作呢?其实和 Map 的合并相似,也是用到了自定义操作符 operator ,来看看怎么实现的。

1
2
3
4
5
6
7
8
9
10
extension InlineAdd<T> on Iterable<T> {
Iterable<T> operator +(T other) => followedBy([other]);
Iterable<T> operator &(Iterable<T> other) => followedBy(other);
}

void main() {
const Iterable<int> values = [10, 20, 30];
print((values & [40, 50]));
// 输出结果:(10, 20, 30, 40, 50)
}

添加了两个操作符:+&。将一个元素或者另一个可迭代对象添加到当前的可迭代对象中,然后返回一个新的可迭代对象,让可迭代对象 terable 有了合并数组的功能。

当数组中有一个为 null 的对象时,该如何过滤掉这个 null 对象呢,很简单可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension CompactMap<T> on Iterable<T?> {
Iterable<T> compactMap<E>([
E? Function(T?)? transform,
]) =>
map(transform ?? (e) => e).where((e) => e != null).cast();
}

void main() {
const list = ['Hello', null, 'World'];
print(list); // [Hello, null, World]
print(list.compactMap()); // [Hello, World]
print(list.compactMap((e) => e?.toUpperCase())); // [HELLO, WORLD]
}

Map 的过滤和合并

下面代码是 Map 类型的 extension,为 Map 类型添加了查找过滤的函数。

1
2
3
4
5
6
7
8
9
10
extension DetailedWhere<K, V> on Map<K, V> {
Map<K, V> where(bool Function(K key, V value) f) => Map<K, V>.fromEntries(
entries.where((entry) => f(entry.key, entry.value)),
);

Map<K, V> whereKey(bool Function(K key) f) =>
{...where((key, value) => f(key))};
Map<K, V> whereValue(bool Function(V value) f) =>
{...where((key, value) => f(value))};
}
  • where : 接受一个函数作为参数,该函数接受 Map 的键和值作为参数,并返回一个布尔值。
  • whereKey : 接受一个只接受键作为参数的函数。
  • whereValue : 这个方法接受一个只接受值作为参数的函数。

下面是调用:

1
2
3
4
5
6
void main(){
const Map<String, int> people = {'John': 20, 'Mary': 21, 'Peter': 22};
print(people.where((key, value) => key.length > 4 && value > 20)); // {Peter: 22}
print(people.whereKey((key) => key.length < 5)); // {John: 20, Mary: 21}
print(people.whereValue((value) => value.isEven)); // {John: 20, Peter: 22}
}

其中 where 方法先使用 entries 获取 Map 的键值对列表,然后使用 entries.where 方法对列表中的每个键值对进行过滤,最后使用 fromEntries 方法将过滤后的键值对列表转换回 Map,最后返回的新的 Map 中只包含满足条件的键值对,达到对 Map 中键值过滤的效果,也让代码更加简洁和易读。

Map 过滤还有另外一种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension Filter<K, V> on Map<K, V> {
Iterable<MapEntry<K, V>> filter(
bool Function(MapEntry<K, V> entry) f,
) sync* {
for (final entry in entries) {
if (f(entry)) {
yield entry;
}
}
}
}

void main(){
const Map<String, int> people = {
'foo': 20,
'bar': 31,
'baz': 25,
'qux': 32,
};
final peopleOver30 = people.filter((e) => e.value > 30);
print(peopleOver30); // 输出结果:(MapEntry(bar: 31), MapEntry(qux: 32))
}

Map 其它一些更有趣的 extension,如 Merge 功能,将两个 Map 合并成一个,代码如下:

1
2
3
4
5
extension Merge<K, V> on Map<K, V> {
Map<K, V> operator |(Map<K, V> other) => {...this}..addEntries(
other.entries,
);
}

上面的代码用到了 operator 关键字,在 Dart 中,operator 关键字用于定义自定义操作符或者重载现有的操作符。通过 operator 关键字,我们可以为自定义类定义各种操作符的行为,使得我们的类可以像内置类型一样使用操作符。

operator + 来定义两个对象相加的行为,operator [] 来实现索引操作,operator == 来定义相等性比较。这种语义式的也更加符合直觉、清晰易懂。

下面来看看 MapMerge 功能调用代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const userInfo = {
'name': 'StellarRemi',
'age': 28,
};

const address = {
'address': 'shanghai',
'post_code': '200000',
};

void main() {
final allInfo = userInfo | address;
print(allInfo);
// 输出结果:{name: StellarRemi, age: 28, address: shanghai, post_code: 200000}
}

调用的时候也很简单直接 userInfo | address;,这种操作在处理数据更新或合并配置等情况下特别有用。使用的时候需要注意的是,如果两个 Map 中有重复的键,那么上述操作会保留第一个 Map 中的值。

小结

怎么样,上面的这些 Dart 的语法是不是很有意思,有没有函数式编程那味儿,后面还会单独一篇来分享 Dart 语言面向对象的设计。好了,今天就到这里,也希望通过本文的分享,能够激发大家对 Dart 语言的兴趣,感谢您的阅读,更多干货文章扫描下方的二维码关注我的公众号“Flutter技术实践”。

Flutter技术实践