- •Структуры и классы
- •Наследование реализации
- •Виртуальные методы
- •Сокрытие методов
- •Вызов базовых версий функций
- •Абстрактные классы и функции
- •Запечатанные классы и методы
- •Конструкторы производных классов
- •Добавление в иерархию конструктора
- •Добавление в иерархию конструкторов с параметрами
- •Интерфейсы
- •IDisposable — сравнительно простой интерфейс, потому что в нем определен только один метод. Большинство интерфейсов содержат гораздо большее количество методов.
- •Определение и реализация интерфейсов
- •Производные интерфейсы
Конструкторы производных классов
В разделе 6.3 обсуждалось, как можно применять конструкторы с индивидуальными классами. Возникает интересный вопрос о том, что случится, если вы станете определять собственные конструкторы для классов, являющихся частью иерархии, унаследованной от других классов, которые тоже могут иметь специальные конструкторы.
Предположим, что ни для одного из своих классов вы не определили никаких явных конструкторов. Это значит, что компилятор создаст для них конструкторы без параметров по умолчанию. При этом достаточно много чего происходит такого, что скрыто от ваших глаз, но компилятор может гарантировать, что по всей иерархии классов каждое поле будет инициализировано своим значением по умолчанию. Когда вы добавляете собственный конструктор, то берете на себя управление процессом конструирования, и вам следует заботиться о том, чтобы не сделать ничего такого, что помешает пройти конструированию гладко по всей иерархии классов.
Возможно, вас удивит, почему возникают какие-то особые проблемы с порожденными классами. Причина в том, что при создании экземпляра производного класса работает более одного конструктора. Конструктора класса, экземпляр которого создается, недостаточно для полной инициализации также должны быть вызваны конструкторы всех его базовых классов. Именно поэтому мы говорим о конструировании через иерархию.
Чтобы увидеть, почему должен быть вызван конструктор базового класса, рассмотрим пример программы, моделирующей работу компании-оператора сотовой связи под названием MortimerPhones.
Пример включает абстрактный базовый класс GenericCustomer, представляющий любого клиента. Существует также неабстрактный класс Nevermore60Customer, который представляет любого заказчика, подключенного к тарифному плану Nevermore60. Все заказчики имеют имя, представленное приватным полем. В режиме Nevermore60 первые несколько минут разговора заказчика оцениваются по повышенной расценке, что вызывает необходимость в поле highCostMinutesUsed, указывающем, сколько именно минут разговора по повышенной расценке использовано.
Определение классов показано ниже.
abstract class GenericCustomer
{
private string name; // прочие методы
}
class Nevermore60Customer: GenericCustomer
{
private uint highCostMinutesUsed;
// прочие методы
}
Мы не будем сейчас думать о том, как могут быть реализованы остальные методы этих классов, а сосредоточим внимание только на процессе конструирования. Если вы просмотрите коды примеров для этой главы, то обнаружите, что определения этих классов включают только конструкторы.
Посмотрим, что произойдет, если использовать операцию new для создания экземпляра класса Nevermore60Customer:
GenericCustomer customer = new Nevermore60Customer();
Понятно, что оба поля-члена и name, и highCostMinutesUsed должны быть инициализированы при инициализации customer. Если вы не применяете своих собственных конструкторов, а полагаетесь на конструкторы по умолчанию, то можно ожидать, что name будет инициализировано значением null, a highCostMinutesUsed нулем. Рассмотрим подробно, что именно произойдет на самом деле.
Поле highCostMinutesUsed не вызывает проблем: конструктор Nevermore60 Customer по умолчанию инициализирует это поле нулем.
А как насчет name? Если посмотреть на определения классов, станет понятно, что конструктор класса Nevermore60Customer не может инициализировать это значение". Это поле объявлено приватным, значит, классы-наследники не имеют доступа к нему. То есть конструктор Nevermore60Customer по умолчанию просто не знает о его существовании. Только код функций-членов GenericCustomer имеет доступ к этому полю. Из этого следует, что если поле name должно быть инициализировано, то это может сделать какой-то из конструкторов GenericCustomer. Вне зависимости от того, насколько велика иерархия классов, то же самое требование распространяется на все классы-предки вплоть до System.Object.
Понимая все это, мы можем посмотреть, что именно происходит при создании каждого экземпляра порожденного класса. Предполагая сквозное использование конструкторов по умолчанию, компилятор сначала выбирает конструктор того класса, экземпляр которого он пытается создать, в данном случае Nevermore60Customer. Первое, что делает конструктор по умолчанию Nevermore60Customer пытается вызвать конструктор по умолчанию своего непосредственного базового класса GenericCustomer. Затем конструктор GenericCustomer пытается вызвать конструктор своего базового класса — System.Object. Класс System.Object не имеет базового класса, поэтому его конструктор просто выполняется и возвращает управление конструктору GenericCustomer. Этот конструктор выполняется и, прежде чем вернуть управление конструктору Nevermore60Customer, инициализирует name значением null. В свою очередь, конструктор Nevermore60Customer выполняется, инициализируя highCostMinutesUsed нулем, после чего завершается. К этому моменту экземпляр успешно сконструирован и инициализирован.
В результате имеем последовательный вызов конструкторов всех классов иерархии, начиная с System.Object и заканчивая инициализируемым классом. Обратите внимание, что в этом процессе каждый конструктор инициализирует поля собственного класса. Именно так все обычно должно работать, и при добавлении собственных конструкторов вы должны стараться следовать этому принципу.
Обратите внимание на последовательность, в соответствие с которой все происходит. Всегда имеется конструктор базового класса, который вызывается первым. Это значит, что нет проблем для конструктора базового класса вызвать любые методы, свойства и обратиться к любым другим членам базового класса, к которым ему открыт доступ, поскольку базовый класс уже сконструирован и его поля инициализированы. Кроме того, это значит, что если классу-наследнику "не нравится" то, как инициализирован базовый класс, он может изменить начальные значения данных, к которым у него есть доступ. Однако хорошая практика программирования почти наверняка означает, что если вы сможете, то попытаетесь предотвратить возникновение подобных ситуаций и доверите конструктору базового класса иметь дело с его собственными полями.
Теперь, зная как работает процесс конструирования, можно поэкспериментировать с ним, добавляя собственные конструкторы.