Bill Carey
Bill Carey

Reputation: 1415

Is it possibly to impose type constraints on a generic member of a class in an inheritance hierarchy?

I'm writing an application in C#, and am wrestling with its implementation of generics. I have an inheritance hierarchy that is mirrored by another inheritance hierarchy (Models and View Models) like so:

class A_Content { }

class B_Content : A_Content
{
    public string Bar;
}

class C_Content : A_Content
{
    public string Foo;
}

class A { public A_Content content; }
class B : A { }
class C : A { }

public class Test
{
    IList<A> A_Collection = new List<A>();

    public Test()
    {
        B b = new B();
        C c = new C();

        b.content = new B_Content();
        c.content = new C_Content();

        A_Collection.Add(b);
        A_Collection.Add(c);
    }
}

This works well enough, but doesn't enforce any type constraints on content, which leaves me casting it to the proper derived class every time I want to use it. I'd like to coax the compiler into enforcing the constraint that B objects only have B_Content content. My first cut at that was:

class A_Content { }

class B_Content : A_Content
{
    public string Bar;
}

class C_Content : A_Content
{
    public string Foo;
}

class A { }
class B : A { B_Content content; }
class C : A { C_Content content; }

public class Test
{
    IList<A> A_Collection = new List<A>();

    public Test()
    {
        B b = new B();
        C c = new C();

        A_Collection.Add(b);
        A_Collection.Add(c);
    }
}

This works nicely, but means that I can't access the common elements of content when all I have is a collection of As. What I'd really like to do is something like:

abstract class A_Content { }

class B_Content : A_Content
{
    public string Bar;
}

class C_Content : A_Content
{
    public string Foo;
}

abstract class A<T> { T content; }
class B : A<B_Content> { }
class C : A<C_Content> { }

public class Test {
    IList<A<A_Content>> A_Collection = new List<A<A_Content>>();

    public Test()
    {
        B b = new B();
        C c = new C();

        A_Collection.Add(b);
        A_Collection.Add(c);
    }
}

This, however, produces an error complaining that B cannot be implicitly converted into an A. I've tried adding an explicit cast to no avail. Is there some way to express the constraints I'm looking for more elegantly than the second model?

Upvotes: 0

Views: 275

Answers (4)

Tigran
Tigran

Reputation: 62286

Hope I right undertsood your intention, so

having a collection like this

IList<A> means that you would like to have a collection of A objects with different implementation scenarios.

That property if the property of a base type. That means that base type has to expose methods/properties => so state and behavior primitives which the child classes has to make a concrete implementation.

Something like this:

class A_Content {  public virtual string Bar {get;set;} }

class B_Content : A_Content
{
    public override string Bar {get;set;};
}

class C_Content : A_Content
{
    public override string Bar {get;set};
}

and somewhere in the code:

public Test()
{
      B b = new B();
      C c = new C();

      A_Collection.Add(b);
      A_Collection.Add(c);

      //so
      A_Collection[0].Bar // B::Bar 
      A_Collection[1].Bar //C::Bar 

}

And you do not need to cast to real object. Simple OOP approach.

Upvotes: 0

phoog
phoog

Reputation: 43056

You can use an interface for this, along with explicit implementation of the interface's member(s):

abstract class A_Content {}
class B_Content : A_Content {}
class C_Content : A_Content {}

interface IA
{
    A_Content content { get; }
}

abstract class A<T> : IA
    where T : A_Content
{
    T content;
    A_Content.content { get { return this.content; } }
}

class B : A<B_Content> {}
class C : A<C_Content> {}

Then you can make a List<IA> to hold a homogeneous collection of B and C objects.

In fact, with C# 4 and higher, you could make the interface generic and covariant; then you can implement the interface implicitly (as long as you use a property rather than a field):

interface IA<out T>
{
    T content { get; }
}

abstract class A<T> : IA<T>
    where T : A_Content
{
    T content { get; set; }
}

class B : A<B_Content> {}
class C : A<C_Content> {}

Now, B still cannot be converted to A<A_Content>, but it can be converted to IA<A_Content>, so you can use a List<IA<A_Content>> to hold your homogeneous collection of objects.

Upvotes: 3

Adam Robinson
Adam Robinson

Reputation: 185663

It's not entirely clear what you're after. Are you trying to make it so that every instance of A has a Content property whose type is A_Content, every B has a Content property that's a B_Content, and so on? If so, you can't do that and have B/C/etc. inherit from A. (not in a non-smelly way, anyway). The signature of A says that the Content property should be able to get (and, presumably, set) any valid value of A_Content. You cannot change the return type of a function or the type of a property or field in a derived class. You could use generics to basically defer the typing of the property all the way down to the usage of the class, but that syntax will be ugly and I'm not certain what it gets you.

For example, you could do this:

public class A<TContent> where TContent : A_Content
{
    public TContent Content { get; set; }
} 

public class B<TContent> : A<TContent> where TContent : B_Content
{
   // nothing here, as the property is already defined above in A
}

public class C<TContent> : A<TContent> where TContent : C_Content
{
   // nothing here, as the property is already defined above in A
}

But this means two things:

  • Anywhere you use A, B, or C you must specify the actual type of TContent (so A_Content, B_Content, etc.). Which is a pain
  • There is absolutely nothing stopping you from doing something like A<B_Content> (which is, in fact, essentially what B is in this case, since we've added nothing to the class).

In short, I think you need to drop back and punt and come up with a new design.

By the way

The reason your second example doesn't fly (with the List) is because you've told the list that it needs to contain A<A_Content>. Since B<B_Content> doesn't satisfy that, it won't work. This is a typical variance question and it confuses a lot of people. But consider this scenario (this code will not compile; it's intended to be demonstrative of the underlying reason):

List<A<A_Content>> list = new List<A<A_Content>>();

list.Add(new B()); // this seems OK so far, right? 

A<A_Content> foo = list[0];

foo.content = new A_Content():

This would obviously break, since foo in reality is a B<B_Content>, so the runtime wouldn't let you set content equal to anything other than an instance of B_Content (or something that inherits from it), but the signature of the class means you should be able to assign anything that'sA_Content` or inherits from it.

Upvotes: 4

Krizz
Krizz

Reputation: 11552

Well, compiler produces an error, because indeed B cannot be converted into A<A_Content>. This is because A<A_Content> is not a superclass of B. The parent class of B class is A<B_Content>.

I am afraid you need to stick to casting. It is needed here, because you have list of As. If you really want to avoid casting (I am not sure why you would like to), you can try with dynamic dispatch.

You can try creating a List<dynamic> instead of List<A>. You will need at least C# 4.0, though.

Upvotes: 1

Related Questions