Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

protected

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 the 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."

like image 598
Jesse McGrew Avatar asked Dec 15 '09 01:12

Jesse McGrew


People also ask

Can child class parents access protected members?

Protected members in a class are similar to private members as they cannot be accessed from outside the class. But they can be accessed by derived classes or child classes while private members cannot.

What happen to protected members of the parent class in child class?

Classes that extends the child class still sees the protected members of the parent class. That's because they are "subclasses of the subclass" and the protected members do not "become private" for to the child class as far as the child-of-child classes are concerned; the members remain protected for them.

Who can access a protected class?

Protected members that are also declared as static are accessible to any friend or member function of a derived class. Protected members that are not declared as static are accessible to friends and member functions in a derived class only through a pointer to, reference to, or object of the derived class.

Can protected members of a class be inherited?

protected inheritance makes the public and protected members of the base class protected in the derived class. private inheritance makes the public and protected members of the base class private in the derived class.


2 Answers

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?

like image 155
Eric Lippert Avatar answered Oct 23 '22 14:10

Eric Lippert


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

like image 30
Hans Passant Avatar answered Oct 23 '22 15:10

Hans Passant