Базовый CS с нуля
Зачем нужны типы
Ты уже знаешь, что типы — это правила интерпретации битов, и что одни и те же биты могут означать разное при разных правилах. Это поднимает практический вопрос: что происходит, когда применяется неправильное правило интерпретации? Что идёт не так, и насколько серьёзно?
Ответ варьируется от «получаешь мусорный вывод» до «программа падает» до «возникают уязвимости безопасности». И механизм во всех случаях один: какой-то код применил правило типа к паттерну битов, который был сохранён с другим правилом в уме.
Именно для предотвращения этого и существуют типы. Система типов — это часть языка программирования, которая отслеживает, какое правило интерпретации принадлежит какому значению, и останавливает тебя — либо во время компиляции, либо во время выполнения — от применения неправильного. Понять, зачем нужны типы, значит понять, чего стоит неверная интерпретация.
После этого урока ты сможешь объяснить основную проблему, которую решают типы (неверная интерпретация битов), описать, что такое ошибка типа в машинных терминах, различить статическую и динамическую проверку типов как две стратегии перехвата одной и той же категории ошибок, и привести конкретный пример того, что происходит без соблюдения типов.
Цена неверной интерпретации. Когда программа читает значение по неправильному правилу типа, результат — бессмысленный паттерн битов, декодированный неверно. Программа действует исходя из этого неправильного значения. В зависимости от того, что программа делает дальше, последствия располагаются в широком диапазоне:
- Молчаливо неверный вывод. Программа продолжает работу, но числа неправильные.
Температура, прочитанная как кодовая точка, даёт бессмыслицу; цена, сохранённая как
строка и обрабатываемая как число, даёт
NaNв JS. - Аварийное завершение. Неверно прочитанное значение используется так, что среда выполнения обнаруживает это как недопустимое, и программа останавливается с ошибкой.
- Уязвимость безопасности. В низкоуровневых языках (C, C++) неверная интерпретация буфера как другого типа может перезаписать память за пределами предназначенной области — переполнение буфера. Это первопричина целых классов эксплойтов.
У всех трёх исходов одно происхождение: неправильный тип, применённый к сохранённым битам.
Что такое ошибка типа. Ошибка типа — это событие, при котором программа пытается применить правило типа к значению, которое было сохранено с другим правилом в уме, и языковая система (компилятор или среда выполнения) обнаруживает это несоответствие и сообщает о нём.
Заметь, что определение не говорит «программа падает». Ошибка типа — это обнаруженное несоответствие. Что происходит после обнаружения, зависит от языка:
- Статическая типизация: компилятор обнаруживает несоответствие до запуска программы и отказывается компилировать. Ошибка появляется как сообщение компилятора.
- Динамическая типизация: среда выполнения обнаруживает несоответствие в момент попытки неправильной операции и бросает исключение (или, в худшем случае, молча производит бессмысленное значение без какой-либо ошибки).
Обе стратегии пытаются перехватить одну и ту же базовую проблему: биты, которые вот-вот будут прочитаны по неправильному правилу.
Статическая типизация: перехват во время компиляции. В статически типизированном языке (TypeScript, Java, Rust, C#) тип каждой переменной известен до запуска программы. Компилятор отслеживает информацию о типе через каждое присваивание, вызов функции и выражение. Если ты пишешь код, который применил бы неправильное правило типа — складывает число и строку так, будто оба числа, — компилятор сообщает об ошибке и отказывается производить исполняемую программу.
Цена: нужно явно объявлять типы (или позволять компилятору выводить их), и нужно исправлять ошибки типов прежде, чем запустить какой-либо код. Выгода: ошибки типов не могут возникнуть во время выполнения в статически типизированной, типобезопасной программе. Класс ошибок устраняется на этапе компиляции.
TypeScript — это JS со статической типизацией поверх. Компилятор TypeScript читает твои аннотации типов, проверяет их и генерирует чистый JavaScript для выполнения средой исполнения.
Динамическая типизация: перехват во время выполнения. В динамически типизированном
языке (чистый JavaScript, Python, Ruby) типы отслеживаются средой выполнения, а не
компилятором. Каждое значение несёт метку типа во время исполнения. При выполнении
операции среда выполнения проверяет, имеют ли типы смысл для этой операции — и если нет,
бросает исключение TypeError.
Цена: ошибки типов появляются только при фактическом выполнении проблемной строки. Баг в редко используемом пути кода может оставаться незамеченным месяцами. Выгода: пишешь код быстрее, без объявления типов, и язык более гибок для исследовательской или прототипной работы.
JavaScript динамически типизирован. Когда ты пишешь "hello" - 5 в JS, среда выполнения
не падает немедленно — она пытается привести типы, производя NaN. Когда ты вызываешь
метод на null, среда выполнения бросает TypeError. Среда выполнения обнаружила
несоответствие, но только в момент исполнения.
Почему это работает
Зачем существует TypeScript, если у JavaScript уже есть система типов времени выполнения? Потому что обнаружение во время выполнения — это позднее обнаружение. В большой кодовой базе баг типа может сидеть в пути кода, задействуемом только при необычных условиях. Статический проверщик типов устраняет весь класс несоответствий типов до того, как код выйдет в продакшн, без необходимости выполнять каждый возможный путь кода. Это производительный аргумент за статические типы: перехватывай ошибки, когда пишешь код, а не когда пользователь на них натыкается.
Прослеживаем ошибку типа от причины до последствий.
Сценарий A — динамическая типизация, JS (без предотвращения):
const price = "9.99"; // случайно строка, а не число
const discount = 0.1;
const final = price - discount; // JS приводит: "9.99" - 0.1 = 9.89 (работает, но случайно)
const tax = price * 1.08; // "9.99" * 1.08 = 10.7892 (тоже работает, JS приводит)
const label = price + " USD"; // "9.99 USD" — конкатенация, а не сложениеJS молча приводит строку к числу для - и *, поэтому арифметика случайно работает. Но
+ конкатенирует вместо сложения. Никакой ошибки не было брошено; неправильный тип
произвёл неправильный результат молча.
Сценарий B — статическая типизация, TypeScript (перехвачено во время компиляции):
const price: number = "9.99"; // Ошибка TS: Type 'string' is not assignable to type 'number'Компилятор TypeScript отказывается компилировать это. Неверное присваивание перехвачено в момент написания, до запуска какого-либо кода. Сообщение об ошибке:
Type 'string' is not assignable to type 'number'.Сценарий C — динамическая типизация, TypeError в JS:
const x = null;
x.toString(); // TypeError: Cannot read properties of null (reading 'toString')Среда выполнения обнаружила попытку вызвать метод на null (у которого нет методов) и
бросила TypeError. Это обнаружение во время выполнения: ошибка появляется только при
фактическом выполнении этой строки.
Все три сценария берут начало в применении неправильного типа к значению. Статическая типизация перехватила до выполнения; динамическая типизация перехватила при выполнении; отсутствие проверки типов позволило пройти молча.
Частая ошибка
«Статическая типизация безопаснее; динамическая опасна». Точнее: статическая типизация перехватывает несоответствия типов раньше; динамическая — позже (или, в случае разрешённого приведения типов, как в JS, может и вовсе не перехватить). Ни одна стратегия не устраняет всех ошибок — только конкретный класс ошибок несоответствия типов. Логические ошибки, алгоритмические ошибки и неверные бизнес-правила невидимы для системы типов. Типобезопасность — это один уровень корректности, а не вся история.
В JavaScript typeof ('5' - 2) возвращает 'number', потому что JS приводит строку. Каков числовой результат '5' - 2?
TypeScript перехватывает ошибки типов во время компиляции, до запуска программы. JavaScript — во время выполнения. Сколько из этих двух стратегий перехватывают ошибки ДО выполнения? Введи число.
В JavaScript null.toString() бросает TypeError. Если функция содержит эту строку, но никогда не вызывается во время тестирования, сколько TypeError выявит тест-сьют из этой строки? Введи число.
Программа на C неверно читает 4-байтный буфер целого числа как символьную строку и пишет за пределы буфера. Сколько байт занимает 32-битное целое (буфер, который был прочитан неверно)?
В TypeScript тип каждой переменной должен быть известен до запуска программы. В JavaScript типы проверяются во время выполнения. Какой язык проверяет типы во время компиляции: TypeScript (1) или JavaScript (2)? Введи число.
Какова основная проблема, для предотвращения которой предназначены системы типов?
Типы существуют потому, что биты не несут встроенного смысла — а неверное применение правила интерпретации производит неправильные результаты, аварийные завершения или уязвимости безопасности. Ошибка типа — обнаруженное несоответствие между сохранённым типом и применённым типом. Статическая типизация (TypeScript, Java, Rust) обнаруживает несоответствия до запуска программы, на этапе компиляции, полностью устраняя класс ошибок для типобезопасного кода. Динамическая типизация (JavaScript, Python) обнаруживает несоответствия во время выполнения, когда неправильная операция фактически пытается выполниться. Обе стратегии служат одной цели: защита от неверной интерпретации битов. Разница — в том, когда срабатывает защита.