Какова реальная причина предотвращения доступа защищенного члена через базовый / родной класс?

Недавно я обнаружил, что метод в производном классе может получить доступ только к защищенным членам экземпляра базового класса через экземпляр производного класса (или один из его подклассов):

class Base
{
    protected virtual void Member() { }
}

class MyDerived : Base
{
    // error CS1540
    void Test(Base b) { b.Member(); }
    // error CS1540
    void Test(YourDerived yd) { yd.Member(); }

    // OK
    void Test(MyDerived md) { md.Member(); }
    // OK
    void Test(MySuperDerived msd) { msd.Member(); }
}

class MySuperDerived : MyDerived { }

class YourDerived : Base { }

Мне удалось обойти это ограничение, добавив статический метод в базовый класс, поскольку методам Base разрешен доступ к Base.Member, и MyDerived может вызывать этот статический метод.

Я до сих пор не понимаю причину этого ограничения, хотя. Я видел несколько разных объяснений, но они не могут объяснить, почему MyDerived.Test() по-прежнему разрешен доступ к MySuperDerived.Member.

Принципиальное объяснение: "Защищенный" означает, что он доступен только для этого класса и его подклассов. YourDerived может переопределить Member(), создав новый метод, который должен быть доступен только для YourDerived и его подклассов. MyDerived не может вызвать переопределенный yd.Member(), потому что он не является подклассом YourDerived, и он не может вызвать b.Member(), потому что b может фактически быть экземпляром YourDerived.

Хорошо, но тогда почему MyDerived может вызвать msd.Member ()? MySuperDerived может переопределить Member(), и это переопределение должно быть доступно только для MySuperDerived и его подклассов, верно?

Вы до конца времени не знаете, вызываете ли вы переопределенный член или нет. И когда элемент является полем, он все равно не может быть переопределен, но доступ по-прежнему запрещен.

Прагматическое объяснение: Другие классы могут добавлять инварианты, о которых ваш класс не знает, и вы должны использовать их открытый интерфейс, чтобы они могли поддерживать эти инварианты. Если бы MyDerived мог иметь прямой доступ к защищенным членам YourDerived, он мог бы сломать эти инварианты.

Мое же возражение применимо и здесь. MyDerived также не знает, какие инварианты мог бы добавить MySuperDerived - он может быть определен в другой сборке другим автором - так почему MyDerived имеет прямой доступ к своим защищенным элементам?

У меня складывается впечатление, что это ограничение времени компиляции существует как ошибочная попытка решить проблему, которая действительно может быть решена только во время выполнения. Но, может быть, я что-то упустил. У кого-нибудь есть пример проблемы, которая может быть вызвана тем, что MyDerived получает доступ к защищенным элементам Base через переменную типа YourDerived или Base, но не существует уже при обращении к ним через переменную типа MyDerived или MySuperDerived?

-

ОБНОВЛЕНИЕ: я знаю, что компилятор просто следует спецификации языка; то, что я хочу знать, является целью этой части спецификации. Идеальным ответом было бы: "Если бы MyDerived мог вызвать YourDerived.Member(), произойдет $NIGHTMARE, но этого не произойдет при вызове MySuperDerived.Member(), потому что $ITSALLGOOD".

6 ответов

Решение

ОБНОВЛЕНИЕ: Этот вопрос был темой моего блога в январе 2010 года. Спасибо за отличный вопрос! Увидеть:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/14/why-cant-i-access-a-protected-member-from-a-derived-class-part-six/


У кого-нибудь есть пример проблемы, которая может быть вызвана тем, что MyDerived получает доступ к защищенным элементам Base через переменную типа YourDerived или Base, но не существует уже при обращении к ним через переменную типа MyDerived или MySuperDerived?

Я довольно смущен вашим вопросом, но я готов дать ему шанс.

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

Первая часть проста:

// Good.dll:

public abstract class BankAccount
{
  abstract protected void DoTransfer(BankAccount destinationAccount, User authorizedUser, decimal amount);
}

public abstract class SecureBankAccount : BankAccount
{
  protected readonly int accountNumber;
  public SecureBankAccount(int accountNumber)
  {
    this.accountNumber = accountNumber;
  }
  public void Transfer(BankAccount destinationAccount, User user, decimal amount)
  {
    if (!Authorized(user, accountNumber)) throw something;
    this.DoTransfer(destinationAccount, user, amount);
  }
}

public sealed class SwissBankAccount : SecureBankAccount
{
  public SwissBankAccount(int accountNumber) : base(accountNumber) {}
  override protected void DoTransfer(BankAccount destinationAccount, User authorizedUser, decimal amount) 
  {
    // Code to transfer money from a Swiss bank account here.
    // This code can assume that authorizedUser is authorized.

    // We are guaranteed this because SwissBankAccount is sealed, and
    // all callers must go through public version of Transfer from base
    // class SecureBankAccount.
  }
}

// Evil.exe:

class HostileBankAccount : BankAccount
{
  override protected void Transfer(BankAccount destinationAccount, User authorizedUser, decimal amount)  {  }

  public static void Main()
  {
    User drEvil = new User("Dr. Evil");
    BankAccount yours = new SwissBankAccount(1234567);
    BankAccount mine = new SwissBankAccount(66666666);
    yours.DoTransfer(mine, drEvil, 1000000.00m); // compilation error
    // You don't have the right to access the protected member of
    // SwissBankAccount just because you are in a 
    // type derived from BankAccount. 
  }
}

Попытка доктора Зла украсть ОДИН... МИЛЛИОН... ДОЛЛАРОВ... с вашего банковского счета в швейцарском банке была сорвана компилятором C#.

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

Но я просто хочу сказать, что "атака", которая здесь сорвана, это попытка выполнить конечный прогон вокруг инвариантов, установленных SecureBankAccount, для прямого доступа к коду в SwissBankAccount напрямую.

Это ответ на ваш первый вопрос, я надеюсь. Если это не ясно, дайте мне знать.

Ваш второй вопрос: "Почему SecureBankAccount также не имеет этого ограничения?" В моем примере SecureBankAccount говорит:

    this.DoTransfer(destinationAccount, user, amount);

Ясно, что "это" имеет тип SecureBankAccount или что-то более производное. Это может быть любое значение более производного типа, включая новый SwissBankAccount. Разве SecureBankAccount не может выполнить конечную проверку инвариантов SwissBankAccount?

Да, конечно! И поэтому авторы SwissBankAccount обязаны понимать все, что делает их базовый класс! Вы не можете просто уйти от какого-то урока волей-неволей и надеяться на лучшее! Реализация вашего базового класса позволяет вызывать набор защищенных методов, предоставляемых базовым классом. Если вы хотите извлечь из него информацию, вам необходимо прочитать документацию по этому классу или код и понять, при каких обстоятельствах будут вызываться ваши защищенные методы, и соответствующим образом написать свой код. Вывод - это способ обмена деталями реализации; если вы не понимаете деталей реализации того, из чего вы производите, не извлекайте из этого.

Кроме того, базовый класс всегда пишется перед производным классом. Базовый класс вас не меняет, и, по-видимому, вы доверяете автору этого класса, чтобы он не пытался вас украсть с помощью будущей версии. (Конечно, изменение базового класса всегда может вызвать проблемы; это еще одна версия хрупкой проблемы базового класса.)

Разница между этими двумя случаями заключается в том, что когда вы наследуете базовый класс, вы должны понимать и доверять поведению одного класса по вашему выбору. Это трудоемкий объем работы. Авторы SwissBankAccount обязаны четко понимать, что SecureBankAccount гарантирует неизменность, прежде чем вызывать защищенный метод. Но они не должны понимать и доверять каждому возможному поведению каждого возможного класса двоюродного брата, который, как оказалось, происходит от одного и того же базового класса. Эти ребята могут быть реализованы кем угодно и делать что угодно. У вас не будет никакой возможности понять какой-либо из их инвариантов перед вызовом, и поэтому у вас не будет возможности успешно написать работающий защищенный метод. Поэтому мы спасем вас от беспокойства и запретим этот сценарий.

Кроме того, мы должны разрешить вам вызывать защищенные методы на приемниках потенциально более производных классов. Предположим, мы этого не допустили и вывели что-то нелепое. При каких обстоятельствах можно было бы вызвать защищенный метод, если бы мы не разрешали вызывать защищенные методы для получателей потенциально производных классов? Единственный раз, когда вы можете вызвать защищенный метод в этом мире, это если вы вызываете свой собственный защищенный метод из закрытого класса! По сути, защищенные методы почти никогда не могут быть вызваны, и вызываемая реализация всегда будет самой производной. Какой смысл "защищен" в этом случае? Ваш "защищенный" означает то же самое, что и "частный", и может быть вызван только из закрытого класса ". Это сделало бы их менее полезными.

Итак, краткий ответ на оба ваших вопроса: "потому что, если бы мы этого не сделали, было бы невозможно использовать защищенные методы вообще". Мы ограничиваем вызовы менее производными типами, потому что если мы этого не сделаем, невозможно безопасно реализовать любой защищенный метод, который зависит от инварианта. Мы разрешаем вызовы через потенциальные подтипы, потому что, если мы не разрешаем это, то мы вообще не разрешаем никаких вызовов.

Это отвечает на ваши вопросы?

Эрик Липперт хорошо объяснил это в одном из своих постов в блоге.

Вы работаете с предположением, что это поведение явно запрещено из-за неясного представления о чистоте дизайна. Я не работаю на Microsoft, но считаю, что правда намного проще: она не запрещена, она просто не поддерживается, потому что это потребует много времени для реализации при относительно небольшом воздействии.

Малоиспользуемый protected internal вероятно, покроет большинство случаев, когда protected само по себе это не совсем так.

"Защищенный" означает в точности, что член доступен только для определяющего класса и всех подклассов.

Как MySuperDerived это подкласс MyDerived, Member доступно для MyDerived, Думайте об этом так: MySuperDerived это MyDerived и, следовательно, его частные и защищенные члены (унаследованные от MyDerived) доступны для MyDerived,

Тем не мение, YourDerived это не MyDerived и, следовательно, его частные и защищенные члены недоступны для MyDerived,

И вы не можете получить доступ Member на примере Base так как Base может быть YourDerived который не является MyDerived ни подкласс MyDerived,

И не делайте этого, используя статические методы, чтобы разрешить доступ. Это побеждает цель инкапсуляции и является большим запахом того, что вы не спроектировали вещи должным образом.

http://msdn.microsoft.com/en-us/library/bcd5672a.aspx

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

Есть документация "что?" вопрос. Теперь я хотел бы знать "Почему?":)

очевидно virtual не имеет ничего общего с этим ограничением доступа.

Хм, я думаю, что вы с чем-то связаны с братом... MyDerived не должен вызывать YourDerived.Member

Если MyDerived может вызвать Base.Member, он может фактически работать с экземпляром YourDerived и может фактически вызывать YourDerived.Member.

Ах, вот тот же вопрос: защищенные члены C# доступны через переменную базового класса

Вы, кажется, думаете об этом совершенно неправильно.

Речь идет не о том, "кто может назвать что", а о том, что можно заменить на что.

Подклассы MyDerived всегда должны заменять MyDerived (включая их переопределенные защищенные методы). Нет такого ограничения на другие подклассы Base, и поэтому вы не можете заменить их вместо MyDerived.

Другие вопросы по тегам