Dart 可迭代集合
— 焉知非鱼Dart可迭代集合。
这个代码实验室教你如何使用实现 Iterable类的集合-例如 List和 Set。迭代类是各种 Dart 应用程序的基本构建模块,你可能已经在使用它们,甚至没有注意到。这个代码实验室将帮助你充分利用它们。
使用嵌入式 DartPad 编辑器,你可以通过运行示例代码和完成练习来测试你的知识。
要想从这个 codelab 中获得最大的收获,你应该具备基本的 Dart 语法知识。
本课程包括以下内容。
- 如何读取一个 Iterable 的元素。
- 如何检查一个 Iterable 的元素是否满足一个条件。
- 如何过滤一个 Iterable 的内容。
- 如何将一个 Iterable 的内容映射到不同的值。
估计完成这个代码实验所需的时间: 60分钟。
什么是集合? #
集合是代表一组对象的对象,这些对象称为元素。迭代元素是集合的一种。
集合可以是空的,也可以包含许多元素。根据不同的目的,集合可以有不同的结构和实现。这些是一些最常见的集合类型:
什么是Iterable? #
Iterable
是一个元素的集合,它可以被依次访问。
在 Dart 中,Iterable
是一个抽象类,这意味着你不能直接实例化它。然而,你可以通过创建一个新的 List
或 Set
来创建一个新的 Iterable
。
List
和 Set
都是 Iterable
,所以它们和 Iterable
类有相同的方法和属性。
Map
在内部使用不同的数据结构,这取决于它的实现。例如,HashMap 使用了一个哈希表,其中的元素(也称为值)是通过一个键获得的。通过使用 Map
的 entries
或 values
属性,Map
的元素也可以作为 Iterable
对象读取。
这个例子显示了一个 int
的 List
,它也是一个 int
的 Iterable
:
Iterable<int> iterable = [1, 2, 3];
与 List
的区别在于,使用 Iterable
,你无法保证按索引读取元素的效率。Iterable
与 List
相比,没有 []
操作符。
例如,考虑以下代码,这是无效的:
Iterable<int> iterable = [1, 2, 3];
int value = iterable[1];
如果你用 []
读取元素,编译器会告诉你 '[]'
这个运算符没有为 Iterable
类定义,这意味着在这种情况下你不能使用 [index]
。
你可以用 elementAt()
来读取元素,它可以遍历迭代的元素,直到它到达那个位置。
Iterable<int> iterable = [1, 2, 3];
int value = iterable.elementAt(1);
继续下一节,了解更多关于如何访问 Iterable
的元素。
读取元素 #
你可以使用 for-in
循环,依次读取一个迭代元素。
例子: 使用 for-in 循环 #
下面的例子展示了如何使用 for-in
循环读取元素。
void main() {
var iterable = ['Salad', 'Popcorn', 'Toast'];
for (var element in iterable) {
print(element);
}
}
在幕后,for-in
循环使用了一个迭代器。然而,你很少看到直接使用迭代器 API,因为 for-in
更容易阅读和理解,而且不容易出错。
关键术语:
- Iterable: Dart Iterable 类。
- Iterator:
for-in
用来从一个 Iterable 对象中读取元素的对象。 for-in
循环: 从一个 Iterable 对象中依次读取元素的简单方法。
例子:使用第一个和最后一个元素 #
在某些情况下,你只想访问一个 Iterable
的第一个或最后一个元素。
在 Iterable
类中,你不能直接访问元素,所以你不能调用 iterable[0]
来访问第一个元素。相反,你可以使用 first
,它可以获取第一个元素。
另外,使用 Iterable 类,你不能使用操作符 []
来访问最后一个元素,但是你可以使用 last
属性。
因为访问一个 Iterable 的最后一个元素需要踏过所有其他元素,所以 last
可能会很慢。在一个空的 Iterable
上使用 first
或 last
会导致一个 StateError。
void main() {
Iterable iterable = ['Salad', 'Popcorn', 'Toast'];
print('The first element is ${iterable.first}');
print('The last element is ${iterable.last}');
}
在这个例子中,你看到了如何使用 first
和 last
来获得一个 Iterable
的第一个和最后一个元素。也可以找到满足条件的第一个元素。下一节将展示如何使用名为 firstWhere()
的方法来实现这一目标。
例子: 使用 firstWhere() #
你已经看到,你可以依次访问一个 Iterable
的元素,你可以很容易地得到第一个或最后一个元素。
现在,你要学习如何使用 firstWhere()
来寻找满足某些条件的第一个元素。这个方法需要你传递一个谓词,它是一个函数,如果输入满足一定的条件就返回 true。
String element = iterable.firstWhere((element) => element.length > 5);
例如,如果你想找到第一个超过 5 个字符的 String
,你必须传递一个当元素大小大于 5 时返回 true 的谓词。
运行下面的例子,看看 firstWhere()
是如何工作的。你认为所有的函数都会给出相同的结果吗?
bool predicate(String element) {
return element.length > 5;
}
main() {
var items = ['Salad', 'Popcorn', 'Toast', 'Lasagne'];
// You can find with a simple expression:
var element1 = items.firstWhere((element) => element.length > 5);
print(element1);
// Or try using a function block:
var element2 = items.firstWhere((element) {
return element.length > 5;
});
print(element2);
// Or even pass in a function reference:
var element3 = items.firstWhere(predicate);
print(element3);
// You can also use an `orElse` function in case no value is found!
var element4 = items.firstWhere(
(element) => element.length > 10,
orElse: () => 'None!',
);
print(element4);
}
在这个例子中,你可以看到三种不同的方式来写一个谓词。
- 作为一个表达式: 测试代码中有一行使用了箭头语法(
=>
)。 - 作为一个块: 测试代码在括号和返回语句之间有多行。
- 作为一个函数: 测试代码在一个外部函数中,作为参数传递给
firstWhere()
方法。
没有正确或错误的方式。使用最适合你的方式,并且让你的代码更容易阅读和理解。
在这个例子中,firstWhereWithOrElse()
调用 firstWhere()
时,使用了可选的命名参数 orElse
,它在没有找到元素时提供了一个替代方案。在这种情况下,返回文本 “None!",因为没有元素满足提供的条件。
注意:如果没有元素满足测试谓词,并且没有提供 orElse
参数,那么 firstWhere()
会抛出一个 StateError。
快速回顾。
Iterable
的元素必须按顺序访问。- 迭代所有元素的最简单方法是使用
for-in
循环。 - 你可以使用
first
和last
getters 来获取第一个和最后一个元素。 - 你也可以用
firstWhere()
找到满足条件的第一个元素。 - 你可以把测试谓词写成表达式、块或函数。
关键术语。
谓词: 当某个条件被满足时,返回 true
的函数。
练习: 练习写一个测试谓词 #
下面的练习是一个失败的单元测试,其中包含一个部分完整的代码片段。你的任务是通过编写代码使测试通过来完成练习。你不需要实现 main()
。
这个练习介绍了 singleWhere()
这个方法的工作原理类似于 firstWhere()
,但在这种情况下,它只期望 Iterable
中的一个元素满足谓词。如果 Iterable
中超过一个或没有元素满足谓词条件,那么该方法会抛出一个 StateError 异常。
singleWhere()
对整个 Iterable
进行步进,直到最后一个元素,如果 Iterable
是无限的或包含一个大的元素集合,这可能会引起问题。
你的目标是实现满足以下条件的 singleWhere()
谓词。
- 元素包含字符 ‘a’。
- 该元素以字符 ‘M’ 开头。
测试数据中的所有元素都是字符串,你可以查看类文档以获得帮助。
String singleWhere(Iterable<String> items) {
return items.singleWhere((element) => element.startsWith('M') && element.contains('a'));
}
检查条件 #
在使用 Iterable
时,有时你需要验证一个集合的所有元素是否满足某些条件。
你可能会想用 for-in
循环来写一个解决方案,比如这个:
for (var item in items) {
if (item.length < 5) {
return false;
}
}
return true;
然而,你可以使用 every()
方法实现同样的目的:
return items.every((element) => element.length >= 5);
使用 every()
方法可以使代码更易读、更紧凑、更不容易出错。
例子: 使用 any() 和 every() #
Iterable
类提供了两个可以用来验证条件的方法。
any()
: 如果至少有一个元素满足条件,则返回 true。every()
: 如果所有元素都满足条件,则返回 true。
运行这个练习来看看它们的作用。
void main() {
var items = ['Salad', 'Popcorn', 'Toast'];
if (items.any((element) => element.contains('a'))) {
print('At least one element contains "a"');
}
if (items.every((element) => element.length >= 5)) {
print('All elements have length >= 5');
}
}
在这个例子中,any()
验证了至少一个元素包含字符 a,every()
验证了所有元素的长度等于或大于 5。
运行代码后,尝试更改 any()
的谓词,使其返回 false:
if (items.any((element) => element.contains('Z'))) {
print('At least one element contains "Z"');
} else {
print('No element contains "Z"');
}
你也可以使用 any()
来验证一个 Iterable
中没有元素满足某个条件。
练习: 验证一个 Iterable 是否满足一个条件 #
下面的练习提供了使用前面例子中描述的 any()
和 every()
方法的练习。在本例中,你的工作对象是一组用户,由具有成员字段 age
的 User
对象表示。
使用 any()
和 every()
实现两个函数。
- 第1部分:实现
anyUserUnder18()
。- 如果至少有一个用户是17岁或更小,则返回 true。
- 第2部分:实现
everyUserOver13()
。- 如果所有用户都是14岁或以上,则返回 true。
bool anyUserUnder18(Iterable<User> users) {
return users.any((user) => user.age < 18);
}
bool everyUserOver13(Iterable<User> users) {
return users.every((user) => user.age > 13);
}
class User {
String name;
int age;
User(
this.name,
this.age,
);
}
快速回顾:
- 虽然你可以使用
for-in
循环来检查条件,但还有更好的方法。 - 方法
any()
可以让你检查任何元素是否满足条件。 - 方法
every()
可以让你验证所有元素是否满足条件。
过滤 #
前面的章节介绍了 firstWhere()
或 singleWhere()
等方法,这些方法可以帮助你找到满足某个谓词的元素。
但是如果你想找到满足某个条件的所有元素呢?你可以使用 where()
方法来实现。
var evenNumbers = numbers.where((number) => number.isEven);
在这个例子中,numbers
包含一个有多个 int
值的 Iterable
,where()
可以找到所有偶数的数字。
where()
的输出是另一个 Iterable
,你可以用它来迭代它或应用其他 Iterable
方法。在下一个例子中,where()
的输出直接在 for-in
循环中使用。
var evenNumbers = numbers.where((number) => number.isEven);
for (var number in evenNumbers) {
print('$number is even');
}
例子: 使用 where() #
运行这个例子,看看如何将 where()
与其他方法如 any()
一起使用。
main() {
var evenNumbers = [1, -2, 3, 42].where((number) => number.isEven);
for (var number in evenNumbers) {
print('$number is even.');
}
if (evenNumbers.any((number) => number.isNegative)) {
print('evenNumbers contains negative numbers.');
}
// If no element satisfies the predicate, the output is empty.
var largeNumbers = evenNumbers.where((number) => number > 1000);
if (largeNumbers.isEmpty) {
print('largeNumbers is empty!');
}
}
在这个例子中,where()
用于查找所有偶数,然后用 any()
检查结果是否包含负数。
在本例的后面,再次使用 where()
来查找所有大于1000的数字,由于没有,结果是一个空的 Iterable
。
注意:如果没有元素满足 where()
中的谓词,那么该方法返回一个空的 Iterable
。与 singleWhere()
或 firstWhere()
不同,where()
不会抛出 StateError 异常。
例子: 使用 takeWhile #
方法 takeWhile()
和 skipWhile()
也可以帮助你从一个 Iterable
中过滤元素。
运行这个例子,看看 takeWhile()
和 skipWhile()
如何分割一个包含数字的 Iterable
。
main() {
var numbers = [1, 3, -2, 0, 4, 5];
var numbersUntilZero = numbers.takeWhile((number) => number != 0);
print('Numbers until 0: $numbersUntilZero');
var numbersAfterZero = numbers.skipWhile((number) => number != 0);
print('Numbers after 0: $numbersAfterZero');
}
输出如下:
Numbers until 0: (1, 3, -2)
Numbers after 0: (0, 4, 5)
在这个例子中,takeWhile()
返回一个 Iterable
,它包含了通往满足谓词的元素的所有元素。另一方面, skipWhile()
返回一个 Iterable
,同时跳过满足谓词的元素之前的所有元素。请注意,满足谓词的元素也会被包含在内。
运行该示例后,将 takeWhile()
改为取元素,直到到达第一个负数。
var numbersUntilNegative =
numbers.takeWhile((number) => !number.isNegative);
注意,条件 number.isNegative
是用 !
否定的。
练习: 从列表中过滤元素 #
下面的练习提供了使用上一练习中的 User
类的 where()
方法的练习。
使用 where()
实现两个函数。
- 第1部分:实现
filterUnder21()
。- 返回一个包含所有21岁以上用户的
Iterable
。
- 返回一个包含所有21岁以上用户的
- 第2部分:实现
findShortNamed()
。- 返回一个包含所有名字长度为 3 或更少的用户的
Iterable
。
- 返回一个包含所有名字长度为 3 或更少的用户的
Iterable<User> filterUnder21(Iterable<User> users) {
return users.where((user) => user.age >= 21);
}
Iterable<User> findShortNamed(Iterable<User> users) {
return users.where((user) => user.name.length <= 3);
}
class User {
String name;
int age;
User(
this.name,
this.age,
);
}
快速回顾:
- 用
where()
过滤一个Iterable
的元素。 where()
的输出是另一个Iterable
。- 使用
takeWhile()
和skipWhile()
来获取元素,直到满足一个条件或之后。 - 这些方法的输出可以是一个空的
Iterable
。
Map #
通过 map()
方法映射 Iterables
,你可以在每个元素上应用一个函数,用一个新的元素替换每个元素。
Iterable<int> output = numbers.map((number) => number * 10);
在这个例子中,Iterable
数字的每个元素都被乘以 10。
你也可以使用 map()
将一个元素转换为不同的对象-例如,将所有 int
转换为 String
,在下面的例子中可以看到。
Iterable<String> output = numbers.map((number) => number.toString());
注意:map()
返回一个懒惰的 Iterable
,这意味着只有在元素被迭代时才会调用所提供的函数。
例子: 使用 map 改变元素 #
运行这个例子,看看如何使用 map()
将一个 Iterable
中的所有元素乘以2,你认为输出会是什么?
main() {
var numbersByTwo = [1, -2, 3, 42].map((number) => number * 2);
print('Numbers: $numbersByTwo.');
}
练习: 映射到不同类型 #
在前面的例子中,你把一个 Iterable
的元素乘以2,输入和输出都是 int
的 Iterable
。
在这个练习中,你的代码接收一个 User
的 Iterable
,你需要返回一个包含用户名和年龄的字符串的 Iterable
。
Iterable
中的每个字符串必须遵循这样的格式。'{name} is {age}'
-例如 'Alice is 21'
。
Iterable<String> getNameAndAges(Iterable<User> users) {
return users.map((user) => '${user.name} is ${user.age}');
}
class User {
String name;
int age;
User(
this.name,
this.age,
);
}
快速回顾:
map()
将一个函数应用于一个Iterable
的所有元素。map()
的输出是另一个Iterable
。- 在
Iterable
被迭代之前,函数不会被计算。
练习: 把所有的东西放在一起 #
现在是练习所学知识的时候了,在最后一个练习中。
这个练习提供了类 EmailAddress
,它有一个构造函数,接收一个字符串。另一个提供的函数是 isValidEmailAddress()
,它测试一个电子邮件地址是否有效。
构造函数/函数 | 类型签名 | 描述 |
---|---|---|
EmailAddress() | EmailAddress(String address) | 为指定的地址创建一个 EmailAddress。 |
isValidEmailAddress() | bool isValidEmailAddress(EmailAddress) | 如果提供的 EmailAddress 有效,返回 true。 |
编写以下代码。
第1部分:实现 parseEmailAddresses()
。
- 编写函数
parseEmailAddresses()
,它接收一个包含电子邮件地址的Iterable<String>
,并返回一个Iterable<EmailAddress>
。 - 使用方法
map()
从String
映射到EmailAddress
。 - 使用构造函数
EmailAddress(String)
创建EmailAddress
对象。
第二部分:实现 anyInvalidEmailAddress()
。
- 编写函数
anyInvalidEmailAddress()
,它接收一个Iterable<EmailAddress>
,并在Iterable
中的任何EmailAddress
无效时返回 true。 - 使用方法
any()
和提供的函isValidEmailAddress()
。
第3部分:实现 validEmailAddresses()
。
- 编写函数
validEmailAddresses()
,它接收一个Iterable<EmailAddress>
并返回另一个只包含有效地址的Iterable<EmailAddress>
。 - 使用方法
where()
来过滤Iterable<EmailAddress>
。 - 使用提供的函数
isValidEmailAddress()
来评估一个EmailAddress
是否有效。
Iterable<EmailAddress> parseEmailAddresses(Iterable<String> strings) {
return strings.map((s) => EmailAddress(s));
}
bool anyInvalidEmailAddress(Iterable<EmailAddress> emails) {
return emails.any((email) => !isValidEmailAddress(email));
}
Iterable<EmailAddress> validEmailAddresses(Iterable<EmailAddress> emails) {
return emails.where((email) => isValidEmailAddress(email));
}
class EmailAddress {
String address;
EmailAddress(this.address);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is EmailAddress &&
runtimeType == other.runtimeType &&
address == other.address;
@override
int get hashCode => address.hashCode;
@override
String toString() {
return 'EmailAddress{address: $address}';
}
}
下一步是什么? #
恭喜你,你完成了 codelab 的学习! 如果你想了解更多,这里有一些下一步的建议。
- 玩玩 DartPad。
- 试试另一个代码实验。
- 阅读 Iterable API 参考资料,了解本 codelab 未涉及的方法。