开发者

What's the real reason for preventing protected member access through a base/sibling class?

开发者 https://www.devze.com 2022-12-13 20:54 出处:网络
I recently discovered that a method in a derived class can only access the base class\'s protected instance members through an instance of the derived class (or one of its subclasses):

I recently discovered that a method in a derived class can only access the base class's protected instance members through an instance of the derived class (or one of its subclasses):

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 { }

I managed to work around this restriction by adding a static method to the base class, since Base's methods are allowed to access Base.Member, and MyDerived can call that static method.

I still don't understand the reason for this limitation, though. I've seen a couple different explanations, but they fail to explain why MyDerived.Test() is still allowed to access MySuperDerived.Member.

The Principled Explanation: 'Protected' means it's only accessible to that class and its subclasses. YourDerived could override Member(), creating a new method that should only be accessible to YourDerived and its subclasses. MyDerived can't call t开发者_Python百科he overridden yd.Member() because it's not a subclass of YourDerived, and it can't call b.Member() because b might actually be an instance of YourDerived.

OK, but then why can MyDerived call msd.Member()? MySuperDerived could override Member(), and that override should only be accessible to MySuperDerived and its subclasses, right?

You don't really know until runtime whether you're calling an overridden member or not. And when the member is a field, it can't be overridden anyway, but access is still forbidden.

The Pragmatic Explanation: Other classes might add invariants that your class doesn't know about, and you must use their public interface so they can maintain those invariants. If MyDerived could directly access protected members of YourDerived, it could break those invariants.

My same objection applies here. MyDerived doesn't know what invariants MySuperDerived might add, either -- it might be defined in a different assembly by a different author -- so why can MyDerived access its protected members directly?

I get the impression that this compile-time limitation exists as a misguided attempt to solve a problem that can really only be solved at runtime. But maybe I'm missing something. Does anyone have an example of a problem that would be caused by letting MyDerived access Base's protected members through a variable of type YourDerived or Base, but does not exist already when accessing them through a variable of type MyDerived or MySuperDerived?

--

UPDATE: I know the compiler is just following the language specification; what I want to know is the purpose of that part of the spec. An ideal answer would be like, "If MyDerived could call YourDerived.Member(), $NIGHTMARE would happen, but that can't happen when calling MySuperDerived.Member() because $ITSALLGOOD."


UPDATE: This question was the subject of my blog in January 2010. Thanks for the great question! See:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/14/why-cant-i-access-a-protected-member-from-a-derived-class-part-six/


Does anyone have an example of a problem that would be caused by letting MyDerived access Base's protected members through a variable of type YourDerived or Base, but does not exist already when accessing them through a variable of type MyDerived or MySuperDerived?

I am rather confused by your question but I am willing to give it a shot.

If I understand it correctly, your question is in two parts. First, what attack mitigation justifies the restriction on calling protected methods through a less-derived type? Second, why does the same justification not motivate preventing calls to protected methods on equally-derived or more-derived types?

The first part is straightforward:

// 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. 
  }
}

Dr. Evil's attempt to steal ONE... MILLION... DOLLARS... from your swiss bank account has been foiled by the C# compiler.

Obviously this is a silly example, and obviously, fully-trusted code could do anything it wants to your types -- fully-trusted code can start up a debugger and change the code as its running. Full trust means full trust. Don't actually design a real security system this way!

But my point is simply that the "attack" that is foiled here is someone attempting to do an end-run around the invariants set up by SecureBankAccount, to access the code in SwissBankAccount directly.

That answers your first question, I hope. If that's not clear, let me know.

Your second question is "Why doesn't SecureBankAccount also have this restriction?" In my example, SecureBankAccount says:

    this.DoTransfer(destinationAccount, user, amount);

Clearly "this" is of type SecureBankAccount or something more derived. It could be any value of a more derived type, including a new SwissBankAccount. Couldn't SecureBankAccount be doing an end-run around SwissBankAccount's invariants?

Yes, absolutely! And because of that, the authors of SwissBankAccount are required to understand everything that their base class does! You can't just go deriving from some class willy-nilly and hope for the best! The implementation of your base class is allowed to call the set of protected methods exposed by the base class. If you want to derive from it then you are required to read the documentation for that class, or the code, and understand under what circumstances your protected methods will be called, and write your code accordingly. Derivation is a way of sharing implementation details; if you don't understand the implementation details of the thing you are deriving from then don't derive from it.

And besides, the base class is always written before the derived class. The base class isn't up and changing on you, and presumably you trust the author of the class to not attempt to break you sneakily with a future version. (Of course, a change to a base class can always cause problems; this is yet another version of the brittle base class problem.)

The difference between the two cases is that when you derive from a base class, you have the behaviour of one class of your choice to understand and trust. That is a tractable amount of work. The authors of SwissBankAccount are required to precisely understand what SecureBankAccount guarantees to be invariant before the protected method is called. But they should not have to understand and trust every possible behaviour of every possible cousin class that just happens to be derived from the same base class. Those guys could be implemented by anyone and do anything. You would have no ability whatsoever to understand any of their pre-call invariants, and therefore you would have no ability to successfully write a working protected method. Therefore, we save you that bother and disallow that scenario.

And besides, we have to allow you to call protected methods on receievers of potentially more-derived classes. Suppose we didn't allow that and deduce something absurd. Under what circumstances could a protected method ever be called, if we disallowed calling protected methods on receivers of potentially-more-derived classes? The only time you could ever call a protected method in that world is if you were calling your own protected method from a sealed class! Effectively, protected methods could almost never be called, and the implementation that was called would always be the most derived one. What's the point of "protected" in that case? Your "protected" means the same thing as "private, and can only be called from a sealed class". That would make them rather less useful.

So, the short answer to both your questions is "because if we didn't do that, it would be impossible to use protected methods at all." We restrict calls through less-derivedtypes because if we don't, it's impossible to safely implement any protected method that depends on an invariant. We allow calls through potential subtypes because if we do not allow this, then we don't allow hardly any calls at all.

Does that answer your questions?


Eric Lippert has explained it well in one of his blog posts.


You seem to be thinking about this completely the wrong way.

It's not about "who can call what", it's about what is substitutable where.

Subclasses of MyDerived should always be substitutable for MyDerived (including their overridden protected methods). There is no such constraint on other subclasses of Base, and so you cannot substitute them in place of a MyDerived.


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

A protected member of a base class is accessible in a derived class only if the access occurs through the derived class type.

There's documentation of the "what?" question. Now I wish I knew "Why?" :)

Clearly virtual has nothing to do with this access restriction.

Hmm, I think you're on to something with the sibling thing... MyDerived shouldn't be able to call YourDerived.Member

If MyDerived can call Base.Member, it might actually be working on an instance of YourDerived and might actually be calling YourDerived.Member

Ah, here is the same question: C# protected members accessed via base class variable


"Protected" means exactly that a member is accessible only to the defining class and all subclasses.

As MySuperDerived is a subclass of MyDerived, Member is accessible to MyDerived. Think of it this way: MySuperDerived is a MyDerived and therefore its private and protected members (inherited from MyDerived) are accessible to MyDerived.

However, YourDerived is not a MyDerived and therefore its private and protected members are inaccessible to MyDerived.

And you can't access Member on an instance of Base because Base might be a YourDerived which is not a MyDerived nor a subclass of MyDerived.

And don't do that using static methods to permit access thing. That's defeating the purpose of encapsulation and is a big smell that you haven't designed things properly.


You're operating on the assumption that this behaviour is explicitly disallowed on account of some vague notion of design purity. I don't work for Microsoft, but I believe that the truth is much simpler: it's not forbidden, it's just not supported, because that would be time-consuming to implement for relatively low impact.

The little-used protected internal will probably cover off the majority of cases where protected alone doesn't quite cut it.

0

精彩评论

暂无评论...
验证码 换一张
取 消