Ответ 1
ОБНОВЛЕНИЕ: Этот вопрос был предметом моего блога в январе 2010 года. Спасибо за большой вопрос! См:
Есть ли у кого-нибудь пример проблема, вызванная позволяя 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 являются инвариантными до вызова защищенного метода. Но им не нужно понимать и доверять каждому возможному поведению любого возможного класса кузена, который просто происходит от того же базового класса. Эти ребята могут быть реализованы кем угодно и сделать что угодно. У вас не было бы никакой способности понимать любой из их инвариантов перед вызовом, и поэтому у вас не было бы возможности успешно написать защищенный метод. Поэтому мы спасаем вас от этого и запрещаем этот сценарий.
Кроме того, мы должны разрешить вам вызывать защищенные методы для получателей потенциально более производных классов. Предположим, мы этого не допустили и вывели что-то абсурдное. В каких обстоятельствах может быть вызван защищенный метод, если мы запретили вызов защищенных методов для получателей потенциально более производных классов? Единственный раз, когда вы могли бы назвать защищенный метод в этом мире, - это если вы вызываете свой защищенный метод из запечатанного класса! Эффективно защищенные методы почти никогда не вызывались, и реализация, которая была вызвана, всегда была бы самой производной. Какой смысл "защищать" в этом случае? Ваш "защищенный" означает то же, что и "частный", и может быть вызван только из запечатанного класса ". Это сделало бы их менее полезными.
Итак, короткий ответ на оба вопроса: "потому что, если мы этого не сделаем, было бы невозможно использовать защищенные методы". Мы ограничиваем вызовы с помощью менее производных типов, потому что, если мы этого не делаем, невозможно безопасно реализовать любой защищенный метод, зависящий от инварианта. Мы разрешаем вызовы через потенциальные подтипы, потому что, если мы этого не допустим, мы практически не допускаем никаких вызовов.
Отвечает ли это на ваши вопросы?