首页 文章

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

提问于
浏览
13

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

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 .

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

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

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

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

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

我得到的印象是,这种编译时限制是作为一种错误的尝试来解决一个实际上只能在运行时解决的问题 . 但也许我通过一个类型为YourDerived或Base的变量来保护成员,但是当通过MyDerived或MySuperDerived类型的变量访问它们时,它们是不存在的?

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

6 回答

  • 0

    更新:这个问题是我的博客在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. 
      }
    }
    

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

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

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

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

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

    this.DoTransfer(destinationAccount, user, amount);
    

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

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

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

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

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

    所以,对你的两个问题的简短回答是"because if we didn't do that, it would be impossible to use protected methods at all."我们通过less-derivedtypes限制调用,因为如果我们不能安全地实现任何依赖于不变量的受保护方法 . 我们允许通过潜在的子类型进行调用,因为如果我们不允许这样做,那么我们几乎不允许任何调用 .

    这会回答你的问题吗?

  • 4

    Eric Lippert在_592673中对此做了很好的解释 .

  • 0

    “受保护”意味着成员只能访问定义类和所有子类 .

    由于 MySuperDerivedMyDerived 的子类,因此 MyDerived 可以访问 Member . 可以这样想: MySuperDerivedMyDerived ,因此 MyDerived 可以访问其私有和受保护成员(继承自 MyDerived ) .

    但是, YourDerived 不是 MyDerived ,因此 MyDerived 无法访问其私有和受保护成员 .

    并且您无法在 Base 的实例上访问 Member ,因为 Base 可能是 YourDerived ,它不是 MyDerived 也不是 MyDerived 的子类 .

    并且不要使用静态方法来允许访问事物 . 那就是打败了封装的目的,这是一个很大的气味,你没有正确设计的东西 .

  • 1

    你是为微软工作的,但我相信真相要简单得多:它不是被禁止的,它只是不被支持,因为实现相对较低的影响会耗费大量时间 .

    这个很少使用的 protected internal 可能会掩盖大多数情况下 protected 并没有完全削减它 .

  • 1

    你似乎完全以错误的方式思考这个问题 .

    它's not about 592694 , it' s关于什么是可替代的地方 .

    MyDerived的子类应该始终可以替代MyDerived(包括它们被重写的受保护方法) . Base的其他子类没有这样的约束,所以你不能用它们代替MyDerived .

  • 17

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

    仅当通过派生类类型进行访问时,才能在派生类中访问基类的受保护成员 .

    有什么记录“什么?”题 . 现在我希望我知道“为什么?” :)

    显然 virtual 与此访问限制无关 .

    嗯,我觉得你对兄弟姐妹的事情有所了解...... MyDerived不应该打电话给YourDerived.Member

    如果MyDerived可以调用Base.Member,它实际上可能正在处理YourDerived的实例,并且实际上可能正在调用YourDerived.Member

    啊,这是同一个问题:C# protected members accessed via base class variable

相关问题