Подтвердить что ты не робот

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

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

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.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?

-

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

4b9b3361

Ответ 1

ОБНОВЛЕНИЕ: Этот вопрос был предметом моего блога в январе 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. 
  }
}

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

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

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

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

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

    this.DoTransfer(destinationAccount, user, amount);

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

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

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

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

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

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

Отвечает ли это на ваши вопросы?

Ответ 3

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

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

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

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

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

Ответ 4

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

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

Ответ 5

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

Это не о том, "кто может что-то вызывать", это о том, что подставимо где.

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

Ответ 6

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

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

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

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

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

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

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