Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения. 

При поддержке компании Friflex буквально на днях вышла печатная версия моей книги. В ней есть раздел о нюансах изоляционной модели памяти в Dart, сквозной проект на пять глав (игра «Крестики-Нолики») и две дополнительные главы: «Алгоритмы, структуры данных на Dart и встроенные коллекции» и «Интероперабельность в Dart (Dart FFI)».

В этой статье я делюсь разделом «Встроенные коллекции Dart». В книге он идет после классических структур данных и их реализации на Dart. Такой подход, по моему мнению, позволяет лучше понять принципы работы встроенных коллекций, их сильные и слабые стороны.

Встроенные коллекции Dart

Ранее мы с вами рассмотрели, как реализуется та или иная структура данных, но не всегда есть время или желание писать их самостоятельно, тем более, что Dart предоставляет свою библиотеку коллекций dart:collection [26], которая содержит ряд готовых реализаций. К тому же, такие встроенные типы данных, как Map, Set, List (с натяжкой, но об этом чуть позже) можно рассматривать как абстрактные типы данных, так как. за их интерфейсом могут скрываться различные реализации, основанные на той или иной структуре данных, предоставляемой dart:collection.

Далее рассмотрим существующие коллекции Dart с их различными реализациями, детально не погружаясь в методы работы с ними. Это связано с тем, что мы достаточно подробно познакомились с их интерфейсом еще в первой главе, а в этой узнаем об основных структурах данных и как они реализуются в коде.

Map

Коллекция (интерфейс) Map представляет собой серию пар «ключ:значение» (MapEntry <K, V>) и предоставляет несколько реализаций:

  • HashMap;

  • LinkedHashMap;

  • SplayTreeMap;

  • UnmodifiableMapView.       

HashMap (хэш-таблица) хранит элементы не упорядочено, тем самым не гарантируя, что итерация по элементам будет осуществляться в том же порядке, как их добавляли в коллекцию. Так как в основе HashMap лежит классическая хэш-таблица, то коллизии решаются путем увеличения массива с последующим перерасчетом результата хэш-функции:

// ex_hashmap_1.dart
import 'dart:collection';
 
void main() {
  // создание экземпляра HashMap и
  // его приведение к интерфейсу Map<K,V>
  final Map<String, int> myHashMap = HashMap();
  myHashMap['one'] = 1;
  myHashMap['two'] = 2;
  myHashMap['three'] = 3;
 
  print(myHashMap['one']); // 1
  myHashMap['one'] = 11;
  print(myHashMap['one']); // 11
 
  print(myHashMap); // {three: 3, one: 11, two: 2}
 
  myHashMap.remove('one');
  print(myHashMap); // {three: 3, two: 2}
 
  for (var MapEntry(:key, :value) in myHashMap.entries) {
	print('key: $key, value: $value');
  }
  // key: three, value: 3
  // key: two, value: 2
}

У конструктора HashMap имеются необязательные именованные аргументы:

HashMap<K, V>({
	bool equals(K,K)?,
	int hashCode(K)?,
	bool isValidKey(dynamic)?,
})

Если при создании экземпляра класса в конструктор передано значение equals, то оно будет использоваться для сравнения уже имеющихся ключей с новыми. В противном случае будет использоваться оператор == ключа (сравнение объектов по их идентификатору).

Аргумент hashCode используется аналогично equals и отвечает за получение хэш-значения ключа, чтобы поместить его в экземпляр HashMap. Передаваемые в конструктор equals и hashCode всегда должны быть согласованы, то есть при equals(a, b) возвращающем true, hashCode(a) должен быть равен hashCode(b). Обычно, если вы передаете в конструктор класса HashMap реализацию метода equals, то вам также следует предоставить соответствующую реализацию метода hashCode:

// ex_hashmap_2.dart
import 'dart:collection';
 
class Person {
  final String name;
  final int age;
 
  Person(this.name, this.age);
 
  @override
  bool operator ==(Object other) =>
  	identical(this, other) ||
  	other is Person &&
      	runtimeType == other.runtimeType &&
      	name == other.name &&
      	age == other.age;
 
  @override
  int get hashCode => name.hashCode ^ age.hashCode;
 
  @override
  String toString() => 'Person{name: $name, age: $age}';
}
 
void main() {
  final Map<Person, String> myHashMap = HashMap<Person, String>(
	equals: (Person a, Person b) => a == b,
	hashCode: (Person p) => p.hashCode,
  );
 
  myHashMap[Person('Alice', 30)] = 'Developer';
  myHashMap[Person('Bob', 25)] = 'Designer';
 
  print(myHashMap);
}
// {Person{name: Bob, age: 25}: Designer,
//  Person{name: Alice, age: 30}: Developer}

Давайте представим, что у вас дома есть сейф с секретными отделениями, где каждый ключ подходит только к одному замку. Методы equals и hashCode позволяют определить, откроет ли ключ правильное отделение. Однако не все ключи подходят для этих методов, что чревато случайными сбоями или непреднамеренными проблемами с закрытием или открытием секретного отделения. Вот здесь на сцену выходит последний аргумент конструктора класса HashMap – isValidKey. Он отвечает за проверку, может ли ключ вообще попытаться открыть какое-либо секретное отделение. Если мы пытаемся использовать неподходящий ключ, isValidKey сразу скажет, что попытка бесполезна, вследствие чего не будет произведен вызов equals и hashCode, то есть при таком раскладе предполагается, что в экземпляре HashMap нет ключа, равного этому объекту. Также данный аргумент можно использовать, чтобы разрешить или запретить вызов оператора [], метода remove или containsKey.

В качестве пример давайте зададим реализацию isValidKey для фильтрации ключей экземпляра HashMap<String, String> таким образом, что последующая работа будет возможна только с ключами, начинающимися с буквы «A»:

// ex_hashmap_3.dart
import 'dart:collection';
 
void main() {
  final Map<String, String> myHashMap = HashMap(
	equals: (String a, String b) => a == b,
	hashCode: (String key) => key.hashCode,
	isValidKey: (dynamic key) {
  	if (key is String) {
    	return key.startsWith('A');
  	}
  	return false;
	},
  );
 
  myHashMap['Alice'] = 'Developer';
  myHashMap['Bob'] = 'Designer';
  myHashMap['Anna'] = 'Manager';
 
  // Проверяем, содержатся ли ключи с таким значением
  print(myHashMap.containsKey('Alice')); // true
  print(myHashMap.containsKey('Bob')); // false
  print(myHashMap.containsKey('Anna')); // true
  print(myHashMap.containsKey(123)); // false
 
  // Использование оператора [] для доступа к элементам.
  print(myHashMap['Alice']); // Developer
  print(myHashMap['Bob']); // null
  print(myHashMap); // {Bob: Designer, Alice: Developer, Anna: Manager}
}

По умолчанию isValidKey просто проверяет, является ли объект экземпляром ключа (один тип данных с ключом или производный от него).

LinkedHashMap позволяет выполнять итерацию в порядке добавления элементов и представляет собой хэш-таблицу, где в каждой ячейке массива – односвязный список. Для использования текущей реализации коллекции Map не нужно импортировать библиотеку dart:collection! Это связано с тем, что LinkedHashMap выступает в качестве реализации по умолчанию. То есть каждый раз при создании экземпляра Map<K,V> создается экземпляр   LinkedHashMap и приводится к интерфейсу Map<K,V>:

// ex_linkedhashmap.dart
void main() {
  final Map<String, int> myMap = {}; // экземпляр LinkedHashMap
  myMap['one'] = 1;
  myMap['two'] = 2;
  myMap['three'] = 3;
 
  print(myMap['one']); // 1
  myMap['one'] = 11;
  print(myMap['one']); // 11
 
  print(myMap); // {one: 11, two: 2, three: 3}
 
  myMap.remove('one');
  print(myMap); // {two: 2, three: 3}
 
  for (var MapEntry(:key, :value) in myMap.entries) {
	print('key: $key, value: $value');
  }
  // key: two, value: 2
  // key: three, value: 3
}

Конструктор LinkedHashMap и принцип работы передаваемых ему аргументов аналогичен HashMap:

LinkedHashMap<K, V>({
	bool equals(K,K)?,
	int hashCode(K)?,
	bool isValidKey(dynamic)?,
})

SplayTreeMap представляет собой сбалансированное двоичное дерево, где узлы упорядочены относительно друг друга, из-за чего итерация по элементам экземпляра SplayTreeMap будет осуществляться в их отсортированном порядке. Данная реализация позволяет выполнять большинство операций (вставка, поиск, изменение, удаление) в лучшем и среднем случае за O(log n):

// ex_splaytreemap_1.dart
import 'dart:collection';
 
void main() {
  // создание экземпляра SplayTreeMap и
  // его приведение к интерфейсу Map<K,V>
  final Map<int, String> mySplayTreeMap = SplayTreeMap();
  // по умолчанию узлы дерева сортируются по возрастанию ключа
 
  mySplayTreeMap[1] = 'one';
  mySplayTreeMap[2] = 'two';
  mySplayTreeMap[3] = 'three';
  mySplayTreeMap[33] = '??';
 
  print(mySplayTreeMap[1]); // one
  mySplayTreeMap[1] = '11';
  print(mySplayTreeMap[1]); // 11
 
  print(mySplayTreeMap); // {1: 11, 2: two, 3: three, 33: ??}
 
  mySplayTreeMap.remove(1);
  print(mySplayTreeMap); // {2: two, 3: three, 33: ??}
 
  for (var MapEntry(:key, :value) in mySplayTreeMap.entries) {
	print('key: $key, value: $value');
  }
  // key: 2, value: two
  // key: 3, value: three
  // key: 33, value: ??
}

Ниже представлен общий вид конструктора данной коллекции:

SplayTreeMap([
  int compare(K key1, K key2)?,
  bool isValidKey(dynamic potentialKey)? // работает как и в HashMap
])

Ключи узлов SplayTreeMap сравниваются с помощью необязательной функции int Function(K, K)? compare, которую можно передать в конструктор при создании экземпляра класса. Если она не была задана, то SplayTreeMap предполагает, что объекты, передаваемые в качестве ключа, реализуют интерфейс Comparable. В качестве примера давайте укажем, что итерация по текущей коллекции производится в порядке уменьшения значения ключа:

// ex_splaytreemap_2.dart
import 'dart:collection';
 
void main() {
  final Map<int, String> mySplayTreeMap = SplayTreeMap(
	(key1, key2) => key2.compareTo(key1),
	// или
	// (key1, key2) => key2 - key1,
  );
 
  mySplayTreeMap[1] = 'one';
  mySplayTreeMap[2] = 'two';
  mySplayTreeMap[3] = 'three';
  mySplayTreeMap[33] = '??';
 
  print(mySplayTreeMap); // {33: ??, 3: three, 2: two, 1: 11}
 
  mySplayTreeMap.remove(1);
  print(mySplayTreeMap); // {33: ??, 3: three, 2: two}
 
  for (var MapEntry(:key, :value) in mySplayTreeMap.entries) {
	print('key: $key, value: $value');
  }
  // key: 33, value: ??
  // key: 3, value: three
  // key: 2, value: two
}

UnmodifiableMapView представляет собой немодифицируемую обертку над экземпляром Map, разрешающую только операции чтения, и может использоваться в тех случаях, когда вам необходимо гарантировать, что объект может быть изменен только в месте его создания:

// ex_unmodifiablemapview.dart
import 'dart:collection';
 
void main() {
  final originalMap = <String, String>{
	'Alice': 'Developer',
	'Bob': 'Designer',
  };
 
  // Создание неизменяемого представления
  var unmodifiableMap = UnmodifiableMapView(originalMap);
 
  // Попытка добавить новый элемент приведет к исключению
  try {
	unmodifiableMap['Charlie'] = 'Data Scientist';
  } on UnsupportedError catch (e) {
	print(e.message); // Cannot modify unmodifiable map
  }
 
  // Попытка удалить элемент также вызовет исключение
  try {
	unmodifiableMap.remove('Alice');
  } on UnsupportedError catch (e) {
	print(e.message); // Cannot modify unmodifiable map
  }
 
  print('Alice: ${unmodifiableMap['Alice']}'); // Alice: Developer
 
  // Изменение исходной карты повлияет на неизменяемое представление
  originalMap['Alice'] = 'Senior Developer';
  originalMap['Tommy'] = 'Trainee';
  print(unmodifiableMap); // {Alice: Senior Developer, Bob: Designer,
                      	//  Tommy: Trainee}
}

Set

Коллекция (интерфейс) Set гарантирует, что в ней не может быть дубликатов объектов, и предоставляет несколько реализаций, в основе которых лежат те же принципы, что и в реализациях коллекции Map:

  • HashSet;

  • LinkedHashSet (используется по умолчанию при создании множества);

  •  SplayTreeSet;

  • UnmodifiableSetView.

HashSet хранит элементы неупорядочено, и поэтому не гарантирует, что итерация по элементам будет осуществляться в том же порядке, в каком их добавляли в коллекцию. Его конструктор и принцип работы передаваемых в него аргументов аналогичен HashMap:

HashSet<E, E>({
    bool equals(E,E)?,
	int hashCode(E)?,
	bool isValidKey(dynamic)?,
})

Такие операции, как: add, contains, remove и length, при условии равновероятностного распределения хэш-кодов элементов, выполняются за константное время О(1):

// ex_hashset.dart
import 'dart:collection';
import 'dart:io';
 
void main() {
  final Set<int> mySet = HashSet();
 
  mySet.addAll([1, 3, 3, 5, 6, 2, 1, 7, 9, -2]);
  print(mySet); // {-2, 1, 2, 3, 5, 6, 7, 9}
  print(mySet.last); // 9
  print(mySet.first); // -2
 
  mySet.remove(9);
  print(mySet); // {-2, 1, 2, 3, 5, 6, 7}
 
  print(mySet.where((it) => it % 2 == 0).toSet()); // {-2, 2, 6}
  print(mySet.where((it) => it > 0).toSet()); // {1, 2, 3, 5, 6, 7}
 
  for(var it in mySet){
	stdout.write('$it '); // -2 1 2 3 5 6 7
  }
}

LinkedHashSet позволяет выполнять итерацию в порядке добавления элементов и представляет собой хэш-таблицу, где в каждой ячейке массива – односвязный список. Временная сложность операций: add, contains, remove и length, конструктор и принцип работы передаваемых в него аргументов аналогичны HashSet:

LinkedHashSet<E, E>({
	bool equals(E,E)?,
	int hashCode(E)?,
	bool isValidKey(dynamic)?,
})

Элементы LinkedHashSet должны иметь согласованные реализации Object.== и Object.hashCode, то есть хэш-код добавляемого во множество объекта, даже после переопределения обозначенных методов, должен быть одинаковым для объектов, которые сравниваются с использованием оператора ==:

// ex_linkedhashset.dart
import 'dart:io';
 
void main() {
  final mySet = <int>{}; // создаем экземпляр LinkedHashSet
 
  mySet.addAll([1, 3, 3, 5, 6, 2, 1, 7, 9, -2]);
  print(mySet); // {1, 3, 5, 6, 2, 7, 9, -2}
  print(mySet.last); // -2
  print(mySet.first); // 1
 
  mySet.remove(9);
  print(mySet); // {1, 3, 5, 6, 2, 7, -2}
  print(mySet.where((it) => it % 2 == 0).toSet()); // {6, 2, -2
  print(mySet.where((it) => it > 0).toSet()); // {1, 3, 5, 6, 2, 7}
 
  for(var it in mySet){
	stdout.write('$it '); // 1 3 5 6 2 7 -2
  }
}

SplayTreeSet представляет собой сбалансированное двоичное дерево, где узлы упорядочены относительно друг друга, из-за чего итерация будет осуществляться в их отсортированном порядке. Конструктор и принцип работы передаваемых в него аргументов аналогичны рассмотренному ранее SplayTreeMap:

SplayTreeSet([
  int compare(E key1, E key2)?,
  bool isValidKey(dynamic potentialKey)? // работает как и в HashMap
])

Данная реализация позволяет выполнять большинство операций (вставка, поиск, изменение, удаление) в лучшем и среднем случае за O(log n):

// ex_splaytreeset.dart
import 'dart:collection';
import 'dart:io';
 
void main() {
  Set<int> mySet = SplayTreeSet();
  // по умолчанию элементы сортируются по возрастанию
  mySet.addAll([1, 3, 3, 5, 6, 2, 1, 7, 9, -2]);
  print(mySet); // {-2, 1, 2, 3, 5, 6, 7, 9}
  print(mySet.last); // 9
  print(mySet.first); // -2
  for (var it in mySet) {
	stdout.write('$it '); // -2 1 2 3 5 6 7 9
  }
 
  mySet = SplayTreeSet(
	(key1, key2) => key2.compareTo(key1), // по убыванию
  );
  mySet.addAll([1, 3, 3, 5, 6, 2, 1, 7, 9, -2]);
  print(mySet); // {9, 7, 6, 5, 3, 2, 1, -2}
  print(mySet.last); // -2
  print(mySet.first); // 9
 
  for (var it in mySet) {
	stdout.write('$it '); // 9 7 6 5 3 2 1 -2
  }
}

UnmodifiableSetView представляет собой немодифицируемую обертку над экземпляром Set, разрешающую только операции чтения, и может использоваться в тех случаях, когда вам необходимо гарантировать, что объект может быть изменен только в месте его создания:

// ex_unmodifiablesetview.dart
import 'dart:collection';
 
void main() {
  final originalSet = <String>{'Alice', 'Bob'};
  // Создание неизменяемого представления
  var unmodifiableSet = UnmodifiableSetView(originalSet);
 
  try {
	unmodifiableSet.remove('Alice');
  } on UnsupportedError catch (e) {
	print(e.message); // Cannot change an unmodifiable set
  }
 
  originalSet.add('Tommy');
  print(unmodifiableSet); // {Alice, Bob, Tommy}
 
  originalSet.remove('Alice');
  print(unmodifiableSet); // {Bob, Tommy}
}

List

В основе типа данных List, лежит не односвязный или двусвязный список, как вы могли подумать, а массив. Поэтому операция добавления в него новых элементов, в худшем случае, будет выполнена за O(n). Сама же коллекция List библиотеки dart:collection предоставляет только немодифицируемую обертку над экземпляром класса – UnmodifiableListView:

// ex_unmodifiablelistview.dart
import 'dart:collection';
 
void main() {
  final originalList = <int>[2, 4, 6, 8];
 
  // Создание неизменяемого представления
  var unmodifiableList = UnmodifiableListView(originalList);
 
  try {
    unmodifiableList.remove(2);
  } on UnsupportedError catch (e) {
    print(e.message); // Cannot remove from an unmodifiable list
  }
 
  try {
    unmodifiableList[0] = 3;
  } on UnsupportedError catch (e) {
    print(e.message); // Cannot modify an unmodifiable list
  }
 
  originalList.add(2);
  print(unmodifiableList); // [2, 4, 6, 8, 2]
 
  originalList[0] = 1;
  print(unmodifiableList); // [1, 4, 6, 8, 2]
}

Queue

Коллекция Queue (Очередь) представляет собой последовательность элементов, где элементы могут быть добавлены или удалены исключительно из ее начала или конца. В коллекции Dart, в отличие от классической структуры данных «Очередь», которую мы рассматривали ранее, есть возможность доступа к элементам с любого конца, что дает возможность для ее использования как в качестве самой очереди, так и для реализации стека.

Данная коллекция предоставляет общий интерфейс (абстрактный тип данных) Queue, который реализуют следующие два класса:

-                ListQueue (используется по умолчанию при создании очереди);

-               DoubleLinkedQueue.

ListQueue представляет собой реализацию очереди на основе списка (массива), а точнее циклического буфера, вместимость которого увеличивается по мере его заполнения. Это гарантирует, что такие операции, как peek и remove будут выполнены за О(1), а add в среднем за О(1) и худшем случае за О(n). Данное обстоятельство делает эту реализацию очереди наиболее подходящей для использования в качестве стека.

Конструктор ListQueue имеет необязательный аргумент — ListQueue([int? initialCapacity]), посредством которого можно задать начальную вместимость очереди, то есть выделить место под  циклический буфер. Также имеются фабричные именованные конструкторы, позволяющие проинициализировать создаваемый экземпляр очереди элементами итерируемой последовательности:

-                ListQueue.from(Iterable elements)

-                ListQueue.of(Iterable<E> elements)

 Ниже приведены основные методы работы с экземпляром ListQueue, приведенного к интерфейсу Queue:

Hidden text
// ex_listqueue.dart
import 'dart:collection';
import 'dart:io';
 
void main() {
  // Создание новой пустой очереди типа ListQueue
  // и приведение к интерфейсу Queue
  Queue<int> queue = ListQueue<int>(50);
 
  // Добавление элементов в очередь
  queue.add(1); // в конец
  queue.addLast(2); // также в конец
  queue.addFirst(0); // в начало
 
  print(queue); // {0, 1, 2}
 
  // Удаление элемента из очереди
  var first = queue.removeFirst(); // удаляет и
                               	// возвращает первый элемент
  print(first); // 0
  queue.removeLast(); // удаляет и возвращает последний элемент
 
  print(queue); // {1}
 
  // Доступ к элементам в очереди
  int firstElement = queue.first; // возвращает первый элемент
  int lastElement = queue.last; // возвращает последний элемент
 
  print('$firstElement $lastElement'); // 1 1
 
  // Проверка на пустоту
  print(queue.isEmpty); // false
  print(queue.isNotEmpty); // true
 
  // Проверка наличия элемента
  print(queue.contains(1)); // true
  print(queue.contains(5)); // false
 
  queue.clear(); // очистка очереди
  print(queue); // {}
 
  queue.addAll([-1, 2, 3, 4, -2, -1, -5, 0]); // добавление элементов
  print(queue); // {-1, 2, 3, 4, -2, -1, -5, 0}
 
  queue.remove(2); // удаление элемента по значению
  print(queue); // {-1, 3, 4, -2, -1, -5, 0}
 
  // удаление элементов по условию
  queue.removeWhere((element) => element < 0);
  print(queue); // {3, 4, 0}
 
  // обращение к элементу очереди по индексу
  print(queue.elementAt(1)); // 4
 
  for (var it in queue) {
	stdout.write('$it '); // 3 4 0
  }
}

DoubleLinkedQueue представляет собой реализацию очереди на основе двунаправленного списка, что гарантирует временную сложность таких операций, как add, удаление с конца и peek, за константное время. В основной конструктор класса не надо передавать никакие значения, а именованные работают по тому же принципу, что и у ListQueue (и методы у них одни и те же).

LinkedList

LinkedList – двусвязный список, элементы которого должны наследоваться от класса LinkedListEntry, представляющего собой узел «на стероидах», который знает о своей принадлежности к списку, месте в нем и одномоментно может находиться только в одном списке. Поэтому, чтобы добавить элемент из одного списка в другой, его сначала нужно удалить из первого.  Такая организация LinkedListEntry позволяет выполнять операции LinkedListEntry.insertAfter, LinkedListEntry.insertBefore и LinkedListEntry.unlink за константное время О(1).

LinkedList не реализует интерфейс List, поэтому у него не такой большой набор базовых методов и не перегружены операторы индексирования. Что касается вычислительной сложности операций, то добавление и удаление элементов с обоих концов списка, а также получение текущей длины осуществляется за О(1).

Ниже представлен пример работы с текущей коллекцией:

Hidden text
// ex_linkedlist.dart
import 'dart:collection';
 
base class Worker extends LinkedListEntry<Worker> {
  final String name;
  final int id;
 
  Worker(this.name, this.id);
 
  @override
  String toString() => '($name, $id)';
}
 
void main() {
  var workersList = LinkedList<Worker>();
 
  // Добавляем элементы в конец LinkedList
  workersList.add(Worker('Stas', 1));
  workersList.add(Worker('Alina', 2));
  Worker marina = Worker('Marina', 3);
  workersList.add(marina);
  print(workersList); // ((Stas, 1), (Alina, 2), (Marina, 3))
 
  // Получение первого и последнего элемента
  print(workersList.first); // (Stas, 1)
  print(workersList.last); // (Marina, 3)
 
  // Добавление нового элемента в начало списка
  workersList.addFirst(Worker('Vlad', 4));
  print(workersList);// ((Vlad, 4), (Stas, 1), (Alina, 2), (Marina, 3))
  // или
  workersList.first.insertBefore(Worker('Max', 5));
  print(workersList); // ((Max, 5), (Vlad, 4), (Stas, 1),
                  	//  (Alina, 2), (Marina, 3))
 
  // Еще один способ добавления нового элемента в конец списка
  workersList.last.insertAfter(Worker('Kira', 9));
  print(workersList); // ((Max, 5), (Vlad, 4), (Stas, 1),
                  	//  (Alina, 2), (Marina, 3), (Kira, 9))
 
  // Проверка наличия элемента и его удаление
  // проводятся по уникальному идентификатору объекта
  print(workersList.contains(marina)); // true
  // Удаление элемента
  print(workersList.remove(marina)); // true
  // Следующий элемент не удалится, т.к. передается новый объект
  print(workersList.remove(Worker('Alina', 2))); // false
  print(workersList); // ((Max, 5), (Vlad, 4), (Stas, 1),
                  	//  (Alina, 2), (Kira, 9))
 
  // Удаление первого и последнего элемента
  workersList.first.unlink();
  workersList.last.unlink();
  print(workersList); // ((Vlad, 4), (Stas, 1), (Alina, 2))
 
  // Удаление элемента по индексу
  workersList.elementAt(1).unlink();
  print(workersList); // ((Vlad, 4), (Alina, 2))
 
  // Проверка, пустой ли список
  print(workersList.isEmpty); // false
  print(workersList.isNotEmpty); // true
 
  // Количество элементов в списке
  print(workersList.length); // 2
 
  // Итерация по списку
  for (Worker worker in workersList) {
	print(worker);
	// (Vlad, 4)
	// (Alina, 2)
  }
 
  // Очистка всего списка
  workersList.clear();
  print(workersList); // ()
}

В качестве заключения

Наверно, у некоторых из вас возникнет вопрос: «Как он в этой статье использует материал из печатной версии книги?». Дело в том, что за мной остались эксклюзивные права на электронную версию. Это позволяет поддерживать мой курс на степик «Основы Dart 3» в актуальном состоянии и параллельно с издательством «Питер» распространять авторскую электронную версию книги «Основы Dart» (без правок от издательства). В электронной версии есть лабораторные работы и разделы, которые вырезали из печатной версии. Авторскую версию книги можно купить на моем Boosty

Хочу выразить огромную благодарность за поддержку и помощь в этот период жизни — жене, дочке, родне и компании-партнеру Friflex. И всему ее коллективу в целом, а Петру Чернышеву (CEO) и Юре Петрову (Flutter Tech Lead, автору тг-канала «Мобильный разработчик») — в частности. Именно благодаря поддержке Friflex в свет и вышла печатная версия книги!

Большущее спасибо всем тем, кто поддерживал денежно (донаты, подписка и т.д.) и морально, особенно: a.alistrat, Коварский О. Г., It People.

И куда же без благодарности тем людям, которые дали мне мотивационного пендаля начать двигаться в сторону печатной версии книги — Кириллу Розову (Android Broadcast) и любителю публично похейтить все технологии, кроме тех, что связаны с Kotlin — Алексею Гладкову (Mobile Developer) =)

P.S. В середине весны приступил к написанию новой книги «Основы Flutter». В ней не будет разбора множества пакетов, только база, что позволит ей более долгое время быть актуальной. Поэтому не ждите в ней каких-то откровений! И быстрого написания тоже. Времени сейчас куда меньше (май и до середины июня, там вообще — суши весла >_<). Да и доча уже в том возрасте, когда вот-вот и заговорит... так что шум в однокомнатной квартире стоит знатный >_< Пока не буду открывать все карты, но, если получится, что было задумано, процесс пойдет куда быстрее и есть вероятность завершения книги в этом году.


Комментарии (0)