Переменные и типы

Предпосылки: Ассемблер (регистры, адреса памяти, боль ручного управления).

Ассемблер | вывод

В ассемблере программист запоминает: «в регистре EAX лежит зарплата, в ячейке 200 — бонус, в ячейке 204 — налоговая ставка». При десяти переменных это ещё терпимо. При пятидесяти — невозможно: голова не держит, какой адрес что хранит, а одна ошибка в номере ячейки ломает программу без каких-либо предупреждений.

В конце 1950-х люди решили: пусть машина сама выбирает адреса и регистры, а программист работает с именами. Вместо MOV EAX, [200] пишут salary = 1500, и специальный переводчик решает, куда положить число. Он читает текст с именами и подбирает для него инструкции и ячейки. Как именно он это делает, разберём в последней заметке серии. Пока достаточно знать, что такой переводчик существует и берёт эту работу на себя.

Одним из первых массовых языков был FORTRAN (1957) — сокращение от FORmula TRANslation, «перевод формул». Он был создан для математиков и инженеров: вместо ассемблерных инструкций они могли писать привычные формулы вроде TOTAL = BASE + BONUS.

Переменная

Посмотрим рядом, что меняется. Слева — ассемблер считает net = salary - tax + bonus, где salary лежит по адресу 200, tax — 204, bonus — 208, результат — в 212:

Ассемблер                         Ruby
MOV EAX, [200]    ; salary        salary = 1500
SUB EAX, [204]    ; - tax         tax = 195
ADD EAX, [208]    ; + bonus       bonus = 300
MOV [212], EAX    ; -> net        net = salary - tax + bonus

Справа нет адресов, нет регистров, нет ручного переноса числа из памяти в процессор и обратно. Имя вроде salary существует для человека: оно подсказывает, что хранится в переменной. Процессор имя не видит — он по-прежнему работает с адресами, только теперь их подбирает переводчик.

Переменная — это имя, за которым стоит значение. Программист придумывает имя, а машина сама находит для значения место в памяти.

salary = 1500
tax_rate = 0.13
bonus = 300

С переменными можно делать то же, что и с числами — складывать, вычитать, сравнивать:

net_salary = salary - (salary * tax_rate) + bonus

Одна строка вместо пяти-шести ассемблерных, понятная даже тому, кто видит код впервые.

Типы данных

У программы в маркетплейсе данные разной природы: количество заказов — целое, цена — с копейками, имя продавца — текст, признак «верифицирован» — да или нет. В языке без различения типов сложить имя продавца с ценой синтаксически ничто не мешает — и бессмысленный результат проедет дальше в отчёт или в расчёт суммы к выплате. Тип данных — это договорённость, как интерпретировать содержимое переменной и какие операции над ней имеют смысл; именно эта договорённость позволяет языку отсечь бессмысленные операции до того, как они пройдут в данные.

Каждый тип в маркетплейсе появляется из конкретной потребности.

Целые числа (integer). Счётчик заказов продавца, номер заказа, год регистрации — всё это целые числа без дробной части. Как именно компьютер хранит целые и отрицательные числа — целые числа.

orders_count = 42
order_number = 40871

Дробные числа (float — от floating point, «плавающая точка»: десятичная точка может сдвигаться между разрядами). Цена товара 199.99, налоговая ставка 0.13, рейтинг 4.7 — значения с дробной частью. У них есть неочевидная особенность: 0.1 + 0.2 в большинстве языков даёт 0.30000000000000004, а не 0.3. Для денег это серьёзно — почему так происходит и что с этим делают, см. числа с плавающей точкой.

price = 199.99
tax_rate = 0.13

Строки (string — «нить, цепочка»: текст как цепочка символов). Имя продавца, адрес доставки, номер телефона — текст, заключённый в кавычки. Номер телефона "555-0142" — тоже строка: цифры в нём не нужны для арифметики.

seller_name = "Alice"
phone = "555-0142"

Логические значения (boolean). Продавец прошёл верификацию или нет, товар в наличии или нет — два состояния. Два значения: true (истина) или false (ложь). Названы в честь математика Джорджа Буля (George Boole), создателя алгебры логики.

verified = true
in_stock = false

Отсутствие значения (nil). Продавец ещё не ввёл адрес доставки — поле существует, но в нём нет ни строки, ни числа. Для такого случая языки заводят отдельное значение: nil в Ruby, None в Python, null в Java. Это не пустая строка и не ноль: это явное «здесь ничего нет». Забыть проверить nil перед использованием — классический источник ошибок, поэтому некоторые языки (Rust, Kotlin) встраивают проверку в сам тип.

shipping_address = nil

Зачем типы нужны? Они отсекают бессмысленные операции. Попробуем сложить строку и число:

"5" + 3
# TypeError: no implicit conversion of Integer into String

Ruby останавливает программу: строка "5" и число 3 — разные сущности, сложение для них не определено. Без такой проверки программа прошла бы дальше с неожиданным результатом, и ошибка нашлась бы гораздо позже — на сравнении сумм, в отчёте или при финансовой сверке.

Как разные языки обращаются с типами

Типы существуют во всех языках программирования, но языки по-разному решают, когда и как их проверять. Оба подхода — для людей, но для разных ситуаций.

Динамическая типизация (Ruby, Python). Программист не указывает тип — язык определяет его сам по значению. Переменная может сменить тип в процессе работы:

x = 5        # целое число
x = "hello"  # теперь строка — Ruby не возражает

Меньше церемоний — код короче, быстрее писать и менять. Это помогает разработчику двигаться быстро, особенно на ранних этапах, когда задача ещё не до конца понятна. Расплата — TypeError из примера выше ловится не до запуска, а когда программа дойдёт до этой строки. Тест прошёл не по всем веткам — значит, ошибка будет ждать в продакшене.

Статическая типизация (C, Rust, Java). Программист указывает тип, а компилятор проверяет его до запуска. В некоторых языках тип вычисляется автоматически:

let salary: i32 = 1500;
salary = "hello";   // ошибка сборки: expected integer, found &str

Строгость проверки зависит от языка. В Rust и Java компилятор остановит сборку — бинарник просто не появится. В C такое присваивание — constraint violation: стандарт требует диагностики, но gcc и clang по умолчанию выдают предупреждение, а не ошибку, и программа всё равно соберётся. Ошибка типа проявится в работе — обычно через повреждение памяти. Флаг -Werror превращает это предупреждение в отказ сборки. В C++ правила строже, и тот же код отвергается как ошибка. Общий принцип один — «статика = ошибки типов ловятся раньше», — но степень этой жёсткости у разных языков разная.

Больше текста при написании — зато ошибки ловятся до запуска, а не в продакшене. Код становится самодокументированным: другой программист видит int salary и сразу понимает, что здесь целое число. Это помогает команде: когда код читают десять человек, явные типы экономят время каждому.

Один и тот же смысл — «создать переменную с числом 5» — на разных языках выглядит по-разному:

Ruby:    x = 5
Python:  x = 5
C:       int x = 5;
Rust:    let x: i32 = 5;
Java:    int x = 5;

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

Адреса и регистры больше не проблема. Зато программа по-прежнему выполняет все шаги подряд — и посчитает зарплату уволенному сотруднику с тем же усердием, что и работающему. Чтобы управлять ходом выполнения, нужны условия.

Sources

  • Sebesta, R., 2019, Concepts of Programming Languages. Pearson.
  • Backus, J. et al., 1957, The FORTRAN Automatic Coding System. Western Joint Computer Conference.
  • ISO/IEC 9899:2018 (C17), §6.5.16.1 — Simple assignment.

Ассемблер | вывод