18

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

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。

原则性解释:“受保护”意味着它只能由该类及其子类访问。YourDerived可以覆盖 Member(),创建一个只能由 YourDerived 及其子类访问的新方法。MyDerived 不能调用被覆盖的 yd.Member(),因为它不是 YourDerived 的子类,也不能调用 b.Member(),因为 b 实际上可能是 YourDerived 的一个实例。

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

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

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

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

我的印象是,这种编译时限制的存在是一种错误的尝试,试图解决一个实际上只能在运行时解决的问题。但也许我错过了一些东西。有没有人举例说明让 MyDerived 通过 YourDerived 或 Base 类型的变量访问 Base 的受保护成员而导致的问题,但在通过 MyDerived 或 MySuperDerived 类型的变量访问它们时存在?

--

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

4

6 回答 6

21

更新:这个问题是我 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 保证不变的内容。但他们不应该理解和信任每一个可能的表亲阶级的每一个可能的行为这恰好是从同一个基类派生的。这些家伙可以由任何人实施并做任何事情。您将无法理解它们的任何调用前不变量,因此您将无法成功编写工作受保护的方法。因此,我们为您省去了麻烦并禁止这种情况。

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

因此,对您的两个问题的简短回答是“因为如果我们不这样做,就根本不可能使用受保护的方法。” 我们通过较少派生类型限制调用,因为如果我们不这样做,就不可能安全地实现任何依赖于不变量的受保护方法。我们允许通过潜在子类型进行调用,因为如果我们不允许这样做,那么我们根本不允许任何调用

这能回答你的问题吗?

于 2009-12-15T04:19:14.920 回答
6

Eric Lippert 在他的一篇博客文章中对此进行了很好的解释。

于 2009-12-15T02:29:56.030 回答
1

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

AsMySuperDerived是 的子类MyDerivedMember可以访问MyDerived. 可以这样想:MySuperDerived是 a MyDerived,因此它的私有成员和受保护成员(继承自MyDerived)可由MyDerived.

但是,YourDerived不是 a MyDerived,因此它的私有成员和受保护成员是不可访问的MyDerived

而且您无法访问Member的实例,因为BaseBase可能YourDerived不是MyDerived.MyDerived

并且不要使用静态方法来允许访问事物。这违背了封装的目的,并且是您没有正确设计事物的大气味。

于 2009-12-15T02:12:22.660 回答
1

您似乎完全错误地考虑了这一点。

这不是关于“谁可以调用什么”,而是关于什么可以替代 where

MyDerived 的子类应该始终可以替代 MyDerived(包括它们被覆盖的受保护方法)。Base 的其他子类没有这样的限制,因此您不能用它们代替 MyDerived。

于 2009-12-15T02:13:41.240 回答
1

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

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

有“什么?”的文档。问题。现在我希望我知道“为什么?” :)

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

嗯,我认为你正在处理兄弟姐妹的事情...... MyDerived 不应该能够调用 YourDerived.Member

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

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

于 2009-12-15T02:23:03.503 回答
1

您的操作是假设由于设计纯度的一些模糊概念而明确禁止这种行为。我不为微软工作,但我相信事实要简单得多:它不是被禁止的,只是不被支持,因为实施起来会很耗时,影响相对较小。

很少使用的protected internal可能会覆盖大多数protected单独使用并不能完全减少它的情况。

于 2009-12-15T03:56:54.110 回答