Нэш Трей - C# 2010. Ускоренный курс для профессионалов (2010) (1160865), страница 112
Текст из файла (страница 112)
На заметку! Эта глава довольно длинная, так как было важно собрать всю полезную и взаимосвязанную информацию в одном месте. Вообще говоря, глава разбита на два больших раздела. В первом описаны ссылочные типы, а во втором — типы значений. Ссылочные типы рассматриваются первыми, и для них выделено больше места, поскольку некоторые материалы касаются как ссылочных, так и типов значений. Глава завершается парой списков вопросов, на которые нужно ответить при проектировании новых типов, 422 Глава )3 Канонические формы ссылочных типов Для начала исследуем канонические формы ссылочных типов Св. В языке СЭ объекты находятся в управляемой куче, и доступ к ним осуществляется через типы значений, хранящие ссылки на них, такие как переменные ссылочных типов.
На заметку! В терминах С++ можно представить себе подобную систему, где все объекты создаются динамически с использованием пен, и обращение к ним производится только через указатели, возвращаемые пен. Именно это происходит в С!.В, но с тем отличием, что среда С(.й отслеживает все эти "указатели", или ссылки, и знает, когда объекты в куче больше не имеют ссылок на них, а, следовательно — когда они могут быть уничтожены. Чтобы быть немного точнее, рассмотрим вот что. С годами сообщество С++ накопило богатый массив идиом, полагающихся на стек и помогающих управлять ресурсами. При создании объекта С++ в стеке компилятор гарантирует, что конструкторы и деструкторы объекта будут вызваны в надлежащее время, тем самым предоставляя контролируемую точку, куда можно поместить код очистки ресурсов.
Доминирующая здесь идиома называется ВА)! (Асцикйбоп Ь (п(т)а)(забоя — захват ресурсов является инициализацией), и она интенсивно используется в С++ и в любом другом объектно-ориентированном языке с детерминированной деструкцией объектов. По сути, идея, лежащая в основе этой идиомы, заключается в том, что любые ресурсы, которые требуют распределения, захватываются в теле конструктора, а освобождение этих ресурсов производится в соответствующем деструкторе.
Эта идиома настолько укоренилась, что для получения устойчивого, безопасного и нейтрального к исключениям кода ею необходимо интенсивно пользоваться, помещая все сопровождающие операции пеи и г(е1еге в конструкторы и деструкторы. В СВ эта идиома не так легко доступна, потому что деструкторы СФ не являются детерминированными. Поэтому подходить к проблемам, решаемым этой идиомой, следует как-то иначе, о чем и пойдет речь в настоящем разделе. Классы должны помечаться как зеа1ей по умолчанию При создании новых классов необходимо автоматически помечать их как зеа1ес! (герметнзированный) и убирать это ключевое слово только в том случае, если есть веская причина полагать, что кому-то понадобится выполнять наследование от данного класса.
Почему не поступить наоборот, оставляя классы не герметичными по умолчанию и добавлять зеа1ег! только в том случае, когда нужно запретить наследование? Ответ: потому, что невозможно предсказать, каким образом кто-то выполнит наследование от класса, при построении которого не были предприняты специальные усилия по поддержке наследования. Очень много существует проектов, где кто-либо пытался наследоваться от класса, который никогда не предназначался для того, чтобы служить базовым. Например, при хорошем дизайне классы, лишенные виртуальных методов, обычно не предназначены для наследования. Отсутствие виртуальных методов, скорее всего, указывает на то, что автор класса не думал о том, что кто-то будет наследоваться от него.
а потому должен был пометить класс словом зеа1ес!. Если класс не является зеа1ес!, и вы собираетесь разрешить другим наследоваться от него, не забудьте снабдить его адекватной документацией, чтобы человек, наследующий свой класс, не ломал себе голову. На заметку! Создать самодокументированный базовый класс вообще нелегко, а при наличии в классе хотя бы одного переопределяемого виртуального метода обойтись без документации почти невозможно. Причины описаны ниже. В поисках канонических форм С№ 423 Даже классы, имеющие виртуальные методы и предназначенные для того, чтобы служить базой для дальнейшего наследования, могут оказаться проблематичными.
Например, если при наследовании от класса, предоставляющего виртуальный метод ВоЯопег)г1по, этот метод необходимо расширить путем его переопределения, должна ли в переопределении вызываться версия базового класса? Если да, то когда: до или после расширяющего метод кода? Имеет ли значение порядок? Возможно, имеет, если в базовом классе объявлены защищенные поля'. В отсутствие по-настоящему хорошей документации по базовому классу, скорее всего, получить ответы на эти вопросы никогда не удастся.
Фактически. это одна нэ причин, почему расширение посредством включения обычно более гибкое, а потому и более мощное по сравнению с расширением через наследование. Расширение включением является динамическим и реализуется во время выполнения, в то время как расширение на основе наследования более ограничено, поскольку статично и осуществляется во время компиляции. И что еще важнее— расширение включением можно проводить. даже если расширяемый класс помечен как яеа1ег). Если веские причины делать класс базовым отсутствуют, всегда помечайте его как яеа1ег(. В противном случае будьте готовы предоставить очень подробную документацию о том, как лучше всего организовать наследование от класса.
Вместо наследования реализации (класса) всегда можно разработать другой дизайн, применив наследование интерфейсов вместе с включением, который выполнит ту же работу. В силу вышесказанного, почти не осталось причин, по которым все проектируемые классы не объявлялись бы как яеа1еб. Не поймите превратно: речь не идет о том, что наследование — зто всегда плохо. Наоборот, при правильном применении оно полезно. К сожалению, часто наследование применяется неправильно. Наличие глубокой иерархии наследования, в противоположность мелкой и плоской — верный признак того, что дизайн нужно пересмотреть. На заметку! Когда листовые (терминальные) классы, унаследованные от других классов с виртуальными методами, помечены как яеа1ег) или когда индивидуальные переопределенные методы помечены как яеа1ег), исполняющая система может превратить их вызовы в невиртуальные, поскольку никакие производные реализации этих методов существовать не могут.
Естественно, это дает выигрыш в производительности. Использование шаблона МЧ! Очень часто при проектировании класса, специально предназначенного служить базовым в иерархии, виртуальные методы объявляются таким образом, чтобы производные классы могли модифицировать поведение. Первое приближение такого базового класса может выглядеть примерно так: иязпд яуясепг роЬ1№с с1аяя Вазе ( роь11с чзгсоя1 чоьб Ооиогк() ( Сопяо1е. Нгзгеьхяе ( "Ваяя. Воиогк О" ) г ) ' В главе 4 обсуждается инкапсуляция и ее важность в объектно-ориентированном дизайне. Важно отметить, что защищенные поля нарушают инкапсуляцию. 424 глава ) з ров|та с1авв Оегтчеб: Вазе ( риЬ11с очеггтбе чотб ОоногЕ О ( Сопво1е.игтсеь|пе( "Оег1чеб.сокогг()" ) ) риЬ1та с1авв Епсгуро|пс ( втаттс чотб Иа|п() ( Вазе Ь = печ Оегтчеб(); Ь.понос~с(): Не удивительно, что вывод этого примера выглядит следующим образом: Оегтчеб.поката() Однако дизайн мог бы быть немного более устойчивым. Предположим, что вы — разработчик вазе, и создали этот класс для миллионов пользователей.
Многие люди по всему миру с удовольствием пользуются класс Вазе, но по какой-то веской причине вы решаете, что в покос)с нужно выполнять некоторую пред- и пост-обработку. Например, возникло желание предоставить отладочную версию Вазе, которая отслеживает количество вызовов метода Оокогх. Если код останется таким, как показано выше, это сделать не удастся, не внеся разрушительных для миллионов пользователей класса Вазе изменений.
Например, можно было бы добавить два новых метода Ргепохогт и РовгОохоге и вежливо попросить всех пользователей заново реализовать их переопределения, чтобы они вызывали эти методы в надлежащее время. Теперь рассмотрим минимальную модификацию исходного дизайна, который даже не изменяет общедоступного интерфейса вазе: из|по Яувгеа; риЬ|1а с1авз Вазе ( риЬ1тс чотб Оокогг О Согеэоиогг О | ) ргасессеб ч|гтиа1чотб Сагепокогг() ( Сопзо1е. Хг|сей|пе ( "Вазе . Ооиогг () " ) | ) риЬ|1с с1азв Оегтчеб: Вазе ( рготесгеб очегг1бе чо1б СогеэокогК() ( Сопзо1е.иг1те| |пе ( "Оеттчеб. Оокогх () " ) ) риЫ1с с1авз ЕпсгуРо|пс ( аеас|с чо1б Ма|о() ( Вазе Ь = пее Оег1чеб() Ь.ОоногК О ) ) ) Этот маленький изящный шаблон называется В)ЪЧ (В(оп-Ч(г(па) 1пгегтасе — не виртуальный интерфейс), и он делает буквально следующее: объявляет общедоступный интерфейс базового класса не виртуальным.
но переопределяемое поведение перемещает В поисках канонических форм СЗ 425 в другой, защищенный, метод под названием Согероиог)с, добавляя необходимый промежуточный уровень. Шаблон В('ч') подобен шаблону Тешр(ате Метйоб (Шаблонный метод), который описан Эрихом (Вымой (Ег(сй Оапппа), Ричардом Хелмом (к(сйагб Не)ш), Ральфом Джонсоном (йа)рЬ дойпзоп) и Джоном Влиссидесом (бойп 7Изз(без) в книге Оеядп Ранегпзк Е(егпеккз о3 йеизаЫе О)хест-Ог(еп(еб Эо/благе (Абб(зоп-укгез)еу Рто1езз(опа), 1995 г). Шаблон В)'ч') широко применяется в библиотеках .МЕТ Ргакпеиюг)к, и по совершенно очевидным причинам он кочует иэ руководства в руководство по проектированию библиотек М(сгозой. Чтобы добавить в метод Роиогк некоторый измеряющий код, понадобится всего лишь модифицировать вазе и содержащую его сборку. Любой из классов, наследующих сборку, изменений не потребует. Другой прием, часто используемый с )Фг) в мире С++, заключается в том, что виртуальный метод объявляется приватным (рг1чате), как показано в следующем коде, который, к сожалению, по изложенным ниже причинам в Се не компилируется: НЕ КОМПИЛИРУЕТСЯ(!!!! оз1по Бузгесп РпЬ1гс с1азз Вазе ( роЬ11с чсгб Роногх() ( Согероиогх(); ) НЕ КОМПИЛИРУЕТСЯ!(!(! рггчате ч1гтиа1 чо1б Ссгероногх() ( Сопзо1е.иггтеЬгпе( "Вазе.роногх()" )к ) ) РиЬЬгс с1азз Реггчеб: Вазе ( НЕ КОМПИЛИРУЕТСЯ!!!!! рг1чате очегггбе чогб Согеооиогх() ( Сопзо1е.иггтеЬ1пе( "Рег1чед.ооногх О " ); ) ) рсЬ11с с1азз Ептгуго1пт ( зтатгс чо1б Маго() Вазе Ь = пен Реггчеб(); Ь.Роиогк(); Этот код скомпилировался бы в начальной версии СВ иэ .МЕТ 1.0, и такая техника вполне корректна для СЬкк, отражая факт, что в то время спецификации СЕЕ как и СВ, стремились как можно ближе соответствовать семантике С++.
Прежде чем разобраться, почему это не работает в СЗ теперь, давайте рассмотрим, зачем оно понадобилось бы в данной ситуации. Между видимостью и доступностью методов существует фундаментальное отличие. Если метод находится в обьявлении класса или структуры, то независимо от уровня защиты этот метод видимый. Традиционно для того, чтобы производный класс мог переопределять метод, он всего лишь должен быть видимым, а не доступным. ма заметку! Единственный смысл наличия ключевого слова рггчате в методе Рггчате чггсца1 С++ заключается в том, что производный класс не должен вызывать реализацию базового класса.















