1. Введение в Dart: типы данных, переменные и null safety
Введение в Dart: типы данных, переменные и null safety
Почему один и тот же код на вид «почти правильный» может спокойно работать на JavaScript, но ломаться в Dart ещё до запуска? Именно за это Dart любят в больших приложениях: язык старается поймать ошибки раньше, чем они превратятся в сломанный интерфейс, потерянные настройки или внезапно неработающий запуск xray-core у пользователя.
Когда вы пишете desktop-приложение для управления сетевым процессом, ошибки в данных стоят дороже, чем в учебных примерах. Если порт пришёл строкой вместо числа, если путь к конфигу неожиданно оказался null, если вы думали, что токен есть всегда, а он не сохранился в настройках, приложение может не просто показать неверный текст, а не запустить основной процесс. Именно поэтому знакомство с Dart нужно начинать не с «синтаксиса вообще», а с того, как язык относится к данным и их отсутствию.
Вы наверняка уже замечали в других языках, что переменная как будто «может быть чем угодно», пока это не приводит к хаосу. Dart устроен строже: он хочет, чтобы вы заранее понимали, какие данные где живут и могут ли они отсутствовать. Это снижает количество случайных ошибок ещё на этапе написания кода.
Как Dart думает о данных
Тип данных — это договор между вами и компилятором о том, что именно лежит в переменной и какие операции с этим значением допустимы. Простыми словами: если в переменной число, вы можете складывать его с другими числами; если строка, вы работаете с текстом; если логическое значение, там всего два состояния — истина или ложь. Это важно, потому что компилятор помогает не смешивать несмешиваемое. В реальном приложении это спасает, когда, например, порт сервера должен быть числом 1080, а не строкой "1080".
В Dart есть несколько базовых типов, с которых начинается почти любой код:
true или falseМикропример: если вы храните количество попыток переподключения, это обычно int. Если храните задержку в секундах 1.5, нужен double. Если храните адрес сервера 127.0.0.1, это String.
Dart различает целые и дробные числа специально. В учебном коде это может казаться мелочью, но в сетевых и desktop-приложениях точный выбор типа упрощает чтение кода. Значение timeoutSeconds = 5 ясно воспринимается как целое количество секунд, а animationScale = 0.85 — как дробный коэффициент.
Здесь полезно увидеть не только синтаксис, но и смысл. Переменная port объявлена как int, поэтому туда нельзя присвоить строку. Переменная isRunning объявлена как bool, и это делает состояние процесса явным: он либо запущен, либо нет. Такой код проще поддерживать, чем набор переменных с расплывчатым назначением.
Переменные: почему var удобно, но не магия
Переменная — это именованная область памяти, где хранится значение. На практике это просто «контейнер с подписью». Но важен не только сам контейнер, а то, можно ли потом заменить его содержимое и как компилятор поймёт его тип.
В Dart вы можете объявлять переменные явно или с выводом типа:
Во втором случае вывод типа означает, что Dart сам определяет тип по начальному значению. Если вы написали var retries = 3;, компилятор понимает, что это int. Это не значит, что тип «плавающий». Наоборот: после инициализации retries остаётся целым числом.
Микропример: если вы написали var port = 1080;, позже нельзя сделать port = '1080';. var экономит время на записи, но не отключает типизацию.
Именно здесь новички часто путают Dart с динамическими языками. var — это не «любой тип», а «выведи тип один раз и дальше соблюдай его». Это хороший выбор, когда тип и так очевиден из правой части выражения.
Но есть три похожих слова, которые нужно чётко различать: var, final и const.
Микропример: адрес, введённый пользователем в поле настроек, может храниться в var или чаще в состоянии объекта. Путь к домашней директории, вычисленный один раз при старте, удобно положить в final. Цвет интерфейса Color(0xFF6750A4) в фиксированной теме может быть const.
!Сравнение var, final, const и nullable типов
Разница между final и const сначала кажется искусственной, но в Flutter она очень практична. final подходит для значений, которые вы узнаёте только в момент запуска: например, путь к файлу конфигурации из системной папки пользователя. const работает для того, что никогда не меняется и известно заранее: текстовые метки, числа, некоторые виджеты и значения конфигурации сборки.
Почему строку с путём здесь лучше не делать const? Потому что в реальном приложении путь часто зависит от машины, пользователя, окружения или платформы. Компилятор не может знать его заранее.
Что такое dynamic и почему он опаснее, чем кажется
dynamic — это специальный тип, который отключает большую часть проверок на этапе компиляции. Простыми словами: вы говорите Dart «поверь мне, я сам разберусь». Это удобно для быстрых экспериментов, но опасно в большом проекте.
На первый взгляд гибко. На практике код с dynamic хуже читается и чаще ломается в неожиданных местах. Когда приложение получает JSON с сервера или читает внешние данные, соблазн использовать dynamic особенно велик. Но затем ошибка проявляется глубоко в UI или логике запуска процесса.
Микропример: если API вернул "enabled": "true" строкой, а вы ожидали bool, dynamic позволит донести эту проблему далеко по коду. Строгий тип помог бы поймать несовпадение раньше.
Профессиональная привычка в Dart — использовать dynamic только там, где это действительно оправдано, и как можно быстрее преобразовывать данные в точные типы.
Null safety: главная защита от «значения нет»
Самая важная идея раннего Dart для практической разработки — null safety. Это механизм, который заставляет явно обозначать, может ли переменная не иметь значения. Простыми словами: Dart больше не считает, что null можно незаметно подложить куда угодно.
Это критично для desktop-приложения. У вас могут отсутствовать:
shared_preferencesЕсли код делает вид, что эти значения существуют всегда, вы получаете падения в неожиданных местах.
В Dart типы по умолчанию non-nullable, то есть не допускают null.
Если значение может отсутствовать, это нужно указать знаком ?:
Nullable-тип — это тип, который может хранить либо обычное значение, либо null. Зачем это знать: так вы прямо в сигнатуре показываете, что отсутствие значения — нормальный сценарий, а не авария. В жизни это выглядит так: пока пользователь не выбрал конфиг, selectedConfigPath равен null, и UI показывает кнопку «Выбрать файл».
Здесь важно не просто знать синтаксис, а почувствовать дисциплину мышления. Если написано String, значение обязано быть. Если String?, отсутствие допустимо, и вы обязаны это обработать.
Как работать с nullable-значениями безопасно
Как только появляется String?, вы уже не можете обращаться к нему так же свободно, как к обычной строке. Dart требует сначала доказать, что значение не null.
Это ограничение не мешает, а защищает. Оно заставляет вас продумать ветки поведения.
Есть несколько основных способов работы с nullable-типами.
Проверка через if
Самый прямой и часто самый читаемый путь:
После такой проверки Dart понимает, что внутри блока configPath уже не null.
Микропример: если путь к конфигу ещё не выбран, вы не пытаетесь запускать процесс, а вместо этого показываете пользователю уведомление.
Оператор ?.
Null-aware access позволяет безопасно обратиться к полю или методу, только если объект не null.
Если configPath равен null, всё выражение тоже даст null, а не ошибку.
Микропример: для необязательного описания профиля profileDescription?.trim() безопаснее, чем прямой вызов trim().
Оператор ??
Null-coalescing operator позволяет задать значение по умолчанию.
Если profileName равен null, используется строка 'Без имени'.
Микропример: в списке профилей пустое имя пользователя не ломает интерфейс — вместо этого показывается резервный текст.
Оператор !
Null assertion operator говорит компилятору: «Я точно знаю, что здесь не null». Это самый опасный из частых операторов работы с null.
Если на самом деле там null, приложение упадёт во время выполнения. Поэтому ! — не способ «исправить ошибку компиляции», а обещание, за которое вы отвечаете.
Микропример: если вы нажимаете «Запустить xray-core» и уверены, что путь уже выбран после валидации формы, ! может быть допустим. Но если пользователь может обойти эту ветку, вы получите аварийное завершение.
late: отложенная инициализация без ?
Иногда значение точно появится позже, но в момент объявления его ещё нет. Для этого существует late — ключевое слово для отложенной инициализации non-nullable переменной.
Смысл такой: переменная не nullable, но вы обещаете инициализировать её до первого чтения. Это полезно, когда значение вычисляется на старте приложения, в методе инициализации, в конструкторе или после обращения к платформенному API.
Микропример: путь к рабочей директории можно получить после запуска приложения и сохранить в late String, если он обязан существовать до показа основного окна.
Но late — ещё одно место, где легко обмануть себя. Если вы попытаетесь прочитать переменную до присваивания, получите runtime-ошибку. Поэтому late полезен, когда жизненный цикл объекта действительно гарантирует инициализацию.
Сравнение здесь такое:
String? configPath; — путь может отсутствовать, и это нормальная бизнес-ситуацияlate String appDir; — путь обязан быть, просто станет известен чуть позжеString appTitle = 'Xray Desktop'; — значение есть сразуЭто не просто синтаксические варианты, а три разные модели мышления о данных.
Worked example: настройки подключения для будущего desktop-приложения
Представьте экран базовых настроек клиента. Пользователь может:
Попробуем описать эти данные корректно с точки зрения типов и null safety.
Шаг 1. localPort объявлен как final int, потому что в рамках текущего объекта настроек он зафиксирован после создания. Почему так: порт — число, и если вы не собираетесь менять его прямо в этом экземпляре, неизменяемость упрощает логику.
Шаг 2. profileName — обычный String, потому что имя профиля должно существовать всегда. Почему так: если экран не допускает пустое сохранение без имени, лучше выражать это в типе, а не надеяться на комментарий.
Шаг 3. note — String?, потому что комментарий необязателен. Почему так: отсутствие комментария — не ошибка, а нормальный пользовательский сценарий.
Шаг 4. autoConnect — bool, потому что здесь только два состояния. Почему так: логическое поле читается лучше, чем строковые значения вроде "yes" и "no".
Теперь представьте отображение этих данных в интерфейсе. Имя можно вывести напрямую, а комментарий — только с резервным значением:
Здесь ?? делает код устойчивым к отсутствию комментария. Без него вы бы либо получали null в тексте, либо были бы вынуждены писать более громоздкую проверку.
Но вот более интересный случай: пользователь ничего не выбрал для пути к конфигу.
Если вы попробуете сразу запускать процесс:
компилятор остановит вас. Это хорошо, потому что запуск без конфига — не техническая, а бизнес-ошибка. Значит, надо явно обработать сценарий:
Такое поведение особенно ценно в приложении для xray-core: система не должна «угадывать», что делать с отсутствующими критическими данными.
Частые ошибки новичков и что за ними стоит
Одна из самых частых ошибок — использовать dynamic, когда не хочется разбираться с типами. На короткой дистанции это кажется быстрее. На длинной дистанции вы теряете подсказки IDE, автодополнение, проверки и уверенность в коде.
Другая ошибка — злоупотреблять !. Если вы регулярно ставите !, чтобы компилятор «отстал», значит проблема не в компиляторе, а в модели данных. Возможно, значение должно быть non-nullable и инициализироваться раньше. Или наоборот — nullable, но с честной обработкой отсутствия.
Третья ошибка — путать final и const. В Flutter это быстро всплывёт, когда вы начнёте строить UI. const не означает просто «не меняется после создания», а означает «значение известно на этапе компиляции». Если это различие не почувствовать сейчас, дальше будут странные ошибки и лишняя путаница.
Есть и менее очевидный edge case: пустая строка '' — это не null. Для Dart это полноценное значение. Поэтому если путь, имя профиля или токен не должны быть пустыми, одной лишь null safety недостаточно. Нужна ещё валидация содержимого.
Микропример: String? token может быть не null, но содержать ''. Для логики авторизации это два разных случая: «токена нет» и «токен испорчен или пуст».
Где эти знания сразу пригодятся в вашем курсе
Как только вы перейдёте к Flutter, типы начнут влиять на UI. Nullable-поле будет означать условный рендеринг. final и const будут влиять на читаемость и производительность интерфейса. Когда начнётся Riverpod, правильно описанные типы состояния сразу уменьшат хаос в провайдерах.
А в desktop-части эти основы станут ещё важнее. Работа с конфигами, путями к файлам, JSON, HTTP-ответами, состоянием процесса и пользовательскими настройками почти целиком строится на вопросах: «Какой здесь тип?» и «Может ли здесь быть null?»
Если из этой главы запомнить только три вещи — это:
var в Dart не делает переменную “бестиповой”: тип просто выводится автоматически и дальше остаётся фиксированным.String и String? — это разные договоры с программой: в первом случае значение обязано быть, во втором отсутствие допустимо и требует обработки.! не исправляет null safety, а откладывает риск до runtime: использовать его стоит только там, где вы действительно гарантируете наличие значения.