Какими могут быть ключи в HashMap (хэш-таблице)?

Ответ

В Dart (и, соответственно, в Flutter) ключом в HashMap (или в Map, который часто реализуется через хэш-таблицу) может быть любой объект, для которого корректно определены методы operator == (оператор равенства) и геттер hashCode.

Ключевое правило: Если два объекта считаются равными по ==, они обязаны иметь одинаковый hashCode. Обратное не обязательно: разные объекты могут иметь одинаковый хэш (коллизия), но это снижает производительность.

Примеры допустимых ключей в Dart:

  1. Встроенные типы с корректной реализацией: int, double, String, bool, DateTime, Uri.

    final Map<String, int> phoneBook = {'Alice': 12345, 'Bob': 67890};
    final Map<DateTime, String> events = {DateTime(2024, 1, 1): 'New Year'};
  2. Экземпляры пользовательских классов, где == и hashCode переопределены. Класс должен быть immutable (все поля final), чтобы его хэш-код не менялся после помещения в карту.

    class ProductKey {
      final String id;
      final String category;
    
      ProductKey(this.id, this.category);
    
      @override
      bool operator ==(Object other) =>
          identical(this, other) ||
          other is ProductKey &&
              runtimeType == other.runtimeType &&
              id == other.id &&
              category == other.category;
    
      @override
      int get hashCode => Object.hash(id, category); // Используем Object.hash
    }
    
    void main() {
      final inventory = HashMap<ProductKey, int>();
      final key = ProductKey('prod_1', 'electronics');
      inventory[key] = 100;
      print(inventory[ProductKey('prod_1', 'electronics')]); // 100
    }

Что НЕЛЬЗЯ использовать в качестве ключа (без переопределения методов):

  • List, Set, Map и другие коллекции по умолчанию. Их равенство сравнивает идентичность ссылок, а не содержимое.
  • Мутабельные (изменяемые) объекты. Если изменить поле объекта после того, как он использовался как ключ, его hashCode изменится, и найти значение по этому ключу станет невозможно.
  • Пользовательские классы без переопределения == и hashCode. Для них будет использоваться сравнение по ссылке (identical), что редко является нужным поведением.

Для автоматической генерации == и hashCode в Dart можно использовать пакет equatable или аннотации вроде @immutable и кодогенерацию.

Ответ 18+ 🔞

А, ну вот, опять про эти ваши хэш-мапы в Dart. Слушай, история проще пареной репы, но если её не понять, то потом будешь как тот самый Гамлет — всех посылать нахуй, потому что код не работает, а почему — хуй поймёшь.

Так вот, смотри. В HashMap (или в обычной Map, она часто под капотом на хэшах) ключом может быть любой объект, который не мудак. Шучу. Точнее, любой объект, у которого нормально определены два штуки: оператор == (равенство) и геттер hashCode.

Главное правило, которое надо выжечь на жопе: Если два объекта по == равны, то их hashCode обязаны быть одинаковыми. Это как закон. Обратное — не обязательно, разные объекты могут случайно иметь одинаковый хэш (это коллизия, бывает), но тогда производительность просядет, как твоё настроение в понедельник утром.

Что можно пихать в ключи, не бздя:

  1. Встроенные типы, которые уже не лохи: int, double, String, bool, DateTime, Uri. У них всё уже сделано за нас.

    final Map<String, int> phoneBook = {'Alice': 12345, 'Bob': 67890};
    final Map<DateTime, String> events = {DateTime(2024, 1, 1): 'New Year'};
  2. Твои кастомные классы, но только если ты не распиздяй. Надо переопределить == и hashCode. И, что критично, класс должен быть immutable — все поля final. Иначе получится манда с ушами: положил объект как ключ, потом его поля поменял, хэш изменился — и всё, прощай значение, ищи ветра в поле.

    class ProductKey {
      final String id;
      final String category;
    
      ProductKey(this.id, this.category);
    
      // Вот тут надо не проебаться. Сравниваем по полям.
      @override
      bool operator ==(Object other) =>
          identical(this, other) ||
          other is ProductKey &&
              runtimeType == other.runtimeType &&
              id == other.id &&
              category == other.category;
    
      // И хэш считаем на основе тех же полей. Используем `Object.hash` — не надо велосипед изобретать.
      @override
      int get hashCode => Object.hash(id, category);
    }
    
    void main() {
      final inventory = HashMap<ProductKey, int>();
      final key = ProductKey('prod_1', 'electronics');
      inventory[key] = 100;
      // Вот тут магия: создаём НОВЫЙ объект с теми же полями, но он находит значение, потому что `==` и `hashCode` совпали.
      print(inventory[ProductKey('prod_1', 'electronics')]); // 100
    }

А что категорически НЕЛЬЗЯ, если ты не самоубийца?

  • List, Set, Map и прочие коллекции по дефолту. Их равенство — это сравнение ссылок, а не содержимого. Полный пиздец, а не ключ.
  • Изменяемые объекты. Я уже говорил, но повторю: если после добавления в карту поменять поля ключа, это всё — накрылось медным тазом. Значение потеряется навсегда, и доверия к такой мапе будет ноль ебать.
  • Твои классы без переопределения == и hashCode. Для них будет работать сравнение по ссылке (identical). Это почти никогда не то, что нужно. Получится, что ProductKey('a', 'b') и ProductKey('a', 'b') — это два разных ключа. Ёперный театр!

Короче, чтобы не ебаться с этим вручную, можно использовать пакет equatable или всякие кодогенераторы. Они за тебя эту рутину нагенерируют. Главное — э бошка думай, что ты делаешь, и не пытайся впихнуть невпихуемое.