Основные принципы программирования: статическая и динамическая типизация

Рассказывает Аарон Краус 


Когда вы изучаете языки программирования, то в разговорах часто слышите фразы наподобие “статически типизированный” или “динамически типизированный”. Эти понятия описывают процесс проверки соответствия типов, и как статическая проверка типов, так и динамическая, относятся к разным системам типов. Система типов — это набор правил, которые присваивают свойство, называющееся “тип”, различным сущностям в программе: переменным, выражениям, функциям или модулями — с конечной целью уменьшения количества ошибок путём подтверждения того, что данные отображаются корректно. 

Не волнуйтесь, я знаю, что это всё звучит запутанно, поэтому мы начнём с основ. Что такое “проверка соответствия типов” и что такое вообще тип?

Тип

Тип, также известный как тип данных, это вид классификации, отмечающий одних из различных видов данных. Я не люблю использовать слово “тип” в этом смысле, поэтому скажем так: тип описывает возможные значения структуры (например, переменной), её семантическое значение и способ хранения в памяти. Если это звучит непонятно, подумайте о целых, строках, числах с плавающей запятой и булевых величинах — это всё типы. Типы можно разбить на категории:

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

В различных языках могут различаться примитивные типы и структуры данных, но принцип везде один. Тип просто определяет набор правил и протоколов, которым должен соответствовать элемент данных.

Проверка соответствия типов

Существование типов не имеет смысла без процесса подтверждения того, что эти типы имеют логический смысл и программа может быть корректно исполнена. Тут и приходит на помощь проверка соответствия типов. Это процесс подтверждения и применения ограничений типов, и он может проходить как во время компиляции (т.е. статически), так и во время выполнения (т.е. динамически). Проверка соответствия типов нужна, чтобы убедиться в типобезопасности программы, что сведёт количество ошибок, связанных с типами, к минимуму. Это такие ошибки, которые возникают, когда операция производится с тем типом, с каким она производиться не должна: например, когда целое число принимается за число с плавающей запятой, или когда происходит сложения строки и целого:

x = 1 + "2"

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

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

Теперь, когда мы знаем, что такое типы и как работает проверка соответствия типов, рассмотрим два основных вида проверки: статическую и динамическую.

Статическая проверка типов

Язык обладает статической типизацией, если тип переменной известен во время компиляции, а не выполнения. Типичными примерами таких языков являются Ada, C, C++, C#, JADE, Java, Fortran, Haskell, ML, Pascal, и Scala.

Большим преимуществом статической проверки типов является тот факт, что большую часть ошибок типов можно отловить на ранней стадии разработки. Статическая типизация обычно приводит к более быстрому исполнению скомпилированного кода, потому что компилятор знает точные типы используемых данных и создаёт оптимизированный машинный код. Статическая проверка типов оценивает лишь информацию, доступную во время компиляции, а также может подтвердить, что проверенные условия соблюдаются для всех возможных вариантов исполнения программы, что избавляет от необходимости проверки перед каждым запуском программы. Без статической проверки типов даже 100%-ное покрытие тестами не всегда поможет выявить некоторые ошибки типизации.

Динамическая проверка типов

Динамическая проверка типов — это процесс подтверждения типобезопасности программы во время её выполнения. Типичными примерами динамически типизированных языков являются Groovy, JavaScript, Lisp, Lua, Objective-C, PHP, Prolog, Python, Ruby, Smalltalk и Tcl.

Большая часть типобезопасных языков в той или иной мере использует динамическую проверку типов, даже если основным инструментом является статическая. Так происходит из-за того, что многие свойства невозможно проверить статически. Предположим, что программа определяет два типа, A и B, где B — подтип A. Если программа пытается преобразовать тип A в тип B, т.е. произвести понижающее приведение, то эта операция будет одобрена лишь в том случае, когда значение на самом деле имеет тип B. Поэтому для подтверждения безопасности операции нужна динамическая проверка типов.

В отличие от статической проверки типов, динамическая может привести к прекращению выполнения программы из-за ошибок типизации. В некоторых языках этого может избежать (например, благодаря обработке ошибок или слабой типобезопасности). Для избежания подобных ситуации рекомендуется использовать юнит-тесты.

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

Типичные заблуждения

Миф 1: статическая / динамическая типизация ==  сильная / слабая типизация

Обычным заблуждение является мнение, что все статически типизированные языки являются сильно типизированными, а динамически типизированные — слабо типизированными. Это неверно, и вот почему.

Сильно типизированный язык — это такой язык, в котором переменные привязаны к конкретным типам данных, и который выдаст ошибку типизации в случае несовпадения ожидаемого и фактического типов — когда бы не проводилась проверка. Проще всего представить сильно типизированный язык как язык с высокой типобезопасностью. Например, в уже использованном выше куске кода сильно типизированный язык выдаст явную ошибку типизации, которая прервёт выполнение программы:

x = 1 + "2"

Мы часто ассоциируем статически типизированные языки, такие как Java и C#, с сильно типизированным (они такими и являются), поскольку тип данных задаётся явно при инициализации переменной — как в этом примере на Java:

String foo = new String("hello world");

Тем не менее, Ruby, Python и JavaScript (все они обладaют динамической типизацией) также являются сильно типизированными, хотя разработчику и не нужно указывать тип переменной при объявлении. Рассмотрим такой же пример, но написанный на Ruby:

foo = "hello world"

Оба языка являются сильно типизированными, но используют разные методы проверки типов. Такие языки, как Ruby, Python и JavaScript не требуют явного определения типов из-за вывода типов — способности программно выводить нужный тип переменной в зависимости от её значения. Вывод типов — это отдельное свойство языка, и не относится к системам типов.

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

$foo = "x";
$foo = $foo + 2; // not an error
echo $foo;       // 2

Поскольку PHP обладает слабой типизацией, ошибки в этом коде нет. Аналогично предыдущему предположению, не все слабо типизированные языки являются динамически типизированными: PHP — это динамически типизированный язык, но вот C — тоже язык со слабой типизацией — воистину статически типизирован.

Миф разрушен.

Хотя статическая / динамическая  и сильная / слабая системы типов и являются разными, они обе связаны с типобезопасностью. Проще всего это выразить так: первая система говорит о том, когда проверяется типобезопасность, а вторая — как.

Миф 2: статическая / динамическая типизация == компилируемые / интерпретируемые языки

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

Когда мы говорим о типизации языка, мы говорим о языке как о целом. Например, неважно, какую версию Java вы используете — она всегда будет статически типизированной. Это отличается от того случая, когда язык является компилируемым или интерпретируемым, поскольку в этом случае мы говорим о конкретной реализации языка. В теории, любой язык может быть как компилируемым, так и интерпретируемым. Самая популярная реализация языка Java использует компиляцию в байткод, который интерпретирует JVM — но есть и иные реализации этого языка, которые компилируются напрямую в машинный код или интерпретируются как есть.

Если это всё ещё непонятно, советую прочесть одну из предыдущих статей этого цикла.

Заключение

Я знаю, что в этой статье было много информации — но я верю, что вы справились. Я бы хотел вынести информацию про сильную / слабую типизацию в отдельную статью, но это не такая важная тема; к тому же, нужно было показать, что этот вид типизации не имеет отношения к проверке типов.

Нет однозначного ответа на вопрос “какая типизация лучше?” — у каждой есть свои преимущества и недостатки. Некоторые языки — такие как Perl и C# — даже позволяют вам самостоятельно выбирать между статической и динамической системами проверки типов. Понимание этих систем позволит вам лучше понять природу возникающих ошибок, а также упростит борьбу с ними.

Перевод статьи «Programming Concepts: Static vs. Dynamic Type Checking»