Ответ
В Dart все классы неявно наследуются от встроенного класса Object. Это означает, что любой экземпляр любого класса имеет набор базовых методов, определенных в Object.
Основные унаследованные члены:
String toString()— возвращает строковое представление объекта. По умолчанию выводит что-то вродеInstance of 'Person'. Я часто переопределяю его для отладки.int get hashCode— возвращает целочисленный хэш-код объекта, используемый в коллекциях вродеHashMapиHashSet.bool operator ==(Object other)— оператор равенства. Важнейший момент: по умолчанию он сравнивает ссылки (эквивалентноidentical(this, other)), а не содержимое объектов.Type get runtimeType— возвращает тип объекта во время выполнения (runtime).
Критическая связь между == и hashCode:
Если вы переопределяете operator ==, вы обязаны также переопределить hashCode. Объекты, которые равны по ==, должны возвращать одинаковый хэш-код. Нарушение этого контракта приведет к некорректной работе хэш-коллекций.
Пример переопределения в классе модели данных Flutter:
class User {
final String id;
final String name;
const User({required this.id, required this.name});
@override
bool operator ==(Object other) {
// Проверка идентичности ссылок (оптимизация)
if (identical(this, other)) return true;
// Проверка типа и сравнение значимых полей
return other is User &&
runtimeType == other.runtimeType &&
id == other.id; // Сравниваем только id, так как он уникален
}
@override
int get hashCode => id.hashCode; // Хэш-код должен зависеть от тех же полей, что и ==
@override
String toString() => 'User(id: $id, name: $name)'; // Удобно для логов
}
void main() {
final user1 = User(id: '1', name: 'Alice');
final user2 = User(id: '1', name: 'Alice');
final user3 = User(id: '2', name: 'Alice');
print(user1 == user2); // true (id одинаковые)
print(user1 == user3); // false (id разные)
// Благодаря правильному hashCode, объекты корректно работают в Set
final usersSet = {user1, user2, user3};
print(usersSet.length); // 2 (user1 и user3, user2 не добавится, так как равен user1)
}
Понимание этого механизма фундаментально для корректной работы с коллекциями, такими как Set и ключами Map, а также для написания тестов с expect().
Ответ 18+ 🔞
А, ну вот, опять про этот ваш Object заговорили. Ну ладно, раз уж начали, давайте разберемся, что это за хитрая жопа такая.
Короче, слушай сюда. В Dart, если ты создал какой-нибудь свой класс, типа class Cat { ... }, то этот твой Cat уже по умолчанию — полупидор. В смысле, он неявно наследуется от какого-то старого деда по имени Object. Это типа как если бы ты думал, что ты сам по себе, а на самом деле у тебя в роду был какой-нибудь граф, и теперь ты обязан таскать его фамильный герб и геморрой.
И этот дед Object надарил всем своим детям-классам кучу базовых методов. Их всего четыре штуки, но они вездесущие, как назойливые мухи.
toString()— это когда объект пытается рассказать о себе словами. По дефолту он такой тупой, что говорит что-то вроде "я — экземпляр класса 'ХуйВПальто'". Бесполезно, блядь. Нормальные люди его сразу переопределяют, чтобы в логах можно было понять, что за манда с ушами перед тобой.hashCode— это типа его цифровой отпечаток пальца, целое число. Нужен для всяких хэш-таблиц (HashMap,HashSet), чтобы быстро искать объекты. Если бы его не было, коллекции бы работали со скоростью улитки в сахаре.operator ==(Object other)— а вот это, ёпта, самое интересное и подлое место. По умолчанию этот оператор сравнивает не то, что внутри объекта, а ссылки на память. То есть он смотрит: "А это один и тот же кусок памяти или два разных?" Если ты создал двух котов с одинаковым именемCat(name: 'Барсик'), то для Dart это будут два абсолютно разных кота, потому что живут они в разных квартирах-ячейках памяти. Доверия к этому дефолтному сравнению — ебать ноль.runtimeType— ну это просто тип объекта в рантайме. Спросил — получил, без сюрпризов.
А теперь главный пиздец, на котором все обламываются. Между == и hashCode есть железобетонная связь, нарушив которую ты получишь ебаный хаос.
Правило простое, как три копейки: если ты переопределил operator ==, чтобы сравнивать объекты по смыслу (например, двух пользователей по id), то ты ОБЯЗАН как собака переопределить и hashCode, чтобы он считался от тех же полей. Равные объекты ДОЛЖНЫ иметь одинаковый хэш-код. Если этого не сделать, твои объекты начнут вести себя в Set или как ключи в Map абсолютно непредсказуемо. Коллекции просто сойдут с ума. Это не рекомендация, это закон.
Смотри, как это выглядит в коде, когда ты делаешь всё по-взрослому, для какой-нибудь модели во Flutter:
class User {
final String id;
final String name;
const User({required this.id, required this.name});
@override
bool operator ==(Object other) {
// Сначала быстрая проверка: а не один ли это и тот же объект? (оптимизация)
if (identical(this, other)) return true;
// А теперь проверяем: другой объект — это вообще User? И сравниваем ВАЖНЫЕ поля.
return other is User &&
runtimeType == other.runtimeType &&
id == other.id; // Сравниваем только id, потому что он уникален. Name нас не ебёт для равенства.
}
@override
int get hashCode => id.hashCode; // Хэш-код В ТОЧНОСТИ от тех же полей, что и в == (id).
@override
String toString() => 'User(id: $id, name: $name)'; // Теперь в логах видно всё, красота.
}
void main() {
final user1 = User(id: '1', name: 'Alice');
final user2 = User(id: '1', name: 'Alice'); // Тот же id, но другой объект в памяти.
final user3 = User(id: '2', name: 'Alice'); // Совсем другой.
print(user1 == user2); // true! Потому что id одинаковые. Удивление пиздец, да?
print(user1 == user3); // false. Ну тут логично.
// А вот магия правильных hashCode и ==:
final usersSet = {user1, user2, user3}; // Пытаемся запихнуть всех в Set.
print(usersSet.length); // 2! user2 не добавился, потому что Set увидел, что он РАВЕН user1. Всё работает как часы.
}
Вот и вся магия. Без этого понимания тесты с expect() будут падать с непонятными ошибками, а Set будет хранить дубликаты, как будто на дворе 2002-й год. Так что не ленись, переопределяй эти методы, когда нужно сравнивать объекты по смыслу, а не по месту жительства в оперативке.