# Система типов в Rust: почему компилятор — ваш лучший друг

> Простыми словами о системе типов в Rust. Разбираем статическую и строгую типизацию, а также смотрим, как проектировать логику через типы (Type-Driven Development), чтобы избегать глупых багов.

- **URL:** https://eagle2000.space/rust/rust-type-system/
- **Date:** 2026-08-01

- **Tags:** rust, dev, theory

---## Введение

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

В этой статье мы простыми словами разберем, как устроена система типов, чем Rust отличается от других языков и как крутая фича под названием **Type-Driven Development** (проектирование через типы) помогает нам переложить заботу о логических ошибках на плечи компилятора.

---

## 1. Как языки работают с типами?

Все системы типов в языках программирования можно разложить по двум осям: **когда происходит проверка** и **насколько строгие правила**.

<div style="background-color:white; padding: 20px">
{{< mermaid >}}
graph TD
    A[Система типов] --> B[Когда проверяем?]
    A --> C[Насколько строгие правила?]
    B --> B1[При компиляции <br>Статическая]
    B --> B2[Во время работы <br>Динамическая]
    C --> C1[Никакой магии <br>Строгая / Сильная]
    C --> C2[Авто-преобразования <br>Слабая]
    
    style B1 fill:#f9f,stroke:#333,stroke-width:2px,color:#000
    style C1 fill:#f9f,stroke:#333,stroke-width:2px,color:#000
{{< /mermaid >}}
</div>

### Проверка типов: до запуска или во время работы?

* **Статическая типизация (проверка ДО запуска):**
  * **Как это работает:** Компилятор проверяет типы всех переменных и аргументов функций еще на этапе сборки. Если вы попытаетесь передать строку туда, где ожидается число, программа просто не соберется.
  * **Где используется:** 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.

> [!NOTE]
> Разделение на строгую и слабую типизацию — это не четкое «да» или «нет», а целый спектр. Но Rust находится на самом строгом его краю: здесь компилятор бьет по рукам за любые попытки смешать несовместимые типы без вашего явного указания.

---

## 2. Проектирование через типы (Type-Driven Development)

Благодаря строгой статической типизации в Rust, компилятор работает как автоматический контролер качества. Мы можем спроектировать наши типы данных так, чтобы некорректное состояние программы было **невозможно написать физически**. Это называется принципом *Make illegal states unrepresentable*.

### Пример из жизни: Этапы заказа

Представьте, что мы пишем логику интернет-магазина. Мы не хотим, чтобы программа случайно отправила клиенту заказ, который еще не оплачен. 

В других языках мы бы писали проверки:
```rust
if order.status == Status::Paid {
    ship(order);
} else {
    // Что делать, если статус не тот? Ошибка в рантайме?
}
```

В Rust мы можем пойти другим путем — создать отдельные типы для каждого состояния заказа:
1. `NewOrder` (Новый заказ)
2. `PaidOrder` (Оплаченный заказ)
3. `ShippedOrder` (Отправленный заказ)

Мы пишем функцию `ship(order: PaidOrder) -> ShippedOrder`. Эта функция физически не примет `NewOrder`! Попытка передать туда новый неоплаченный заказ вызовет ошибку компиляции. Нам не нужно писать юнит-тесты на эту логику — компилятор сам гарантирует, что шаги процесса нельзя перепутать.

Давайте посмотрим, как это выглядит в коде, на интерактивных слайдах ниже:


### 💻 Система типов и Type-Driven Development


#### Шаг 1

```rust
// ?compile_fail
fn main() {
    let integer: i32 = 42;
    let float: f64 = 3.14;
    
    // Ошибка! Типы не соответствуют друг другу.
    let result = integer + float; 
}
```

**Строгая типизация: Неявное приведение**
- В Rust отсутствует автоматическое (неявное) приведение типов.
- Попытка сложить целое число `i32` и вещественное `f64` приведет к ошибке компилятора E0308.
- Это предотвращает случайные потери точности в вычислениях.


#### Шаг 2

```rust
fn main() {
    let integer: i32 = 42;
    let float: f64 = 3.14;
    
    let result = (integer as f64) + float;
    println!("Результат сложения: {}", result);
}
```

**Строгая типизация: Явное приведение**
- Для безопасного приведения типов используется оператор `as`.
- Мы явно приводим `i32` к `f64` перед выполнением сложения.
- Теперь компилятор уверен в наших намерениях, и код успешно выполняется.


#### Шаг 3

```rust
struct NewOrder {
    id: u64,
}

struct ShippedOrder {
    id: u64,
    tracking_number: String,
}

impl NewOrder {
    fn new(id: u64) -> Self {
        NewOrder { id }
    }
    
    fn ship(self, tracking: &str) -> ShippedOrder {
        ShippedOrder {
            id: self.id,
            tracking_number: tracking.to_string(),
        }
    }
}

impl ShippedOrder {
    fn deliver(self) {
        println!("Заказ {} успешно доставлен с трек-номером {}!", self.id, self.tracking_number);
    }
}
fn main() {
    let order = NewOrder::new(1024);
    
    // 1. Мы не можем вызвать order.deliver() напрямую — компилятор выдаст ошибку!
    // 2. Сначала мы должны перевести заказ в состояние отправленного (ShippedOrder):
    let shipped = order.ship("RU-777-XYZ");
    
    // 3. Теперь мы можем безопасно вызвать доставку:
    shipped.deliver();
}
```

**Type-Driven Development: Безопасное состояние**
- Мы кодируем правила логики программы в типах `NewOrder` и `ShippedOrder`.
- Метод `ship` определен только для `NewOrder`, возвращая `ShippedOrder`.
- Метод `deliver` определен только для `ShippedOrder`.
- Ошибиться невозможно: компилятор не позволит вызвать доставку для нового неотправленного заказа.


---

## Проверь свои знания!

Пройдите короткий интерактивный тест, чтобы закрепить понимание классификации систем типов и Type-Driven Development.


### 📝 Система типов в Rust


**1. К какой категории по времени проверки типов относится Rust?**

- [ ] Динамическая система типов
- [ ] Статическая система типов
- [ ] Опциональная (гибридная) система типов

**2. Разрешены ли в Rust неявные (автоматические) преобразования типов?**

- [ ] Да, для любых совместимых числовых типов
- [ ] Да, но только при сложении целых чисел и чисел с плавающей точкой
- [ ] Нет, неявные преобразования запрещены; типы необходимо приводить явно

**3. В чем заключается суть концепции Type-Driven Development (разработки через типы)?**

- [ ] В обязательном написании тестов до написания кода
- [ ] В кодировании бизнес-логики и инвариантов непосредственно в структурах и типах данных для проверки компилятором
- [ ] В автоматической генерации кода на основе текстового описания типов

**4. Какое утверждение о границе между сильной и слабой типизацией верно?**

- [ ] Это четкое бинарное деление: язык либо на 100% строгий, либо на 100% слабый
- [ ] Это непрерывный спектр, зависящий от количества допускаемых языком неявных преобразований
- [ ] Сила типизации определяется исключительно скоростью работы программы

