通过base/sibling类阻止受保护成员访问的真正原因是什么?

Jes*_*rew 14 c# protected

我最近发现派生类中的方法只能通过派生类(或其子类之一)的实例访问基类的受保护实例成员:

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 { }
Run Code Online (Sandbox Code Playgroud)

我设法通过向基类添加静态方法来解决此限制,因为允许Base的方法访问Base.Member,而MyDerived可以调用该静态方法.

不过,我仍然不明白这种限制的原因.我已经看到了几个不同的解释,但他们无法解释为什么仍然允许MyDerived.Test()访问MySuperDerived.Member.

Principled说明:'受保护'意味着它只能被该类及其子类访问.YourDerived 可以覆盖Member(),创建一个只能由YourDerived及其子类访问的新方法.MyDerived无法调用重写的yd.Member()因为它不是YourDerived的子类,并且它不能调用b.Member(),因为b实际上可能是YourDerived的一个实例.

好的,但是为什么MyDerived可以调用msd.Member()?MySuperDerived可以覆盖Member(),并且只有MySuperDerived及其子类才能访问该覆盖,对吧?

直到运行时才知道您是否正在调用被覆盖的成员.当成员是一个字段时,它无论如何都不能被覆盖,但仍然禁止访问.

实用主义解释:其他类可能会添加您的类不了解的不变量,您必须使用它们的公共接口,以便它们可以维护这些不变量.如果MyDerived可以直接访问YourDerived的受保护成员,它可能会破坏这些不变量.

我同样的反对意见适用于此.MyDerived不知道MySuperDerived可能添加的不变量 - 它可能由不同的作者在不同的程序集中定义 - 为什么MyDerived可以直接访问其受保护的成员?

我得到的印象是,这种编译时限制是作为一种错误的尝试来解决一个实际上只能在运行时解决的问题.但也许我错过了一些东西.有没有人有这将通过类型YourDerived或基地的变量让MyDerived访问基地的保护成员造成的,但问题的例子不是通过类型MyDerived或MySuperDerived的变量访问他们的时候就已经存在?

-

更新:我知道编译器只是遵循语言规范; 我想知道的是规范那一部分的目的.一个理想的答案就是,"如果MyDerived可以调用YourDerived.Member(),$ NIGHTMARE就会发生,但是当调用MySuperDerived.Member()时就不会发生这种情况,因为$ ITSALLGOOD."

Eri*_*ert 17

更新:这个问题是我的博客在2010年1月的主题.感谢您提出的好问题!看到:

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的变量访问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. 
  }
}
Run Code Online (Sandbox Code Playgroud)

邪恶博士试图从你的瑞士银行账户窃取一个......百万......美元......被C#编译器挫败了.

显然,这是一个愚蠢的例子,显然,完全信任的代码可以对你的类型做任何想做的事情 - 完全可信的代码可以启动调试器并在运行时更改代码.完全信任意味着完全信任.实际上不要用这种方式设计真正的安全系统!

但我的观点是,这里被挫败的"攻击"是有人试图围绕SecureBankAccount设置的不变量进行最终运行,直接访问SwissBankAccount中的代码.

我希望能回答你的第一个问题.如果不清楚,请告诉我.

你的第二个问题是"为什么SecureBankAccount也没有这个限制?" 在我的例子中,SecureBankAccount说:

    this.DoTransfer(destinationAccount, user, amount);
Run Code Online (Sandbox Code Playgroud)

显然,"this"属于SecureBankAccount类型或更多派生类型.它可以是更多派生类型的任何值,包括新的SwissBankAccount.SecureBankAccount无法围绕SwissBankAccount的不变量进行最终运行吗?

是的,一点没错!正因为如此,在SwissBankAccount的作者需要了解他们的基类做的一切!你不能只是从一些阶级中汲取灵感并希望最好!允许基类的实现调用基类公开的受保护方法集.如果您想从中派生,那么您需要阅读该类或代码的文档,并了解在何种情况下将调用受保护的方法,并相应地编写代码.推导是一种共享实现细节的方式; 如果你不理解你从中得到的东西的实现细节,那么就不要从中得到它.

此外,基类始终派生类之前编写.基类没有改变你的想法,并且你可能相信这个类的作者不会试图用未来的版本偷偷摸摸地打破你.(当然,对基类的更改总是会导致问题;这是脆弱基类问题的另一个版本.)

这两种情况的区别在于,当您从基类派生时,您可以选择理解和信任的一个类的行为.这是一项易处理的工作量.SwissBankAccount的作者需要准确理解SecureBankAccount在调用受保护方法之前保证不变的内容.但是他们不应该理解和信任恰好从同一个基类派生的每个可能的堂兄类的每个可能的行为.这些家伙可以由任何人实施并做任何事情.你没有任何能力去理解它们的任何预调用不变量,因此你没有能力成功编写一个有效的受保护方法.因此,我们为您节省了麻烦和不允许这种情况.

此外,我们必须允许您在可能更多派生类的接收者上调用受保护的方法.假设我们不允许这样做,并推断出一些荒谬的东西.在什么情况下可以被保护的方法不断被调用,如果我们不允许上潜在的,更多的派生类的接收器调用保护方法?你可以在这个世界中唯一一次调用受保护的方法就是你从密封的类中调用自己的受保护方法!实际上,几乎永远不会调用受保护的方法,并且被调用的实现始终是最有效的实现.在这种情况下,"受保护"的重点是什么?您的"受保护"与"私有"意味着相同,只能从密封的类中调用.这会使它们变得不那么有用.

因此,对你的两个问题的简短回答是"因为如果我们不这样做,就根本不可能使用受保护的方法." 我们通过less-derivedtypes限制调用,因为如果我们不这样做,就不可能安全地实现任何依赖于不变量的受保护方法.我们允许通过潜在的子类型进行调用,因为如果我们不允许这样,那么我们几乎不允许任何调用.

这会回答你的问题吗?

  • 首先,就像我说的那样,在将可访问性与安全性混淆时要小心.无法访问的方法仍可由完全受信任的代码调用; 完全受信任的代码只会重写元数据,直到可以访问该方法.或者,另一种看待它的方式是"安全"只是一种特殊的不变量.真的,这个功能是关于*能够编写可靠的代码*. (3认同)

Han*_*ant 5

埃里克·利珀特(Eric Lippert)在一篇博客文章中对此进行了很好的解释。

  • Eric,Dude是我的*儿子*。抱歉。 (12认同)
  • 老兄,利珀特先生是我的*父亲*。 (11认同)
  • 好,你让我在那里。 (6认同)