Лекция 10 (1160810)
Текст из файла
Лекция 10
По настоящему переменный набор параметров поддерживают только C/C++ и С#.
Пример:
printf(const char *,…);
В этих языках есть специальные макросы для работы с переменным числом параметров:
va_list
va_end
va_start
Общая идея: все параметры, которые загружаются в переменную list совместимы с целочисленными ТД или с указателями, а интерпретация остаётся на совести программиста, например, в printf(); параметры выбираются из стека в зависимости от информации, которая находится в процедуре в строке.
Это решение сразу стало критиковаться в связи с ненадёжностью, так как в этом случае ни компилятор, ни кто-то ещё не в состоянии произвести ни динамический, ни статический контроль. Зачем такая конструкция была введена (В С++ понятно из-за концепции совместимости)? Её наличием легко была решена проблема, связанная с вводом-выводом.
Даже в языках, не поддерживающих переменный список параметров, есть процедуры, которые реально поддерживают переменный список. Например, стандартный Паскаль: write(…); - это как бы псевдопроцедура (процедура, которую нельзя написать на самом языке), про которую компилятор знает достаточно много, отсюда у неё и получается переменный список параметров. Здесь же два подхода: вопрос удобства программирования (вывод разнородной информации) или языковая концептуальная целостность, упрощения языка и повышения надёжности. Такой подход принят в семействе языков, опирающихся на язык Паскаль: Модула 2, Оберон и так далее – нет процедур с переменным числом параметров. Надо вывести конструкцию вида: строка-int-строка. Оберон:
IMPORT InOut;
Это стандартный библиотечный модуль, содержащий все процедуры ввода-вывода.
InOut.WriteString(“count = “);
InOut.WriteInt(cnt);
InOut.WriteCh(‘.’);
InOut.WriteLn;
А на Си: printf(“count = %d.\n”, cnt); и если мы забудем указать переменную cnt, то формально ошибки не будет, но будет напечатано какое-то число из стека (оно будет проинтерпретировано как целое число, загружающая программа загружает параметры в стек и их выгружает и, формально, никакого слома программы не произойдёт), чего при описанной выше системе ввода-вывода в принципе не может быть (мы либо просто забываем cnt, либо никаких ошибок быть не может). Но с другой стороны здесь язык становится несколько проще, и программировать становится удобней.
В С++ у нас есть класс CString, и его метод S.Format(“…”,…); реализует sprintf();. Многие программисты продолжают придерживаться этого подхода, следовательно, остаётся ненадёжность.
Один из подходов – статически параметризуемые ТД. Мы уже говорили о статическом перекрытии операций. Это некоторые процедуры с одинаковыми именами, но разным типом и числом (ограниченным, т.е. псевдобесконечного списка параметров написать нельзя) параметров. В Аде таким образом реализована процедура PUT( ,…).
В С++ богатый набора встроенных операций, которые можно переопределять (в том числе и стандартные знаки операций).
Надёжная библиотека ввода-вывода:
iostream.h
(“<<”, “>>” операции сдвига вправо, влево)
<< e - вывод объекта в канал
ch >> obj - операция ввода из канала
Есть ТД iostream (поток ввода-вывода). Каждая операция возвращает:
iostream& operator << (iostream& int, T x);
iostream& operator >> (iostream& int, T x);
И для каждого ТД, для которого хотим определить ввод-вывод, переопределяем эти операции (как правило, на базе введённых в iostream ТД). А возвращаемое значение – это ссылка на первый аргумент. Выполняется слево-направо. Получается мощная и красивая схема:
cout << “count = “ << cnt << ‘.’ <<endl;
Надёжно и читабельно. Также существуют объекты специального типа, которые служат модификаторами ввода-вывода, они позволяют управлять форматом, например, endl, модификаторы, позволяющие изменять ширину поля и т.д.. Вся мощь библиотеки stdio.h сохранена и никаких проблем с надёжностью нет. Но это достигнуто за счёт переопределения стандартных знаков операций, и это наглядно. Но язык усложняется, за счёт введения достаточно нетривиальных вещей.
Альтернативный подход: концепция динамической идентификации типов в C#. Наблюдение за процедурами с переменным числом параметров показывает, что есть некий список параметров, который мы можем проинициализировать. А макросы va_start, va_end позволяют нам итерировать по этому списку. Проблема Си/С++ в том, что мы не знаем, что же на самом деле подставляется в эти параметры и из-за этого соответствующая ненадёжность.
Если у нас язык таков, что Object – базовый ТД, из которого происходят элементы всех классов (все объекты), к нему можно свести любой тип, с помощью механизма обёрток (boxing). В Delphi - это TObject, в Java – Object, а в C# - object. К тому же в этих ЯП поддерживается динамическая идентификация типа (проверка является ли этот объект объектом данного типа, иначе генерируется исключение).
ТД ParamArray – представляет переменный список параметров в качестве массива, каждый из элементов которого - объект, типа Object, который на самом деле есть ссылка на объект класса или обёртка (псевдообъект) структуры или простого значения. У обрабатывающей процедуры последний параметр ParamArray и она работает с ним как с массивом, и вывод осуществляется в зависимости от того, что за элементы в нём. Тут нет никакого статического контроля, но есть динамический. Но кто упрятывает параметры в обёртки. Если мы делаем это сами, то это можно реализовать на любом ЯП самим описав ТД ParamArray и т.д.. Это неудобно и мы теряем все преимущества переменного списка параметров. Очевидно, что это должен делать не программист. В C# это встроенный ТД (ParamArray).
Общее правило - компилятор ищет точное соответствие:
void f(T1 x);
void f(T1 x, T2 x);
void f(T1 x, ParamArray objc);
T1 a, T2 b, T1 c, T3 d
f(a);
f(a, b);
f(a, d);
Пусть T1, T2 и T3 никак не связаны (С наследованием чуть сложнее). Следовательно, точное соответствие не было найдено и он выбирает третий вариант функции, автоматически формируя массив из одного элемента.
f(c, a, b, d);
В данном случае формируется массив из трёх элементов. Таким образом построена библиотека ввода-вывода (Паскаль: WriteLn(форматная строка, ParamArray);), с контролем типов объектов.
В Java более простые правила. Ввод-вывод строк: у каждого объекта (он происходит от ТД Object) есть метод toString, который и применяется по необходимости (компилятор смотрит на контекст):
String s;
int i;
s+i;
Где ”+” – операция конкатенации для строк. i завертывается в оболочку класса Int (который происходит от ТД Object), вызывается метод toString и к строке s добавляется символьное представление i. Следовательно, получаем одну процедуру вывода: вывод строки, а её аргумент – это совокупность объектов, для которых вызывается метод toString. Для нового класса переопределяем метод toString. Похоже на прошлый подход.
ParamArray и прочее в C# появилось, чтобы программисты с С++ легче переходили на С#, а во-вторых в некоторых случаях (например, когда мы пишем процедуры обработчики событий) у нас приходит переменный список параметров и априори мы не можем предсказать какой он будет. Но оно надёжно за счёт динамической идентификации типов. В С++ это тоже есть, но там нет того, что каждый объект происходит от общего и поэтому там ограниченные возможности подобного рода решений.
Глава 5
Определение новых ТД
П.1. Концепция уникальности ТД (Основные проблемы)
Более точная формулировка понятий, строгая и сильная типизация. Их постоянно мешают и непонятно, то ли Си со строгой, то ли с сильной типизацией. Мы определим концепцию уникальности типов, и язык имеет тем боле сильную или строгую типизацию, чем он ближе к этой концепции. Лучше всех удовлетворяет язык Ада. 4 пункта:
1)Каждый объект данных имеет ТД, и этот тип единственен и статически определим.
Типы данных это как бы классы эквивалентности, на которые разбиваются все объекты. То есть не может быть ни константы, ни переменной без ТД. Говоря «статически определим» имеем в виду, что, глядя на текст программы, мы всегда можем определить к какому ТД он принадлежит (Паскаль: по виду константы определяли её тип), иначе – сообщение об ошибке. В Аде у нас есть уточнитель типа данных (например, когда константа принадлежит к нескольким перечислимым ТД): T’e и компилятор всегда знает всё о ТД. Такая концепция во всех традиционных ЯП, и именно из-за этого они с одной стороны являются достаточно эффективными (компилятор может оптимизировать). Но недостатком такого подхода является то, что все ТД разбиваются на непересекающиеся классы, и теряется гибкость.
2)Типы эквивалентны тогда и только тогда, когда их имена совпадают.
Это именная эквивалентность типов, все традиционные современные ЯП поддерживают её (с небольшими отклонениями). Существует ещё и структурная эквивалентность (в первых ЯП, когда типизации уделяли мало внимания речь шла именно о ней – PL|I): ТД совпадают, если их структуры совпадают. От структурной эквивалентности отказались (так как при нём тип – это набор значений), сейчас главное в типе – набор операций (даже на основе одной и той же структуры). Так же, если мы рассмотрим сложные ЯП, структурная эквивалентность двух рекурсивных типов может быть алгоритмически неразрешима. Но иногда отступают от именной эквивалентности: в большинстве старых ЯП есть синонимия типов, например, в Паскале:
Type T = integer;
Типы T и integer теперь синонимы. Данные одного типа могут появляться везде, где появляются данные другого типа и наоборот. В Си и C++ то же (typedef). Здесь типы эквивалентны, если имена совпадают, либо они синонимичны. Это было введено из соображений лучшей документируемости. С точки зрения надёжности, такая конструкция даёт мало. Например, пусть у нас есть три типа данный, синонима integer: Length, Width, Square. Но мы можем к площади прибавить ширину или длине присвоить отрицательное значение.
В Аде (производные типы, введём с помощью специальной концепции нового ТД, который наследует всё множество значений и всё множество операций, эти величины):
type Length is new Integer;
Width is new Integer;
Square is new Integer;
Это совершенно разные типы и их нельзя смешивать.
3)Каждый ТД имеет свой набор операций с фиксированным набором параметров.
Имеется в виду, что фиксированы их число и типы. Переменный список параметров – отклонение от этой концепции.
4)Неэквивалентные ТД несовместимы по операциям.
Тип данных int имеет один набор операций (+, -, *). В Аде у типов Length, Width и Square одни и те же операции, но так как они не эквивалентны из-за разных имён, то их смешивать нельзя. Но мы можем переопределить операцию *:
Length*Width=Square
И как следствие, получить более надёжную программу. Если грамотно определить систему типов, то многие ошибки будут найдены ещё на стадии компиляции.
ЯП, полностью поддерживающий 1)-4) – надёжный. Эта концепция по языку Ада, а другие ЯП ослабляют свойства этой типизации и менее надёжны. В Аде нет синонимии типов, но мы можем переопределять операции, также полностью запрещено неявное (вставляемое компилятором) приведение типов (нарушение 4)):
v:=e
Типы v и e должны совпадать. А компилятор может вставлять или не вставлять контроль. Но между подтипами неявное преобразование возможно, но оно может сопровождаться контролем:
A: Tarr(1..10);
B: Tarr(2..11);
A := B;
Присвоение корректно, так как совпадают динамические атрибуты A’Length и B’Length. Если компилятор не может определить их длины (А и В – формальные параметры) он там вставляет проверку на динамический атрибут «длина». В других ЯП неявные преобразования, которые ухудшают надёжность.
Характеристики
Тип файла документ
Документы такого типа открываются такими программами, как Microsoft Office Word на компьютерах Windows, Apple Pages на компьютерах Mac, Open Office - бесплатная альтернатива на различных платформах, в том числе Linux. Наиболее простым и современным решением будут Google документы, так как открываются онлайн без скачивания прямо в браузере на любой платформе. Существуют российские качественные аналоги, например от Яндекса.
Будьте внимательны на мобильных устройствах, так как там используются упрощённый функционал даже в официальном приложении от Microsoft, поэтому для просмотра скачивайте PDF-версию. А если нужно редактировать файл, то используйте оригинальный файл.
Файлы такого типа обычно разбиты на страницы, а текст может быть форматированным (жирный, курсив, выбор шрифта, таблицы и т.п.), а также в него можно добавлять изображения. Формат идеально подходит для рефератов, докладов и РПЗ курсовых проектов, которые необходимо распечатать. Кстати перед печатью также сохраняйте файл в PDF, так как принтер может начудить со шрифтами.