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

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

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 { }

Мне удалось работать вокруг этого ограничения путем добавления статического метода для базового класса, так как методам Основы позволяют получить доступ к Основе. Участник и MyDerived могут назвать тот статический метод.

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

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

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

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

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

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

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

--

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

14
задан Jesse McGrew 15 December 2009 в 02:34
поделиться

6 ответов

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

https://blogs.msdn.microsoft.com/ericlippert/2010/01/14/why-cant-i-access-a-protected-member-from-a-deved-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 гарантирует неизменность перед вызовом защищенного метода. Но они не должны понимать и доверять каждому возможному поведению каждого возможного класса родственников , который просто является производным от одного и того же базового класса. Эти ребята могут быть реализованы кем угодно и делать что угодно. У вас не будет никакой возможности понять какой-либо из их инвариантов перед вызовом, и, следовательно, у вас не будет возможности успешно написать работающий защищенный метод. Таким образом, мы избавляем вас от этого беспокойства и запрещаем такой сценарий.

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

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

17
ответ дан 1 December 2019 в 12:13
поделиться

Похоже, вы думаете об этом совершенно неверно.

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

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

0
ответ дан 1 December 2019 в 12:13
поделиться

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

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

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

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

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

1
ответ дан 1 December 2019 в 12:13
поделиться

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

5
ответ дан 1 December 2019 в 12:13
поделиться

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

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

1
ответ дан 1 December 2019 в 12:13
поделиться

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

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

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

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

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

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

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

0
ответ дан 1 December 2019 в 12:13
поделиться
Другие вопросы по тегам:

Похожие вопросы: