Словари Map в Dart

Проблема с использованием списков для связанных данных

Возьмём обычный школьный журнал. В нем есть список учеников, и для каждого ученика оценки по разным предметам. Как можно связать ученика с его оценками, используя списки и переменные?

Пробуем хранить данные в списках:

Dart - Хранение в списках

Светлая тема Темная тема
void main() {
  List students = ['Аня', 'Боря', 'Наташа', 'Глеб', 'Даша'];
  List<List<int>> studentGrades = [
    [5, 4, 5], // Оценки Ани по Математике, Физике, Информатике
    [3, 5, 4], // Оценки Бори
    [5, 5, 5], // Оценки Наташи
    [4, 3, 4], // Оценки Глеба
    [5, 4, 3], // Оценки Даши
  ];

  String student = 'Наташа';

  // Узнать оценки ученика
  int indexOfStudent = -1; // Переменная для хранения индекса
  for (int i = 0; i < students.length; i++) {
    if (students[i] == student) {
      indexOfStudent = i;
      break; // Если нашли, то выходим из цикла
    }
  }

  if (indexOfStudent != -1) {
    print('$student: ${studentGrades[indexOfStudent]}');
  } else {
    print('Ученик $student не найден.');
  }
}

Результат:

Наташа: [5, 5, 5]

Почему это "плохо"?

На первый взгляд это работает. Но давайте подумаем о производительности и удобстве.

  1. Производительность (Скорость поиска):
    • Когда мы ищем "Наташу", нам приходится перебирать весь список students элемент за элементом, пока не найдем ее имя.
    • Если 5 учеников, это быстро, а если 5 000 000 ? Нам все равно придется начинать с первого ученика и идти по порядку.
    • В худшем случае (если ученик в конце списка или его нет вообще), придется проверить все N элементов. Это называется линейное время, или O(N) в Big O нотации.
    • Это очень долго и не производительно!
  2. Связь данных:
    • Также приходится поддерживать два отдельных списка (students и studentGrades), где соответствие между именем и оценками держится только за счет одинаковых индексов.
    • Что, если мы случайно удалим имя из одного списка, но не из другого? Данные рассинхронизируются! Это хрупко и подвержено ошибкам!

Используем вложенные списки

Dart - Вложенные списки

Светлая тема Темная тема
void main() {
  List<List<dynamic>> students = [
    ['Аня', [5, 4, 5]],
    ['Боря', [3, 5, 4]],
    ['Наташа', [5, 5, 5]],
    ['Глеб', [4, 3, 4]],
    ['Даша', [5, 4, 3]],
  ];

  String student = 'Наташа';
  List? studentGrades;

  for (var studentEntry in students) {
    if (studentEntry[0] == student) {
      // Приводим тип
      studentGrades = studentEntry[1] as List; 
      break;
    }
  }

  if (studentGrades != null) {
    print('$student: $studentGrades');
  } else {
    print('Ученик $student не найден.');
  }
}

Результат:

Наташа: [5, 5, 5]

Почему это все еще "плохо"?

  1. Проблема скорости поиска (O(N)) осталась: Перебираем каждый элемент списка students в поисках нужного имени.
  2. Типобезопасность страдает: Приходится делать приведение типов (as List<int>), что может привести к ошибкам в рантайме, если данные не соответствуют ожиданиям. Код становится менее надежным.

Решение проблемы: Map

Именно для таких сценариев, где есть пары "ключ-значение" и нужен быстрый поиск по ключу, была придумана структура данных Map (также известная как словарь, хеш-таблица или ассоциативный массив).

В Map обращаемся по ключу (имя ученика), дай значение (его оценки), связанное с этим ключом". И Map делает это почти мгновенно, независимо от того, сколько у вас данных!

Dart - Использование Map

Светлая тема Темная тема
void main() {
  Map<String, List<int>> students = {
    'Аня': [5, 4, 5],
    'Боря': [3, 5, 4],
    'Наташа': [5, 5, 5],
    'Глеб': [4, 3, 4],
    'Даша': [5, 4, 3],
  };

  // Вот так просто! И Главное очень быстро!
  List? studentGrades = students['Наташа']; 
  
  studentGrades != null
    ? print(studentGrades)
    : print('Ученик не найден');
}

Результат:

[5, 5, 5]

Почему Map это круто и легко?

  1. Мгновенный поиск (O(1) - Константное время)!
    • Поиск элемента по ключу занимает константное время (O(1)). Это означает, что время поиска не зависит от количества элементов. Будь у вас 5 учеников или 5 миллионов, поиск займет примерно одно и то же минимальное время!
  2. Естественная связь данных:
    • Пары "ключ-значение" в Map неразрывно связаны. Не нужно беспокоиться о синхронизации двух списков.
    • Map обеспечивает типобезопасность: Map<String, List<int>> четко указывает, что ключи — строки, а значения — списки целых чисел.

Map

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

Что такое Map

Map одна из самых мощных и часто используемых структур данных!

Map Это объект, который представляет собой коллекцию пар ключ-значение.

Map концепция

Другой пример, вместо школьного журнала представьте телефонные контакты. У каждого телефона (уникального ключа) есть свое определение (значение).

Ключ уникален, и по нему вы можете быстро найти соответствующее значение.

Телефонные контакты

Важные характеристики Map

Dart - Основы работы с Map

Светлая тема Темная тема
void main() {

  // Имя (ключ) : Номер телефона (значение)
  Map<String, String> phoneBook = {
    'Маша': '+7 911 987 65 43',
    'Артём': '+7 922 555 11 22',
  };
    
  // Получаем значение по ключу
  print('Телефон Маши: ${phoneBook['Маша']}'); 
    
  // Обновляем номер Маши
  phoneBook['Маша'] = '+7 900 777 88 99';
  print('Новый телефон Маши: ${phoneBook['Маша']}');
  
  // Добавляем новый контакт
  phoneBook['Юля'] = '+7 933 444 00 11'; 
  print('Телефон Юли: ${phoneBook['Юля']}');
  
  // Если нет значения в словаре, то будет null
  print('Телефон Джигана: ${phoneBook['Джиган']}');

  print('Вся телефонная книга: $phoneBook');
}

Результат:

Телефон Маши: +7 911 987 65 43
Новый телефон Маши: +7 900 777 88 99
Телефон Юли: +7 933 444 00 11
Телефон Джигана: null

Вся телефонная книга: 
{
  Маша: +7 900 777 88 99, 
  Артём: +7 922 555 11 22, 
  Юля: +7 933 444 00 11
}
Работа с Map

Очень быстрый поиск, добавление и удаление данных

В отличие от списков (List), где для поиска элемента часто приходится перебирать каждый элемент по порядку (в худшем случае — до конца списка), Map позволяет найти нужное значение практически мгновенно!

Производительность: O(1) – Константное время
Это одно из самых важных понятий в программировании!

  1. List (список): Поиск элемента по значению может занимать линейное время O(n), где n - количество элементов.
    То есть, чем больше список, тем дольше поиск.

  2. Map (словарь): Поиск элемента по ключу в Map (в среднем) занимает константное время O(1). Это означает, что время поиска не зависит от количества элементов. Будь у вас 10 элементов или 10 миллионов, поиск по ключу займет примерно одно и то же время!

Сравнение производительности

Как это работает? Хеш-таблицы в двух словах
Map в Dart, по умолчанию (для LinkedHashMap), реализован на основе хеш-таблиц

  1. Хеширование ключа - при добавлении пар "ключ-значение" в Map, Dart берет ключ и вычисляет его хеш-код (hashCode).
  2. Индексация - на основе этого хеш-кода вычисляется индекс во внутреннем массиве данных Map.
  3. Хранение - пара "ключ-значение" затем помещается в соответствующую "ячейку" этого массива.
  4. Поиск - запрашивает значение по ключу, вычисляет хеш-код ключа, ищет индекс и мгновенно извлекает значение.

Более подробно

Более подробно как устроена Map внутри и что такое Хэш Таблица, будем изучать в курсе «Алгоритмы и структуры данных»

Получение значений

Получение значений

Индексатор [ ] - Самый простой способ получить значение по ключу. Если ключа нет, вернет null.

Dart - Индексатор

Светлая тема Темная тема
Map user = {'name': 'Юля', 'age': '18'};  

print(user['name']); // Юля  
print(user['city']); // null

containsKey(Object? key) - Проверяет, содержит ли Map указанный ключ.

Dart - containsKey

Светлая тема Темная тема
print(user.containsKey('name')); // true  
print(user.containsKey('address')); // false

containsValue(Object? value) - Проверяет, содержит ли Map указанное значение.

Dart - containsValue

Светлая тема Темная тема
print(user.containsValue('Юля')); // true  
print(user.containsValue('25')); // false

entries - Возвращает итерируемый объект, содержащий все пары "ключ-значение"

Dart - entries

Светлая тема Темная тема
print(user.entries); // (MapEntry(name: Юля), MapEntry(age: 18))  
print(user.entries.first.key); // name

keys - Возвращает все ключи (итерируемый объект)
values - Возвращает все значения (итерируемый объект)

Dart - keys и values

Светлая тема Темная тема
print(user.keys); // (name, age)  
print(user.values); // (Юля, 18)
Методы получения Дополнительные методы

Добавление и обновление значений

Индексатор []= для добавления новой пары "ключ-значение" или обновления значения, если ключ уже существует.

Dart - Добавление и обновление

Светлая тема Темная тема
main() {
  Map programmerSalary = {'Python': 150000, 'Go': 300000};  

  programmerSalary['Flutter'] = 200000; // Добавление  
  programmerSalary['Python'] = 180000; // Обновление  

  print(programmerSalary);
}

Результат:

{
  Python: 180000, 
  Go: 300000, 
  Flutter: 200000,
}

putIfAbsent() метод работает как "Запиши, если ещё нет"

Помогает добавить что-то в Map только если этого "чего-то" там ещё нет.
Если ключ уже есть, то "новое" значение (которое добавляем) даже не будет создаваться

Dart - putIfAbsent

Светлая тема Темная тема
void main() {
  Map<String, int> mathGrades = {
    'Юля': 5, 
    'Максим': 4,
  };

  print('Начальные оценки: $mathGrades');

  // Добавляем Артёма (его ещё нет в списке)
  mathGrades.putIfAbsent("Артём", () {
    print("Артёма не было, ставим ему 3");
    return 3; // Это значение вернется, если Артёма не было
  });

  print(mathGrades); // Показываем весь словарь

  // Пытаемся добавить Максима, но он уже есть в словаре
  mathGrades.putIfAbsent("Максим", () {
    // Если ключ "Максим" уже есть в словаре, то код ниже не выполнится
    print("Максим не было, ставим ему 3");
    return 3;
  });

  print(mathGrades); // Показываем весь словарь
}

Результат:

Начальные оценки: {Юля: 5, Максим: 4}
Артёма не было, ставим ему 3

{
  Юля: 5, 
  Максим: 4, 
  Артём: 3,
}

update() обновляет значение, связанное с key.

update вызывается для обновления существующего значения. Получает текущее значение и возвращает новое.

ifAbsent вызывается, если key не найден в Map. Если не указан, и ключ не найден, будет выброшена ошибка.

Dart - update

Светлая тема Темная тема
Map productCounts = {'BitCoins': 100, 'Ethereum': 400};  
  
// Обновляем существующее значение  
productCounts.update('BitCoins', (value) => value + 100);  
print(productCounts); 
  
// Обновляем или добавляем, если нет
productCounts.update('ShibaInu',  
  (value) => value + 100, // Эта часть не будет вызвана, т.к. 'ShibaInu' нет  
  ifAbsent: () => 100000000, // Добавляем 'ShibaInu'  
);  

print(productCounts);

Результат:

{
  BitCoins: 200, 
  Ethereum: 400, 
  ShibaInu: 100000000,
}

updateAll( ) обновляет все значения в Map, применяя функцию update к каждой паре "ключ-значение".

Dart - updateAll

Светлая тема Темная тема
Map prices = {'milk': 2, 'bread': 3, 'eggs': 4};  

prices.updateAll((key, value) => value * 2); // Удваиваем все цены  
print(prices); // {milk: 4, bread: 6, eggs: 8}
Добавление и обновление 1 Добавление и обновление 2 Добавление и обновление 3

Удаление значений

remove() удаляет пару "ключ-значение" по указанному ключу. Возвращает удаленное значение или null, если ключ не найден.

Dart - remove

Светлая тема Темная тема
void main() {

  Map<String, String> settings = {
    'sound': 'on', 
    'vibrate': 'on', 
    'notifications': 'off',
  }; 
	
  final removedValue = settings.remove('sound');  
  print('Removed: $removedValue'); // Удаленное значение: on  
	
  print(settings); // {vibrate: on, notifications: off}  
	
  settings.remove('none'); // Проигнорируется  

}

removeWhere() удаляет все пары "ключ-значение", для которых функция колбэк возвращает true.

Dart - removeWhere

Светлая тема Темная тема
void main() {
  Map<String, int> students = {
    'Маша': 17, 
    'Юля': 20, 
    'Артём': 15, 
    'Антон': 21,
  };

  // Удаляем Студентов возраст которых меньше 18
  students.removeWhere((key, value) => value < 18);
  
  print(students); // { Юля: 20, Антон: 21 }
}

clear() удаляет все элементы из Map.

Dart - clear

Светлая тема Темная тема
main() {

  Map<String, int> students = {
    'Маша': 17, 
    'Юля': 20, 
    'Артём': 15, 
    'Антон': 21,
  };
	
  students.clear();

  print(students); // { }

}
Удаление 1 Удаление 2

Перебор значений в Map

forEach() выполняет функцию колбэк для каждой пары "ключ-значение".

Dart - forEach

Светлая тема Темная тема
void main(List args) {

  Map<String, int> students = {
    'Маша': 17,
    'Юля': 20,
    'Артём': 15,
    'Антон': 21,
  };

  students.forEach((key, value) => print("Студент $key, возраст $value"));
}

Результат:

Студент Маша, возраст 17
Студент Юля, возраст 20
Студент Артём, возраст 15
Студент Антон, возраст 21

for-in

for-in с entries перебор пар MapEntry.
for-in с keys или values перебор только ключей или только значений.

Dart - for-in циклы

Светлая тема Темная тема
void main(List args) {

  Map<String, int> students = {'Маша': 17, 'Юля': 20, 'Артём': 15, 'Антон': 21};

  for (var entry in students.entries) {
    print('Ключ: ${entry.key}, Значение: ${entry.value}');
  }

  for (var key in students.keys) {
    print('Ключ: $key');
  }

  for (var value in students.values) {
    print('Значение: $value');
  }
}

Результат:

Ключ: Маша, Значение: 17
Ключ: Юля, Значение: 20
Ключ: Артём, Значение: 15
Ключ: Антон, Значение: 21

Ключ: Маша
Ключ: Юля
Ключ: Артём
Ключ: Антон

Значение: 17
Значение: 20
Значение: 15
Значение: 21
Перебор 1 Перебор 2

Дополнительно

length isEmpty isNotEmpty containsKey - получение размера Map, проверка на пустоту, проверка на ключи или значения

Dart - Дополнительные методы

Светлая тема Темная тема
void main(List args) {

  Map<String, int> students = {'Маша': 17, 'Юля': 20, 'Артём': 15, 'Антон': 21};

  print(students.length); // 4 пары
  print(students.isEmpty); // false
  print(students.isNotEmpty); // true
  
  // ! Содержит ли Map значение или ключ
  print(students.containsKey('Маша'));  // true
  print(students.containsValue('Андрей')); // false
  
}
Дополнительные методы

Использование Spread-оператора, if и for для создания данных Map

Как и для списков, в Dart можно использовать spread-оператор (...) для объединения Map, а также if и for для условного добавления элементов или создания Map на основе итерации.

Spread-оператор ...

Это инструмент, который позволяет "высыпать" содержимое одной коллекции (List, Set или Map) в другую, новую коллекцию. Проще говоря, spread-оператор распаковывает элементы из одной коллекции и вставляет их в другую коллекцию.

Dart - Spread-оператор

Светлая тема Темная тема
void main() {
  // Обычный завтрак
  final Map<String, int> basicBreakfast = {
    'Молоко': 1,
    'Хлопья': 1,
  };

  // Переменная, которая говорит, сегодня праздник или нет
  final bool isHoliday = true; 

  // Делаем "супер-завтрак"!
  final Map<String, int> superBreakfast = {
    ...basicBreakfast, // Развернём сюда данные из другой Map
    'Ягоды': 5, // Добавим ягоды
    
    // Если сегодня праздник, то будет торт
    if (isHoliday) 'Торт': 1, 

    // Сколько ложек сахара
    for (int i = 1; i <= 3; i++) 'Ложек_Сахара_$i': i, 
  };

  print('Мой супер завтрак: $superBreakfast');

}

Результат:

Мой супер завтрак: 
{
  Молоко: 1, 
  Хлопья: 1, 
  Ягоды: 5, 
  Торт: 1, 
  Ложек_Сахара_1: 1, 
  Ложек_Сахара_2: 2, 
  Ложек_Сахара_3: 3
}
Spread-оператор