Поделиться через


Why is deriving a public class from an internal class illegal?

In C# it is illegal to declare a class D whose base class B is in any way less accessible than D. I'm occasionally asked why that is. There are a number of reasons; today I'll start with a very specific scenario and then talk about a general philosophy.

Suppose you and your coworker Alice are developing the code for assembly Foo, which you intend to be fully trusted by its users. Alice writes:

public class B
{
  public void Dangerous() {...}
}

And you write

public class D : B
{
... other stuff ...
}

Later, Alice gets a security review from Bob, who points out that method Dangerous could be used as a component of an attack by partially-trusted code, and who further points out that customer scenarios do not actually require B to be used directly by customers in the first place; B is actually only being used as an implementation detail of other classes. So in keeping with the principle of least privilege, Alice changes B to:

internal class B
{
  public void Dangerous() {...}
}

Alice need not change the accessibility of Dangerous, because of course "public" means "public to the people who can see the class in the first place".

So now what should happen when Alice recompiles before she checks in this change? The C# compiler does not know if you, the author of class D, intended method Dangerous to be accessible by a user of public class D. On the one hand, it is a public method of a base class, and so it seems like it should be accessible. On the other hand, the fact that B is internal is evidence that Dangerous is supposed to be inaccessible outside the assembly. A basic design principle of C# is that when the intention is unclear, the compiler brings this fact to your attention by failing. The compiler is identifying yet another form of the Brittle Base Class Failure, which long-time readers know has shown up in numerous places in the design of C#.

Rather than simply making this change and hoping for the best, you and Alice need to sit down and talk about whether B really is a sensible base class of D; it seems plausible that either (1) D ought to be internal also, or (2) D ought to favour composition over inheritance. Which brings us to my more general point:

More generally: the inheritance mechanism is, as we've discussed before, simply the fact that all heritable members of the base type are also members of the derived type. But the inheritance relationship semantics are intended to model the "is a kind of" relationship. It seems reasonable that if D is a kind of B, and D is accessible at a location, then B ought to be accessible at that location as well. It seems strange that you could only use the fact that "a Giraffe is a kind of Animal" at specific locations.

In short, this rule of the language encourages you to use inheritance relationships to model the business domain semantics rather than as a mechanism for code reuse.

Finally, I note that as an alternative, it is legal for a public class to implement an internal interface. In that scenario there is no danger of accidentally exposing dangerous functionality from the interface to the implementing type because of course the interface is not associated with any functionality in the first place; an interface is logically "abstract". Implementing an internal interface can be used as a mechanism that allows public components in the same assembly to communicate with each other over "back channels" that are not exposed to the public.

Comments

  • Anonymous
    November 13, 2012
    The comment has been removed

  • Anonymous
    November 13, 2012
    What I have always wondered was why public members are allowed on internal classes! If one was to prevent internal classes from declaring public members then the restriction on public intheritance of internal classes would not present these problems.

  • Anonymous
    November 13, 2012
    It's unfortunate that inheritance is the only real mechanism for code reuse that C# provides. I dare say that if there were other means available, inheritance wouldn't have to be [ab]used so much.

  • Anonymous
    November 13, 2012
    Gabe - inheritance is not the only mechanism for code re-use. You have generics / extension methods these are quite capable of enforcing DRY in a none inherited way.

  • Anonymous
    November 13, 2012
    I think that one of the most confusing parts of thinking about inheritance is that the term itself is overloaded.  There is in fact no analog between software inheritance and the real-world inheritance models that are often used to explain it.  For instance, a giraffe does not inherit from some more generic animal class.  A giraffe is a giraffe, and when a new giraffe is "compiled", it is a run-time instance that inherits from two run-time instances of its own class, namely its mother and father, who are themselves giraffes.  In point of fact, the notion of a "giraffe" or even of an "animal" is merely a convenient abstraction invented by the human mind to extrapolate similarities between different instances of species as a classification mechanism.  This extrapolation is performed ex post facto, not at design time.  There is no such thing as a physical "animal" as a base class for any species, nor is there some genetic code mixture that could be used to create a generic animal.  So trying to think about inheritance using real-world models necessarily leads to mis-interpretation of the concept and its proper use in software development.  This is all a long winded and pedantic way of saying that I think software inheritance is easier to understand if we stick to conceptual software models to explain it, rather than trying to extend the concept into the type of run-time inheritance that exists in the real, physical world, which seems to me to be conceptually closer to dependency injection than inheritance, although that is a really imperfect analog itself. I agree. See my earlier blog post on this subject, in which I make several of your points. -- Eric

  • Anonymous
    November 13, 2012
    Ah, great minds.....   ;) And fools never differ. :-) -- Eric

  • Anonymous
    November 15, 2012
    Are internal class C { public void M() { [...] } } and  internal class C { internal void M() { [...] } } always equivalent or are there some corner cases?

  • Anonymous
    November 15, 2012
    Walt Smith: While the analogy breaks down eventually (as all do), it's hard to imagine a better way to understand inheritance than relating it to different species (even if the word "inheritance" has different meanings). Even my 3-year-old understands that giraffes and zebras are both animals, yet different species. She also understands that trees have a "bark" and dogs have a "bark", yet they're two totally unrelated things that just happen to have the same name. I'm curious what sort of conceptual software model you use to explain things, because I have a hard time imagining something could be easier to understand than a simple animal hierarchy.

  • Anonymous
    November 20, 2012
    @Walt Smith "There is no such thing as a physical "animal" as a base class for any species, nor is there some genetic code mixture that could be used to create a generic animal." That's why that class is abstract.

  • Anonymous
    November 26, 2012
    Excellent article. Thank you! :)