Типизация — то как язык работает с данными разного типа (числами, строками, объектами).
Dart — язык со статической типизацией.
Статическая: Типы проверяются до запуска (на этапе компиляции). Если в коде ошибка типов — она вылезет сразу в IDE, и приложение не соберется.
Так как Dart — это однопоточный язык, работа с памятью устроена вокруг Isolates. У каждого изолята своя выделенная память: своя куча (Heap), свой стек и свой сборщик мусора (GC). Они не делят память между собой.
Сборщик мусора работает внутри изолятов и делится на две фазы, чтобы не тормозить UI:
1. Young Space (Молодое поколение):
➤ Туда попадают все новые объекты (например, виджеты).
➤ Память маленькая и очищается очень быстро, не вызывая фризов анимации.
2. Old Space (Старое поколение):
➤ Сюда попадают объекты, пережившие несколько очисток в Young Space (синглтоны, стейты).
➤ Очищается редко, так как проверка старого поколения работает медленнее.
1. Массив (List) vs Связанный список (LinkedList)
Разница в том, как элементы лежат в оперативной памяти.
➤ List (Массив): Элементы лежат строго друг за другом.
— Чтение: Моментально O(1). Мы знаем адрес первого элемента и индекс нужного.
— Вставка/Удаление: Медленно O(n). Если удалить первый элемент, придется сдвигать все остальные в памяти.
Пример: Обычная лента новостей, где важен порядок и быстрый доступ.
➤ Linked List (Связный список): Элементы разбросаны, каждый хранит ссылку на соседа.
— Чтение: Медленно O(n). Чтобы найти 10-й элемент, нужно пройти по цепочке с 1-го по 9-й.
— Вставка/Удаление: Очень быстро O(1). Нужно просто перекинуть ссылки у соседних элементов.
Пример: Очередь скачивания файлов или музыкальный плейлист.
2. List vs Set (Список против Множества)
Выбор зависит от того, важна ли уникальность данных и скорость поиска.
➤ List: Упорядоченная коллекция. Элементы могут повторяться.
➤ Set: Коллекция уникальных элементов. Порядок обычно не гарантирован.
В чем подвох на собесе? Скорость поиска .contains().
В List поиск идет перебором — O(n).
В Set поиск идет через хэш-таблицу — O(1) (моментально).
Пример: Хранить список ID "лайкнутых" постов лучше в Set, чтобы мгновенно проверять, нажал ли юзер на сердечко.
3. Map (Словарь / Хэш-таблица)
Хранит данные парами Ключ — Значение.
➤ Как работает: Ключ прогоняется через хэш-функцию, которая превращает его в число — адрес в памяти. Это позволяет находить значение по ключу моментально за O(1).
➤ Суть: Идеально для кэширования и работы с JSON.
Пример: Кэш пользователей, где ключ — userId, а значение — объект User.
4. Set (Множество) — HashSet vs LinkedHashSet
В Set все элементы уникальны. Поиск в нем всегда O(1).
➤ HashSet: Самый быстрый. Не гарантирует никакой порядок элементов.
➤ LinkedHashSet: (Дефолтный в Dart). Работает почти так же быстро, но сохраняет порядок вставки элементов.
5. Для чего нужно ключевое слово typedef?
typedef — это псевдоним для типа.
Чаще всего его используют для:
➤ сокращения длинных сигнатур функций;
➤ улучшения читаемости кода;
➤ явного описания, какую функцию мы ожидаем.
Простыми словами: typedef нужен, чтобы дать имя сложному типу, чаще всего функции. Это делает код чище и понятнее.
Равенство: operator == и hashCode
Принцип сравнения обьектов в Set / Как Dart понимает, что два разных объекта в памяти — это "один и тот же" элемент? Сравнивается по hashCode и equal
operator == (Equal)
По умолчанию Dart сравнивает ссылки (адреса в памяти). Два разных объекта с одинаковыми данными будут не равны. Чтобы сравнивать их по значениям полей, нужно переопределять оператор ==.
hashCode (можешь повторяться)
Это числовой идентификатор обьекта. Состояние может быть одинаковым у обьектов, а адрес разный.
Зачем он нужен: Когда ты кладешь объект в Set или Map, Dart не сравнивает его со всеми остальными через == (это долго). Он сначала смотрит на hashCode, чтобы мгновенно найти нужную "корзину" в памяти.
Зачем переопределять оба? Почему переопределяется и hashCode, и == (equal)?
Потому что Map и Set сначала ищут "полку" по хэш-коду, а потом проверяют конкретный объект через ==. Если переопределить только одно, объект просто не найдется
Они должны быть согласованы.
Если два объекта равны через ==, то у них обязан быть одинаковый hashCode.
Это нужно для правильной работы Set и Map
Различаются по времени инициализации: Compile-time (до запуска) или Runtime (во время работы приложения).
1. var (Неявная типизация)
➤ Это: ключевое слово и используется для создания переменных. Она значит изменяемая - variable. В процессе работы ей можно присвоить новое значение в рамках ТИПА!
➤ Суть: Dart (анализатор) сам определяет тип по первому значению (когда уже инициализировали переменную).
➤ Особенность: Значение менять можно, а вот тип — нельзя.
var name = 'Dart';
name = 'Flutter'; // Ок, значение поменяли
// name = 123; // Ошибка! Тип String уже зафиксирован
Чем отличается var от dynamic?
Частый вопрос на собеседовании, так как оба позволяют не писать название типа явно.
➤ var: Это статика. Тип определяется один раз при создании. После этого переменная ведет себя так, будто ты написал String или int.
➤ dynamic: Это динамика. Ты буквально говоришь Dart: "Отключи проверку типов для этой переменной". Её тип может меняться прямо в процессе работы программы.
var a = 'Hello';
// a = 10; // ОШИБКА: String нельзя заменить на int
dynamic b = 'Hello';
b = 10; // ОК: динамика позволяет менять тип на лету
2. final (Неизменяемая в Runtime)
➤ Суть: Значение присваивается только один раз.
Сам обьект заменить мы НЕ можем, а вот положить что-то внутрь мы можем
➤ Когда использовать: Когда мы не знаем значение заранее (например, результат из API или текущее время).
final time = DateTime.now(); // Ок, время узнаем только при запуске
final response = await api.getData(); // Ок, ждем данные из сети
// time = DateTime.now(); // Ошибка! Перезаписывать нельзя
3. const (Константа компиляции)
➤ Суть: Значение должно быть известно до запуска приложения (жесткая константа).
➤ Оптимизация: Экономит память. Если создать 100 одинаковых const виджетов, Dart создаст в памяти всего один объект.
const pi = 3.14; // Ок, значение известно при написании кода
const mainFont = 'Metrophobic';
// const t = DateTime.now(); // Ошибка! До запуска мы не знаем время
const перед виджетами, которые не меняются, чтобы Flutter не тратил силы на их перерисовку.
В программировании есть такие понятия как АСИНХРОННОСТЬ и МНОГОПОТОЧНОСТЬ — они близки друг к другу, но имеют принципиальное различие:
➤ Многопоточность: 2 и более потока идут параллельно в одно и то же время.
➤ Асинхронность: Она имитирует многопоточность, хотя по факту выполняется одна и та же функция, то есть поток всего ОДИН.
И так как в Dart поток всего один, нам нужны инструменты, чтобы управлять этим процессом «ожидания», не блокируя интерфейс. Для этого в языке есть три главных компонента: Future, async и await.
1. Future ("Коробка с обещанием")
➤ Суть: Объект, который представляет результат асинхронной операции. Сначала он пуст, а через время в него упадет либо результат, либо ошибка.
Future<String> fetchUser() async {
// Имитация задержки сети
await Future.delayed(Duration(seconds: 2));
return "User: Ivan";
}
2. Ключевые слова async / await
Чтобы работать с этой коробкой внутри функции, мы помечаем саму функцию словом async. Это сигнал для компилятора, что внутри будет происходить асинхронная магия и функция обязательно вернет Future
➤ async: — Модификатор функции. Ставится в сигнатуре метода (перед фигурными скобками). Он говорит компилятору: "Внутри этой функции будет ожидание, поэтому функция обязана возвращать Future".
И самое важное — слово await. Это точка паузы. Когда поток доходит до await, он говорит этой конкретной функции: "Стой и жди, пока коробка (Future) наполнится". Но сам поток не замирает — он выпрыгивает из этой функции и бежит дальше рисовать UI или обрабатывать клики. Как только данные в Future упали, поток возвращается и продолжает выполнять код после await
➤ await: Точка паузы. Пишется перед вызовом другого асинхронного метода. Заставляет код внутри этой функции остановиться и ждать результата, не блокируя при этом всё остальное приложение.
➤ А как поток понимает, когда пора вернуться в функцию после await?
Это происходит благодаря Event Loop (Циклу событий). Когда асинхронная операция завершается, она кидает событие в очередь. Event Loop на очередном круге видит: "О, данные для того await пришли!", берет их и возвращает поток в ту функцию, которую мы поставили на паузу
3. Stream (Поток данных)
➤ Суть: Если Future возвращает значение один раз, то Stream — это труба, по которой данные могут лететь постоянно (например, сообщения в чате или изменение цен на бирже).
Если Dart однопоточный, как он понимает, к чему возвращаться? Вся асинхронность в Dart держится на Event Loop (Цикл событий).
Простыми словами:Event Loop — это бесконечный цикл (карусель), который крутится внутри твоего приложения и проверяет очереди задач.
1. Microtask Queue: Срочные внутренние задачи (выполняются первыми до победного).
2. Event Queue: Клики, отрисовка UI, ответы от сервера (Future).
Как он работает:
1. Event Loop сначала проверяет, есть ли что-то в Microtask. Если есть — выполняет всё, пока очередь не опустеет.
2. Потом берет одно событие из Event Queue и выполняет его.
3. Снова бежит проверять Microtask. И так по кругу.
1. Mixins (Миксины) — Ключевое слово with
➤ Зачем: В Dart нельзя наследоваться от двух классов одновременно (нет множественного наследования). Миксины позволяют "подмешать" готовую логику в любой класс.
➤ Суть: Это не семья (наследование), это способность или "плагин" с кодом.
mixin Swimmable {
void swim() => print("Я умею плавать!");
}
class Animal {}
// Подмешиваем способность через with
class Dolphin extends Animal with Swimmable {}
void main() {
Dolphin().swim(); // Работает!
}
extends) — это то, кто ты есть (Дельфин — это Животное). Миксин (with) — это то, что ты умеешь (Дельфин умеет плавать). Можно подмешивать сколько угодно способностей через запятую.
1.1 Может ли mixin наследоваться от другого класса?
Обычный mixin не может использовать extends как класс.
Но mixin может ограничивать, на какой класс его можно применять, через on.
1.2 Может ли mixin иметь конструктор?
Нет, обычный mixin не может иметь конструктор.
Потому что mixin — это не полноценный класс для создания объекта, а кусок поведения, который подмешивается в другой класс.
2. Extensions (Расширения) — Ключевое слово extension on
➤ Зачем: Чтобы добавить новые методы в классы, которые ты не можешь менять (например, системные String, int, List).
➤ Суть: Это "тюнинг". Мы не меняем оригинал, но добавляем ему новые "фишки", чтобы код был чище.
extension MyString on String {
bool get isValidEmail => this.contains('@');
}
void main() {
String email = "test@mail.com";
print(email.isValidEmail); // true. Мы сами добавили этот метод в String!
}
1. Есть ли в Dart интерфейсы?
Да. Любой класс можно использовать как интерфейс через implements.
class Dog implements Animal {}
2. Можно ли имплементировать обычный класс и несколько классов?
Да. Можно обычный класс и сразу несколько через запятую.
class C implements A, B {}
3. Что такое abstract class?
Это класс-заготовка для наследования. Создать его объект напрямую нельзя.
abstract class Animal {}
4. Abstract и late — в чём разница?
abstract — реализации нет, наследник обязан сделать.
late — значение будет присвоено позже.
late String name;
5. Abstract class и mixin — отличие
abstract class — база через extends.
mixin — поведение через with.
class Dog extends Animal with CanRun {}
В Dart все проблемы делятся на две категории: те, которые мы можем исправить на ходу, и те, из-за которых программа должна немедленно остановиться.
1. Exception (Исключение) — "Проблема, которую можно решить"
➤ Что это: Ситуация, которую мы предвидим. Например: пропал интернет, сервер не отвечает или файл не найден.
➤ Как работаем: Оборачиваем опасный код в try-catch. Если что-то пойдет не так, приложение не вылетит, а просто выполнит запасной план (покажет ошибку юзеру).
try {
var data = await api.fetchData(); // Пробуем загрузить
} on SocketException {
print("Нет интернета!"); // Обрабатываем конкретную проблему
} catch (e) {
print("Произошло что-то странное: $e");
}
2. Error (Ошибка) — "Косяк программиста"
➤ Что это: Критическая проблема в логике кода. Например: обращение к элементу списка по индексу, которого нет (out of bounds), или переполнение памяти.
➤ Как работаем: Ошибки не нужно ловить через try-catch. Их нужно находить и исправлять в коде раз и навсегда.
3. Finally — "Уборка в любом случае"
➤ Суть: Этот блок кода выполнится всегда. Успешно ли прошел запрос или всё упало с ошибкой — finally отработает в конце.
➤ Для чего: Идеально для закрытия файлов, отключения от базы данных или выключения индикатора загрузки (лоадера).
try {
showLoading(); // Включили крутилку
await api.load();
} finally {
hideLoading(); // Выключили крутилку В ЛЮБОМ СЛУЧАЕ
}
Exception — это внешние обстоятельства, которые мы обрабатываем. Error — это наши ошибки в коде, которые мы фиксим. Finally — это гарантированная очистка ресурсов.
Null Safety - это страховка того что приложение не упадет , это такое себе условие, а может ли быть переменная пустой (null) или нет.
В Dart все переменные по умолчанию не могут быть null (non-nullable). Если ты создал String name, ты обязан положить туда строку, иначе код не скомпилируется.
1. Операторы ? и !
➤ Символ ? (Nullable): Говорит Dart, что переменная может содержать null.
➤ Символ ! (Bang operator): Твоя клятва компилятору: "Я уверен, что тут сейчас не null, мамой клянусь!". Если там окажется null — приложение упадет.
String name = 'Ivan'; // Не может быть null
String? bio; // Может быть null
// print(bio.length); // Ошибка! Dart не даст вызвать метод у того, кто может быть null
print(bio!.length); // ОК, но если bio == null -> КРАШ
2. Операторы ?? и ?. (Безопасность)
➤ Оператор ??: Позволяет задать "запасное" значение, если слева оказался null.
➤ Оператор ?.: Вызывает метод только если объект существует (не null).
String userBio = bio ?? 'Информация отсутствует'; // Защита
print(bio?.length); // Если bio null, выведет null, но не упадет
3. Ключевое слово late
➤ Суть: Мы обещаем Dart, что переменная точно будет инициализирована позже, но до того, как мы к ней обратимся.
➤ Для чего: Обычно используется в initState(), когда значение переменной зависит от контекста, который недоступен в момент создания класса.
В Dart есть два крайних типа: Object? (самый верхний) и Never (самый нижний).
1. Object? (Верхушка иерархии)
➤ Что это: Базовый класс для абсолютно всех типов. В него можно положить что угодно: число, строку или null.
➤ Суть: В отличие от dynamic, тип Object? — строгий. Ты не можешь вызвать метод у переменной, пока не проверишь её тип.
Object? data = "Привет";
// data.toUpperCase(); // ОШИБКА: Сначала нужно проверить тип!
if (data is String) {
print(data.toUpperCase()); // ТЕПЕРЬ МОЖНО
}
2. Never (Тип-тупик)
➤ Что это: Тип, который не имеет значений. Используется для функций, которые никогда не возвращают результат.
➤ Суть: Функция с типом Never либо всегда выкидывает ошибку (throw), либо содержит бесконечный цикл.
Never crashApp(String msg) {
throw Exception(msg); // Функция "взрывается", код дальше не идет
}
void checkAuth(String? token) {
if (token == null) crashApp("Нет токена");
print(token.length); // Dart уверен, что тут token НЕ null, благодаря Never выше
}