У мові програмування Dart класи Iterable
та List
надають базовий набір методів для модифікації та фільтрації списків. Однак відсутність бібліотеки на кшталт LINQ-to-Objects у C# у багатьох випадках залишає розробників без лаконічних однорядкових операцій. Звісно ж, будь-яку задачу можна вирішити, використовуючи лише методи з dart.core
, проте нерідко це потребує рішення в декілька рядків, і такий код виходить неочевидним і складнішим для розуміння.
Бібліотека dartx надає розробникам можливість комбінування і використання елегантних та читабельних однорядкових операцій над колекціями (та іншими типами у Dart) завдяки механізму екстеншенів.
Зараз ми розглянемо, як наступні задачі вирішуються за допомогою базових методів з мови програмування Dart та з використанням методів бібліотеки dartx:
- отримати перший / останній елемент списку або null / значення за замовчуванням / за умовою;
- трансформувати / відфільтрувати / відвідати елементи списку в залежності від їх індексу;
- перетворити список на мапу;
- впорядкувати список;
- виокремити унікальні елементи списку;
- отримати найменше / найбільше / середнє арифметичне значення певного атрибута елементів списку;
- вилучити елементи списку, що є
null
.
Та розглянемо декілька додаткових корисних екстеншенів.
Всі приклади вимагають посилання на пакет dartx у pubspec.yaml
файлі, а також import
у файлі з кодом:
import 'package:dartx/dartx.dart';
Цей типовий код для отримання першого та останнього елементу списку у чистому Dart:
final first = list.first;
final last = list.last;
призведе до ексепшену StateError
під час виконання програми, якщо список list
порожній. Щоб його позбутися, доведеться перевіряти чи список не порожній і явно повертати null
:
final firstOrNull = list.isNotEmpty ? list.first : null;
final lastOrNull = list.isNotEmpty ? list.last : null;
З використанням dartx ця задача спрощується до:
final firstOrNull = list.firstOrNull;
final lastOrNull = list.lastOrNull;
Аналогічно, цей код поверне null
, якщо index
поза межами list
:
final elementAtOrNull = list.elementAtOrNull(index);
Пам’ятаючи, що гетери .first
та .last
призведуть до ексепшена, коли список list
порожній, отримання першого та останнього елементів або вказаного значення у чистому Dart має такий вигляд:
final firstOrDefault = (list.isNotEmpty ? list.first : null) ?? defaultValue;
final lastOrDefault = (list.isNotEmpty ? list.last : null) ?? defaultValue;
З пакетом dartx:
final firstOrDefault = list.firstOrDefault(defaultValue);
final lastOrDefault = list.lastOrElse(defaultValue);
Аналогічно, цей код поверне defaultValue
, якщо index
поза межами list
:
final elementAtOrDefault = list.elementAtOrDefault(index, defaultValue);
Таким є код для одержання першого та останнього елементів списку, що відповідають деякій умові або null
у чистому Dart:
final firstWhere = list.firstWhere((x) => x.matches(condition));
final lastWhere = list.lastWhere((x) => x.matches(condition));
Неважко здогадатись, що і цей код може призводити до StateError
ексепшену. Цю ситуацію можна виправити параметром orElse
:
final firstWhereOrNull = list.firstWhere((x) => x.matches(condition), orElse: () => null);
final lastWhereOrNull = list.lastWhere((x) => x.matches(condition), orElse: () => null);
З dartx імплементація скорочується до:
final firstWhereOrNull = list.firstOrNullWhere((x) => x.matches(condition));
final lastWhereOrNull = list.lastOrNullWhere((x) => x.matches(condition));
Нерідкою є необхідність формування нового списку, де кожен елемент певним чином залежить від свого індексу в оригінальному списку. Для прикладу, розглянемо задачу трансформації кожного елементу у рядок String
з додаванням його індексу.
Це один з варіантів реалізації на чистому Dart:
final newList = list.asMap()
.map((index, x) => MapEntry(index, '$index $x'))
.values
.toList();
З dartx все набагато простіше:
final newList = list.mapIndexed((index, x) => '$index $x').toList();
Цей код містить .toList()
, бо цей та більшість інших методів повертають ліниві Iterable
.
Для іншого прикладу розглянемо задачу колекціонування тільки елементів з непарними індексами.
На чистому Dart це:
final oddItems = [];
for (var i = 0; i < list.length; i++) {
if (i.isOdd) {
oddItems.add(list[i]);
}
}
Або в одному ланцюзі:
final oddItems = list.asMap()
.entries
.where((entry) => entry.key.isOdd)
.map((entry) => entry.value)
.toList();
З dartx це:
final oddItems = list.whereIndexed((x, index) => index.isOdd).toList();
або ж:
final oddItems = list.whereNotIndexed((x, index) => index.isEven).toList();
Як вивести вміст списку, вказуючи серед іншого індекси елементів?
На чистому Dart:
for (var i = 0; i < list.length; i++) {
print('$i: ${list[i]}');
}
З dartx:
list.forEachIndexed((element, index) => print('$index: $element'));
Припустимо, необхідно конвертувати список унікальних об’єктів Person
на Map<String, Person>
, де ключами є person.id
, а значеннями — повні об’єкти Person
.
На чистому Dart це має такий вигляд:
final peopleMap = people.asMap().map((index, person) => MapEntry(person.id, person));
З dartx:
final peopleMap = people.associate((person) => MapEntry(person.id, person));
або:
final peopleMap = people.associateBy((person) => person.id);
А ось як виглядає реалізація задачі формування мапи, де ключами є дата DateTime
, а значеннями — список List<Person>
людей, які народились у цей день.
На чистому Dart:
final peopleMapByBirthDate = people.fold<Map<DateTime, List<Person>>>(
<DateTime, List<Person>>{},
(map, person) {
if (!map.containsKey(person.birthDate)) {
map[person.birthDate] = <Person>[];
}
map[person.birthDate].add(person);
return map;
},
);
Значно простішим є варіант з використанням dartx:
final peopleMapByBirthDate = people.groupBy((person) => person.birthDate);
При використанні методу з чистого Dart потрібно пам’ятати, що:
list.sort();
модифікує оригінальний список list
, та щоб отримати нову колекцію, потрібно змінити код на:
final orderedList = [...list]..sort();
dartx надає метод, що повертає новий екземпляр списку:
final orderedList = list.sorted();
та:
final orderedDescendingList = list.sortedDescending();
Припустимо, необхідно впорядкувати список на основі деякої властивості елементів.
На чистому Dart:
final orderedPeople = [...people]
..sort((person1, person2) => person1.birthDate.compareTo(person2.birthDate));
З dartx:
final orderedPeople = people.sortedBy((person) => person.birthDate);
та:
final orderedDescendingPeople = people.sortedByDescending((person) => person.birthDate);
До речі, з dartx також можливо впорядковувати списки за кількома ознаками:
final orderedPeopleByAgeAndName = people
.sortedBy((person) => person.birthDate)
.thenBy((person) => person.name);
та:
final orderedDescendingPeopleByAgeAndName = people
.sortedByDescending((person) => person.birthDate)
.thenByDescending((person) => person.name);
Один з варіантів вирішення цієї задачі на чистому Dart такий:
final unique = list.toSet().toList();
Ця реалізація не гарантує збереження порядку розташування елементів, а альтернативою є тільки рішення у кілька рядків.
З dartx реалізація спрощується до:
final unique = list.distinct().toList();
та:
final uniqueFirstNames = people.distinctBy((person) => person.firstName).toList();
Для отримання найменшого / найбільшого елемента списку можна, наприклад, його впорядкувати й взяти перший / останній елементи:
final min = ([...list]..sort()).first;
final max = ([...list]..sort()).last;
Той самий підхід можна використати для впорядкування за певною ознакою:
final minAge = (people.map((person) => person.age).toList()..sort()).first;
final maxAge = (people.map((person) => person.age).toList()..sort()).last;
або ж:
final youngestPerson =
([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).first;
final oldestPerson =
([...people]..sort((person1, person2) => person1.age.compareTo(person2.age))).last;
Неважко здогадатись, що dartx пропонує значно простіші рішення, які, до речі, повернуть null
для порожнього списку:
final youngestPerson = people.minBy((person) => person.age);
final oldestPerson = people.maxBy((person) => person.age);
Якщо елементи списку реалізують Comparable
, можна також використовувати методи без селекторів:
final min = list.min();
final max = list.max();
Також не є складними задачі отримання середнього арифметичного:
final average = list.average();
або ж:
final averageAge = people.averageBy((person) => person.age);
та суми num
елементів списку:
final sum = list.sum();
або num
властивостей комплексних елементів:
final totalChildrenCount = people.sumBy((person) => person.childrenCount);
У чистому Dart:
final nonNullItems = list.where((x) => x != null).toList();
З dartx:
final nonNullItems = list.whereNotNull().toList();
Бібліотека dartx надає широкий набір корисних методів. Надалі ми не будемо зупинятися на кожному детально, проте я сподіваюсь, що назви і код достатньо очевидні.
final report = people.joinToString(
separator: '\n',
transform: (person) => '${person.firstName}_${person.lastName}',
prefix: '<<️',
postfix: '>>',
);
final allAreAdults = people.all((person) => person.age >= 18);
final allAreAdults = people.none((person) => person.age < 18);
final first = list.first;
final second = list.second;
final third = list.third;
final fourth = list.fourth;
final youngestPeople = people.sortedBy((person) => person.age).takeFirst(5);
final oldestPeople = people.sortedBy((person) => person.age).takeLast(5);
final orderedPeopleUnder50 = people
.sortedBy((person) => person.age)
.firstWhile((person) => person.age < 50)
.toList();
final orderedPeopleOver50 = people
.sortedBy((person) => person.age)
.lastWhile((person) => person.age >= 50)
.toList();
Додам, що бібліотека dartx пропонує ще більше зручних екстеншенів для Iterable
, List
та інших типів у Dart. Найкращим способом ознайомитися з усіма її можливостями є перегляд програмного коду.
Викажіть авторам пакету підтримку, поставивши йому 👍🏻 на pub.dev та ⭐️ на GitHub.
Опубліковано на DOU у червні 2021.