Введение#
В мире программирования типы данных — это не просто скучные ярлыки, которые говорят компьютеру, сколько памяти выделить под переменную. Система типов определяет то, как мы пишем код, как думаем о логике нашей программы и насколько спокойно мы спим по ночам, не боясь внезапных падений в продакшене.
В этой статье мы простыми словами разберем, как устроена система типов, чем Rust отличается от других языков и как крутая фича под названием Type-Driven Development (проектирование через типы) помогает нам переложить заботу о логических ошибках на плечи компилятора.
1. Как языки работают с типами?#
Все системы типов в языках программирования можно разложить по двум осям: когда происходит проверка и насколько строгие правила.
graph TD
A[Система типов] --> B[Когда проверяем?]
A --> C[Насколько строгие правила?]
B --> B1[При компиляции
Статическая]
B --> B2[Во время работы
Динамическая]
C --> C1[Никакой магии
Строгая / Сильная]
C --> C2[Авто-преобразования
Слабая]
style B1 fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style C1 fill:#f9f,stroke:#333,stroke-width:2px,color:#000
Проверка типов: до запуска или во время работы?#
Статическая типизация (проверка ДО запуска):
- Как это работает: Компилятор проверяет типы всех переменных и аргументов функций еще на этапе сборки. Если вы попытаетесь передать строку туда, где ожидается число, программа просто не соберется.
- Где используется: Rust, Go, C++, C#, Java, Haskell.
- Плюсы: Ошибки ловятся моментально на вашем компьютере, а не у пользователя. Плюс программа работает быстрее, ведь в процессе работы не нужно тратить время на проверку типов.
Динамическая типизация (проверка ВО ВРЕМЯ работы):
- Как это работает: Переменным все равно, что хранить. Типы проверяются на лету, прямо во время выполнения программы.
- Где используется: Python, JavaScript, Ruby, PHP.
- Плюсы: Код пишется очень быстро и гибко, не нужно описывать типы.
- Минусы: Легко пропустить опечатку или передать не тот объект, и программа упадет посреди ночи у реального клиента.
Гибриды: Некоторые динамические языки добавляют подсказки типов (type hints), как в Python. Они помогают редактору кода подсвечивать ошибки, но сам интерпретатор Python во время работы их игнорирует. В языке Julia типы проверяются динамически, но оптимизируются для высокой скорости.
Правила игры: строгие или мягкие?#
Строгая (сильная) типизация:
- Суть: Язык не делает никаких поблажек и неявных преобразований. Если вам нужно сложить целое число
42и вещественное3.14, вам придется явно сказать компилятору привести их к общему знаменателю. Никакого «само рассосется». - Где используется: Rust, Python.
- Суть: Язык не делает никаких поблажек и неявных преобразований. Если вам нужно сложить целое число
Слабая типизация:
- Суть: Язык пытается «помочь» и неявно преобразует типы на ходу. Например, в JavaScript сложение строки
"5"и числа10даст строку"510", а в языке C компилятор может молча привести указатель к числу. - Где используется: JavaScript, C.
- Суть: Язык пытается «помочь» и неявно преобразует типы на ходу. Например, в JavaScript сложение строки
Разделение на строгую и слабую типизацию — это не четкое «да» или «нет», а целый спектр. Но Rust находится на самом строгом его краю: здесь компилятор бьет по рукам за любые попытки смешать несовместимые типы без вашего явного указания.
2. Проектирование через типы (Type-Driven Development)#
Благодаря строгой статической типизации в Rust, компилятор работает как автоматический контролер качества. Мы можем спроектировать наши типы данных так, чтобы некорректное состояние программы было невозможно написать физически. Это называется принципом Make illegal states unrepresentable.
Пример из жизни: Этапы заказа#
Представьте, что мы пишем логику интернет-магазина. Мы не хотим, чтобы программа случайно отправила клиенту заказ, который еще не оплачен.
В других языках мы бы писали проверки:
if order.status == Status::Paid {
ship(order);
} else {
// Что делать, если статус не тот? Ошибка в рантайме?
}В Rust мы можем пойти другим путем — создать отдельные типы для каждого состояния заказа:
NewOrder(Новый заказ)PaidOrder(Оплаченный заказ)ShippedOrder(Отправленный заказ)
Мы пишем функцию ship(order: PaidOrder) -> ShippedOrder. Эта функция физически не примет NewOrder! Попытка передать туда новый неоплаченный заказ вызовет ошибку компиляции. Нам не нужно писать юнит-тесты на эту логику — компилятор сам гарантирует, что шаги процесса нельзя перепутать.
Давайте посмотрим, как это выглядит в коде, на интерактивных слайдах ниже:
Проверь свои знания!#
Пройдите короткий интерактивный тест, чтобы закрепить понимание классификации систем типов и Type-Driven Development.
История обновлений
- Создана новая статья о системе типов Rust и Type-Driven Development с примерами кода и интерактивными тестами.

