Типизация данных Объекты данных Любая программа несет в себе набор операций, которые применяются к определенным данным в определенной последовательности. Каждый язык программирования задает три категории характеристик: 1) допустимые значения данных и способы размещения значений в памяти компьютера; 2) допустимые операции (встроенные в язык и рукотворные, то есть создаваемые программистом); 3) операторы, управляющие последовательностью применения операций к данным. Области хранения данных в аппаратуре компьютера (памяти, регистрах и внешних запоминающих устройствах) имеют довольно простую структуру, которая группирует последовательности битов информации в байты или слова. Однако в вычислительной среде предусматриваются более сложные формы хранения данных: стеки, массивы, числа, символьные строки и т. д. Элементы данных, рассматриваемые как единое целое в некий момент выполнения программы, обычно называют объектами данных. В ходе вычислений существует великое множество различных объектов данных. Мало того, объекты данных и отношения между ними динамически меняются в разные моменты вычислительного процесса. Одни объекты данных определяются программистом (переменные, константы, массивы, файлы и т. п.). Программист создает эти объекты и явно управляет ими (с помощью объявлений и операторов в программе). Другие объекты данных самостоятельно создаются компьютерной системой. К таким объектам данных (стекам, записям активации процедур и функций, файловым буферам и спискам свободной памяти) программист непосредственного доступа не имеет. Подобные объекты формируются системой автоматически и обеспечивают ≪машинную≫ организацию вычислений. Объект данных является контейнером, содержащим значения данных и другую информацию о данных. В этом контейнере значения хранятся и из него они извлекаются. Объект данных представляет собой сложную конструкцию, имеющую многочисленный набор атрибутов. Самым важным атрибутом считается тип данных. В целом, атрибуты определяют специфику допустимых значений, логическую организацию этих значений и особенности работы с ними. Значение данных может быть отдельным числом, символом или указателем на другой объект данных. Когда говорят о значении объекта данных, имеют в виду следующее: в области памяти компьютера, выделенной объекту, размещается некоторая комбинация битов, соответствующая значению. В ходе вычислений одни объекты данных существуют с самого начала выполнения программы, а другие создаются динамически в какие-то моменты времени. Одни объекты данных уничтожаются на определенных этапах вычислений, а другие сохраняются до конца работы программы. Следовательно, у каждого объекта данных свой период жизни, в течение которого он может быть задействован для хранения значений данных. Объект данных считается элементарным, если содержащееся в нем значение всегда рассматривается как единое целое. Если же этот объект состоит из набора других объектов, его называют структурой данных. Любой объект данных в течение своей жизни подвергается изменениям. Хотя состав атрибутов объекта данных не меняется, этого нельзя сказать об их содержании. Приведем самые важные атрибуты (характеристики) объекта данных: ‰. Тип. Этот атрибут задает возможные значения, применимые операции и формат хранения данных. ‰. Местоположение. Данный атрибут хранит координаты области памяти, отведенной под объект данных. Конкретная область памяти назначается системными программными средствами, управляющими памятью. Программист обычно не имеет доступа к этим средствам. ‰. Значение. Это текущая величина (или набор величин) объекта данных. ‰. Имя. Перечень имен, по которым к объекту данных обращаются в процессе вычислений. Имена могут назначаться как статически (в начале работы), так и динамически, в определенные моменты времени вычислений. ‰. Принадлежность. Сведения о принадлежности данного объекта к другим объектам данных (например, ссылки на эти объекты). Переменные и константы Переменная является объектом данных, который программист явно определяет и называет. Имя переменной считают нечувствительным к регистру, если не имеет значения, какими буквами оно записано (заглавными или строчными). Впрочем, во многих языках имена переменная и ПЕРЕМЕННАЯ обозначают разные объекты данных. В течение жизни значение переменной может многократно изменяться. Говорят, что при этом происходит новое связывание переменной с ее значением. Константой называют объект данных с неизменным значением. Различают обычные и именованные константы. Для обычной константы задается только значение. В именованной константе значение связывается с явно определяемым именем (это связывание происходит только один раз). Например, программа на языке C может содержать следующее объявление: int Х; что означает описание простой целой переменной с именем Х. По этому объявлению система создает объект данных, выделяя под него память. Изначально этот объект значения не имеет. Далее может быть записан оператор присваивания Х = 15, связывающий объект данных Х со значением 15. Для определения константы можно написать объявление const int ARG=45; Здесь указывается, что в жизненном цикле программы объект данных с именем ARG постоянно должен быть связан со значением 45. Предусмотрен и другой способ объявления константы — макроопределение: #define ARG 45 Макроопределение обрабатывается во время компиляции программы и приводит к тому, что все ссылки на имя ARG в программе будут заменены на константу 45. Заметим, что у константы 45 имеются два имени: определенное программистом имя ARG и буквальное имя 45. Оба эти имени могут быть использованы в программе для ссылки на один и тот же объект данных со значением 45. Имя 45 считается литералом, который именует объект данных, содержащий значение 45. Способ создания и использования литералов определяется самим языком программирования. Важно понимать тонкое отличие значения 45, являющегося целым числом, которое хранится в памяти компьютера во время выполнения программы, от имени 45, состоящего из двух символов, 4 и 5, и представляющего собой десятичную форму записи в тексте программы того же числа. Следует знать, что макроопределение дает команду компилятору приравнять ARG значению 45, а служебное слово const указывает компилятору, что переменная ARG всегда содержит значение 45. Типы данных Физический мир, являющийся предметной областью программы, содержит разнообразные объекты, рассматриваемые в задачах вычислений как элементы данных. Это могут быть дома, машины, деревья, самолеты, собаки, люди, туфли и т. д. Все эти физические объекты отличаются друг от друга, имеют разнообразные характеристики. Для удобства вычислений физические объекты можно классифицировать, выделяя, например, класс домов, класс автомобилей. Программы же обрабатывают представления элементов данных. Представления элементов данных в императивных языках программирования создаются с помощью объектов данных. Различные категории (классы) объектов характеризуются типами данных. Тип данных — это механизм классификации объектов данных. Тип данных определяет: 1) значения объектов; 2) операции, применимые к значениям; 3) размещение значений в машинной памяти. Иными словами, тип данных — это двойка <Тип_данных> =< <Алгебра_операций>, <Формат_размещения_значений> > где <Алгебра_операций> = (<Значения>, <Операции>) В каждом языке имеется некий набор встроенных примитивных типов данных. Дополнительно в языке предусматриваются средства, позволяющие программисту создавать новые типы данных. Одно из главных различий между ранними языками программирования (Fortran, Cobol) и более поздними (C++, C#, Java, Ada) относится к типам данных, определяемым программистом. Современный подход заключается в том, что язык должен предоставлять средства для расширения набора типов данных. Основными элементами описания типа данных являются: ‰. служебные атрибуты, которые характеризуют объекты данного типа; ‰. значения, которые могут принимать объекты данного типа; ‰. операции, которые определяют возможные действия над объектами данного типа. Например, при описании типа данных ≪массив≫ в состав служебных атрибутов входят: ‰. количество размерностей массива; ‰. допустимый диапазон изменения индекса для каждой размерности; ‰. тип данных отдельного элемента массива. Значения могут задаваться перечнем чисел (диапазоном или перечислением), которые фиксируют допустимые значения элементов массива. Набор возможных операций для типа ≪массив≫ образуют: ‰. операция индексного доступа к элементам массива (каждому элементу приписывается определенный индекс); ‰. операция по созданию массива; ‰. операция изменения формы массива; ‰. операции запроса атрибутов массива (верхней и нижней границ диапазона индексов, размера); ‰. операция присвоения значения массива; ‰. арифметические операции над парами массивов. При реализации типа данных обычно рассматриваются: ‰. способ представления объектов данных этого типа в памяти компьютера (в процессе вычислений); ‰. способ представления операций, определенных для этого типа данных. Он задает конкретные алгоритмы и процедуры для обработки выбранной формы размещения объектов данных в памяти. Реализация типа данных определяет, каким образом данные виртуальной среды вычислений отображаются на аппаратные средства компьютера. Здесь может использоваться как программное, так и аппаратное моделирование. Кроме того, при описании типа данных следует определить его синтаксис, то есть форму записи типа в тексте программы. И описание (спецификация), и реализация достаточно слабо зависят от конкретных синтаксических форм, применяемых в конкретном языке. Как правило, синтаксис атрибутов типа данных определяется формой объявления типа, принятой в языке. Значения могут представляться литералами или именованными константами. Вызов операций задается при помощи специальных символов, встроенных процедур или функций. Синтаксическое объявление обрабатывается компилятором, который в данном случае решает две задачи: ‰. максимально эффективно разместить данные в памяти; ‰. проверить соответствие данных указанному типу. Каждая переменная (константа) считается экземпляром типа. Тип данных является важнейшим элементом аппарата прогноза и контроля языка. Например, именно знание типа переменной позволяет решать следующие вопросы: ‰. можно или нельзя присвоить переменной конкретное значение; ‰. можно или нельзя применить к переменной какую-то операцию. В частности, запрещается заносить в переменную целого типа вещественные значения и применять к ней операцию конкатенации (объединения), допустимую для строкового типа. Типы делятся на элементарные и составные. Элементарные (базовые) типы имеют атомарные значения, значениями составных типов являются структуры. Элементарные типы данных Элементарный объект данных может содержать только одно значение. Тип таких объектов, для которых задан набор операций, называется элементарным типом данных. Конечно, в каждом языке программирования присутствует свой перечень элементарных типов данных, но вещественный и целый типы, а также перечисления и указатели имеются почти во всех языках. Содержание атрибутов объекта данных находится в той части его контейнера, которая называется дескриптором. Основные атрибуты объекта данных элементарного типа остаются неизменными в течение всего времени его жизни. Это относится, например, к имени и типу. Они востребованы в любой момент вычислений и хранятся в дескрипторе всегда. Другие атрибуты нужны лишь на этапе компиляции, поэтому на этапе выполнения из дескриптора изымаются. За счет этого экономится память под объект. Тип определяет допустимые значения, которые могут содержаться в объекте данных как экземпляре данного типа. Например, целочисленный тип задает всевозможные целые числа, которые могут быть значениями целочисленных объектов данных. В частности, язык C разрешает четыре типа целых чисел: int, short, long и char. Каждый из этих типов достаточно просто реализуется аппаратными средствами обычного компьютера и поддерживает разную точность вычислений. Тип short обеспечивает наименьшую разрядность числа, тип long — наибольшую разрядность, а тип int ориентирован на самый эффективный формат представления целых чисел в памяти компьютера. Формат для int может совпадать с форматом для short или long, а может использовать и некоторую промежуточную форму. Символы в языке C хранятся как 8-битные целые числа типа char, который принадлежит к семейству целочисленных типов. Набор значений для элементарных целочисленных объектов данных является упорядоченной последовательностью с наименьшим и наибольшим элементом. Набор операций для какого-то типа данных задает возможные действия с объектами этого типа. Эти операции могут быть элементарными, то есть быть встроенными в язык, являться частью его определения, или они могут определяться программистом в виде процедур и функций. Охарактеризуем спецификации некоторых элементарных операций. Спецификации будем задавать в следующем формате: символ операции: тип первого операнда × тип второго операнда → тип результата где символ × обозначает прямое (декартово) произведение множеств, представля емых значениями типов для операндов. Здесь подразумевается спецификация двухместной (иначе бинарной) операции. Обычно спецификацию называют сигнатурой операции. В языке C такая запись именуется прототипом функции. 1. Целочисленное сложение — это операция, операндами которой являются два целочисленных объекта данных, а результатом — также целочисленный объект данных, обычно содержащий значение, равное сумме значений операндов. Если обозначить имя целого типа как integer, то спецификация записывается следующим образом: + : integer × integer → integer 2. Операция проверки на равенство значений двух целочисленных объектов данных. Результатом выполнения этой операции является булево значение (истина или ложь). Если обозначить имя булева типа как boolean, то спецификация примет вид: = : integer × integer →boolean 3. Операция извлечения квадратного корня. Она применима к объектам данных вещественного типа (с именем real): sqrt : real → real Операция моделирует математическую функцию: каждому входному набору допустимых операндов она сопоставляет однозначный результат. У каждой операции имеется область определения (множество допустимых значений операндов) и область значений (множество возможных значений для результата). Действие операции определяет, каковы будут результаты для данного набора входных значений (операндов). Действие операции обычно задается с помощью алгоритма. При определении операции могут возникнуть отдельные трудности: ‰. Для некоторых значений операндов невозможно определить действие операции. Например, операция извлечения квадратного корня не определена для отрицательных целых чисел. ‰. Неявные операнды. Обычно при обращении к операции в программе ей передается некоторый набор явных операндов. Однако в операции могут быть задействованы неявные операнды (например, глобальные переменные). В этих условиях нелегко выявить все данные, которые влияют на результат операции. ‰. Побочные эффекты (неявные результаты). Операция может не только выдавать явный результат, но и как-то модифицировать значения, хранящиеся в других объектах данных. Такие неявные результаты называются побочными эффектами. Например, функция не только возвращает результат, но и изменяет значения переданных ей параметров. Реализацию элементарного типа данных определяют: ‰. способ представления объектов рассматриваемого типа в памяти компьютера; ‰. набор алгоритмов, положенных в основу заданных операций. Способ представления элементарных типов данных сильно зависит от аппаратных средств компьютера. Например, для целочисленных и вещественных величин используется двоичное представление, которое обеспечивается аппаратурой компьютера. Атрибуты элементарного типа данных, вносимые в дескриптор объекта данных, могут выбираться из двух соображений: 1. Эффективность. В этом случае решаются две задачи: минимизировать затраты памяти и повысить скорость вычислений. В силу этого компилятор заносит в дескриптор объекта минимальное количество атрибутов (как правило, только имя и тип), все остальные атрибуты типа рассматриваются лишь на этапе компиляции. Такой подход характерен для языка C. 2. Гибкость. В данном подходе доминируют такие показатели, как гибкость и надежность (контроль) вычислений. В дескрипторы заносится максимальное количество атрибутов. Расплатой становятся повышенные затраты памяти и понижение скорости вычислений, поскольку аппаратура компьютера работу с атрибутами не поддерживает и все обращения к ним приходится реализовывать программным путем. Этот подход применяется в функциональных и логических языках. Способ представления объекта данных обычно описывается в терминах необходимого размера области памяти (количества необходимых слов, байтов или битов) и компоновки атрибутов и значений данных в пределах этой области. Обычно адрес первого слова или байта в такой области памяти и характеризует местоположение объекта данных. Любая операция может быть реализована одним из трех способов: ‰. Непосредственно аппаратурой компьютера. Например, если целые числа хранятся в памяти в ≪аппаратных≫ форматах, тогда сложение и вычитание целых чисел выполняется с помощью арифметических операций, встроенных в процессор. ‰. Как процедура или функция. Например, операция извлечения квадратного корня, как правило, не поддерживается аппаратными средствами. Она может быть реализована как программируемая функция. ‰. Как встраиваемая последовательность кодов микропрограммного уровня. Например, как подпрограмма микропрограммного управления процессором. Объявления В ходе создания программы программист задает имя и тип каждого используемого объекта данных. Явное объявление — это оператор программы, сообщающий компилятору сведения об именах и типах объектов данных, которые применяются в программе. Место размещения объявления в программе определяет время жизни соответствующего объекта. Например, если в начале функции, написанной на языке C, размещено объявление float х, у;, то оно указывает: во время выполнения функции потребуется два объекта данных типа float, которым назначаются имена х и у. Время жизни объекта х и объекта у ограничено периодом работы функции. После завершения выполнения функции эти объекты уничтожаются. Во многих языках допускаются также неявные объявления, или объявления по умолчанию. Они применяются лишь тогда, когда отсутствуют явные объявления. Например, в программе на языке Fortran можно использовать простую переменную с именем NUMBER, не объявляя ее явным образом. Компилятор по умолчанию считает, что эта переменная имеет целый тип INTEGER, так как ее имя начинается с буквы, входящей в перечень I, J, K, L, M, N. В противном случае переменной был бы приписан вещественный тип REAL. Механизм неявных объявлений все еще далек от совершенства. Следует отметить, что неявные объявления могут быть пагубными для надежности программы, поскольку препятствуют выявлению на этапе компиляции различных опечаток или ошибок программиста. Скажем, в языке Fortran переменные, которые программистом были случайно оставлены необъявленными, получат типы по умолчанию и очень неожиданные атрибуты, что может привести к неявным и трудно распознаваемым ошибкам. Многие программисты, пишущие на языке Fortran, теперь не используют неявные объявления. В силу этого компилятор отказывается от неявного объявления переменных, что позволяет избежать потенциальных проблем со случайно необъявленными переменными. Некоторые проблемы неявного объявления можно устранить, если использовать подход, разработанный в скриптовых языках. Здесь имена отдельных типов должны начинаться с конкретных специальных символов. Например, в языке Perl все переменные с именами, начинающимися с символа $, считаются скалярными величинами, которые могут быть строками или числами. Если имя начинается с символа @, то именуемая переменная является массивом; если имя начинается с символа %, то оно обозначает хешированную структуру. Подобный подход создает различные пространства имен для переменных с различными типами. В этом случае имена @music и %music абсолютно различны, поскольку принадлежат различным пространствам имен. Более того, читатель программы всегда знает тип переменной, когда читает ее имя. Данный подход существенно отличается от подхода в языке Fortran, так как там допускаются как явные, так и неявные объявления, поэтому тип переменной не всегда определяется написанием ее имени. Другая разновидность неявного объявления основывается на контексте. Иногда это называют логическим выводом типа. В простейшем случае контекст задает тип начального значения, присваиваемого переменной в операторе объявления. Например, в языке C# объявление переменной var включает в себя начальное значение, тип которого должен стать типом переменной. Рассмотрим следующие объявления var sum = 0; var total = 0.0; var name = "Liza"; Здесь типами переменных sum, total и name становятся int, float и string соответственно. Имейте в виду, что это статически типизированные переменные, у них типы неизменны в течение всего периода жизни переменной. В языке Visual Basic (начиная с девятой версии), а также в функциональных языках ML, Haskell, OCaml и F# применяется вывод типа. В этих функциональных языках контекст, в котором появляется имя, является базисом для определения ее типа. В общем случае при объявлении объекта данных можно также указать: ‰. неизменное значение объекта (если это константа) или начальное значение (если объект является переменной); ‰. размещение внутри другого объекта-контейнера; ‰. конкретное местоположение в памяти; ‰. конкретную форму представления. При объявлении операции прописывают ее сигнатуру (типы операндов и результата). В языке C эта информация помещается в заголовок описания функции, ее прототип. Например, прототип float fun(int a, float a) объявляет функцию fun со следующей сигнатурой: fun: int × float → float Впрочем, для встроенных в язык элементарных операций такие сведения не нужны. Компилятору они известны по умолчанию. Объявления поддерживают решение следующих важных задач: ‰. Размещение данных в памяти. Если объявление содержит сведения о типе и атрибутах объекта данных, то компилятор оптимизирует представление этого объекта данных в памяти. ‰. Оптимизация управления памятью. Содержащаяся в объявлении информация о времени жизни объектов данных повышает эффективность работы процедур управления памятью. Например, в языке C все объекты, объявленные в функции, имеют одинаковое время жизни и могут быть расположены в единой области памяти, которая удаляется при выходе из функции. ‰. Выбор операции. В большинстве языков используются специальные символы для обозначения родственных операций. Например, символ ≪+≫ обозначает как целочисленное, так и вещественное сложение. Говорят, что такой символ является перегружаемым, поскольку может обозначать целое семейство операций сложения, каждая из которых реализует сложение объектов своего типа. Объявления позволяют компилятору определить во время компиляции, какая именно форма операции подразумевается в конкретном случае. ‰. Контроль типов. С точки зрения повышения надежности программ, объявления имеют очень ценное качество: они позволяют проводить статический контроль типов. Статический контроль выполняется в период компиляции, предваряющий период работы программ. Статический контроль типов На аппаратном уровне компьютера контроль типов не осуществляется. Например, аппаратная реализация операции целочисленного сложения не способна проверить, являются ли переданные ей два операнда целыми числами; для нее это просто последовательности битов. Таким образом, на уровне аппаратуры обычные компьютеры очень ненадежны и не реагируют на ошибки в типах данных. Контроль типов проверяет факт получения каждой операцией (в программе) нужного количества операндов правильного типа. Например, перед выполнением оператора присваивания y=x∗b+z компилятор должен проверить наличие у каждой операции (умножения, сложения и присваивания) операндов правильного типа. Если операция умножения ∗ определена только для целых или вещественных чисел, а x принадлежит к символьному типу данных, то фиксируется ошибка в типе операнда. Контроль типов может осуществляться в период выполнения программы (динамический контроль типов) или в период компиляции (статический контроль типов). Значительное преимущество применения универсальных языков программирования состоит в том, что в их реализациях предусмотрен контроль типов для операций. Отсюда вывод — программист защищен от этих коварных ошибок. Большинство традиционных императивных и объектно-ориентированных языков программирования ориентировано на повсеместное применение статического контроля типов (в период компиляции). Необходимые при этом сведения частично поступают из явных объявлений. Перечислим исходные данные, необходимые для организации статического контроля типов. Во-первых, для каждой операции определены количество и типы данных для операндов и результата. Во-вторых, тип каждого объекта данных (переменной или экземпляра типа) известен и не меняется в ходе выполнения программы. В-третьих, типы всех констант тоже понятны. Тип литерала легко определяется из синтаксиса его записи в тексте программы, а тип именованной константы зафиксирован в ее определении. Перечисленная информация собирается компилятором при анализе (просмотре) текста программы и заносится в таблицу символов, которая накапливает все сведения о типах переменных и операций. После завершения сбора компилятор проверяет все операции программы на предмет правильности типов их операндов. После проверки типов операндов i-й операции определяется тип ее результата, а полученная информация сохраняется для проверки следующей, (i+1)-й операции программы, в которой результат i-й операции может использоваться как операнд. Поскольку статический контроль типов охватывает все операции программы, то проверке подвергаются все варианты вычислений и отпадает необходимость в дальнейшем контроле. В силу этого не требуется включать в контейнеры объектов данных сложные дескрипторы и производить контроль типов на этапе вычислений. В итоге значительно выигрывают в скорости вычислений и эффективности использования памяти. Специфика статического контроля типов существенно воздействует не только на объявления и структуру текста программы, но и на системные механизмы управления данными в период вычислений, организацию самого процесса компиляции, включая возможности раздельной компиляции программных фрагментов и т. д. В некоторых языках статический контроль типов для определенных конструкций просто невозможен. В подобных ситуациях рассматриваются две возможности: 1. Заменить статический контроль на динамический. Правда, при этом резко возрастают затраты памяти, поскольку в период вычислений придется хранить дескрипторы для всех объектов данных (невзирая на то, что проверяются они достаточно редко). 2. Полностью отказаться от идеи контроля типов. Конечно, это чревато появлением серьезных и трудноуловимых ошибок, но иногда приходится принимать подобное решение, если цена динамического контроля типов слишком высока. Динамический контроль типов Динамический контроль типов для операндов некоторой операции осуществляется в период вычислений, непосредственно перед исполнением операции. Реализация динамического контроля базируется на использовании сложного дескриптора, который хранится вместе с объектом данных и описывает его тип и другие атрибуты. Например, в логическом объекте данных находится и его значение, и дескриптор, свидетельствующий о принадлежности объекта к логическому типу. При выполнении любой операции проверяется дескриптор каждого операнда. Операция производится, только если типы операндов правильны; в противном случае формируется сообщение об ошибке. Мало того, к результату операции тоже приписывается дескриптор, характеризующий его тип. Следовательно, будущие операции, которые задействуют данный объект как операнд, тоже смогут проверить его тип. Динамический контроль типов характерен для скриптовых, функциональных и логических языков. В этих языках отсутствуют явные объявления типов переменных. Типы у переменных могут меняться в ходе выполнения программы. Переменные называются не имеющими типа, поскольку в действительности у них нет никакого постоянного типа. Возможность исполнения любой операции проверяется путем динамического контроля типов операндов. Основным преимуществом динамического контроля считают гибкость программы. Так как объявлений типов нет, объект данных может поменять свой тип в любой момент вычислений. Программист может создавать настраиваемые программы, способные работать с данными любого типа. Наряду с достоинствами динамический контроль типов вносит ряд существенных недостатков: 1. Понижение надежности вычислений. Фактически работа над возможными ошибками типизации переносится с подготовительного этапа, этапа компиляции, на исполнительный этап — этап вычислений. При этом степень охвата ошибок сокращается, минимизируются возможности устранения ошибок. Если при компиляции речь шла о тотальном контроле всех операций программы, то здесь проверке подвергаются лишь отдельные объекты данных, задействованные в текущих вычислениях. Многие из объектов остаются вне контроля. Например, вне поля зрения остаются объекты данных, обрабатываемые в других режимах работы программы, которые понадобятся в обозримом будущем. 2. Снижение скорости вычислений. Динамический контроль типов основан на информации, которая хранится в дескрипторах объектов данных. Как правило, доступ к дескрипторам должен быть реализован на программном уровне, так как аппаратура таких действий не обеспечивает. Это выливается в существенное замедление скорости вычислений. 3. Возрастание накладных затрат памяти. Поскольку каждый объект данных несет в себе сложный дескриптор, который содержит обширные сведения и должен храниться в течение всего вычислительного процесса, динамический контроль типов предъявляет повышенные запросы к требуемой памяти. Обзор составных типов данных Объекты данных таких типов характеризуются наличием многочисленных атрибутов. Проиллюстрируем самый популярный составной тип данных — массив. Объект данных типа «массив» На рисунке показана лента памяти, в которой размещается экземпляр типа ≪массив≫. Все элементы массива примыкают друг к другу, образуя в памяти непрерывную ленту, имеют один тип и занимают одинаковую порцию памяти. Массив поддерживает произвольный способ доступа к его элементам. Имя i-го элемента массива А записывается как A[i], где i называется индексом элемента. Произвольный доступ означает, что время доступа к A[i] не зависит от значения i. Тип данных ≪запись≫ имеет свои достоинства, позволяя внутри одной капсулы разместить разнотипные элементы Объект данных типа «запись» Каждый элемент записи может иметь свой собственный тип и свои правила размещения. Требования ≪непрерывности≫ занимаемой ≪ленты≫ памяти здесь отсутствуют. Напротив, элементы записи могут быть разбросаны по различным местам памяти компьютера. Элементы (компоненты) записи именуются и называются полями. Запись поддерживает именной доступ к ее полям. Именной доступ означает, что если имя R обозначает запись с полем, именуемым как а, то поле указывается как R.а. Тип данных ≪указатель≫ обслуживает составные типы данных. Объект данных типа «указатель» Указатель, как правило, размещается в машинном слове, независимо от размера объекта данных, на который он указывает. Этому есть простое объяснение: указатель хранит лишь адрес объекта (в данном случае объекта типа Т). Наконец, для типа данных ≪множество≫ характерна самая высокая плотность хранения данных. Объект данных типа «множество» Под элемент множества отводится всего один бит. Этот бит имеет единичное значение, если элемент действительно входит в объект типа множества. В противном случае значение бита равно нулю. Системы типизации данных Иногда всю сумму правил, связанных с типами данных и их экземплярами, объединяют в понятие система типизации данных. Таким способом стремятся подчеркнуть высокий удельный вес этих понятий и правил, их значимость в теории и практике языков программирования. Системы типизации данных определяют полный жизненный цикл объектов данных в программных вычислениях и включают в себя несколько категорий правил: 1) определения атрибутов переменных, их связывания; 2) определения типов данных; 3) определения типов выражений; 4) правила преобразования и проверки типов данных. Атрибуты переменной Вернемся еще раз к обсуждению фундаментального понятия языков программирования — понятию переменной. С одной стороны, с точки зрения аппаратуры, переменная является абстракцией ячейки памяти компьютера. С другой стороны,с точки зрения программы, переменная — это абстракция объекта данных. Несмотря на кажущуюся простоту, переменная является достаточно сложным объектом, имеющим много свойств. Свойства переменной можно пояснить с помощью шести атрибутов (характеристик), в состав которых входят: имя, тип, адрес,значение, время жизни, область видимости. Основные атрибуты переменной Имя идентифицирует переменную в программе, является самой распространенной (но не единственной) категорией программных имен. Синонимом слова ≪имя≫ является слово ≪идентификатор≫. Большинство переменных имеют имена, хотя и не все. Тип определяет возможные значения, операции, способ размещения переменной в памяти. Например, для типа int в языке Java определен диапазон значений от – 2147483648 до +2147483647 и разрешены операции сложения, вычитания, умножения, деления и деления по модулю. Адрес указывает ячейку памяти под переменную. Поскольку в компьютерах ячейки имеют разные размеры, удобен термин ≪абстрактная ячейка≫. Адрес переменной является адресом той области машинной памяти, где она хранится. Соответствие между переменной и адресом не такое простое, как может показаться на первый взгляд. Во многих языках одна и та же переменная имеет разные адреса в разное время программных вычислений. Например, если в подпрограмме используется локальная переменная, которая берется из стека периода выполнения при вызове подпрограммы, то при разных вызовах подпрограммы у данной переменной будут разные адреса. Это в некотором смысле различные экземпляры одной и той же переменной. Адрес переменной иногда называют левым значением (l-значением), так как имя переменной в левой части оператора присваивания обозначает именно адрес. Достаточно часто несколько переменных имеют один и тот же адрес. Если для доступа к одной и той же области памяти используют несколько имен переменной, такие переменные называют алиасами. Применение алиасов затрудняет чтение программ, поскольку разрешает изменять значение одной и той же области памяти при помощи нескольких переменных. Например, если переменные с именами total и sum являются алиасами, любое изменение значения total изменяет значение sum, и наоборот. Человек, читающий программу, всегда должен помнить, что total и sum — это разные имена одной и той же ячейки памяти. Поскольку в программе может быть любое количество алиасов, запомнить все это достаточно сложно. Кроме того, использование алиасов усложняет проверку программы. В программах алиасы могут создаваться различными способами. Например, могут применяться такие типы данных, как вариантные записи и объединения. Две переменные-указатели считаются алиасами, если они указывают на одну и ту же область памяти. Это справедливо и для переменных-ссылок. Такая разновидность алиасов является побочным эффектом природы указателей и ссылок. Если указатель в языке С++ адресует переменную с именем, то разыменованный указатель и переменная с именем становятся алиасами. Во многих языках алиасы создаются с помощью параметров подпрограмм. Значение переменной характеризуется содержимым ячейки (области) памяти под переменную. Память компьютера удобно представлять в терминах не физических, а абстрактных ячеек. Ячейки большинства компьютеров, являющиеся прямо адресуемыми единицами, имеют размер, равный 8-разрядному байту. Этот размер слишком мал для большинства программных переменных. Будем полагать, что абстрактная ячейка памяти имеет размер, достаточный для хранения соответствующей переменной. Например, хотя в реализации числа с плавающей точкой занимают четыре физических байта, вообразим, что число с плавающей точкой размещается в одной абстрактной ячейке памяти. Такое же допущение принимаем для экземпляра любого элементарного типа данных. Значение переменной иногда называют правым значением (r-значением), поскольку именно его обозначает имя переменной, указанное в правой части оператора присваивания. Для получения доступа к правому значению переменной надо знать ее левое значение. Значительно усложнить этот процесс могут, например, правила видимости. Время жизни переменной определяется периодом существования переменной в программе. На это время переменной назначается ячейка памяти, хранящая ее значение. Область видимости — это сегмент, фрагмент программы, в пределах которой переменная доступна Связывание В жизненном цикле переменной ее атрибуты необходимо связывать. Связывание (binding) — это процесс установления связи между атрибутами. Момент времени, когда эта связь устанавливается, называют временем связывания. Связывание и время связывания — важнейшие понятия как семантики всего языка программирования, так и его системы типизации данных. Связывание может происходить во время проектирования языка, при разработке компилятора языка, при компиляции, загрузке или выполнении программы. Символ плюс (+), например, обычно связывается с операцией сложения во время проектирования языка. Тип данных, например int в языке C, связывается с диапазоном возможных значений во время разработки компилятора. В языке Java переменная связывается с конкретным типом данных во время компиляции программы. Переменная может связываться с ячейкой памяти при загрузке программы в память. Аналогичное связывание иногда задерживается до начала выполнения программы, например, для переменных, объявленных в методах языка Java. Вызов библиотечной подпрограммы связывается с кодом подпрограммы на этапе редактирования связей и загрузки. Как правило, программист-практик выносит за скобки этапы создания языка и реализующего его компилятора. Он ограничивается лишь этапами компиляции и выполнения программы. Приведем два примера связывания. Связывание типа — имя связывается с типом Связывание типа var x : integer; int y; /* Pascal /* С */ */ Связывание значения — имя связывается со значением: Связывание значения x := 15; /* Pascal */ y = 25; /* С */ Рассмотрим оператор присваивания для языка Java: sum = sum + 7; Перечислим необходимые связывания для компонентов этого оператора: ‰. Тип переменной sum — связывание во время компиляции. ‰. Смысл символа операции + — связывание во время компиляции, после определения типов операндов. ‰. Внутреннее представление константы 7 — связывание во время компиляции. ‰. Значение переменной sum — связывание во время выполнения указанного оператора. Понимание связывания атрибутов с сущностями программы является необходимым условием для освоения семантики языка. В частности, следует разобраться, как фактические параметры оператора вызова связываются с формальными параметрами подпрограммы. Для определения текущего значения переменной нужно понимать механизм связи переменной с ячейкой памяти и необходимыми операциями. По времени различают две разновидности связывания: статическое и динамическое Разновидности связывания Статическое связывание выполняется в период компиляции, а динамическое —в период выполнения программы. На аппаратном уровне и уровне операционной системы процесс связывания достаточно сложен, так как адресуемые ячейки памяти подвергаются многократной загрузке/выгрузке в виртуальном пространстве компьютера. Сосредоточимся на специфике статического и динамического связывания. Статическое связывание типа. Явные и неявные объявлений объявления являются единственными инструментами связывания имен переменных с типами на этапе компиляции. Динамическое связывание типов При динамическом связывании оператор объявления переменной не содержит имени типа. Вместо этого тип переменной определяется при присвоении ей значения оператором присваивания. При выполнении оператора присваивания переменная из его левой части получает тип переменной, выражения или значения, находящегося в его правой части. Такое присваивание может связать переменную с новым адресом и новой ячейкой памяти, потому что форматы хранения величин в разных типах отличаются друг от друга. Динамическое связывание постулирует: любой переменной может быть присвоено значение любого типа. Более того, во время выполнения программы тип переменной может меняться многократно. Важно понимать, что тип у переменной с динамически связанным типом имеет лишь временный характер. Если тип связан с переменной статически, то имя переменной навсегда привязано к этому типу. Напротив, если тип связан с переменной динамически, то имя переменной лишь временно привязано к типу. На самом деле имена таких переменных никогда не ограничены типами. Более точно: имена могут быть связаны с переменными, а переменные могут быть связаны с типами. Языки с динамической типизацией существенно отличаются от языков со статическим связыванием типов. Основное преимущество динамического связывания переменных с типом заключается в повышении гибкости программирования. В частности, программу обработки числовых данных в языке, применяющем динамическое связывание типов, можно написать в форме настраиваемой программы. Такая программа сможет работать с данными любого числового типа. Любой тип входных данных считается приемлемым, поскольку переменные, предназначенные для их хранения, будут привязываться к этому типу в ходе присваивания. Напротив, статическое связывание типов не позволяет написать на языке С программу для обработки данных без фиксации их типа. До середины 1990-х годов самые популярные языки программирования повсеместно использовали статическое связывание типа, редким исключением были некоторые функциональные языки, такие как LISP. Однако с тех пор произошел существенный сдвиг в сторону динамического связывания типов. В языках Python, Ruby, JavaScript и PHP теперь принято динамическое связывание типа. Например, программа на языке JavaScript может содержать следующий оператор: list = [4.3, 8.2]; Независимо от предыдущего типа переменной list, в результате такого присваивания она превратится в одномерный массив из двух элементов, являющихся числами с плавающей точкой. Если же далее последует оператор list = 75; то переменная list станет целочисленной скалярной переменной. Возможность динамического связывания типа была введена в язык C# версии 2010 года. Включение в объявление переменной зарезервированного слова dynamic разрешает динамическое связывание типа, это показано в следующем примере: dynamic any; Можно отметить некоторое сходство данного объявления с объявлением object any; Объявления схожи в том, что переменной any может быть присвоено значение любого типа (даже типа object), как и для объявления с фиксированным типом object. Различие же заключается в исключении возможностей взаимодействия с фрагментами на таких динамически типизированных языках, как IronPython и IronRuby (.NET версий языков Python и Ruby соответственно). Тем не менее это полезно, если данные неизвестного типа поступили в программу из внешнего источника. Члены класса, свойства, параметры метода, возвращаемые методом величины и локальные переменные — все они могут быть объявлены динамичными. В чисто объектно-ориентированных языках — например, Ruby — все переменные являются ссылками и не имеют типов, а все данные считаются объектами, и любая переменная может ссылаться на любой объект. В некотором смысле, все переменные в таких языках имеют одинаковый тип — это ссылки. Однако в отличие от ссылок в языке Java, которые ограничены возможностью ссылаться лишь на один конкретный тип, переменные в Ruby могут ссылаться на любой объект. Недостатки динамического связывания типов. Прежде всего снижается надежность программ, поскольку возможности обнаружения ошибок много меньшие, чем у компилятора для языка со статическим связыванием типов. Динамическое связывание типов позволяет присвоить любой переменной значение любого типа. В этом случае неправильные типы на правой стороне оператора присваивания не будут распознаны как ошибки, вместо этого тип переменной-приемника изменится на этот неправильный тип. Допустим, что в конкретной программе на JavaScript имеются две скалярные числовые переменные a и b, а также массив m. Допустим также, что в программе должен быть операторприсваивания: a = b; Вместо него был введен неправильный оператор: a = m; В языке JavaScript (или любом другом языке с динамическим связыванием типов) интерпретатор ошибку не обнаружит. Тип переменной a просто будет изменен на тип массива. Поскольку при дальнейшем использовании переменной a ожидалось скалярное значение, результаты станут непредсказуемыми. В языке со статическим связыванием типов, таком как Java, компилятор обнаружит ошибку a = m, и программа не будет выполнена. Этот недостаток частично присутствует и в языках со статическим связыванием типов (Fortran, С и С++), которые в некоторых случаях автоматически приводят результат из правой части оператора присваивания к типу переменной из его левой части. Самым большим недостатком динамического связывания типов является его стоимость. Стоимость реализации динамического связывания атрибутов весьма значительна, особенно во время вычислений. Именно в это время должна выполняться проверка типов. Для ее обеспечения каждая переменная несет в себе сложный дескриптор. Для хранения любой переменной задействуется область памяти переменного размера, поскольку формат сохраняемого значения меняется от типа к типу. В языках с динамическим связыванием типов чаще всего используются интерпретаторы. У аппаратных средств компьютера нет команд, типы операндов которых неизвестны в период компиляции. Потому компилятор не может создавать машинные команды для выражения x + y, где типы x и y неизвестны в период компиляции. Чистая интерпретация, как правило, выполняется в 10 раз медленнее, чем эквивалентный машинный код. Конечно, если язык реализуется чистым интерпретатором, то время на выполнение динамического связывания типов скрыто и кажется более дешевым. С другой стороны, в языках со статическим связыванием типов обычно применяются компиляторы, так как программы на этих языках транслируются в очень эффективный машинный код. Время жизни Жизнь переменных напрямик связана с их ресурсным обеспечением. Конечно, главным требуемым ресурсом является память. Каждая переменная должна ≪владеть≫ своей порцией памяти. Динамически распределяемый ресурс памяти принято называть пулом (pool). Обычно говорят о пуле свободной памяти, откуда берут порцию памяти. Размер этого пула в ходе вычислений часто изменяется. Жизнь переменной начинается с ее размещения в памяти (allocation). При размещении происходит следующее: 1) из пула свободной памяти извлекается ячейка памяти; 2) извлеченная ячейка связывается с переменной. Жизнь переменной прекращается при ее удалении из памяти (deallocation): 1) связь переменной с ячейкой памяти разрывается (переменная ≪открепляется≫ от ячейки); 2) ячейка памяти возвращается в пул свободной памяти. Время жизни переменной — это период времени, в течение которого переменная связана с определенной ячейкой памяти. Таким образом, время жизни переменной начинается при ее связывании с определенной ячейкой памяти и заканчивается при ее откреплении от этой ячейки. Для обсуждения связывания переменных с ячейками памяти удобно выделить четыре разновидности переменных (согласно времени их жизни). Назовем данные разновидности статическими, стековыми, явными динамическими и неявными динамическими переменными. Статические переменные Статической называют переменную, которая связывается с ячейкой памяти до начала выполнения программы и сохраняет связь с той же самой ячейкой памяти вплоть до завершения программы. Статические переменные очень полезны в программировании. Достаточно часто в программах применяют глобальные переменные, которые должны быть доступны во всех местах программы. Кроме того, глобальные переменные обычно нужны в течение всего времени выполнения программы. Сумма этих свойств свидетельствует о целесообразности статического связывания глобальных переменных с памятью. Иногда требуется, чтобы переменные, объявляемые в подпрограммах, сохраняли свои значения между отдельными выполнениями подпрограммы. Это означает, что такие подпрограммы должны иметь локальные статические переменные. Другим достоинством статических переменных считается их эффективность. Дело в том, что при обращении к статическим переменным используется быстрая прямая адресация, тогда как другие разновидности переменных зачастую нуждаются в более медленной косвенной адресации. По этой же причине минимизируются затраты времени на размещение и удаление статических переменных. К недостатку статического связывания с памятью относят уменьшение гибкости. Например, в языках, которые поддерживают только статические переменные, нельзя применять рекурсивные подпрограммы. Дополнительным минусом считается невозможность использования несколькими переменными одной и той же области памяти. Допустим, что в программе есть две подпрограммы, причем в каждой объявляется свой собственный статический массив. Несмотря на то что подпрограммы никогда не вызываются одновременно, память под массивы нельзя использовать совместно. В языках С и С++ разрешается спецификатор static в объявлении локальных переменных для функции; соответствующие переменные при этом считаются статическими. Применение static для объявления переменной в классе C++, Java и C# означает, что переменная становится переменной класса, а не переменной экземпляра (объекта). Переменные класса создаются статически за некоторое время до начальной инициализации класса. Стековые переменные Стековыми называются переменные, удовлетворяющие двум условиям: ‰. связывание с памятью осуществляется при обработке операторов объявления переменных; ‰. типы переменных связываются статически. Обработка (elaboration) такого объявления приводит к распределению памяти и реализации процессов связывания, указанных в объявлении. Эти действия происходят при достижении фрагмента программного кода, с которым связано объявление, в процессе выполнения программы. Важно подчеркнуть, что обработка происходит во время вычислений. Например, объявления переменных, которые записаны в начале Java-метода, обрабатываются при вызове метода, а созданные по этим объявлениям переменные будут удалены, когда метод завершит свою работу. Как следует из их названия, память стековым переменным выделяется из стека периода прогона (выполнения). Некоторые языки — например, C++ и Java — позволяют размещать объявления переменных в любом месте операторов. В реализациях этих языков все стековые переменные, объявленные в функции или методе (не считая те переменные, которые объявлены во вложенных блоках), связываются с памятью только с началом выполнения функции или метода. В таких случаях переменная становится видимой с момента объявления, но связывание с памятью (и инициализация, если она указана в объявлении) происходит лишь тогда, когда функция или метод начинает выполняться. Тот факт, что видимость переменной опережает ее связывание с памятью, не влияет на семантику языка. Достоинства стековых переменных. В большинстве случаев рекурсивным подпрограммам требуется такая форма динамической локальной памяти, которая обеспечивает для каждой активной копии рекурсивной подпрограммы свою собственную версию локальных переменных. Эта потребность удовлетворяется стековыми переменными. Наличие у подпрограмм стековой локальной памяти полезно даже при отсутствии рекурсии, поскольку все подпрограммы могут совместно использовать одну и ту же область памяти для хранения своих локальных переменных. Недостатки стековых переменных (по сравнению со статическими переменными): ‰. дополнительные затраты времени на размещение в памяти и удаление из памяти; ‰. уменьшение скорости доступа из-за использования косвенной адресации; ‰. подпрограммы не имеют возможности хранить предысторию вычислений. Впрочем, время на размещение/удаление стековых переменных минимизируется за счет того, что переменные объявляются в начале подпрограммы и размещаются/ удаляются все вместе, а не отдельно каждая переменная. Язык Fortran 95 позволяет разработчикам использовать стековые переменные для локальных вычислений, но содержит оператор Save list Этот оператор вставляется в подпрограмму и объявляет в качестве статических все переменные, перечисленные в списке list. В языках Java, С++ и C# локальные переменные в методах становятся стековыми переменными по умолчанию. В языке Ada все нединамические переменные, определенные в подпрограммах, считаются стековыми. Все атрибуты и характеристики переменной, кроме памяти, статически связываются со стековыми переменными. Имеются исключения для некоторых составных типов. Явные динамические переменные Явные динамические переменные — это безымянные ячейки памяти, размещаемые и удаляемые с помощью явных операторов программы, которые задействуются в период выполнения. Обращаться к этим переменным, располагаемым в области памяти по имени куча, можно только с помощью указателей и ссылок. Куча (heap) состоит из коллекции ячеек памяти с весьма неорганизованной структурой, вызванной непредсказуемостью их применения. Явные динамические переменные создаются или оператором (например, в языке С++), или вызовом предусмотренной библиотечной подпрограммы (например, в языке С). В языке С++ существует оператор размещения new, операндом которого является имя типа. При выполнении этого оператора в куче создается явная динамическая переменная, имеющая тип операнда, и возвращается указатель на нее. При создании явной динамической переменной задействуется как статическое, так и динамическое связывание. Поскольку явная динамическая переменная связывается с типом во время компиляции, то связывание типа здесь считается статическим. С другой стороны, подобные переменные связываются с ячейками памяти в момент их создания, то есть динамически, при выполнении программы. Помимо операторов либо подпрограмм для размещения явных динамических переменных, в некоторых языках есть средства их удаления. Проиллюстрируем работу с явной динамической переменной с помощью средств языка С++: int ∗pointer; // создать указатель pointer = new int; // разместить явную динамическую переменную delete pointer; // удалить явную динамическую переменную, // на которую указывает указатель pointer В этом фрагменте создается указатель pointer, способный хранить адрес явной динамической переменной с типом int. Далее в куче с помощью оператора new размещается явная динамическая переменная, имеющая тип int. Адрес этой безымянной переменной заносится в указатель pointer. Затем переменная удаляется из кучи оператором delete. Язык C++ требует явного удаления с помощью оператора delete, поскольку здесь не используется неявная очистка памяти, такая как сборка мусора. В языке Java все данные, за исключением основных скалярных величин, являютсяобъектами. Объекты языка Java представляют собой явные динамические объекты, доступные через ссылочные переменные. В языке Java нет средств явного удаления динамических переменных; вместо этого используется неявная сборка мусора. В языке C# имеются как явные динамические, так и стековые объекты, все они удаляются неявно. Кроме того, C# поддерживает стиль указателей языка C++. Такие указатели используются для ссылок на кучу, стек и даже на статические переменные и объекты. Эти указатели несут ту же степень опасности, что и в C++, а объекты, на которые они ссылаются в куче, не могут быть удалены неявно. Указатели включены в C# для поддержки взаимодействия с компонентами C и C++. Чтобы воспрепятствовать их применению и довести до читателя тот факт, что в коде используются указатели, заголовок любого метода, где определяется указатель, должен содержать зарезервированное слово unsafe (небезопасно). Явные динамические переменные востребованы в динамических структурах (например, связанных списках, деревьях), размеры которых динамически изменяются в период вычислений. Подобные структуры проще всего создавать на основе указателей или ссылок, а также явных динамических переменных. Недостатком явных динамических переменных являются трудности корректного использования указателей и ссылок, стоимость ссылок на переменные, а также сложность реализации требуемого механизма управления памятью. Особенно существенна проблема сложности и стоимости управления кучей. Неявные динамические переменные Неявные динамические переменные связываются с динамической памятью ≪куча≫ только при присваивании им значений. Фактически все их атрибуты и характеристики вновь связываются при каждом присвоении переменной нового значения. Например, рассмотрим оператор присваивания на языке JavaScript: volumes = [24, 94, 76, 50, 41]; Независимо от того, использовалась ли переменная volumes раньше и для чего она использовалась, теперь это массив из пяти числовых значений. Достоинством таких переменных считается высокая степень гибкости, позволяющая писать обобщенные программы. Одним из недостатков неявных динамических переменных являются высокие накладные расходы на обслуживание всех динамических атрибутов, среди которых могут быть: тип элементов массива, диапазон индексов и многие другие. К другому недостатку следует отнести потерю компилятором возможностей распознавания многих ошибок. Тип выражения Как и каждая переменная, каждое выражение тоже должно иметь тип. Он должен быть известен и зафиксирован в период выполнения. Этот тип зависит от типа переменных в выражении, а также от операций выражения. Например, выражение 2 + 3 имеет целый тип, так как и 2 и 3 являются целыми числами, а сумма целых — тоже целое число. Тип выражения логически выводится в период компиляции. Набор правил для определения типа выражения входит в систему типизации языка. Система типизации отвергает выражение, если оно не сопоставимо с типом. Правила системы типизации определяют корректность использования каждой операции языка в выражении. В качестве простейшего, исторического примера типизации выражений опишем систему для языка Фортран. Пример. Система типизации для арифметических выражений в начальном Фортране Выражение является или переменной, или константой, или формируется применением одной из операций +, –, /, * к двум подвыражениям. Тип выражения: int или real. Выражение имеет тип, если, и только если, к нему применимо одно из правил: ‰. Переменные с именами, которые начинаются с букв I .. N, имеют тип int, переменные с другими именами имеют тип real. Например: COUNT имеет тип real. ‰. Число имеет тип real, если содержит десятичную точку, в противном случае —int. Например: 0.5, .5 , 5. , 5.0 — все имеют тип real. ‰. Классификация переменных и констант на int и real распространяется и на выражения. Если выражения E и F имеют одинаковый тип, то E + F, E – F, E / F, E * F — выражения того же типа. ‰. Например, выражение I + J имеет тип int, выражение X + Y имеет тип real. Выражение I + X в стандартном Фортране не разрешено. Арифметические операции С каждой операцией (ор) сопоставляется правило, которое определяет тип выражения Е ор F в терминах типов для Е и для F. Например: если Е и F имеют тип int, то и Е + F имеет тип int. Перегрузка операции — множественность смыслов Некоторые символы операций (+, *) являются перегружаемыми, то есть в разных случаях они имеют разный смысл. В Фортране + обозначает как целое, так и вещественное сложение. Поэтому выражение сложения имеет два возможных типа: real, int. Следовательно, для сложения нужны два правила: 1. Если Е и F имеют тип int, то Е + F также имеет тип int; 2. Если Е и F имеют тип real, то Е + F также имеет тип real. Приведение — преобразование типов Система типизации в оригинальном Фортране отвергала выражения X + I и 2 * 3.142, так как один операнд в них был целым, а другой — вещественным числом. Конечно, с современной точки зрения все это выглядит очень наивно. Современные системы типизации выражений демонстрируют совсем другой уровень гибкости — пройден достаточно длинный путь эволюции. В частности, большинство современных языков программирования рассматривают выражение 2 ∗ 3.142 как 2.0 ∗ 3.142, то есть как произведение двух вещественных чисел за счет приведения. Приведение — это преобразование объекта данных из одного типа в другой, выполняемое неявно (автоматически) или явно.