Inheritance and Representation

Inheritance and Representation

Rate This
  • Comments 12

(Note: Not to be confused with Representation and Identity)

Here's a question I got this morning:

class Alpha<X>
  where X : class
{}
class Bravo<T, U>
  where T : class
  where U : T
{
  Alpha<U> alpha;
}

This gives a compilation error stating that U cannot be used as a type argument for Alpha's type parameter X because U is not known to be a reference type. But surely U is known to be a reference type because U is constrained to be T, and T is constrained to be a reference type. Is the compiler wrong?

Of course not. Bravo<object, int> is perfectly legal and gives a type argument for U which is not a reference type. All the constraint on U says is that U must inherit from T (*). int inherits from object, so it meets the constraint. All struct types inherit from at least two reference types, and some of them inherit from many more. (Enum types inherit from System.Enum, many struct types implement interface types, and so on.)

The right thing for the developer to do here is of course to add the reference type constraint to U as well.

That easily-solved problem got me thinking a bit more deeply about the issue. I think a lot of people don't have a really solid understanding of what "inheritance" means in C#. It is really quite simple: a derived type which inherits from a base type implicitly has all inheritable members of the base type. That's it! If a base type has a member M, then a derived type has a member M as well. (**)

People sometimes ask me if private members are inherited; surely not! What would that even mean? But yes, private members are inherited, though most of the time it makes no difference because the private member cannot be accessed outside of its accessibility domain. However, if the derived class is inside the accessibility domain then it becomes clear that yes, private members are inherited:

class B
{
  private int x;
  private class D : B
  {

D inherits x from B, and since D is inside the accessibility domain of x, it can use x no problem.

I am occasionally asked "but how can a value type, like int, which is 32 bits of memory, no more, no less, possibly inherit from object?  An object laid out in memory is way bigger than 32 bits; it's got a sync block and a virtual function table and all kinds of stuff in there."  Apparently lots of people think that inheritance has something to do with how a value is laid out in memory. But how a value is laid out in memory is an implementation detail, not a contractual obligation of the inheritance relationship! When we say that int inherits from object, what we mean is that if object has a member -- say, ToString -- then int has that member as well. When you call ToString on something of compile-time type object, the compiler generates code which goes and looks up that method in the object's virtual function table at runtime. When you call ToString on something of compile-time type int, the compiler knows that int is a sealed value type that overrides ToString, and generates code which calls that function directly. And when you box an int, then at runtime we do lay out an int the same way that any reference-typed object is laid out in memory.

But there is no requirement that int and object be always laid out the same in memory just because one inherits from the other; all that is required is that there be some way for the compiler to generate code that honours the inheritance relationship.

------------

(*) or be identical to T, or possibly to inherit from a type related to T by some variant conversion.

(**) Of course that's not quite it; there are some odd corner cases. For example, a class which "inherits" from an interface must have an implementation of every member of that interface, but it could do an explicit interface implementation rather than exposing the interface's members as its own members. This is yet another reason why I'm not thrilled that we chose the word "inherits" over "implements" to describe interface implementations. Also, certain members like destructors and constructors are not inheritable.

  • ... or Bravo<IComparable, int>

  • "Accessibility domain" is a nice way of referring to it.

  • Memory layout is an implementation detail. But whether a type is a reference type or a value type is not. And I think it's quite confusing that value type inherits a reference type.

    (I'm not saying this was a bad decision, just that it can be confusing.)

  • >>Derived type which inherits from a base type implicitly has all inheritable members of the base type.

    That is what Cardelli and Wegner stated as:

    "{a1:t 1, .. ,an:tn, .. ,am:tm } ≤ {a 1:u1, ..,an:un }

    iff ti ≤ ui for i ∈ 1..n.

    i.e., a record type A is a subtype of another record type B if A has all the attributes (fields) of B, and possibly

    more, and the types of the common attributes are respectively in the subtype relation."

  • "Is the compiler wrong? Of course not."

    This made me smile.

  • (sorry if this double posts, it looks like the first one got eaten)

    I am confused by the "constructors are not inheritable" statement.  I always think of them as being inherited in the sense that the base class's constructor must be satisfied by derived classes.  For example:

    public class Foo { public Foo(int i){}  }

    public class Bar : Foo{

    public Bar(int i) : base(i){} // ok

    public Bar(): base(0){  } // ok

    public Bar(){} // illegal since base c-tor isn't satisfied

    }

    Foo's constructor definitely is not inherited in the same sense as a method, but I still think of it as being inherited.  Most likely, this is just an example of my mental model being out of sync with reality.

  • (sorry if this double posts, it looks like the first one got eaten)

    I completely agree with "implements" being a better term for talking about interfaces in relation to classes then is "inherits". I use "implements when talking about classes (here goes) implementing a certain interface.

    @Chris B: Take a look at it the other way around:

    public class Foo

    {

    public Foo() { }

    public Foo(int value) { }

    }

    public class Bar : Foo

    {

    public Bar() : base() { }

    }

    You can't call new Bar(1) as it does define a constructor that matches that signature. It didn't inherit any constructor.

  • @Johnathan,

    Sure, I have to explicitly define the constructor for Bar(int) if I need one, but I do have access to the Foo(int) constructor from Bar. It is natural that no Foo constructor can initialize a new instance of Bar (not all Foos are Bars), but all Bar constructors must be capable of initializing Foos (all Bars are Foos).  Therefore, all Foo constructors must accessible from any Bar constructor.  I guess I am having trouble differentiating the inheritance of a protected method from the inheritance of a constructor.

    For example:

    public class Foo {

    protected Foo(int i) { }  

    protected void M() { }

    }

    public class Bar : Foo{

    public Bar(int i) : base(i) { }

    public Bar(): this(0) {  }

    public void M2(){ base.M(); }

    }

    Are both M and Foo(int) not implicitly defined on Bar in this case?

  • Glad you mentioned the sync block... value types don't conform to the public interface of System.Object (they can't be used with the `lock` statement) so they aren't really subtypes of System.Object.

    Unless and until they get boxed, anyway.

    I seem to recall that one of your earlier posts mentioned that none of "inherits", "implements", "derives from", and "satisfies a where constraint" include LSP-substitutability.  When you redefine terms, almost anything is possible, and even the sort of reasoning this blog post is based on breaks down.

  • Except that lock isn't part of Object's public interface.  There is no method or other accessible member of Object that relates to the lock statement in C#.  Rather, lock is syntactic sugar for calling Monitor.Enter and Monitor.Exit wrapped into a try/finally block.  Technically, you could call Monitor.Enter and pass in a value type, but it's not very useful, which I assume is why C# doesn't allow you to use it in the lock statement.

  • Java is OK - inner class D has two x members: inaccessible inherited from B and accessible in outer class. :)

  • Forgive me if this is a double post.  I don't know if my first post got eaten, just like Chris B's and Jonathan van de Veen's, or if it's just awaiting moderation, but I see no indication that it was successfully submitted.

    ----------

    >> A derived type which inherits from a base type implicitly has all inheritable members of the base type. That's it!

    Well, no, not exactly.  I'd say it's more accurate (and more important) to say that a derived type that inherits from a base can be implicitly used as a base type.  Inheriting the base's members is kind of a side effect of that.

    Basically, you've described composition ("has-a" relationships), not inheritance ("is-a" relationships).  A derived type must maintain its base's composition, but only so that it will still be usable as its base.

    A car, for instance, has wheels, doors, and an engine.  All cars have these traits (for the sake of discussion, we'll ignore Jeeps on the beach, "yard-cars" in the South, and Doc Brown's Delorean in 2015).  A sports car, therefore, has wheels, doors, and an engine, as do luxury cars and family sedans.  They each share these traits, because they all share the same is-a relationship with the base class "car".  Someone who needs to use a car doesn't need specific instructions to use a sports car, a luxury car, or a family sedan, they only need to know how to use a car, and any of these types of cars will work for them.

    When stressing the "is-a" relationship of inheritance, then it does indeed make it surprising that "where T : class" is combined with "where U : T" (read ":" as "is a"), that it doesn't follow that "where U : class" is implicit.  The fact that it doesn't is the result of two decisions (and I'm not necessarily criticizing the decisions themselves) that are confusing to newbies:

    * Value types nominally derive from reference types.  Value types *aren't* (not is-a) reference types, not the way a family sedan *is a* car, but they can be implicitly *converted* to reference types.

    * In a generic constraint, "class" actually means "reference type", and an interface is a reference type.  Except that an interface is less like a type and more like a contract that can be, as you said, implemented by either a reference type or a value type.  You could say that interfaces provide more of a "does-a" relationship than an "is-a" relationship.  But we don't have a separate concept in C# for implementing a "does-a" relationship, other than pretending it's the same as inheriting an "is-a" relationship.  So we treat interfaces like types instead of contracts, and, using the same implicit conversion as above, treat an interface type as a reference type, even if it's implemented by a value type.

Page 1 of 1 (12 items)