Iterable 컬렉션

Iterable 컬렉션 (Iterable collections)

Iterable 객체 — 리스트(List), 집합(Set) 같은 것들 — 를 사용하는 방법을 알려 주는 인터랙티브 가이드예요. 예제를 직접 실행하고 연습문제를 풀면서 익힐 수 있어요.

출처: Iterable collections

본문

이 튜토리얼은 Iterable 클래스를 구현하는 컬렉션 — 예를 들어 ListSet — 을 사용하는 방법을 가르쳐 줘요.

Iterable은 온갖 종류의 Dart 애플리케이션의 기본 구성 요소이며, 여러분도 아마 눈치채지 못한 채 이미 사용하고 있을 거예요. 이 튜토리얼은 그 Iterable을 최대한 활용하는 데 도움을 줄 거예요.

임베디드 DartPad 에디터를 사용해 예제 코드를 실행하고 연습문제를 풀면서 배운 내용을 확인해 볼 수 있어요.

이 튜토리얼을 제대로 이해하려면 Dart 문법에 대한 기본 지식이 있어야 해요.

이 튜토리얼이 다루는 내용이에요:

  • Iterable의 요소를 읽는 방법.
  • Iterable의 요소가 조건을 만족하는지 확인하는 방법.
  • Iterable의 내용을 필터링하는 방법.
  • Iterable의 내용을 다른 값으로 매핑하는 방법.

예상 소요 시간: 60분.

참고 이 페이지는 예제와 연습문제를 보여 주기 위해 임베디드 DartPad를 사용해요. DartPad 대신 빈 상자가 보인다면 DartPad 문제 해결 페이지를 확인하세요.

이 튜토리얼의 연습문제는 일부가 완성된 코드 조각들을 담고 있어요. DartPad에서 코드를 완성하고 Run 버튼을 눌러 배운 내용을 확인해 볼 수 있어요. main 함수 안의 테스트 코드나 그 아래 코드는 수정하지 마세요. 도움이 필요하면 각 연습문제 아래의 Hint나 Solution 드롭다운을 펼쳐 보세요.

컬렉션이란 무엇인가 (What are collections?)

컬렉션(collection) 은 객체의 그룹을 나타내는 객체이며, 그 객체들을 요소(element) 라고 불러요. Iterable은 컬렉션의 한 종류예요.

컬렉션은 비어 있을 수도 있고 많은 요소를 포함할 수도 있어요. 목적에 따라 컬렉션은 서로 다른 구조와 구현을 가질 수 있어요. 가장 흔한 컬렉션 타입을 몇 가지 꼽아 볼게요:

  • List: 인덱스로 요소를 읽는 데 사용해요.
  • Set: 한 번만 나타날 수 있는 요소를 담는 데 사용해요.
  • Map: 키를 사용해 요소를 읽는 데 사용해요.

Iterable이란 무엇인가 (What is an Iterable?)

Iterable은 순차적으로 접근할 수 있는 요소들의 컬렉션이에요.

Dart에서 Iterable은 추상 클래스라서 직접 인스턴스화할 수 없어요. 하지만 새 List나 Set을 만들어 새 Iterable을 만들 수 있어요.

List와 Set은 둘 다 Iterable이므로 Iterable 클래스와 같은 메서드와 프로퍼티를 가져요.

Map은 구현에 따라 내부에서 다른 데이터 구조를 사용해요. 예를 들어 HashMap은 키로 얻는 요소(값이라고도 함)를 담는 해시 테이블을 사용해요. Map의 요소는 map의 entriesvalues 프로퍼티를 사용해 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의 항목들을 그 위치의 항목에 도달할 때까지 하나씩 지나가요.

Iterable<int> iterable = [1, 2, 3];
int value = iterable.elementAt(1);

iterable의 구현과 항목 수에 따라 elementAt은 선형 복잡도를 가질 수 있고 비쌀 수 있어요. 특정 항목에 반복해서 접근할 계획이라면, iterable에 .toList()를 호출해서 한 번 리스트로 변환한 뒤 [] 연산자를 쓰는 것을 고려해 보세요.

final items = veryLargeIterable().toList();

final tenthItem = items[9];
final hundredthItem = items[99];
final thousandthItem = items[999];
final lastItem = items.last;

Iterable의 요소에 접근하는 방법에 대해 더 배우려면 다음 섹션으로 계속 진행하세요.

요소 읽기 (Reading elements)

for-in 루프를 사용해 iterable의 요소를 순차적으로 읽을 수 있어요.

예시: for-in 루프 사용하기

다음 예시는 for-in 루프로 요소를 읽는 방법을 보여 줘요.

void main() {
  const iterable = ['Salad', 'Popcorn', 'Toast'];
  for (final element in iterable) {
    print(element);
  }
}

상세 무대 뒤에서는 for-in 루프가 iterator를 사용해요. 그러나 for-in이 읽고 이해하기 쉽고 오류도 덜 발생하기 때문에 Iterator API를 직접 쓰는 경우는 드물어요.

핵심 용어

  • Iterable: Dart의 Iterable 클래스.
  • Iterator: for-in이 Iterable 객체에서 요소를 읽는 데 사용하는 객체.
  • for-in 루프: Iterable에서 요소를 순차적으로 읽는 쉬운 방법.

예시: first와 last 사용하기

어떤 경우에는 Iterable의 첫 번째 또는 마지막 요소에만 접근하고 싶을 수 있어요.

Iterable 클래스에서는 요소에 직접 접근할 수 없으므로 iterable[0]을 호출해 첫 번째 요소에 접근할 수 없어요. 대신 첫 번째 요소를 얻는 first를 사용할 수 있어요.

또한 Iterable 클래스에서는 [] 연산자로 마지막 요소에 접근할 수 없지만, last 프로퍼티를 사용할 수 있어요.

경고 Iterable의 마지막 요소에 접근하려면 다른 모든 요소를 지나가야 하므로 last는 느릴 수 있어요. 비어 있는 Iterable에서 firstlast를 사용하면 StateError가 발생해요.

void main() {
  Iterable<String> iterable = const ['Salad', 'Popcorn', 'Toast'];
  print('The first element is ${iterable.first}');
  print('The last element is ${iterable.last}');
}

이 예시에서 firstlast로 Iterable의 첫 번째와 마지막 요소를 얻는 방법을 봤어요. 조건을 만족하는 첫 번째 요소를 찾는 것도 가능해요. 다음 섹션에서 firstWhere()라는 메서드로 그렇게 하는 방법을 보여 줄게요.

예시: firstWhere() 사용하기

Iterable의 요소에 순차적으로 접근할 수 있고, 첫 번째나 마지막 요소를 쉽게 얻을 수 있다는 걸 이미 봤어요.

이제 firstWhere()로 특정 조건을 만족하는 첫 번째 요소를 찾는 방법을 배워 볼게요. 이 메서드는 술어(predicate) 를 전달해야 하는데, 술어는 입력이 특정 조건을 만족하면 true를 반환하는 함수예요.

String element = iterable.firstWhere((element) => element.length > 5);

예를 들어 5자보다 많은 첫 번째 String을 찾고 싶다면, 요소 크기가 5보다 클 때 true를 반환하는 술어를 전달해야 해요.

다음 예시를 실행해 firstWhere()가 어떻게 동작하는지 확인해 보세요. 모든 함수가 같은 결과를 줄 거라고 생각하나요?

bool predicate(String item) {
  return item.length > 5;
}

void main() {
  const items = ['Salad', 'Popcorn', 'Toast', 'Lasagne'];

  // You can find with a simple expression:
  var foundItem1 = items.firstWhere((item) => item.length > 5);
  print(foundItem1);

  // Or try using a function block:
  var foundItem2 = items.firstWhere((item) {
    return item.length > 5;
  });
  print(foundItem2);

  // Or even pass in a function reference:
  var foundItem3 = items.firstWhere(predicate);
  print(foundItem3);

  // You can also use an `orElse` function in case no value is found!
  var foundItem4 = items.firstWhere(
    (item) => item.length > 10,
    orElse: () => 'None!',
  );
  print(foundItem4);
}

이 예시에서 술어를 작성하는 세 가지 방법을 볼 수 있어요:

  • 표현식(expression)으로: 테스트 코드가 화살표 문법(=>)을 사용한 한 줄이에요.
  • 블록(block)으로: 테스트 코드가 중괄호 사이에 여러 줄과 return 문으로 구성돼요.
  • 함수(function)로: 테스트 코드가 외부 함수 안에 있고, 그것을 firstWhere() 메서드에 매개변수로 전달해요.

옳고 그른 방법은 없어요. 여러분에게 가장 잘 맞고 코드를 더 읽기·이해하기 쉽게 만드는 방식을 쓰세요.

마지막 예시는 선택적 명명 매개변수 orElse와 함께 firstWhere()를 호출해요. orElse는 요소를 찾지 못했을 때 대안을 제공해요. 이 경우 조건을 만족하는 요소가 없으므로 'None!' 텍스트가 반환돼요.

참고 조건을 만족하는 요소가 없고 orElse 매개변수도 제공되지 않으면, firstWhere()StateError를 throw해요.

빠른 복습

  • Iterable의 요소는 순차적으로 접근해야 해요.
  • 모든 요소를 순회하는 가장 쉬운 방법은 for-in 루프를 쓰는 것이에요.
  • firstlast 게터로 첫 번째와 마지막 요소를 얻을 수 있어요.
  • firstWhere()로 조건을 만족하는 첫 번째 요소를 찾을 수도 있어요.
  • 테스트 술어를 표현식, 블록, 함수로 작성할 수 있어요.

핵심 용어:

  • Predicate(술어): 특정 조건을 만족하면 true를 반환하는 함수.

연습문제: 테스트 술어 작성 연습

다음 연습문제는 일부가 완성된 코드 조각이 있는 실패하는 단위 테스트예요. 테스트가 통과하도록 코드를 작성해 연습문제를 완성하는 게 여러분의 과제예요. main()을 구현할 필요는 없어요.

이 연습문제는 singleWhere()를 소개해요. 이 메서드는 firstWhere()와 비슷하게 동작하지만, 이 경우 Iterable의 요소 하나만 술어를 만족하길 기대해요. Iterable에서 술어 조건을 만족하는 요소가 하나보다 많거나 없으면, 이 메서드는 StateError 예외를 throw해요.

경고 singleWhere()는 Iterable의 마지막 요소까지 전체를 지나가므로, Iterable이 무한하거나 아주 큰 요소 컬렉션을 담고 있으면 문제가 될 수 있어요.

여러분의 목표는 다음 조건을 만족하는 singleWhere()의 술어를 구현하는 거예요:

  • 요소가 문자 'a'를 포함한다.
  • 요소가 문자 'M'으로 시작한다.

테스트 데이터의 모든 요소는 문자열이에요. 도움이 필요하면 클래스 문서를 확인할 수 있어요.

// Implement the predicate of singleWhere
// with the following conditions
// * The element contains the character `'a'`
// * The element starts with the character `'M'`
String singleWhere(Iterable<String> items) {
  return items.singleWhere(TODO('Implement the outlined predicate.'));
}

정답:

String singleWhere(Iterable<String> items) {
  return items.singleWhere(
          (element) => element.startsWith('M') && element.contains('a'));
}

힌트: String 클래스의 containsstartsWith 메서드를 활용하면 돼요.

조건 확인하기 (Checking conditions)

Iterable을 다룰 때는 컬렉션의 모든 요소가 어떤 조건을 만족하는지 확인해야 할 때가 있어요.

다음처럼 for-in 루프를 사용한 해결책을 쓰고 싶어질 수도 있어요:

for (final item in items) {
  if (item.length < 5) {
    return false;
  }
}
return true;

하지만 every() 메서드로 같은 일을 할 수 있어요:

return items.every((item) => item.length >= 5);

every() 메서드를 사용하면 코드가 더 읽기 쉽고, 간결하며, 오류가 덜 발생해요.

예시: any()와 every() 사용하기

Iterable 클래스는 조건을 확인하는 데 사용할 수 있는 메서드 두 개를 제공해요:

  • any(): 적어도 하나의 요소가 조건을 만족하면 true를 반환해요.
  • every(): 모든 요소가 조건을 만족하면 true를 반환해요.

이 연습문제를 실행해 실제 동작을 확인해 보세요.

void main() {
  const items = ['Salad', 'Popcorn', 'Toast'];

  if (items.any((item) => item.contains('a'))) {
    print('At least one item contains "a"');
  }

  if (items.every((item) => item.length >= 5)) {
    print('All items have length >= 5');
  }
}

이 예시에서 any()는 적어도 하나의 요소가 문자 a를 포함하는지 확인하고, every()는 모든 요소의 길이가 5보다 크거나 같은지 확인해요.

코드를 실행한 뒤, any()의 술어를 바꿔서 false가 반환되게 해 보세요:

if (items.any((item) => item.contains('Z'))) {
  print('At least one item contains "Z"');
} else {
  print('No item contains "Z"');
}

any()로 Iterable의 어떤 요소도 특정 조건을 만족하지 않는지 확인할 수도 있어요.

연습문제: Iterable이 조건을 만족하는지 확인하기

다음 연습문제는 이전 예시에서 설명한 any()every() 메서드 사용 연습을 제공해요. 이 경우 age 멤버 필드를 가진 User 객체로 표현되는 사용자 그룹을 다루게 돼요.

any()every()를 사용해 두 함수를 구현하세요:

  • 1부: anyUserUnder18() 구현하기.
    • 적어도 한 명의 사용자가 17세 이하이면 true를 반환해요.
  • 2부: everyUserOver13() 구현하기.
    • 모든 사용자가 14세 이상이면 true를 반환해요.
bool anyUserUnder18(Iterable<User> users) {
  // TODO: Implement the anyUserUnder18 function.
}

bool everyUserOver13(Iterable<User> users) {
  // TODO: Implement the everyUserOver13 function.
}

class User {
  final String name;
  final int age;

  User(
    this.name,
    this.age,
  );
}

정답:

bool anyUserUnder18(Iterable<User> users) {
  return users.any((user) => user.age < 18);
}

bool everyUserOver13(Iterable<User> users) {
  return users.every((user) => user.age > 13);
}

힌트: Iterable 클래스의 anyevery 메서드를 사용하는 걸 기억하세요. 이 메서드들의 예시와 도움말은 이전의 any()와 every() 예시를 참고하세요.

빠른 복습

  • 조건을 확인하려고 for-in 루프를 쓸 수도 있지만, 더 나은 방법이 있어요.
  • any() 메서드로 어떤 요소가 조건을 만족하는지 확인할 수 있어요.
  • every() 메서드로 모든 요소가 조건을 만족하는지 확인할 수 있어요.

필터링하기 (Filtering)

이전 섹션들에서 특정 술어를 만족하는 요소를 찾는 데 도움이 되는 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 (final number in evenNumbers) {
  print('$number is even');
}

예시: where() 사용하기

다음 예시를 실행해 where()any() 같은 다른 메서드와 함께 사용하는 방법을 확인해 보세요.

void main() {
  var evenNumbers = const [1, -2, 3, 42].where((number) => number.isEven);

  for (final 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 예외를 throw하지 않아요.

예시: takeWhile 사용하기

takeWhile()skipWhile() 메서드도 Iterable에서 요소를 필터링하는 데 도움이 될 수 있어요.

다음 예시를 실행해 takeWhile()skipWhile()이 숫자를 담은 Iterable을 어떻게 나누는지 확인해 보세요.

void main() {
  const numbers = [1, 3, -2, 0, 4, 5];

  var numbersUntilZero = numbers.takeWhile((number) => number != 0);
  print('Numbers until 0: $numbersUntilZero');

  var numbersStartingAtZero = numbers.skipWhile((number) => number != 0);
  print('Numbers starting at 0: $numbersStartingAtZero');
}

이 예시에서 takeWhile()은 술어를 만족하는 요소 앞의 모든 요소를 담은 Iterable을 반환해요. 반면 skipWhile()은 술어를 만족하지 않는 첫 번째 요소와 그 뒤의 모든 요소를 담은 Iterable을 반환해요.

예시를 실행한 뒤 takeWhile()을 바꿔서 첫 번째 음수에 도달할 때까지 요소를 가져오게 해 보세요.

var numbersUntilNegative = numbers.takeWhile(
  (number) => !number.isNegative,
);

number.isNegative 조건이 !로 부정된다는 점을 주목하세요.

연습문제: 리스트에서 요소 필터링하기

다음 연습문제는 이전 연습문제의 User 클래스와 함께 where() 메서드를 사용하는 연습을 제공해요.

where()를 사용해 두 함수를 구현하세요:

  • 1부: filterOutUnder21() 구현하기.
    • 나이가 21세 이상인 모든 사용자를 담은 Iterable을 반환해요.
  • 2부: findShortNamed() 구현하기.
    • 이름 길이가 3 이하인 모든 사용자를 담은 Iterable을 반환해요.
Iterable<User> filterOutUnder21(Iterable<User> users) {
  // TODO: Implement the filterOutUnder21 function.
}

Iterable<User> findShortNamed(Iterable<User> users) {
  // TODO: Implement the findShortNamed function.
}

class User {
  final String name;
  final int age;

  User(
    this.name,
    this.age,
  );
}

정답:

Iterable<User> filterOutUnder21(Iterable<User> users) {
  return users.where((user) => user.age >= 21);
}

Iterable<User> findShortNamed(Iterable<User> users) {
  return users.where((user) => user.name.length <= 3);
}

힌트: Iterable 클래스의 where 메서드를 활용하는 걸 기억하세요. where 사용에 대한 도움말과 예시는 이전의 where() 예시를 참고하세요.

빠른 복습

  • where()로 Iterable의 요소를 필터링해요.
  • where()의 출력은 또 다른 Iterable이에요.
  • takeWhile()skipWhile()으로 조건이 충족될 때까지 또는 그 이후의 요소를 얻어요.
  • 이 메서드들의 출력은 빈 Iterable일 수 있어요.

매핑하기 (Mapping)

map() 메서드로 Iterable을 매핑하면 각 요소에 함수를 적용해서 각 요소를 새 요소로 교체할 수 있어요.

Iterable<int> output = numbers.map((number) => number * 10);

이 예시에서 Iterable numbers의 각 요소는 10으로 곱해져요.

map()으로 요소를 다른 객체로 변환할 수도 있어요. 예를 들어 모든 int를 String으로 변환할 수 있는데, 다음 예시에서 볼 수 있어요:

Iterable<String> output = numbers.map((number) => number.toString());

참고 map()은 지연(lazy) Iterable을 반환해요. 즉 제공한 함수는 요소가 순회될 때만 호출돼요.

예시: map으로 요소 변경하기

다음 예시를 실행해 map()으로 Iterable의 모든 요소를 2배로 만드는 방법을 확인해 보세요. 출력이 뭘 거라고 생각하나요?

void main() {
  var numbersByTwo = const [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) {
  // TODO: Implement the getNameAndAges function.
}

class User {
  final String name;
  final int age;

  User(
    this.name,
    this.age,
  );
}

정답:

Iterable<String> getNameAndAges(Iterable<User> users) {
  return users.map((user) => '${user.name} is ${user.age}');
}

힌트: Iterable 클래스의 map 메서드를 활용하는 걸 기억하세요. map 사용에 대한 도움말과 예시는 이전의 map 예시를 참고하세요. 여러 값을 하나의 문자열로 합치려면 문자열 보간(string interpolation)을 고려해 보세요.

빠른 복습

  • map()은 Iterable의 모든 요소에 함수를 적용해요.
  • map()의 출력은 또 다른 Iterable이에요.
  • 함수는 Iterable이 순회될 때까지 평가되지 않아요.

종합 연습문제: 배운 것 모두 쓰기

배운 것을 마지막 연습문제 하나로 연습할 시간이에요.

이 연습문제는 생성자가 문자열을 받는 EmailAddress 클래스를 제공해요. 또 제공되는 함수는 이메일 주소가 유효한지 검사하는 isValidEmailAddress()예요.

생성자/함수 타입 시그니처 설명
EmailAddress() EmailAddress(String address) 지정된 주소에 대한 EmailAddress를 만들어요.
isValidEmailAddress() bool isValidEmailAddress(EmailAddress) 제공된 EmailAddress가 유효하면 true를 반환해요.

다음 코드를 작성해 보세요:

1부: parseEmailAddresses() 구현하기.

  • 이메일 주소를 담은 Iterable<String>을 받고 Iterable<EmailAddress>를 반환하는 함수 parseEmailAddresses()를 작성해요.
  • map() 메서드를 사용해 String에서 EmailAddress로 매핑해요.
  • 생성자 EmailAddress(String)을 사용해 EmailAddress 객체를 만들어요.

2부: anyInvalidEmailAddress() 구현하기.

  • Iterable<EmailAddress>을 받고, Iterable 안의 어떤 EmailAddress가 유효하지 않으면 true를 반환하는 함수 anyInvalidEmailAddress()를 작성해요.
  • 제공된 함수 isValidEmailAddress()와 함께 any() 메서드를 사용해요.

3부: validEmailAddresses() 구현하기.

  • Iterable<EmailAddress>을 받고 유효한 주소만 담은 또 다른 Iterable<EmailAddress>을 반환하는 함수 validEmailAddresses()를 작성해요.
  • where() 메서드를 사용해 Iterable<EmailAddress>을 필터링해요.
  • EmailAddress가 유효한지 판단하려면 제공된 함수 isValidEmailAddress()를 사용해요.
Iterable<EmailAddress> parseEmailAddresses(Iterable<String> strings) {
  // TODO: Implement the parseEmailAddresses function.
}

bool anyInvalidEmailAddress(Iterable<EmailAddress> emails) {
  // TODO: Implement the anyInvalidEmailAddress function.
}

Iterable<EmailAddress> validEmailAddresses(Iterable<EmailAddress> emails) {
  // TODO: Implement the validEmailAddresses function.
}

class EmailAddress {
  final String address;

  EmailAddress(this.address);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is EmailAddress && address == other.address;

  @override
  int get hashCode => address.hashCode;

  @override
  String toString() => 'EmailAddress{address: $address}';
}

정답:

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));
}

다음 단계

축하해요, 튜토리얼을 끝냈어요! 더 배우고 싶다면 다음을 추천할게요:

더 알아보기