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

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

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 ]

17 голосов
/ 15 декабря 2009

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

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


У кого-нибудь есть пример проблема, которая будет вызвана позволяя MyDerived доступ к базе защищенные члены через переменную типа 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. 
  }
}

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

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

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

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

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

    this.DoTransfer(destinationAccount, user, amount);

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

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

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

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

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

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

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

5 голосов
/ 15 декабря 2009

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

1 голос
/ 15 декабря 2009

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

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

1 голос
/ 15 декабря 2009

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

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

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

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

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

0 голосов
/ 15 декабря 2009

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

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

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

Очевидно, virtual не имеет никакого отношения к этому ограничению доступа.

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

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

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

0 голосов
/ 15 декабря 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...