И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 25
Текст из файла (страница 25)
Переопределение этих операцийпозволяет гибко управлять поведением объектов.Во-вторых, появляется возможность создания универсальныхклассов, в частности контейнеров, которые могут взаимодействовать с любыми объектами. Например, если в определении классаStack из гл. 7 изменить тип хранимых объектов с i n t на Obj ect,то стек будет в состоянии хранить значения любых объектов.
Этовозможно, поскольку в соответствии с правилами совместимостипроизводного и базового классов ссылке на базовый класс можноприсвоить значение ссылки на любой производный класс. Следовательно, ссылке на Object можно присвоить любую другуюссылку.Правда, что делать потом с этими ссылками? Ведь функция Pop ()тоже вернет ссылку на Object. Пусть у нас есть два класса: X и Y.Рассмотрим пример работы с классом Stack, объекты которогохранят ссылки на Object:Stack s = new Stack(32);s.Push(new X()); s.Push(new Y());124Y у = s.Pop(); // ошибка — несоответствие типовX х = s.Pop(); // ошибка — несоответствие типовОшибка возникает, так как нет неявного преобразования от базового класса к производному.
Причем проблема не в том, что такоепреобразование не поддерживается, а в том, что такое неявное преобразование небезопасно.Преобразовать ссылку базового класса Base в ссылку на производный класс Derived можно только, если динамический тип ссылкиТ ковариантен производному классу Derived.Класс Т1 ковариантен классу Т2, если Т1 совпадает с Т2 илиявляется его наследником. Аналогично Т1 контравариантен классуТ2, если Т2 ковариантен Т1. Классы Т1 и Т2 инвариантны, если онине ковариантны и не контравариантны одновременно.В языках C# и Java есть операция явного преобразования ссылокиз класса X в класс Y (X и Y различны):(Y) XЭта операция имеет содержательный смысл (т. е.
выполняется вовремя работы программы), если Y ковариантен X. Если Y контравариантен X, то по определению Y является базовым для X, а любаяссылка на производный класс неявно преобразуема в базовый,поэтому компилятор проигнорирует это преобразование.
Наконец, если X и Y инвариантны, то компилятор выдаст сообщениеоб ошибке.Важнейшей особенностью операции преобразования ссылокв Java и C# является то, что она контролирует корректность преобразования. В случае если преобразование ссылок невозможно, тогенерируется исключение (см. гл. 9). Заметим, что в C++ операцияпреобразования ничего не контролирует. Есть вариант контролируемой операции, но он применим только к полиморфным классам(см. подразд. 8.2).По определению операция преобразования ссылки х типа Xв контравариантный тип Y корректна, если динамический тип хковариантен Y.Пустая ссылка null приводима в любой тип.Итак, чтобы пример со стеком компилировался, следует вставитьоперации преобразования:Y у = (Y)s.Pop ();X х = (X )s .P o p ();Оба языка содержат большой набор классов-контейнеров (коллекций), способных хранить любые объекты.
В Java они находятся в пакете java.util, в C# — в пространстве имен System.Collections.Однако вспомним, что наряду с классами в этих языках есть массивы и типы значений. Если бы они не были совместимы с классом125Object, то полезность последнего была бы ограничена.
Заметим,что в некоторых объектно-ориентированных языках (например,SmallTalk) любое значение принадлежит некоторому классу (т. е. дажезначения простых типов являются экземплярами соответствующихклассов). Такой подход упрощает язык, но приводит к неэффективности операций со значениями простых типов. Поэтому в C# и Javaпринят компромиссный подход: для эффективности выделены типызначений, а для общности (и удобства) введены понятия классовоболочек и операций преобразования между типами значенийи классами-оболочками. Эти операции называются упаковкой и распаковкой (boxing-unboxing).Классы-оболочки. Упаковка и распаковкаДля каждого простого типа данных в стандартной библиотекеимеется класс-оболочка.
Этот класс полезен в двух отношениях.Во-первых, он содержит набор методов, облегчающих работу с данными простых типов. Например, класс оболочка Int32 для типаint в языке C# содержит методы, преобразующие значение в различные виды текстового представления и обратно (Parse, TryParse,ToString).Во-вторых, компилятор умеет преобразовывать значения простыхтипов в экземпляры класса-оболочки (эта операция называется упаковкой) и обратно — из классов-оболочек в значения (эта операцияназывается распаковкой).В языке C# операция упаковки никак не обозначается и выполняется неявно:int i = 25;Object obj = i; // произошла упаковка// Чтобы узнать, в какой объект упаковано значение,// напечатаем имя типа объекта и текстовое// представлениеConsole.WriteLine(obj.GetType().FullName ++ob j .ToString());В этом случае будет напечатаноSystem.Int32 :25Распаковка имеет вид явной операции преобразования к типузначения (продолжение предыдущего примера):int j = (int)obj;Console.WriteLine(j);В этом случае будет напечатано 25.126В языке Java операция упаковки выглядит как создание объектакласса-оболочки, а операция распаковки — как в языке С#, т.
е. видявного приведения к типу оболочки:int i = 25;Object obj = Integer(i); // произошла упаковка// Чтобы узнать, в какой объект упаковано значение,// напечатаем имя типа объекта и текстовое// представлениеSystem.out.println(obj.getClass().getName() ++obj.toString());int j = (Integer)obj;System.out.println (j);В этом случае будет напечатаноjava.lang.Integer:2525Начиная с 2005 г. в Java поддерживаются операции автоупаковкии автораспаковки (в C# они были введены с самой первой версии).Автоупаковка означает, что операция упаковки выполняется автоматически (иначе говоря, то, что в Java назвали автоупаковкой, в C#всегда называлось просто упаковкой):Object obj = 30;// (30) ;// вставлено: obj = new IntegerАвтораспаковка — это автоматическая распаковка из ссылки,объявленной как класс-оболочка, в значение соответствующего простого типа.В языке C# эти операции имеют видInt32 refInt = 32; // (авто)упаковкаint i = reflnt; // автораспаковкаа в языке JavaInteger reflnt = 32; // автоупаковкаint i = reflnt; // автораспаковкаВ языке C# классы-оболочки существуют для всех типов значений,в том числе для перечислений (System.
Enum). Структуры (они ведьтоже типы значений) тоже могут упаковываться и распаковыватьсяв объекты в динамической памяти в контексте, требующем ссылки.Классы-оболочки совместно с операциями автоупаковки и распаковки позволяют трактовать абсолютно все значения в программе какобъекты, но без потери эффективности при выполнении операцийс простыми типами данных.127Например, напишем пример работы с объектом класса Stack наязыке Java:Stack s = new Stack ();int [] x = {1,2,3,4,5,6,7};for (int k : x) s.Push(k);while (!s .IsEmpty ()) System.out.println(s.Pop ());double [] xx = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};for (double k : xx) s.Push(k);while" (! s .IsEmpty () ) System.out.println(s.Pop());Видно, что стек действительно может хранить значения произвольных типов.Правда, упаковка и распаковка требуют определенных накладныхрасходов, однако появление обобщенных типов позволило исправитьэту проблему без ущерба для гибкости языка.Запрещение наследованияВ языках C# и Java есть возможность запрета наследования.Если класс в языке C# помечен модификатором sealed (в Java дляэтой цели служит модификатор final), то этот класс не может бытьбазовым ни для какого класса.
Например, классы-оболочки (см.подразд. 8.2), структуры, встроенный класс String не могут наследоваться. Проектирование полиморфных иерархий — достаточнонетривиальное дело, поэтому некоторые авторы рекомендуют явнозакрывать свои классы для наследования, если они не проектируютсяспециально как иерархии (см. подразд. 8.3).Заметим, что закрытие класса для наследования может в некоторых случаях увеличить эффективность вызова методов (см.
подразд. 8.2).8.2. Динамический полиморфизмПонятие полиморфизма подразумевает наличие нескольких вариантов реализации одной сущности. Виды полиморфизма различаютсяпо виду сущности и по способу связывания сущности с вариантомреализации.В рассматриваемых языках (C++, С#, Java) воплощена разновидность статического полиморфизма, называемая перегрузкойфункций. При перегрузке имеется сущность — операция. Сущностьвыражается именем операции (например, операция «+» или методp r i n t l n ) . Набор вариантов ее реализации — различные вариантыодноименных функций в одной области видимости, различающиеся128профилем параметров.
Связывание вызова функции с вариантом еереализации происходит статически (транслятором) при сопоставлении количества и типов фактических и формальных параметровв вызове и реализации функции.При динамическом полиморфизме сущностью является операцияили действие, выражаемое профилем метода класса, а вариантамиреализации — заместители метода в производных классах.В C++ и C# введено понятие виртуальных функций (методов).Механизм виртуальных методов заключается в том, что при вызовевиртуального метода через указатель или ссылку связывание вызоваметода с конкретной реализацией метода зависит от динамическоготипа указателя или ссылки и выполняется во время работы программы.
Для невиртуальных методов связывание вызова с реализациейзависит от статического типа (поэтому неважно, как вызываетсяневиртуальный метод: через ссылку или объект) и происходит статически.Тип данных (класс), содержащий хотя бы одну виртуальную функцию, называется полиморфным типом (классом), а объект этоготипа — полиморфным объектом.В языке Java нет термина «виртуальный метод», но только потому,что все методы в Java являются виртуальными, поэтому любой классязыка Java полиморфен.В языке C# есть как виртуальные, так и невиртуальные методы,при этом класс Object содержит виртуальные методы (например,ToString () и Finalize () ), поэтому обычные классы C# такжеполиморфны (за исключением статических классов).В C++ и C# виртуальность функции определяется описателемv i r t u a l перед объявлением метода.