Подвох от protected при наследовании в C#

Перед выходными мне попался перевод на русский язык заметки "Hide and seek". В ней рассматривалось правило сокрытия методов при наследовании класса. В примере был приведен код, где обращение к методу базового класса можно было принять за попытку обращения к закрытому методу вне его класса. Но меня заинтересовал другой момент.

В приведенном там примере при наследовании использовался модификатор protected. В чем может быть особенность?

Предположим, есть некий базовый класс Parent. Другой разработчик унаследовал от него свой класс Child. А мы, уже наследуя его, пишем свой класс Grandchild. Это будет выглядеть так:

class Parent { public int F() { return 1; } }
class Child : Parent { }
class Grandchild : Child { public int Y() { return this.F(); } }

Вопрос: чему будет равна переменная result в следующем примере?

Grandchild g = new Grandchild();
bool result = g.F() == g.Y();

Ответ: true. Пока все просто.

Теперь разработчик класса Child объявляет внутри своего класса метод F() вот так:

class Parent { public int F() { return 1; } }
class Child : Parent { new protected int F() { return 2; } }
class Grandchild : Child { public int Y() { return this.F(); } }

Вопрос: а теперь чему будет равна переменная?

bool result = g.F() == g.Y();

Ответ: false.

Тут необходимо вспомнить, что "объявление нового члена скрывает унаследованный только в области видимости нового члена". Т.е. объявление нового метода как protected скроет базовый внутри своего класса и, что важно, его потомков. Но вне класса базовый метод будет по прежнему виден. Поэтому вызов к g.F() по сути будет вызовом Parent.F(). А вот метод g.Y() в своем теле обратиться к Child.F(). В итоге получаем

bool result = 1 == 2;
Отсюда и ответ false. Если же использовать private вместо protected, то получим исходный результат – true.

Вот еще один повод подумать, прежде чем использовать модификатор protected.

Кроме того, если вы еще не прочитали заметку, на которую я ссылался, или ее оригинал на английском, то рекомендую это сделать. Там объясняется почему используется именно такое правило наследования.

Небольшое дополнение, поскольку в комментариях заговорили об очевидности данного подведения: я не утверждаю, что это не документированное поведение. Более того, выше дано объяснение почему происходит все именно так. Вопрос использовании модификатора protected. В данном случае, это при наследовании приводит к тому, что визуально одинаковый код (вызов метода F) ведет себя по абсолютно по разному внутри и вне класса. И именно возникающая двойственность поведения мне кажется неправильной.

Комментарии (24) -

Тут все просто - методы перекрываются в зависимости от типа инстанса объекта.

Просто взрыв мозга Smile Спасибо

По моему тут все очевидно. Просто нужно внимательно код читать.

Переварил. Всё очевидно Smile

Очевидно при наличии этого кода и/или хорошей документации на него. А это не всегда так. Исходный код все трех классов тут рядом только для примера.

А что неочевидно при отсутствии исходного кода ?
Ключевое слово new в данном случае используется как new modifier.
Если прочитать документацию
msdn.microsoft.com/.../51y09td4(VS.71).aspx
то все быстро станет на свои места, даже без наличия кода всех классов. А если не читать документации - можно в любом простом месте ходить по полю с посеянными граблями.

Хотя бы Рихтера читать надо, если в мсдн лазить лень.

присоединяюсь к мнению, что все очевидно, по спецификации. интрига могла бы еще быть если бы было не  this.F(), а base.F().. хотя и в этом случае все очевидно.

Стоп, а причем тут MSDN и Рихтер. Я разве утверждал что это недокументированное поведение? Отсылка к new вообще не понятна.

Вопрос в другом: из-за protected (а вы зачем-то к new пристали), одинаковый на вид код (вызов метода F) ведет себя по абсолютно по разному внутри и вне класса. Причем, использование public и private таких результатов, к счастью, не дают.

т.е. Вы таки настаиваете на том, что такое поведение именно из-за protected модификатора, а не из-за new ;) ? То что использование private к такому не приводит - очевидно, а вы public проверили (с new ;) ) ?

всё именно так, как в документации (eng), так что в данное случаи очевидное не стало невероятным

Такое поведение из-за правила сокрытия при наследовании. При этом protected дает такой эффект.

То что использование private к такому не приводит - очевидно, а вы public проверили (с new ;) ) ?

А вы хотите сказать что public даст в этой ситуации false?  Тут и проверять не надо – и так ясно что новый метод перекроет старый полностью. При этом вызов F() и вне и внутри класса приведет к вызову одного и того же метода.

С пабликом "прогнал", естественно при модификаторе public у Grandchild'а F() будет от Child а не от Parent...

Но в целом по линку вполне внятно и четко объясняется:
Use the new modifier to explicitly hide a member inherited from a base class. To hide an inherited member, declare it in the derived class using the same name, and modify it with the new modifier.
.....
Name hiding through inheritance takes one of the following forms:
-A method introduced in a class or struct hides properties, fields, and types, with the same name, in the base class. It also hides all base class methods with the same signature. For more information, see 3.6 Signatures and overloading.

Из Signatures and overloading.

The signature of a method consists of the name of the method and the type and kind (value, reference, or output) of each of its formal parameters, considered in the order left to right. The signature of a method specifically does not include the return type, nor does it include the params modifier that may be specified for the right-most parameter.


Скорей всего Вас в заблуждение ввело то, что о сигнатуре метода всегда думают как о совокупности модификаторов доступа, возвращаемого типа, имени метода и параметров. Документация же говорит об обратном, учитывается только имя метода и тип+вид формального параметра. Даже return type не учитывается, не говоря уже о модификаторах доступа public\protected.
Так что все правильно - ваш public int F() и protected new int F() - по сигнатуре один и тот же метод - F(), который перекрывается в Child.

PS: ИМХО перекрытие методов  - не самая лучшая практика, обычно если надо изменить поведение при сохранении сигнатуры (полиморфизм) - сделайте аггрегацию Parent  в Child'е и нужные методы парента пробросьте as is а ненужные - модифицируйте. Писанины больше, теряются некоторые "фичи", но в целом - нагляднее и понятнее. В целом абстракцию от реализации ИМХО лучше делать через интерфейсы а не через конкретные наследованные классы (особенно с длинными цепочками наследования).

Меня не вводило в заблуждение. Еще раз скажу - выше все объясняется (почему такое поведение). Причем IMHO той строки достаточно.
Да, поведение документировано. Но именно возникающая двойственность поведения мне кажется неправильной. Ощущение, что правила созданы для пары private / public.  И вот для нее они уже работают вполне логично (причем IMHO лучше, чем наследование в том же С++).
Кроме того, к protected у меня очень осторожное отношение (еще со времен С++). Поэтому я стараюсь использовать именно private / public. А protected часто выглядит как брешь в проектировании интерфейса класса.
Поэтому цель заметки – не поиски недокументированного (неочевидного), а напоминание. И желание обратить внимание тех, кто не замечал этого ранее.

Думаю спор, которого в общем то и не было, на этом можно закрыть. Плюс в последних наших с вами сообщениях много вышло воды не по делу, так что если вы не против я немного их откорректирую?

SamousPrime 07.09.2010 17:44:06

Ну автор прямо Америку открыл Smile. Читайте тематические книжки - меньше будет подобных удивлений.

АбсолютноSmile И это 100 % не аргумент не использовать protected. Это аргумент не использовать new. Тогда уж нужно отказаться и от private т. к. такое поведение тоже "не логично":
class Parent { public int F() { return 1; } }
class Child : Parent {
    new private int F() { return 2; }
    public int Y() { return this.F(); }
}

Тут g.F() == g.Y() тоже вернет false Smile

SamousPrime :
Ну автор прямо Америку открыл.

Т.е. вы считаете такое поведение логичным?

FifitN:
Тут g.F() == g.Y() тоже вернет false

Есть, на мой взгляд, одно существенное отличие. В вашем случае вы сами переопределяете метод. Т.е. это уже сам себе злобный Буратино.

)))) Пожалуй вы правы) Но это все равно не повод обвинять protectedSmile, в вашем примере, злобный Буратино разработчик библиотеки, который использует  new protected )

@ FiftiN: Ага, вот только расхлебывать достанется другому. Я понимаю что все это отлавливается, что в идеальном мире всегда все документировано. и т.д. Smile

А вот я хочу спасибо сказать автору!!! И всем участникам беседы тоже!

Теперь знаю что именно почитать и подучить нужно Smile Без теории действительно ни куда!

При "правильном" подходе и в стакане можно утонуть.

Давайте рассмотрим для чего обычно используются protected члены.

1. Переопределение поведения объекта разработчиком библиотеки, от которой наследуются(пример System.Windows.Forms.Control метод OnPaint и тд)
2. Члены доступные только в наследниках. Например св-во доступное только внутри объекта.

Для чего писать new у protected члена? Вот это как раз bad design. Как и давать публичный доступ к OnPaint

ИМХО: наружу должен быть доступен только тот минимум, который нужен. Это к тому, что protected меньшее зло(если вообще зло), чем открывать все возможные методы
Короче - пример надуман Smile

@ WaSaMaSa:

Для чего писать new у protected члена?

Откуда по��учился пример я указал в самом начале.

ИМХО: наружу должен быть доступен только тот минимум, который нужен.

С процитированным согласен. Опять же – я не призывают отказываться от protected. Вопрос в том, что на мой взгляд его применение необходимо продумывать даже тщательнее, чем private и public. Но получается что OnPaint в вашем примере не совсем и спрятан. Smile

Кроме того, protected подразумевает наследование, а это уже повод задуматься. Возможно в конкретной ситуации можно использовать другой подход, без такой сильной связи.

Да глубокая иерархия объектов очень большое зло. Как говорит мой знакомый: "Если глубина больше двух потомков - пора задуматься".

Делегирование спасет мир!Smile

Но есть маленькая деталь - если вы разрабатываете библиотеку для использования большим количеством людей, то надо придерживаться стиля, который они смогут быстрее понять. А если вы все начнете делегировать - представьте сколько дополнительных сущностей у вас появится.

А так же проблемы с производительностью. Системе проще вызвать переопределенный метод, чем найти объект исполнителя в памяти(лишнее действие) и запустить его метод.

Но получается что OnPaint в вашем примере не совсем и спрятан.
Немного не понял что вы имели ввиду. Я приводил пример того, как применяется protected и в данном случае ИМХО все нормально. А как бы вы реализовали возможность кастомной отрисовки контрола?

@ WaSaMaSa:

"Если глубина больше двух потомков - пора задуматься".

Кратко: +1 Smile

А так же проблемы с производительностью. Системе проще вызвать переопределенный метод, чем найти объект исполнителя в памяти(лишнее действие) и запустить его метод.

Как правило, не будет тут такого падения производительности, чтобы заметить это. Операция не насколько ресурсоемкая. Разумеется, есть особые случае (вызов 1 000 000  раз подряд и т. д.)

Немного не понял что вы имели ввиду.

Я указал на то, что protected метод не является закрытым (это к фразе, что наружу должен быть доступен только тот минимум, который нужен). По сути OnPaint открыт наружу и потребуется только два лишних шага (наследоваться и сделать метод-заглушку) чтобы вызывать его как обычный метод. С private такой номер не пройдет. Ну это так – ворчание ради строгости фразы Smile

Подвожу итог:

По моему паниковать по поводу "особенного" поведения не стоит. Посмотрите на С++ и подобные. Нечего на protected пенять, коли new мешает ;)

private - сецификатор доступа для не наследуемого поведения.
protected - спецификатор доступа для наследования потомком поведения родителя.
new - для ПЕРЕОПРЕДЕЛЕНИЯ потомком спецификатор доступа поведения родителя.

new protected - наследовать поведение родителя до текущего класса, и в потомках использовать ПЕРЕОПРЕДЕЛЕННОЕ поведение

new private - для текущего класса ПЕРЕОПРЕДЕЛИТЬ поведение родительского класса ТОЛЬКО для текущего класса, для потомков использовать поведение базового класса.

я бы добавил protected public / protected private - но это аналогично new protected / new private

Добавить комментарий