- •Тема 6. Основы программирования на языке c#
- •Условная операция
- •Операции checked и unchecked
- •Операция поглощения null
- •Безопасность типов
- •Преобразования типов
- •Неявные преобразования
- •Явные преобразования
- •Упаковка и распаковка
- •Проверка равенства объектов
- •Виртуальный метод Equals()
- •Статический метод Equals()
- •Проверка типов значений на равенство
- •Перегрузка операций
- •Как работают операции
- •Пример перегрузки операции: структура Vector
- •Добавление дополнительных перегрузок
- •Перегрузка операций сравнения
- •Пользовательские приведения
- •Реализация пользовательских приведений
- •Приведение между классами
- •Приведение между базовым и производным классами
- •Упаковывающие и распаковывающие приведения
- •Множественные приведения
Приведение между классами
Рисунок 7.1
Пример иерархии классов
нельзя определять приведение между классами, если один из них является наследником другого (приведение такого рода, как вы увидите, уже существует);
приведение может быть определено внутри определения как исходного типа, так и типа назначения.
Чтобы проиллюстрировать эти требования, предположим, что есть иерархия классов, показанная на рис. 7.1.
Другими словами, классы С и D непрямо унаследованы от А. В этом случае единственными допустимыми пользовательским приведением между типами А, В, С и D будут приведения между классами С и D, потому что эти классы не наследуют друг друга. Код таких приведений может выглядеть следующим образом (предполагается, что они будут явными, как это обычно бывает при определении приведений между пользовательскими типами):
public static explicit operator D(C value)
{
// и т.д.
}
public static explicit operator C(D value)
{
// и т.д.
}
При определении каждой из этих операций приведения у вас есть выбор, куда их поместить внутрь определения класса С или же внутрь определения класса D, но никуда более. C# требует, чтобы определение приведения было помещено в определение либо исходного класса (или структуры), либо целевого. Побочным эффектом этого требования является невозможность определить приведение между двумя классами, не имея доступа на редактирование исходного кода хотя бы одного из них. И это разумно, поскольку подобным образом предотвращается определение приведений к вашим классам от независимых разработчиков.
После того как определено некоторое приведение внутри одного класса, определить такое же приведение внутри другого класса уже нельзя. Очевидно, что должна существовать только одна версия приведения для каждого преобразования, иначе компилятор не будет знать, какую выбрать.
Приведение между базовым и производным классами
Чтобы увидеть, как работают такие приведения, начнем с рассмотрения случая, когда и исходный, и целевой тип относятся к ссылочным, и возьмем для этого два класса MyBase и MyDerived, причем MyDerived будет прямым или непрямым наследником MyBase.
Сначала пойдем от MyDerived к MyBase; всегда можно (предполагая, что конструкторы доступны) записать так:
MyDerived derivedObject = new MyDerived();
MyBase baseCopy = derivedObject;
В этом случае выполняется неявное приведение MyDerived к MyBase. Это работает, поскольку существует правило, что любой ссылке типа MyBase разрешено ссылаться на объекты класса MyBase либо на объекты-наследники MyBase. В объектно-ориентированном программировании экземпляры классов-наследников являются в полном смысле экземплярами базового класса, плюс имеют некоторые дополнения. Все функции и поля, определенные в базовом классе, определены также в классе-наследнике.
В качестве альтернативы можно записать следующим образом:
MyBase derivedObject = new MyDerived();
MyBase baseObject = new MyBase();
MyDerived derivedCopy1 = (MyDerived) derivedObject; // нормально
MyDerived derivedCopy2=(MyDerived) baseObject;//генерирует исключение
Такой код абсолютно допустим в C# (в смысле синтаксиса) и иллюстрирует приведение от типа базового класса к классу-наследнику. Однако последний оператор во время выполнения программы сгенерирует исключение. Поскольку ссылка базового класса в принципе может указывать на экземпляр класса-наследника, допускается, чтобы этот объект был на самом деле экземпляром класса-наследника, к которому выполняется приведение. И если это именно такой случай, то приведение пройдет успешно, и ссылка производного типа будет указывать на объект. Если же, однако, объект на самом деле не будет экземпляром производного типа (или типа, унаследованного от него), такое приведение завершится неудачей и приведет к исключению.
Обратите внимание, что приведения, предлагаемые компилятором, которые преобразуют базовый класс в производный, на самом деле не осуществляют никакого преобразования данных. Все, что они делают просто устанавливают ссылку на объект, если данное преобразование законно. В этом смысле природа таких приведений весьма отличается от тех, что вы определяете сами. Так, в представленном выше примере SimpleCurrency были определены приведения, трансформирующие данные из структуры Currency во встроенный тип float и обратно. В случае приведения float в Currency на самом деле создавался новый экземпляр структуры Currency, который инициализировался нужными значениями. Предопределенное приведение между базовым и производным классом этого не делает. Если вы действительно хотите преобразовать экземпляр MyBase в настоящий объект MyDerived, со значениями, базирующимися на содержимом экземпляра MyBase, вам не обязательно использовать для этого синтаксис приведения. Самым разумным будет в классе-наследнике определить конструктор, принимающий экземпляр базового класса в качестве параметра, и поручить ему выполнение соответствующей инициализации:
class DerivedClass : BaseClass
{
public DerivedClass(BaseClass rhs)
{
// Инициализация объекта по базовому экземпляру
}
// и т.д.
