Передача значений из одного класса в другой
Недавно начал изучать классы в с++, интересует вопрос: как передать значение, полученное в одном классе в другой.
Пробовал через указатели, но ничего не вышло.
Нужно, чтобы значение переменной а передалось переменную b.
На самом деле из вашего примера кода непонятно, что вы стараетесь достичь.:)
Тем не менее, касаясь вашего кода, то параметры функций являются их локальными переменными, которые прекратят свою жизнь после завершения работы функций. Поэтому в этой функции
указатель a будет содержать адрес объекта, который уже не будет существовать, что приведет к неопределенному поведению программы. Вы должны создать копию локальной переменной в динамической памяти и адрес копии присвоить члену класса a . При этом следует не забыть освободить память, выделенную динамически.
Программа может выглядеть следующим образом.
Или можно переписать программу следующим образом
В этом примере объявляется ссылка на объект класса B , но имеющая тип A . Используя эту ссылку вы можете обращаться только к методам, объявленным в классе A .
Что касается вашего вопроса
как передать значение, полученное в одном классе в другой.
то это делается с помощью методов класса, которые возвращают полученное значение.:)
Наследование в C++: beginner, intermediate, advanced
В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.
Beginner
Что такое наследование?
Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.
Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.
Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.
В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.
Важное примечание: приватные переменные и методы не могут быть унаследованы.
Типы наследования
В C ++ есть несколько типов наследования:
- публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
- защищенный ( protected ) — все унаследованные данные становятся защищенными;
- приватный ( private ) — все унаследованные данные становятся приватными.
Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .
Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .
Конструкторы и деструкторы
В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.
Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.
Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .
Множественное наследование
Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.
Проблематика множественного наследования
Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.
Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.
Intermediate
Проблема ромба
Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .
К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.
Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:
- вызвать метод конкретного суперкласса;
- обратиться к объекту подкласса как к объекту определенного суперкласса;
- переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .
Проблема ромба: Конструкторы и деструкторы
Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.
Виртуальное наследование
Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.
Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).
Абстрактный класс
В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.
Интерфейс
С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).
Advanced
Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.
Наследование от реализованного или частично реализованного класса
Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.
В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.
Интерфейс
Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.
Интерфейс: Пример использования
Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.
Приложение выполняющее абстрактную бизнес логику должно настраиваться из отдельного конфигурационного файла. На раннем этапе разработки, форматирование данного конфигурационного файла до конца сформировано не было. Вынесение парсинга файла за интерфейс предоставляет несколько преимуществ.
Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.
Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.
Заключение
Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.
C#. Вложенные классы. Вложенные статические классы. Объявление и использование вложенных классов. Примеры
Вложенные классы. Вложенные статические классы. Объявление и использование вложенных классов. Примеры
Содержание
- 1. Понятие вложенного класса. Общая форма объявления вложенного класса
- 2. Как выглядит общая форма объявления объекта вложенного класса?
- 3. Какие модификаторы доступа могут применяться к вложенным классам?
- 4. Пример, который демонстрирует использование классов с различными модификаторами доступа
- 5. Объявление и использование статического вложенного класса в нестатическом. Пример
- 6. Можно ли в статическом классе объявить нестатический вложенный класс?
- 7. Пример объявления и использования статического вложенного класса в другом статическом классе
Поиск на других ресурсах:
1. Понятие вложенного класса. Общая форма объявления вложенного класса
В языке C# любой класс в своей реализации может содержать объявление другого класса. Класс, который объявляется в пределах фигурных скобок другого класса, называется вложенным классом.
В простейшем случае, общая форма объявления вложенного класса в классе имеет вид:
- Outer – имя класса, который содержит в своем теле объявление другого класса с именем Inner ;
- Inner – имя класса, который объявляется в классе Outer .
Классы Outer и Inner могут содержать различные спецификаторы, которые определяют доступ ( private , protected , public ) или другие свойства класса ( static , sealed и т.д.).
⇑
2. Как выглядит общая форма объявления объекта вложенного класса?
Объект вложенного класса можно объявить в случае, когда вложенный класс объявлен как видимый. Это значит, что видимый вложенный класс должен быть объявлен с любым модификатором доступа кроме private.
Если вложенный класс объявлен как не private-класс, то создание экземпляра этого класса имеет следующий вид:
- Outer – имя внешнего класса, в котором объявляется вложенный класс Inner ;
- Inner – имя вложенного класса, который объявлен в пределах фигурных скобок класса Outer ;
- objInner – имя объекта (экземпляра) класса, который создается;
- Outer.Inner() – имя конструктора по умолчанию, который вызывается для создания объекта класса. Если в классе Inner реализованы другие параметризированные конструкторы, то этот вызов может быть другим.
⇑
3. Какие модификаторы доступа могут применяться к вложенным классам?
К вложенным классам могут применяться точно такие же модификаторы доступа как и к обычным – невложенным классам:
- private . В этом случае вложенный класс есть видимым в пределах фигурных скобок < >внешнего класса;
- public . В этом случае вложенный класс доступен в пределах внешнего класса, из экземпляра внешнего класса, из унаследованного класса а также за пределами сборки;
- protected . В этом случае класс есть доступен в пределах внешнего класса а также из методов унаследованных классов;
- internal . Вложенный класс есть доступен в пределах сборки и недоступен за пределами сборки;
- protected internal . Вложенный класс может быть доступен из методов внешнего класса, методов унаследованных классов или недоступен из методов за пределами текущей сборки.
Действие модификаторов точно такое же как и в случае с членами данных (методами) класса.
⇑
4. Пример, который демонстрирует использование классов с различными модификаторами доступа
В примере демонстрируется объявление и доступ к вложенным классам, которые содержат различные модификаторы доступа.
Объявляется класс Outer , который содержит:
- две внутренние public -переменные с именами d и sd . Переменная sd объявлена как статическая;
- внутренний метод GetInner1() , демонстрирующий доступ к private-классу Inner1 ;
- вложенный класс Inner1 , который объявлен как private ;
- вложенный класс Inner2 , который объявлен как public ;
- вложенный класс Inner3 , который объявлен как internal ;
- вложенный класс Inner4 , который объявлен как protected ;
- вложенный класс Inner5 , который объявлен как protected internal .
В любом из вложенных классов объявляются:
- одна нестатическая переменная целого типа;
- одна статическая переменная целого типа.
Листинг класса Outer следующий:
Использование вложенных классов в некотором методе может быть следующим:
Как видно из вышеприведенного кода, создавать объекты можно только для классов Inner2 , Inner3 , Inner5 . Чтобы создать объект protected-класса Inner4 , нужно объявить другой класс, который наследует класс Outer , например:
Данный пример также показывает, что вложенный private -класс скрывает доступ к объявленной в нем статической переменной из методов других классов. Так же, скрывает доступ и вложенный protected -класс.
⇑
5. Объявление и использование статического вложенного класса в нестатическом. Пример
В нестатическом классе может быть объявлен статический класс (с ключевым словом static ). Это означает, что статический класс данного класса есть уникальным общим ресурсом. Доступ к элементам статического класса осуществляется непосредственно по имени статического класса, перед которым через точку следует имя (имена) внешнего класса (классов).
Пример. Объявляется класс Outer , содержащий объявление вложенного статического ( static ) класса Inner . В классе Inner объявляется статическая внутренняя public -переменная. Текст класса Outer следующий:
Использование статического вложенного класса может быть приблизительно таким:
⇑
6. Можно ли в статическом классе объявить нестатический вложенный класс?
Да, можно. В статическом классе можно объявлять нестатические вложенные классы. Однако, в статическом классе нельзя объявлять нестатические переменные и методы.
Более подробно об использовании статических переменных, методов, классов описывается в теме:
Например. Пусть объявляется статический класс Outer , содержащий реализацию нестатического класса Inner
Использование классов Outer и Outer.Inner может быть, например, таким:
⇑
7. Пример объявления и использования статического вложенного класса в другом статическом классе
В статическом классе может быть объявлен другой вложенный статический класс. Здесь следует помнить, что в статических классах объявленные методы и переменные должны обязательно быть статическими (за исключением классов). А вот классы могут быть и нестатическими.
Пусть заданы объявления статических классов Outer и Inner :
Тогда, использование статических элементов этих классов может быть следующим:
Name already in use
csharp_for_beginners / c11.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
ГЛАВА 11. Наследование
Наследование является одним из трех основополага ющих принципов объектно-ориентированного про граммирования, поскольку оно допускает создание иерархических классификаций. Благодаря наследованию можно создать общий класс, в котором определяются ха рактерные особенности, присущие множеству связанных элементов. От этого класса могут затем наследовать другие, более конкретные классы, добавляя в него свои индивиду альные особенности.
В языке C# класс, который наследуется, называется базовым, а класс, который наследует, — производным. Следовательно, производный класс представляет собой спе циализированный вариант базового класса. Он наследует все переменные, методы, свойства и индексаторы, опреде ляемые в базовом классе, добавляя к ним свои собственные элементы.
Поддержка наследования в C# состоит в том, что в объяв ление одного класса разрешается вводить другой класс. Для этого при объявлении производного класса указывается базовый класс. Рассмотрим для начала простой пример. Ниже приведен класс TwoDShape, содержащий ширину и высоту двухмерного объекта, например квадрата, пря моугольника, треугольника и т.д.
Класс TwoDShape может стать базовым, т.е. отправной точкой для создания классов, описывающих конкретные типы двумерных объектов. Например, в приведенной ниже программе класс TwoDShape служит для порождения производного класса Triangle. Обратите особое внимание на объявление класса Triangle.
При выполнении этой программы получается следующий результат.
В классе Triangle создается особый тип объекта класса TwoDShape (в данном слу чае — треугольник). Кроме того, в класс Triangle входят все члены класса TwoDShape, к которым, в частности, добавляются методы Area() и ShowStyle(). Так, описание типа треугольника сохраняется в переменной Style, метод Area() рассчитывает и воз вращает площадь треугольника, а метод ShowStyle() отображает тип треугольника. Обратите внимание на синтаксис, используемый в классе Triangle для наследова ния класса TwoDShape.
Этот синтаксис может быть обобщен. Всякий раз, когда один класс наследует от другого, после имени базового класса указывается имя производного класса, отделяе мое двоеточием. В C# синтаксис наследования класса удивительно прост и удобен в ис пользовании.
В класс Triangle входят все члены его базового класса TwoDShape, и поэтому в нем переменные Width и Height доступны для метода Area(). Кроме того, объекты t1 и t2 в методе Main() могут обращаться непосредственно к переменным Width и Height, как будто они являются членами класса Triangle. На рис. 11.1 схематически показано, каким образом класс TwoDShape вводится в класс Triangle.
Рис. 11.1. Схематическое представление класса Triangle
Несмотря на то что класс TwoDShape является базовым для класса Triangle, в то же время он представляет собой совершенно независимый и самодостаточный класс. Если класс служит базовым для производного класса, то это совсем не означает, что он не может быть использован самостоятельно. Например, следующий фрагмент кода считается вполне допустимым.
Разумеется, объект класса TwoDShape никак не связан с любым из классов, произво дных от класса TwoDShape, и вообще не имеет к ним доступа.
Ниже приведена общая форма объявления класса, наследующего от базового класса.
Для любого производного класса можно указать только один базовый класс. В C# не предусмотрено наследование нескольких базовых классов в одном производном клас се. (В этом отношении C# отличается от C++, где допускается наследование нескольких базовых классов. Данное обстоятельство следует принимать во внимание при переносе кода C++ в С#.) Тем не менее можно создать иерархию наследования, в которой про изводный класс становится базовым для другого производного класса. (Разумеется, ни один из классов не может быть базовым для самого себя как непосредственно, так и косвенно.) Но в любом случае производный класс наследует все члены своего базового класса, в том числе переменные экземпляра, методы, свойства и индексаторы.
Главное преимущество наследования заключается в следующем: как только будет создан базовый класс, в котором определены общие для множества объектов атрибу ты, он может быть использован для создания любого числа более конкретных произ водных классов. А в каждом производном классе может быть точно выстроена своя собственная классификация. В качестве примера ниже приведен еще один класс, про изводный от класса TwoDShape и инкапсулирующий прямоугольники.
В класс Rectangle входят все члены класса TwoDShape, к которым добавлен метод IsSquare(), определяющий, является ли прямоугольник квадратом, а также метод Area(), вычисляющий площадь прямоугольника.
Доступ к членам класса и наследование
Как пояснялось в главе 8, члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. Но наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в производный класс и входят все члены его базового класса, в нем все равно оказываются недоступными те члены базового класса, которые являются закры тыми. Так, если сделать закрытыми переменные класса TwoDShape, они станут недо ступными в классе Triangle, как показано ниже.
// Использовать открытые свойства для установки и // получения значений закрытых членов класса. using System;
// Класс для треугольников, производный от // класса TwoDShape. class Triangle : TwoDShape < public string Style; // тип треугольника
// Продемонстрировать применение модификатора доступа protected. using System;
// Добавить конструктор в класс Triangle. using System;
// Класс для двумерных объектов. class TwoDShape < double pri_width; double pr.i_height;
// Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape < string Style;
> В данном примере конструктор класса Triangle инициализирует наследуемые члены класса TwoDShape вместе с его собственным полем Style.
Когда конструкторы определяются как в базовом, так и в производном классе, про цесс построения объекта несколько усложняется, поскольку должны выполняться конструкторы обоих классов. В данном случае приходится обращаться к еще одному ключевому слову языка С#: base, которое находит двоякое применение: во-первых, для вызова конструктора базового класса; и во-вторых, для доступа к члену базового класса, скрывающегося за членом производного класса. Ниже будет рассмотрено первое при менение ключевого слова base.
Вызов конструкторов базового класса
С помощью формы расширенного объявления конструктора производного класса и ключевого слова base в производном классе может быть вызван конструктор, опре деленный в его базовом классе. Ниже приведена общая форма этого расширенного объявления:
где список_аргументов обозначает любые аргументы, необходимые конструктору в базовом классе. Обратите внимание на местоположение двоеточия.
Для того чтобы продемонстрировать применение ключевого слова base на кон кретном примере, рассмотрим еще один вариант класса TwoDShape в приведенной ниже программе. В данном примере определяется конструктор, инициализирующий свойства Width и Height. Затем этот конструктор вызывается конструктором класса Triangle.
Теперь конструктор класса Triangle объявляется следующим образом.
В данном варианте конструктор Triangle() вызывает метод base с параметрами w и h. Это, в свою очередь, приводит к вызову конструктора TwoDShape(), инициа лизирующего свойства Width и Height значениями параметров w и h. Они больше не инициализируются средствами самого класса Triangle, где теперь остается ини циализировать только его собственный член Style, определяющий тип треугольника. Благодаря этому класс TwoDShape высвобождается для конструирования своего по добъекта любым избранным способом. Более того, в класс TwoDShape можно ввести функции, о которых даже не будут подозревать производные классы, что предотвра щает нарушение существующего кода.
С помощью ключевого слова base можно вызвать конструктор любой формы, определяемой в базовом классе, причем выполняться будет лишь тот конструктор, па раметры которого соответствуют переданным аргументам. В качестве примера ниже приведены расширенные варианты классов TwoDShape и Triangle, в которые вклю чены как используемые по умолчанию конструкторы, так и конструкторы, принимаю щие один аргумент.
Вот к какому результату приводит выполнение этого кода.
А теперь рассмотрим вкратце основные принципы действия ключевого слова base. Когда в производном классе указывается ключевое слово base, вызывается конструктор из его непосредственного базового класса. Следовательно, ключевое слово base всегда обращается к базовому классу, стоящему в иерархии непосредственно над вызываю щим классом. Это справедливо даже для многоуровневой иерархии классов. Аргумен ты передаются базовому конструктору в качестве аргументов метода base(). Если же ключевое слово отсутствует, то автоматически вызывается конструктор, используемый в базовом классе по умолчанию.
Наследование и сокрытие имен
В производном классе можно определить член с таким же именем, как и у чле на его базового класса. В этом случае член базового класса скрывается в производном классе. И хотя формально в C# это не считается ошибкой, компилятор все же выдаст сообщение, предупреждающее о том, что имя скрывается. Если член базового клас са требуется скрыть намеренно, то перед его именем следует указать ключевое слово new, чтобы избежать появления подобного предупреждающего сообщения. Следует, однако, иметь в виду, что это совершенно отдельное применение ключевого слова new, не похожее на его применение при создании экземпляра объекта.
Ниже приведен пример сокрытия имени.
Прежде всего обратите внимание на использование ключевого слова new в следую щей строке кода.
В этой строке компилятору, по существу, сообщается о том, что вновь создавае мая переменная i намеренно скрывает переменную i из базового класса А и что авто ру программы об этом известно. Если же опустить ключевое слово new в этой строке кода, то компилятор выдаст предупреждающее сообщение.
Вот к какому результату приводит выполнение приведенного выше кода.
В классе В определяется собственная переменная экземпляра i, которая скрывает переменную i из базового класса А. Поэтому при вызове метода Show() для объек та типа В выводится значение переменной i, определенной в классе В, а не той, что определена в классе А.
Применение ключевого слова base для доступа к скрытому имени
Имеется еще одна форма ключевого слова base, которая действует подобно клю чевому слову this, за исключением того, что она всегда ссылается на базовый класс в том производном классе, в котором она используется. Ниже эта форма приведена в общем виде:
где член может обозначать метод или переменную экземпляра. Эта форма ключевого слова base чаще всего применяется в тех случаях, когда под именами членов произво дного класса скрываются члены базового класса с теми же самыми именами. В качестве примера ниже приведен другой вариант иерархии классов из предыдущего примера.
Выполнение этого кода приводит к следующему результату.
Несмотря на то что переменная экземпляра i в производном классе В скрывает переменную i из базового класса А, ключевое слово base разрешает доступ к перемен ной i, определенной в базовом классе.
С помощью ключевого слова base могут также вызываться скрытые методы. Например, в приведенном ниже коде класс В наследует класс А и в обоих классах объявляется метод Show(). А затем в методе Show() класса В с помощью ключевого слова base вызывается вариант метода Show(), определенный в классе А.
Выполнение этого кода приводит к следующему результату.
Как видите, в выражении base.Show() вызывается вариант метода Show() из ба зового класса.
Обратите также внимание на следующее: ключевое слово new используется в при веденном выше коде с целью сообщить компилятору о том, что метод Show(), вновь объявляемый в производном классе В, намеренно скрывает другой метод Show(), определенный в базовом классе А.
Создание многоуровневой иерархии классов
В представленных до сих пор примерах программ использовались простые иерар хии классов, состоявшие только из базового и производного классов. Но в C# мож но также строить иерархии, состоящие из любого числа уровней наследования. Как упоминалось выше, многоуровневая иерархия идеально подходит для использования одного производного класса в качестве базового для другого производного класса. Так, если имеются при класса, А, В и С, то класс С может наследовать от класса В, а тот, в свою очередь, от класса А. В таком случае каждый производный класс наследует ха рактерные особенности всех своих базовых классов. В частности, класс С наследует все члены классов В и А.
Для того чтобы показать, насколько полезной может оказаться многоуровневая иерархия классов, рассмотрим следующий пример программы. В ней производный класс Triangle служит в качестве базового для создания другого производного клас са — ColorTriangle. При этом класс ColorTriangle наследует все характерные осо бенности, а по существу, члены классов Triangle и TwoDShape, к которым добавляет ся поле color, содержащее цвет треугольника.
При выполнении этой программы получается следующей результат.
Благодаря наследованию в классе ColorTriangle могут использоваться опреде ленные ранее классы Triangle и TwoDShape, к элементам которых добавляется лишь та информация, которая требуется для конкретного применения данного класса. В этом отчасти и состоит ценность наследования, поскольку оно допускает повторное использование кода.
Приведенный выше пример демонстрирует еще одно важное положение: ключе вое слово base всегда обозначает ссылку на конструктор ближайшего по иерархии базового класса. Так, ключевое слово base в классе ColorTriangle обозначает вызов конструктора из класса Triangle, а ключевое слово base в классе Triangle — вызов конструктора из класса TwoDShape. Если же в иерархии классов конструктору базо вого класса требуются параметры, то все производные классы должны предоставлять эти параметры вверх по иерархии, независимо от того, требуются они самому произ водному классу или нет.
Порядок вызова конструкторов
В связи с изложенными выше в отношении наследования и иерархии классов мо жет возникнуть следующий резонный вопрос: когда создается объект производного класса и какой конструктор выполняется первым — тот, что определен в производном классе, или же тот, что определен в базовом классе? Так, если имеется базовый класс А и производный класс В, то вызывается ли конструктор класса А раньше конструктора класса В? Ответ на этот вопрос состоит в том, что в иерархии классов конструкторы вы зываются по порядку выведения классов: от базового к производному. Более того, этот порядок остается неизменным независимо от использования ключевого слова base. Так, если ключевое слово base не используется, то выполняется конструктор по умол чанию, т.е. конструктор без параметров. В приведенном ниже примере программы демонстрируется порядок вызова и выполнения конструкторов.
Вот к какому результату приводит выполнение этой программы.
Как видите, конструкторы вызываются по порядку выведения их классов.
Если хорошенько подумать, то в вызове конструкторов по порядку выведения их классов можно обнаружить определенный смысл. Ведь базовому классу ничего не из вестно ни об одном из производных от него классов, и поэтому любая инициализация, которая требуется его членам, осуществляется совершенно отдельно от инициализа ции членов производного класса, а возможно, это и необходимое условие. Следова тельно, она должна выполняться первой.
Ссылки на базовый класс и объекты производных классов
Как вам должно быть уже известно, C# является строго типизированным языком программирования. Помимо стандартных преобразований и автоматического про движения простых типов значений, в этом языке строго соблюдается принцип совме стимости типов. Это означает, что переменная ссылки на объект класса одного типа, как правило, не может ссылаться на объект класса другого типа. В качестве примера рассмотрим следующую программу, в которой объявляются два класса одинаковой структуры.
Несмотря на то что классы X и Y в данном примере совершенно одинаковы по своей структуре, ссылку на объект типа Y нельзя присвоить переменной ссылки на объект типа X, поскольку типы у них разные. Поэтому следующая строка кода оказывается неверной и может привести к ошибке из-за несовместимости типов во время компи ляции.
Вообще говоря, переменная ссылки на объект может ссылаться только на объект своего типа.
Но из этого принципа строгого соблюдения типов в C# имеется одно важное ис ключение: переменной ссылки на объект базового класса может быть присвоена ссыл ка на объект любого производного от него класса. Такое присваивание считается впол не допустимым, поскольку экземпляр объекта производного типа инкапсулирует эк земпляр объекта базового типа. Следовательно, по ссылке на объект базового класса можно обращаться к объекту производного класса. Ниже приведен соответствующий пример.
В данном примере класс Y является производным от класса X. Поэтому следующая операция присваивания:
считается вполне допустимой. Ведь по ссылке на объект базового класса (в данном слу чае — это переменная х2 ссылки на объект класса X) можно обращаться к объекту про изводного класса, т.е. к объекту, на который ссылается переменная у.
Следует особо подчеркнуть, что доступ к конкретным членам класса определяется типом переменной ссылки на объект, а не типом объекта, на который она ссылается. Это означает, что если ссылка на объект производного класса присваивается перемен ной ссылки на объект базового класса, то доступ разрешается только к тем частям это го объекта, которые определяются базовым классом. Именно поэтому переменной х2 недоступен член b класса Y, когда она ссылается на объект этого класса. И в этом есть своя логика, поскольку базовому классу ничего не известно о тех членах, которые до бавлены в производный от него класс. Именно поэтому последняя строка кода в при веденном выше примере была закомментирована.
Несмотря на кажущийся несколько отвлеченным характер приведенных выше рас- суждений, им можно найти ряд важных применений на практике. Одно из них рас сматривается ниже, а другое — далее в этой главе, когда речь пойдет о виртуальных методах.
Один из самых важных моментов для присваивания ссылок на объекты произво дного класса переменным базового класса наступает тогда, когда конструкторы вы зываются в иерархии классов. Как вам должно быть уже известно, в классе нередко определяется конструктор, принимающий объект своего класса в качестве параметра. Благодаря этому в классе может быть сконструирована копия его объекта. Этой осо бенностью можно выгодно воспользоваться в классах, производных от такого класса. В качестве примера рассмотрим очередные варианты классов TwoDShape и Triangle. В оба класса добавлены конструкторы, принимающие объект в качестве параметра.
В представленном выше примере объект t2 конструируется из объекта t1 и поэто му подобен ему. Ниже приведен результат выполнения кода из данного примера.
Обратите особое внимание на следующий конструктор класса Triangle:
Он принимает объект типа Triangle в качестве своего параметра и передает его (с помощью ключевого слова base) следующему конструктору класса TwoDShape.
Самое любопытное, что конструктор TwoDShape() предполагает получить объект класса TwoDShape, тогда как конструктор Triangle() передает ему объект класса Triangle. Как пояснялось выше, такое вполне допустимо, поскольку по ссылке на объект базового класса можно обращаться к объекту производного класса. Следова тельно, конструктору TwoDShape() можно на совершенно законных основаниях пере дать ссылку на объект класса, производного от класса TwoDShape. А поскольку кон структор TwoDShape() инициализирует только те части объекта производного класса, которые являются членами класса TwoDShape, то для него не имеет никакого значения, содержит ли этот объект другие члены, добавленные в производном классе.
Виртуальные методы и их переопределение
Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы ин тересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя из типа объекта, к которому происходит обраще ние по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект. Так, если базовый класс содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.
Метод объявляется как виртуальный в базовом классе с помощью ключевого сло ва virtual, указываемого перед его именем. Когда же виртуальный метод переопре деляется в производном классе, то для этого используется модификатор override. А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract (подробнее данный вопрос рассма тривается далее в этой главе).
Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов, которая пред ставляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благо даря ей в C# реализуется динамический полиморфизм.
Ниже приведен пример, демонстрирующий виртуальные методы и их переопре деление.
Вот к какому результату приводит выполнение этого кода.
В коде из приведенного выше примера создаются базовый класс Base и два произ водных от него класса — Derived1 и Derived2. В классе Base объявляется виртуаль ный метод Who(), который переопределяется в обоих производных классах. Затем в методе Main() объявляются объекты типа Base, Derived1 и Derived2. Кроме того, объявляется переменная baseRef ссылочного типа Base. Далее ссылка на каждый тип объекта присваивается переменной baseRef и затем используется для вызова мето да Who(). Как следует из результата выполнения приведенного выше кода, вариант выполняемого метода Who() определяется по типу объекта, к которому происходит обращение по ссылке во время вызова этого метода, а не по типу класса переменной baseRef.
Но переопределять виртуальный метод совсем не обязательно. Ведь если в произ водном классе не предоставляется собственный вариант виртуального метода, то ис пользуется его вариант из базового класса, как в приведенном ниже примере.
Выполнение этого кода приводит к следующему результату.
В данном примере метод Who() не переопределяется в классе Derived2. Поэтому для объекта класса Derived2 вызывается метод Who() из класса Base.
Если при наличии многоуровневой иерархии виртуальный метод не переопределя ется в производном классе, то выполняется ближайший его вариант, обнаруживаемый вверх по иерархии, как в приведенном ниже примере.
Вот к какому результату приводит выполнение этого кода.
В данном примере класс Derived3 наследует класс Derived2, который наследует класс Derived1, а тот, в свою очередь, — класс Base. Как показывает приведенный выше результат, выполняется метод Who(), переопределяемый в классе Derived1, поскольку это первый вариант виртуального метода, обнаруживаемый при продви жении вверх по иерархии от классов Derived3 и Derived2, где метод Who() не пере определяется, к классу Derived1.
И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к ин дексаторам.
Что дает переопределение методов
Благодаря переопределению методов в C# поддерживается динамический поли морфизм. В объектно-ориентированном программировании полиморфизм играет очень важную роль, потому что он позволяет определить в общем классе методы, которые становятся общими для всех производных от него классов, а в производных классах — определить конкретную реализацию некоторых или же всех этих методов. Переопределение методов — это еще один способ воплотить в C# главный принцип полиморфизма: один интерфейс — множество методов.
Удачное применение полиморфизма отчасти зависит от правильного понимания той особенности, что базовые и производные классы образуют иерархию, которая про двигается от меньшей к большей специализации. При надлежащем применении ба зовый класс предоставляет все необходимые элементы, которые могут использоваться в производном классе непосредственно. А с помощью виртуальных методов в базовом классе определяются те методы, которые могут быть самостоятельно реализованы в производном классе. Таким образом, сочетая наследование с виртуальными методами, можно определить в базовом классе общую форму методов, которые будут использо ваться во всех его производных классах.
Применение виртуальных методов
Для того чтобы стали понятнее преимущества виртуальных методов, применим их в классе TwoDShape. В предыдущих примерах в каждом классе, производном от класса TwoDShape, определялся метод Area(). Но, по-видимому, метод Area() лучше было бы сделать виртуальным в классе TwoDShape и тем самым предоставить возможность переопределить его в каждом производном классе с учетом особенностей расчета пло щади той двумерной формы, которую инкапсулирует этот класс. Именно это и сдела но в приведенном ниже примере программы. Ради удобства демонстрации классов в этой программе введено также свойство name в классе TwoDShape.
При выполнении этой программы получается следующий результат.
Рассмотрим данный пример программы более подробно. Прежде всего, метод Area() объявляется как virtual в классе TwoDShape и переопределяется в классах Triangle и Rectangle по объяснявшимся ранее причинам. В классе TwoDShape ме тод Area() реализован в виде заполнителя, который сообщает о том, что пользователь данного метода должен переопределить его в производном классе. Каждое переопре деление метода Area() предоставляет конкретную его реализацию, соответствующую типу объекта, инкапсулируемого в производном классе. Так, если реализовать класс для эллипсов, то метод Area() должен вычислять площадь эллипса.
У программы из рассматриваемого здесь примера имеется еще одна примечатель ная особенность. Обратите внимание на то, что в методе Main() двумерные формы объявляются в виде массива объектов типа TwoDShape, но элементам этого массива присваиваются ссылки на объекты классов Triangle, Rectangle и TwoDShape. И это вполне допустимо, поскольку по ссылке на базовый класс можно обращаться к объ екту производного класса. Далее в программе происходит циклическое обращения к элементам данного массива для вывода сведений о каждом объекте. Несмотря на всю свою простоту, данный пример наглядно демонстрирует преимущества наследования и переопределения методов. Тип объекта, хранящийся в переменной ссылки на базо вый класс, определяется во время выполнения и соответственно обусловливает даль нейшие действия. Так, если объект является производным от класса TwoDShape, то для получения его площади вызывается метод Area(). Но интерфейс для выполнения этой операции остается тем же самым независимо от типа используемой двумерной формы.
Применение абстрактных классов
Иногда требуется создать базовый класс, в котором определяется лишь самая об щая форма для всех его производных классов, а наполнение ее деталями предостав ляется каждому из этих классов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в производных классах, а не в самом ба зовом классе. Подобная ситуация возникает, например, в связи с невозможностью по лучить содержательную реализацию метода в базовом классе. Именно такая ситуация была продемонстрирована в варианте класса TwoDShape из предыдущего примера, где метод Area() был просто определен как заполнитель. Такой метод не вычисляет и не выводит площадь двумерного объекта любого типа.
Создавая собственные библиотеки классов, вы можете сами убедиться в том, что у метода зачастую отсутствует содержательное определение в контексте его базового класса. Подобная ситуация разрешается двумя способами. Один из них, как показано в предыдущем примере, состоит в том, чтобы просто выдать предупреждающее со общение. Такой способ может пригодиться в определенных ситуациях, например при отладке, но в практике программирования он обычно не применяется. Ведь в базовом классе могут быть объявлены методы, которые должны быть переопределены в произ водном классе, чтобы этот класс стал содержательным. Рассмотрим для примера класс Triangle. Он был бы неполным, если бы в нем не был переопределен метод Area(). В подобных случаях требуется какой-то способ, гарантирующий, что в производном классе действительно будут переопределены все необходимые методы. И такой способ в С# имеется. Он состоит в использовании абстрактного метода.
Абстрактный метод создается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в ба зовом классе. Это означает, что он должен быть переопределен в производном классе, поскольку его вариант из базового класса просто непригоден для использования. Не трудно догадаться, что абстрактный метод автоматически становится виртуальным и не требует указания модификатора virtual. В действительности совместное исполь зование модификаторов virtual и abstract считается ошибкой.
Для определения абстрактного метода служит приведенная ниже общая форма.
Как видите, у абстрактного метода отсутствует тело. Модификатор abstract мо жет применяться только в методах экземпляра, но не в статических методах (static). Абстрактными могут быть также индексаторы и свойства.
Класс, содержащий один или больше абстрактных методов, должен быть также объявлен как абстрактный, и для этого перед его объявлением class указывается мо дификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект аб страктного класса с помощью оператора new приведет к ошибке во время компиляции.
Когда производный класс наследует абстрактный класс, в нем должны быть реа лизованы все абстрактные методы базового класса. В противном случае производный класс должен быть также определен как abstract. Таким образом, атрибут abstract наследуется до тех пор, пока не будет достигнута полная реализация класса.
Используя абстрактный класс, мы можем усовершенствовать рассматривавшийся ранее класс TwoDShape. Для неопределенной двухмерной фигуры понятие площади не имеет никакого смысла, поэтому в приведенном ниже варианте класса TwoDShape метод Area() и сам класс TwoDShape объявляются как abstract. Это, конечно, озна чает, что во всех классах, производных от класса TwoDShape, должен быть переопреде лен метод Area().
Как показывает представленный выше пример программы, во всех производных классах метод Area() должен быть непременно переопределен, а также объявлен аб страктным. Убедитесь в этом сами, попробовав создать производный класс, в котором не переопределен метод Area(). В итоге вы получите сообщение об ошибке во время компиляции. Конечно, возможность создавать ссылки на объекты типа TwoDShape по- прежнему существует, и это было сделано в приведенном выше примере программы, но объявлять объекты типа TwoDShape уже нельзя. Именно поэтому массив shapes сокращен в методе Main() до 4 элементов, а объект типа TwoDShape для общей двух мерной формы больше не создается.
Обратите также внимание на то, что в класс TwoDShape по-прежнему входит метод ShowDim() и что он не объявляется с модификатором abstract. В абстрактные клас сы вполне допускается (и часто практикуется) включать конкретные методы, которые могут быть использованы в своем исходном виде в производном классе. А переопре делению в производных классах подлежат только те методы, которые объявлены как abstract.
Предотвращение наследования с помощью ключевого слова sealed
Несмотря на всю эффективность и полезность наследования, иногда возникает по требность предотвратить его. Допустим, что имеется класс, инкапсулирующий по следовательность инициализации некоторого специального оборудования, например медицинского монитора. В этом случае требуется, чтобы пользователи данного класса не могли изменять порядок инициализации монитора, чтобы исключить его непра вильную настройку. Но независимо от конкретных причин в C# имеется возможность предотвратить наследование класса с помощью ключевого слова sealed.
Для того чтобы предотвратить наследование класса, достаточно указать ключевое слово sealed перед определением класса. Как и следовало ожидать, класс не допу скается объявлять одновременно как abstract и sealed, поскольку сам абстрактный класс реализован не полностью и опирается в этом отношении на свои производные классы, обеспечивающие полную реализацию.
Ниже приведен пример объявления класса типа sealed.
Как следует из комментариев в приведенном выше фрагменте кода, класс В не мо жет наследовать класс А, потому что последний объявлен как sealed.
И еще одно замечание: ключевое слово sealed может быть также использовано в виртуальных методах для предотвращения их дальнейшего переопределения. До пустим, что имеется базовый класс В и производный класс D. Метод, объявленный в классе В как virtual, может быть объявлен в классе D как sealed. Благодаря этому в любом классе, наследующем от класса предотвращается переопределение данного метода. Подобная ситуация демонстрируется в приведенном ниже фрагменте кода.
Метод MyMethod() герметизирован в классе D, и поэтому не может быть переопре делен в классе X.
В C# предусмотрен специальный класс object, который неявно считается базовым классом для всех остальных классов и типов, включая и типы значений. Иными слова ми, все остальные типы являются производными от object. Это, в частности, означает, что переменная ссылочного типа object может ссылаться на объект любого другого типа. Кроме того, переменная типа object может ссылаться на любой массив, по скольку в C# массивы реализуются как объекты. Формально имя object считается в C# еще одним обозначением класса System.Object, входящего в библиотеку классов для среды .NET Framework.
В классе object определяются методы, приведенные в табл. 11.1. Это означает, что они доступны для каждого объекта.
Некоторые из этих методов требуют дополнительных пояснений. По умолчанию метод Equals(object) определяет, ссылается ли вызывающий объект на тот же са мый объект, что и объект, указываемый в качества аргумента этого метода, т.е. он опре деляет, являются ли обе ссылки одинаковыми. Метод Equals(object) возвращает логическое значение true, если сравниваемые объекты одинаковы, в противном слу чае — логическое значение false. Он может быть также переопределен в создаваемых классах. Это позволяет выяснить, что же означает равенство объектов для создаваемого класса. Например, метод Equals(object) можно определить таким образом, чтобы в нем сравнивалось содержимое двух объектов.
Таблица 11.1. Методы класса object
| Метод | Назначение |
|---|---|
| public virtual bool Equals(object ob) | Определяет, является ли вызывающий объект таким же, как и объект, доступный по ссылке ob |
| public static bool Equals(object objA, object objB) | Определяет, является ли объект, доступный по ссылке objA, таким же, как и объект, доступный по ссылке objB |
| protected Finalize() | Выполняет завершающие действия перед «сборкой мусора”. В C# метод Finalize() доступен посредством деструктора |
| public virtual int GetHashCode() | Возвращает хеш-код, связанный с вызывающим объектом |
| public Type GetType() | Получает тип объекта во время выполнения программы |
| protected object MemberwiseClone() | Выполняет неполное копирование объекта, т.е. копируются только члены, но не объекты, на которые ссылаются эти члены |
| public static boolReferenceEquals(obj objA, object objB) | Определяет, делаются ли ссылки objA и objB на один и тот же объект |
| public virtual string ToString() | Возвращает строку, которая описывает объект |
Метод GetHashCode() возвращает хеш-код, связанный с вызывающим объектом. Этот хеш-код можно затем использовать в любом алгоритме, где хеширование при меняется в качестве средства доступа к хранимым объектам. Следует, однако, иметь в виду, что стандартная реализация метода GetHashCode() не пригодна на все случаи применения.
Как упоминалось в главе 9, если перегружается оператор ==, то обычно приходит ся переопределять методы Equals(object) и GetHashCode(), поскольку чаще всего требуется, чтобы метод Equals(object) и оператор == функционировали одинаково. Когда же переопределяется метод Equals(object), то следует переопределить и ме тод GetHashCode(), чтобы оба метода оказались совместимыми.
Метод ToString() возвращает символьную строку, содержащую описание того объекта, для которого он вызывается. Кроме того, метод ToString() автоматически вызывается при выводе содержимого объекта с помощью метода WriteLine(). Этот метод переопределяется во многих классах, что позволяет приспосабливать описание к конкретным типам объектов, создаваемых в этих классах. Ниже приведен пример применения данного метода.
При выполнении этого кода получается следующий результат.
Упаковка и распаковка
Как пояснялось выше, все типы в С#, включая и простые типы значений, являются производными от класса object. Следовательно, ссылкой типа object можно вос пользоваться для обращения к любому другому типу, в том числе и к типам значений. Когда ссылка на объект класса object используется для обращения к типу значения, то такой процесс называется упаковкой. Упаковка приводит к тому, что значение про стого типа сохраняется в экземпляре объекта, т.е. «упаковывается» в объекте, который затем используется как и любой другой объект. Но в любом случае упаковка происхо дит автоматически. Для этого достаточно присвоить значение переменной ссылочного типа object, а об остальном позаботится компилятор С#.
Распаковка представляет собой процесс извлечения упакованного значения из объекта. Это делается с помощью явного приведения типа ссылки на объект класса object к соответствующему типу значения. Попытка распаковать объект в другой тип может привести к ошибке во время выполнения.
Ниже приведен простой пример, демонстрирующий упаковку и распаковку.
В этом примере кода выводится значение 10. Обратите внимание на то, что значе ние переменной х упаковывается в объект простым его присваиванием переменной obj, ссылающейся на этот объект. А затем это значение извлекается из объекта, до ступного по его ссылке obj, и далее приводится к типу int.
Ниже приведен еще один, более интересный пример упаковки. В данном случае значение типа int передается в качестве аргумента методу Sqr(), который, в свою очередь, принимает параметр типа object.
Вот к какому результату приводит выполнение этого кода.
В данном примере значение переменной х автоматически упаковывается при пере даче методу Sqr().
Упаковка и распаковка позволяют полностью унифицировать систему типов в С#. Благодаря тому что все типы являются производными от класса object, ссылка на значение любого типа может быть просто присвоена переменной ссылочного типа object, а все остальное возьмут на себя упаковка и распаковка. Более того, методы класса object оказываются доступными всем типам, поскольку они являются про изводными от этого класса. В качестве примера рассмотрим довольно любопытную программу.
В результате выполнения этой программы выводится значение 10. Дело в том, что метод ToString() возвращает строковое представление объекта, для которого он вы зывается. В данном случае строковым представлением значения 10 как вызывающего объекта является само значение 10!
Класс object как универсальный тип данных
Если object является базовым классом для всех остальных типов и упаковка зна чений простых типов происходит автоматически, то класс object можно вполне ис пользовать в качестве «универсального» типа данных. Для примера рассмотрим про грамму, в которой сначала создается массив типа object, элементам которого затем присваиваются значения различных типов данных.
Выполнение этой программы приводит к следующему результату.
Как показывает данный пример, по ссылке на объект класса object можно обра щаться к данным любого типа, поскольку в переменной ссылочного типа object до пускается хранить ссылку на данные всех остальных типов. Следовательно, в массиве типа object из рассматриваемого здесь примера можно сохранить данные практиче ски любого типа. В развитие этой идеи можно было бы, например, без особого труда создать класс стека со ссылками на объекты класса object. Это позволило бы хранить в стеке данные любого типа.
Несмотря на то что универсальный характер класса object может быть довольно эффективно использован в некоторых ситуациях, было бы ошибкой думать, что с по мощью этого класса стоит пытаться обойти строго соблюдаемый в C# контроль типов. Вообще говоря, целое значение следует хранить в переменной типа int, строку — в пе ременной ссылочного типа string и т.д.
А самое главное, что начиная с версии 2.0 для программирования на C# стали до ступными подлинно обобщенные типы данных — обобщения (более подробно они рассматриваются в главе 18). Внедрение обобщений позволило без труда определять классы и алгоритмы, автоматически обрабатывающие данные разных типов, соблюдая типовую безопасность. Благодаря обобщениям отпала необходимость пользоваться классом object как универсальным типом данных при создании нового кода. Уни версальный характер этого класса лучше теперь оставить для применения в особых случаях.