Переменные и типы
Предпосылки: Ассемблер (регистры, адреса памяти, боль ручного управления).
В ассемблере программист запоминает: «в регистре 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 StringRuby останавливает программу: строка "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.