И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 32
Текст из файла (страница 32)
Частичная специализация порождает новый вариант параметризованного шаблона, который является частным случаем общего шаблона. Набор аргументов,подходящих для частичной специализации, подходит и для общего163шаблона, но является «более точным». Например, общий шаблониспользует просто имя любого типа, а частичная специализация —тип указателя, тип массива, функциональный тип и т. п. Другимвариантом специализации является меньшее число параметров, чемв общем случае, и т.д. Точное определение понятия «более точный»дано в стандарте C++, и здесь мы не будем вдаваться в детали.Пример шаблона, нуждающегося в частичной специализации, —шаблон класса Stack (другой пример — класс DynVector), содержащий ряд нетривиальных операций. Как уже отмечалось, длялюбого нового типа хранимых элементов порождается новый наборопераций.
Причем для ряда типов эти операции делают одно и то же.Например, для указателей все операции над стеком неразличимы.Однако для каждого указательного типа порождается свой набор, чтоприводит к недопустимому разрастанию машинного кода, которыйпроизводит одни и те же операции.Рассмотрим специализацию стека для указателей. Она будетреализована как частный случай (т. е. как конкретизация шаблономсамого себя) — стек, хранящий указатели типа void*:template <typename Т> class Stack; // общий шаблонtemplate ctypename T> class Stack <T*>// специализация: private Stack <void*> {// наследует конкретизацию общего шаблона// закрытым образомtypedef Stack <void*> BASE;publ i c :explicit Stack(int sz) : BASE(sz) {}T * Pop() { return (T*)BASE::P o p ();}void Push (T * p) { B A S E ::Push (p); }void Swap () {B A S E ::swap ();}// ...
переадресация других методов};Stack <int> s(256); // общий шаблонStack <const char *> strs(64);// частично специализированныйs t r s .P u s h ("sample");10.3. Особенности реализациипараметрического полиморфизма в языкахC# и JavaВ языках C# и Java синтаксис обобщенных типов проще, чемв языке C++, поскольку параметром обобщения здесь может бытьтолько тип (поэтому указывать этот факт не надо):164class и мя-обобщен ного-типа <список-параметров>{ объявления-членов }Конкретизация в C# и Java имеет вид такой же, как и в C++, и рассматривается как имя нового типа:class X <Т> { ...
}class Y { ... }// обобщенный тип// обычный типX<Y> s = new X<Y>(); // создан экземпляр конкретного// классаX <Y> si = s; // две ссылки на объект конкретного// классаРассмотрим пример обобщенного типа стек на C# и Java (используя код из гл.
7), который несмотря на простоту, демонстрируетхарактерные различия между подходами к параметрическому полиморфизму в этих двух языках (хотя общего в этих подходах большепо сравнению с шаблонами C++):class Stack<T>{Т [] body;int top;public Stack(int size){body = new T[size];top = 0;}public T Pop() { return b o d y [— top]; }public void Push(T x) { body[top++] = x; }public bool IsEmpty {get { return top ==0; }}public bool IsFull {get { return top == body.Length;}}}Приведем пример использования созданного стека:Stack<int> s = new Stack<int>(16);int [] x = {1,2,3,4,5,6,7};foreach (int k in x) s.Push(k);while (!s.IsEmpty) Console.WriteLine(s.Pop ()) ;Stack <double> sd = new Stack <double>(32);double [] xx = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};foreach (double k in xx) sd.Push(k);while (! sd.
IsEmpty) Console.WriteLine (sd.PopO);165Обратим внимание на два момента.Во-первых, нельзя использовать объекты стеки для храненияразнотипных данных. Универсальный стек (из Obj ect) мог быть использован для работы с обоими массивами (int [ ] х и double [ ] хх),но в данном примере следует создавать два стека. Попытка записатьв stack<int> целое число будет пресечена транслятором.
Такимобразом, созданный стек безопасен с точки зрения типов. Работаяс таким стеком, можно быть уверенным, что никаких исключений,связанных с неверным преобразованием ссылок, не будет.Во-вторых, получившийся вариант более эффективен вследствие отсутствия неявных операций упаковки в объект типов значений. Ранее эта операция незаметно происходила при обращениик Push (). Сейчас мы имеем свой вариант Push () для каждого видастека. В конечном счете компилятор генерирует отдельный код дляконкретизации типом значений, а также для конкретизации ссылочными типами. Таким образом, если в программе есть конкретизацииStack <double>, Stack<int>, Stack<String>, Stack<char[]>,то будет сгенерировано три набора функций членов: по одному набору для каждого типа значений (double, int) и один набор дляссылочных типов (String, char [ ]).
Отметим, что возможностьгенерировать один и тот же код для типов-ссылок связана с тем, чтос точки зрения стека типы отличаются только размером. Если размертипов одинаковый, то код функций, копирующих значения этоготипа, тоже одинаковый.Каждый набор сгенерированных методов не использует функцииупаковки-распаковки и проверяемые преобразования типов (правильность соответствия типов уже проверена компилятором), чтотакже повышает эффективность по сравнению с универсальнымтипом.Теперь рассмотрим реализацию обобщенного типа на Java:Stack<Integer> s = new Stack<Integer> (32);int [] x = {1,2,3,4,5,6,7} ;for (int k : x) s.Push(k);while (!s .IsEmpty()) System.out.println (s.Pop ());Stack <Double> sd = new Stack<Double> (12);double [] xx = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};for (double k : xx) sd.Push(k);while (!sd.IsEmpty()) System.out.println(sd.Pop());По сравнению с предыдущим вариантом имеем два важных изменения.Во-первых, в стеке хранятся ссылки типа Object (как в универсальном типе Stack), а не объекты типа параметра т, как в С#.
Этосвязано с тем, что в Java нельзя создавать массивы объектов типапараметра, как нельзя создавать и объекты типа параметра (new Т ()запрещено) внутри обобщенного класса.166Во-вторых, вместо Stack<int> (Stack<double>) используетсяStack<Integer> (Stack<Double>). Это связано с тем, что аргументами конкретизаций могут быть только ссылочные типы, поэтомунеобходимо пользоваться классами-обертками.Эти ограничения продиктованы уже упоминавшимся в подразд.
10.1 обстоятельством: компилятор Java не сохраняет информацию о конкретизированных типах в программе (она используетсятолько на этапе трансляции). В отличие от С#, где для каждой разновидности типа генерируется свой набор функций-членов, в Javaиспользуется только код, сгенерированный по объявлению обобщенного типа. В объявлении никакой информации о типе нет, поэтомукод функций членов генерируется, как для ссылок на тип Object.Более того, обобщенный тип стек можно использовать и как обычный универсальный тип (хотя делать это настоятельно не рекомендуется). В нашем случае универсальный тип Stack (см.
пример егоиспользования под разд. 7.1) будет прекрасно без всяких измененийработать и с обобщенным типом Stack.Зачем же тогда обобщенные типы в Java? Как и в С#, для надежности: использование параметризованных контейнеров позволяетгарантировать, что они хранят объекты требуемого типа и не возбуждают исключения, связанные с неверными преобразованиями.Очевидно, что повышенная безопасность — это основная цель добавления в язык механизма обобщений.Основной принцип современного индустриального программирования заключается в том, что безопасность важнее эффективности.Рассмотрим еще одну конструкцию, связанную с обобщениями, —ограничения.Зададимся вопросом, что компилятору необходимо знать о типепараметре, чтобы скомпилировать объявления обобщенного типа.В языке C# необходимо знать, является ли тип типом значений,и если является, то каков его размер.
Про остальные типы известно,что это ссылки. Данной информации компиляторам Java и C# достаточно, чтобы скомпилировать объявления, в которых значениятипа-параметра только присваиваются и передаются как параметр(строго говоря, можно вызывать операции для класса Object, поскольку каждый тип гарантированно их поддерживает).Однако ни про какие другие операции компилятор ничего незнает. Поэтому вызов «нестандартных» операций изнутри обобщенийприводит к выдаче ошибки, т. е. компилятор не знает, поддерживается ли эта операция, поэтому и выдает ошибку, ведь гарантироватьправильность вызова операции нельзя.Ограничение — это конструкция, сообщающая компиляторуо том, что тип параметра поддерживает некоторые специфичные операции, которые можно применять к значениям этого типа.
В моментконкретизации компилятор проверит, правилен ли тип-аргумент, т. е.поддерживает ли он перечисленные в ограничении операции.167Как сообщить о наличии у типа некоторых операций, отличныхот операций над Ob j ect, и откуда вообще берутся такие операции?Конечно же, новые операции появляются в языках C# и Java приобъявлении производного класса (от Object или от другого типа),когда программист определяет эти операции в объявлении класса.Других способов определения новых операций нет.
Вот и решениепроблемы: сообщить компилятору о том, что тип-параметр долженсовпадать или быть производным от типа, где определяется этаоперация.В C# синтаксис ограничений следующий:where список-ограниченийОграничение появляется после имени обобщенного класса. Ограничение может быть:• требованием наследоваться от определенного класса BaseClass(Т whereТ :BaseClass);• требованием быть типом значений (where Т: struct);• требованием иметь доступный конструктор умолчания (whereТ : new).Например:class Gen <Т,Х> where Trstruct, IComparable, X:new(){ ... }В Java допускается только требование наследования от определенного типа (Т extends BaseClass), поэтому ограничение ставитсявнутри списка параметров:class Gen <Т extends Comparable, X > { ...
}Рассмотрим пример контейнера, про элементы которого предполагается, что они являются производными от некоторого классаGood, содержащего операцию int getPrice (). Это необходимодля реализации операции вычисления суммарной цены всех элементов контейнера. Конечно, можно описать контейнер в следующемвиде:class Container <Т extends Good>{int getOverallPrice (){. . .
T x; ... x .getPrice (); // корректно}}Однако такое решение не универсально, поскольку от любогопользователя контейнера требуется обязательно наследовать откласса Good, что слишком ограничительно (может мне ничего больше и не надо кроме контейнера, а мне подсовывают еще и класс168в нагрузку). Лучшее решение (и самое общее) — ввести интерфейс,декларирующий метод getPrice (), и использовать его:interface IPricedltem{int getPrice ();}class Container <T extends IPricedItem>{int getOverallPrice(){. .
. T x;... x .getPrice (); // корректно}}Теперь любой класс, реализовавший интерфейс IPricedltem,может использовать обобщенный контейнер.Теперь рассмотрим пример контейнера, использующего упорядочение элементов. Чтобы сделать его максимально общим, следуетиспользовать стандартный интерфейс IComp а г able:class MyDict <Т> where Т: IComparable{// теперь можно использовать операцию сравнения// элементов контейнера: CompareTo(Object х)}Еще лучше использовать обобщенный стандартный интерфейс(для каждого подобного универсального класса и интерфейса существует обобщенный вариант):class MyDict <Т> where Т: IComparable<T>{// теперь можно использовать операцию сравнения// элементов контейнера: int CompareTo (Т х)}MyDict<String> stringDict = new MyDict<String> ();// корректно: строки поддерживают сравнениеMyDict<int> intDict = new MyDict<int> ();// тоже корректно: целые поддерживают сравнениеMyDict<Object> intDict = new MyDict< Object >();// ошибка компиляции — объекты в общем случае нельзя// сравниватьЯсно, что главная цель введения ограничений — повышение надежности обобщенных классов и методов..