views:

116

answers:

3

I'm struggling with some Generic constraint issues when trying to implement a library that allows inheritance and hoping someone can help.

I'm trying to build up a class library that has 3 flavours to it, each building on top of the other. To me it seemed like a perfect opportunity to use Generics as I can't quite do what I want through pure inheritance. The code's below (this should paste straight into VS) with some explanation afterwards:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    #region Base Classes

    public class GenericElement { }

    /// <summary>Visit to a GenericElement</summary>
    public class Generic_Visit<E> where E : GenericElement
    {
        public E Element { get; set; }
    }

    /// <summary>Collection of Visits</summary>
    public class Generic_Route<V, E>
        where V : Generic_Visit<E>
        where E : GenericElement
    {
        public List<V> Visits { get; set; }
        public Double Distance { get; set; }
    }

    /// <summary>Collection of Routes</summary>
    public class Generic_Solution<R, V, E>
        where R : Generic_Route<V, E>
        where V : Generic_Visit<E>
        where E : GenericElement
    {
        public List<R> Routes { get; set; }

        public Double Distance
        {
            get
            {
                return this.Routes.Select(r => r.Distance).Sum();
            }
        }
    }

    #endregion

    #region TSP Classes

    public class Concrete_TSPNode : GenericElement { }

    public abstract class Generic_TSPVisit<E> : Generic_Visit<E>
        where E : Concrete_TSPNode
    {
        public Double Time { get; set; }
    }

    public abstract class Generic_TSPRoute<V, E> : Generic_Route<V, E>
        where V : Concrete_TSPVisit
        where E : Concrete_TSPNode
    {
        public Double Time
        {
            get
            {
                return this.Visits.Select(v => v.Time).Sum();
            }
        }
    }

    public abstract class Generic_TSPSolution<R, V, E> : Generic_Solution<R, V, E>
        where R : Concrete_TSPRoute
        where V : Concrete_TSPVisit
        where E : Concrete_TSPNode
    {
        public Double Time
        {
            get
            {
                return this.Routes.Select(r => r.Time).Sum();
            }
        }
    }

    public class Concrete_TSPVisit : Generic_TSPVisit<Concrete_TSPNode> { }

    public class Concrete_TSPRoute : Generic_TSPRoute<Concrete_TSPVisit, Concrete_TSPNode> { }

    public class Concrete_TSPSolution : Generic_TSPSolution<Concrete_TSPRoute, Concrete_TSPVisit, Concrete_TSPNode> { }

    #endregion

    #region VRP

    public class Concrete_VRPNode : Concrete_TSPNode { }

    public abstract class Generic_VRPVisit<E> : Generic_TSPVisit<E> where E : Concrete_VRPNode
    {
        public Double Capacity { get; set; }
    }

    public abstract class Generic_VRPRoute<V, E> : Generic_TSPRoute<V, E>
        where V : Concrete_VRPVisit
        where E : Concrete_VRPNode
    {
        public Double Capacity
        {
            get
            {
                return this.Visits.Select(v => v.Capacity).Sum();
            }
        }
    }

    public abstract class G_VRPSolution<R, V, E> : Generic_TSPSolution<R, V, E>
        where R : Concrete_VRPRoute
        where V : Concrete_VRPVisit
        where E : Concrete_VRPNode
    {
        public Double Capacity
        {
            get
            {
                return this.Routes.Select(r => r.Capacity).Sum();
            }
        }
    }

    public class Concrete_VRPVisit : Generic_VRPVisit<Concrete_VRPNode> { }

    public class Concrete_VRPRoute : Generic_VRPRoute<Concrete_VRPVisit, Concrete_VRPNode> { }

    public class Concrete_VRPSolution : Generic_TSPSolution<Concrete_VRPRoute, Concrete_VRPVisit, Concrete_VRPNode> { }

    #endregion
}

The idea behind the code is that there are a set of base classes that expose properties using Generics. This allows me to have strongly typed collections for example.

There are then 2 of the 3 stages build upon these, TSP and VRP in the example which have 4 concrete classes (these are what the developer using the class library should be interacting with as the generic constraints just get a bit crazy) - Element, Visit, Route and Solution.

There are also some classes prefixed Generic for both TSP and VRP. These allow the inheritance that I want as it exposes the Generic Types. If I don't use these (say Concrete_VRPRoute inherits Concrete_TSPRoute) then I have to keep casting the type of item returned by the Visits collection to get the Capacity property for example.

I'm fairly confident all the types line up correctly, but when I try to build I get the following errors and I really don't know how to tackle them.

Error 1 The type 'V' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_Route'. There is no implicit reference conversion from 'V' to 'Test.Generic_Visit'.

Error 2 The type 'V' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_Solution'. There is no implicit reference conversion from 'V' to 'Test.Generic_Visit'.

Error 3 The type 'R' cannot be used as type parameter 'R' in the generic type or method 'Test.Generic_Solution'. There is no implicit reference conversion from 'R' to 'Test.Generic_Route'

Error 4 The type 'V' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_TSPRoute'. There is no implicit reference conversion from 'V' to 'Test.Concrete_TSPVisit'.

Error 5 The type 'V' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_TSPSolution'. There is no implicit reference conversion from 'V' to 'Test.Concrete_TSPVisit'.

Error 6 The type 'R' cannot be used as type parameter 'R' in the generic type or method 'Test.Generic_TSPSolution'. There is no implicit reference conversion from 'R' to 'Test.Concrete_TSPRoute'.

Error 7 The type 'Test.Concrete_VRPVisit' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_TSPSolution'. There is no implicit reference conversion from 'Test.Concrete_VRPVisit' to 'Test.Concrete_TSPVisit'.

Error 8 The type 'Test.Concrete_VRPRoute' cannot be used as type parameter 'R' in the generic type or method 'Test.Generic_TSPSolution'. There is no implicit reference conversion from 'Test.Concrete_VRPRoute' to 'Test.Concrete_TSPRoute'.'Test.Concrete_TSPRoute'.

+2  A: 

OK, let’s examine the first one. The error is:

The type 'V' cannot be used as type parameter 'V' in the generic type or method 'Test.Generic_Route'. There is no implicit reference conversion from 'V' to 'Test.Generic_Visit'.

It complains about this declaration:

public abstract class Generic_TSPRoute<V, E> : Generic_Route<V, E>
    where V : Concrete_TSPVisit
    where E : Concrete_TSPNode

This establishes two definitions:

  • V is a Concrete_TSPVisit (or a descendent of it)

  • E is a Concrete_TSPNode (or a descendent of it)

Now let’s see what Generic_Route<V, E> lets us put in:

public class Generic_Route<V, E>
    where V : Generic_Visit<E>
    where E : GenericElement

The second constraint is fine because Concrete_TSPNode is a GenericElement. The first one is problematic: Remember that E is a Concrete_TSPNode or a descendent of it, therefore Generic_Visit<E> could be:

  • Generic_Visit<Concrete_TSPNode>, or

  • Generic_Visit<some subclass of Concrete_TSPNode>

However, we also know from earlier that V is a Concrete_TSPVisit (or a descendent of it).

  • Concrete_TSPVisit inherits from Generic_TSPVisit<Concrete_TSPNode>

  • Generic_TSPVisit<Concrete_TSPNode> inherits from Generic_Visit<Concrete_TSPNode>

Notice something? This requires it to be a Generic_Visit<Concrete_TSPNode>. It is emphatically not allowed to be Generic_Visit<some subclass of Concrete_TSPNode>.

In other words, imagine I write this:

var route = new Generic_TSPRoute<Concrete_TSPVisit, Concrete_TSPNode_Subclass>();

According to your hierarchy, Concrete_TSPVisit is a Generic_Visit<Concrete_TSPNode> and therefore has a property that looks like

public Concrete_TSPNode Element { get; set; }

If I retrieve a value from this property, it is only guaranteed to be a Concrete_TSPNode but not necessarily a Concrete_TSPNode_Subclass.

EDIT:

I’ll leave this answer because it explains the reason for the compiler error, but Enigmativity’s answer actually provides the solution to the problem.

Timwi
@Timwi : Thanks for the comprehensive answer. That does help me see the problem (I've never quite understood the flavours of variance, I should read Eric Lipperts blog more closely). Could you explain the 'setter/getter' problem about being read only? I might be missing something obvious... but can't see why using an interface will limit me to being read only?
Ian
@Ian: Using an interface doesn’t limit you to being read-only. Using *co-variance* does. For example, an `IEnumerable<string>` can double as an `IEnumerable<object>` because `IEnumerable<T>` is co-variant in `T`, which it can be because it is read-only (`T` only occurs as a return type). An `IComparer<object>` can double as an `IComparer<string>` because it is contra-variant in `T`, which it can be because it is write-only (`T` only occurs as a parameter type).
Timwi
@Ian: P.S. I just added some more to my answer to explain the problem when `Generic_Visit<E>` has both a setter and a getter.
Timwi
@Timwi: Ok, the mud is starting to clear a little. I'll do some reading up. It's looking like I'm not going to be able to get this working though by the sounds of it...?
Ian
No need for co-variance or contra-variance. Generics introduced in .NET 2.0 works just fine. You just need a recursive generic definition. http://stackoverflow.com/questions/3626937/how-to-use-inheritance-when-using-generic-constraints/3627284#3627284 :-)
Enigmativity
This is a really interesting answer. I'm tryign to get my head around it all and its making me realise that what I've done with generics is really basic. I'd +1 you but I don't think I should do that to an answer I don't actually understand. :)
Chris
+1 it for comprehensiveness instead then Chris
Ian
+5  A: 

It's a piece of generic cake. You need to define the generic classes in terms of themselves. A recursive generic definition.

Base Classes:

public class Generic_Element<E>
    where E : Generic_Element<E>
{
}

/// <summary>Visit to a Generic_Element</summary>
public class Generic_Visit<V, E>
    where V : Generic_Visit<V, E>
    where E : Generic_Element<E>
{
    public E Element { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Route<R, V, E>
    where R : Generic_Route<R, V, E>
    where V : Generic_Visit<V, E>
    where E : Generic_Element<E>
{
    public List<V> Visits { get; set; }
    public Double Distance { get; set; }
}

/// <summary>Collection of Routes</summary>
public class Generic_Solution<S, R, V, E>
    where S : Generic_Solution<S, R, V, E>
    where R : Generic_Route<R, V, E>
    where V : Generic_Visit<V, E>
    where E : Generic_Element<E>
{
    public List<R> Routes { get; set; }

    public Double Distance
    {
        get
        {
            return this.Routes.Select(r => r.Distance).Sum();
        }
    }
}

TSP Classes:

public class Generic_Tsp_Element<E> : Generic_Element<E>
where E : Generic_Tsp_Element<E>
{
}

/// <summary>Visit to a Generic_Element</summary>
public class Generic_Tsp_Visit<V, E> : Generic_Visit<V, E>
    where V : Generic_Tsp_Visit<V, E>
    where E : Generic_Tsp_Element<E>
{
    public Double Time { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Tsp_Route<R, V, E> : Generic_Route<R, V, E>
    where R : Generic_Tsp_Route<R, V, E>
    where V : Generic_Tsp_Visit<V, E>
    where E : Generic_Tsp_Element<E>
{
    public Double Time
    {
        get
        {
            return this.Visits.Select(v => v.Time).Sum();
        }
    }
}

/// <summary>Collection of Routes</summary>
public class Generic_Tsp_Solution<S, R, V, E> : Generic_Solution<S, R, V, E>
    where S : Generic_Tsp_Solution<S, R, V, E>
    where R : Generic_Tsp_Route<R, V, E>
    where V : Generic_Tsp_Visit<V, E>
    where E : Generic_Tsp_Element<E>
{
    public Double Time
    {
        get
        {
            return this.Routes.Select(r => r.Time).Sum();
        }
    }
}

public class Concrete_Tsp_Element : Generic_Tsp_Element<Concrete_Tsp_Element> { }

public class Concrete_Tsp_Visit : Generic_Tsp_Visit<Concrete_Tsp_Visit, Concrete_Tsp_Element> { }

public class Concrete_Tsp_Route : Generic_Tsp_Route<Concrete_Tsp_Route, Concrete_Tsp_Visit, Concrete_Tsp_Element> { }

public class Concrete_Tsp_Solution : Generic_Tsp_Solution<Concrete_Tsp_Solution, Concrete_Tsp_Route, Concrete_Tsp_Visit, Concrete_Tsp_Element> { }

VRP Classes:

public class Generic_Vrp_Element<E> : Generic_Element<E>
where E : Generic_Vrp_Element<E>
{
}

/// <summary>Visit to a Generic_Element</summary>
public class Generic_Vrp_Visit<V, E> : Generic_Visit<V, E>
    where V : Generic_Vrp_Visit<V, E>
    where E : Generic_Vrp_Element<E>
{
    public Double Capacity { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Vrp_Route<R, V, E> : Generic_Route<R, V, E>
    where R : Generic_Vrp_Route<R, V, E>
    where V : Generic_Vrp_Visit<V, E>
    where E : Generic_Vrp_Element<E>
{
    public Double Capacity
    {
        get
        {
            return this.Visits.Select(v => v.Capacity).Sum();
        }
    }
}

/// <summary>Collection of Routes</summary>
public class Generic_Vrp_Solution<S, R, V, E> : Generic_Solution<S, R, V, E>
    where S : Generic_Vrp_Solution<S, R, V, E>
    where R : Generic_Vrp_Route<R, V, E>
    where V : Generic_Vrp_Visit<V, E>
    where E : Generic_Vrp_Element<E>
{
    public Double Capacity
    {
        get
        {
            return this.Routes.Select(r => r.Capacity).Sum();
        }
    }
}

public class Concrete_Vrp_Element : Generic_Vrp_Element<Concrete_Vrp_Element> { }

public class Concrete_Vrp_Visit : Generic_Vrp_Visit<Concrete_Vrp_Visit, Concrete_Vrp_Element> { }

public class Concrete_Vrp_Route : Generic_Vrp_Route<Concrete_Vrp_Route, Concrete_Vrp_Visit, Concrete_Vrp_Element> { }

public class Concrete_Vrp_Solution : Generic_Vrp_Solution<Concrete_Vrp_Solution, Concrete_Vrp_Route, Concrete_Vrp_Visit, Concrete_Vrp_Element> { }

The final result is non-generic concrete classes that can be used like this:

var e = new Concrete_Tsp_Element();
var v = new Concrete_Tsp_Visit();
v.Element = e;
v.Time = 0.5;
var r = new Concrete_Tsp_Route();
r.Visits = new List<Concrete_Tsp_Visit>(new[] { v });
r.Distance = 2.1;
var s = new Concrete_Tsp_Solution();
s.Routes = new List<Concrete_Tsp_Route>(new[] { r });
Console.WriteLine(s.Distance);
Console.WriteLine(s.Time);
Console.ReadLine();

Enjoy! Enjoy!

Enigmativity
Looks interesting. I'll give it a try tomorrow and let you know the results :)
Ian
Holy crap... this actually works, doesn’t it? I’m very impressed. And I am not easy to impress when it comes to C#! — Do you know of any good websites or blog entries that talk about this strategy of recursive generic types and when to use them? I want to build an intuition for it so that I know to use it when it is appropriate.
Timwi
@Timwi - This works like a treat - I just coded up this before posting to make sure I got the syntax correct. Recursive generics is a form of the GoF Bridge pattern. I use this technique all the time to create strongly bound types that inherit common code - it's particularly good for implementing MVVM code where I want to make my ViewModel be constrained to the Model and the View constrained to the ViewModel.
Enigmativity
@Ian - Trust me, it works. I tested my code before posting. :-)
Enigmativity
@Enigmativity: I tried to remove some of the generic constraints and it still seems to compile fine with about half of them removed. What do you have to say in your defence? :)))
Timwi
@Enigmativity: I posted a new answer with the redundant constraints removed. It seems to compile and have all the type-safety required, and it doesn’t have any generic recursion in it. What is it that your recursive solution enables that mine doesn’t?
Timwi
Blimey. This recursive stuff is really doing my head in. My first instinct would be to say it wouldn't compile but I think that just shows my ignorance of generics. I'll have to ponder this more when I have more time. :)
Chris
@Timwi - Your answer removes the generic constraint on the `Generic_Element` type allowing it to be anything which is not what I think Ian was after.
Enigmativity
@Enigmativity: Regardless, you can still constrain that without recursion.
Timwi
@Timwi - No, you can't. The recursion is required to allow the base classes to define methods that will derive down in the concrete classes so that the concrete classes can refer to themselves. That's the recursive part.
Enigmativity
@Enigmativity: You're code's actually slightly wrong compared to the original question, but after the modifications it does still work. There errors being that Generic_VRP_Visit<V, E> should inherit Generic_TSP_Visit<V, E> to pick up the 'Time' property, rather than directly inheriting Generic_Visit<V, E>. But I've changed that in my code and it works like a charm. Thank you :)
Ian
A: 

Here is the same thing as Enigmativity’s answer, but with all the redundant constraints removed. This still compiles, it doesn’t have any generic recursion in it, and as far as I can tell is still as type-safe as was required. Enigmativity, what am I missing? :)

public class Generic_Element { }

public class Generic_Visit<E>
{
    public E Element { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Route<V>
{
    public List<V> Visits { get; set; }
    public Double Distance { get; set; }
}

/// <summary>Collection of Routes</summary>
public class Generic_Solution<R, V>
    where R : Generic_Route<V>
{
    public List<R> Routes { get; set; }

    public Double Distance
    {
        get
        {
            return this.Routes.Select(r => r.Distance).Sum();
        }
    }
}

public class Generic_Tsp_Element : Generic_Element
{
}

/// <summary>Visit to a Generic_Element</summary>
public class Generic_Tsp_Visit<E> : Generic_Visit<E>
{
    public Double Time { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Tsp_Route<V, E> : Generic_Route<V>
    where V : Generic_Tsp_Visit<E>
{
    public Double Time
    {
        get
        {
            return this.Visits.Select(v => v.Time).Sum();
        }
    }
}

/// <summary>Collection of Routes</summary>
public class Generic_Tsp_Solution<R, V, E> : Generic_Solution<R, V>
    where R : Generic_Tsp_Route<V, E>
    where V : Generic_Tsp_Visit<E>
{
    public Double Time
    {
        get
        {
            return this.Routes.Select(r => r.Time).Sum();
        }
    }
}

public class Concrete_Tsp_Element : Generic_Tsp_Element { }

public class Concrete_Tsp_Visit : Generic_Tsp_Visit<Concrete_Tsp_Element> { }

public class Concrete_Tsp_Route : Generic_Tsp_Route<Concrete_Tsp_Visit, Concrete_Tsp_Element> { }

public class Concrete_Tsp_Solution : Generic_Tsp_Solution<Concrete_Tsp_Route, Concrete_Tsp_Visit, Concrete_Tsp_Element> { }

public class Generic_Vrp_Element : Generic_Element
{
}

/// <summary>Visit to a Generic_Element</summary>
public class Generic_Vrp_Visit<V, E> : Generic_Visit<E>
{
    public Double Capacity { get; set; }
}

/// <summary>Collection of Visits</summary>
public class Generic_Vrp_Route<R, V, E> : Generic_Route<V>
    where V : Generic_Vrp_Visit<V, E>
{
    public Double Capacity
    {
        get
        {
            return this.Visits.Select(v => v.Capacity).Sum();
        }
    }
}

/// <summary>Collection of Routes</summary>
public class Generic_Vrp_Solution<S, R, V, E> : Generic_Solution<R, V>
    where R : Generic_Vrp_Route<R, V, E>
    where V : Generic_Vrp_Visit<V, E>
{
    public Double Capacity
    {
        get
        {
            return this.Routes.Select(r => r.Capacity).Sum();
        }
    }
}

public class Concrete_Vrp_Element : Generic_Vrp_Element { }

public class Concrete_Vrp_Visit : Generic_Vrp_Visit<Concrete_Vrp_Visit, Concrete_Vrp_Element> { }

public class Concrete_Vrp_Route : Generic_Vrp_Route<Concrete_Vrp_Route, Concrete_Vrp_Visit, Concrete_Vrp_Element> { }

public class Concrete_Vrp_Solution : Generic_Vrp_Solution<Concrete_Vrp_Solution, Concrete_Vrp_Route, Concrete_Vrp_Visit, Concrete_Vrp_Element> { }
Timwi
@Timwi - Your answer doesn't constrain the element type `E`. I can create classes with any type of `E` I like. My solution constrains it so that it must inherit from `Generic_Element<E>` which I think is what Ian wanted. So my constraints are not *redundant*. :-p
Enigmativity
@Enigmativity: I know, so what. You can still add a constraint to the `E` on `Generic_Visit<E>` if you need to and it still compiles. The point is that I don’t see what your *recursive* constraints add.
Timwi
@Timwi - Your solution also allows you to use any type for the `S` generic parameter in the `Generic_Vrp_Solution<S, R, V, E>` type. By removing the constraints you have, well, removed the constraints. So, for example, if you want the solution type to return itself you need to use the full set of constraints.
Enigmativity