views:

54

answers:

2

The problem:

class StatesChain : IState, IHasStateList {
    private TasksChain tasks = new TasksChain();

     ...

    public IList<IState> States {
        get { return _taskChain.Tasks; }
    }

    IList<ITask> IHasTasksCollection.Tasks {
        get { return _taskChain.Tasks; } <-- ERROR! You can't do this in C#!
                                             I want to return an IList<ITask> from
                                             an IList<IStates>.
    }
}

Assuming the IList returned will be read-only, I know that what I'm trying to achieve is safe (or is it not?). Is there any way I can accomplish what I'm trying? I wouldn't want to try to implement myself the TasksChain algorithm (again!), as it would be error prone and would lead to code duplication. Maybe I could just define an abstract Chain and then implement both TasksChain and StatesChain from there? Or maybe implementing a Chain<T> class?

How would you approach this situation?

The Details: I have defined an ITask interface:

public interface ITask {
    bool Run();
    ITask FailureTask { get; }
}

and a IState interface that inherits from ITask:

public interface IState : ITask {
    IState FailureState { get; }
}

I have also defined an IHasTasksList interface:

interface IHasTasksList {
    List<Tasks> Tasks { get; }
}

and an IHasStatesList:

interface IHasTasksList {
    List<Tasks> States { get; }
}

Now, I have defined a TasksChain, that is a class that has some code logic that will manipulate a chain of tasks (beware that TasksChain is itself a kind of ITask!):

class TasksChain : ITask, IHasTasksList {
    IList<ITask> tasks = new List<ITask>();

    ...

    public List<ITask> Tasks { get { return _tasks; } }

    ...
}

I am implementing a State the following way:

public class State : IState {
    private readonly TaskChain _taskChain = new TaskChain();

    public State(Precondition precondition, Execution execution) {
        _taskChain.Tasks.Add(precondition);
        _taskChain.Tasks.Add(execution);
    }

    public bool Run() {
        return _taskChain.Run();
    }

    public IState FailureState {
        get { return (IState)_taskChain.Tasks[0].FailureTask; }
    }

    ITask ITask.FailureTask {
        get { return FailureState; }
    }
}

which, as you can see, makes use of explicit interface implementations to "hide" FailureTask and instead show FailureState property.

The problem comes from the fact that I also want to define a StatesChain, that inherits both from IState and IHasStateList (and that also imples ITask and IHasTaskList, implemented as explicit interfaces) and I want it to also hide IHasTaskList's Tasks and only show IHasStateList's States. (What is contained in "The problem" section should really be after this, but I thought puting it first would be way more reader friendly).

(pff..long text) Thanks!

+2  A: 

In short, no it is not safe, since a "read-only" IList<> doesn't exist (contract-wise). It's only the implementation which will reject entries, but that's too late, as the call itself would require the interface type parameter to be both co- and contravariant at the same time.

You could, however, return an IEnumerable<> instead, which is covariant in C# 4. Since this is enough to use LINQ, that shouldn't be too much of a drawback and expresses the read-only nature better.

Lucero
I've long wished that the hierarchy of `Collections.Generic` interfaces included read-only variants. Covariance is yet another reason why that would have been great. But because almost all collection interfaces are writable we can't get covariance. Gah!...
romkyns
@romkyns, there are several other "missing" interfaces in the BCL, such as `IArithmetic<>` on numeric primitives (which would enable generic math operations by using the interface as type constraint), `ICharSequence` (or similar to enable any char sequence to be used for instance as `Regex` or `StringReader` source instead of a string) and so on...
Lucero
+1  A: 

On the line where you get an error, you're trying to return IList<IStates> as if it was an instance of type IList<ITask>. This doesn't work automtaically, because the two types are different (no matter that the generic parameters are related).

In C# 3.0 or older, there is no way to achieve that automatically. C# 4.0 adds support for covariance and contravariance, which serves exactly this purpose. But as you noted, this works only when the returned collection is read-only. The IList<T> type doesn't guarantee that, so it isn't annotated as covariant in .NET 4.0.

To make this work using C# 4.0, you'll need to use truly read-only type, which has covariant annotation in the framework - the best option in your case is IEnumerable<T> (although you could define your own using the out T modifier).

To add more details, in C# 4.0, you can declare an interface as either covariant or contra-variant. The first case means that the compiler will allow you to perform the conversion you needed in your example (the other case is useful for write-only classes). This is done by adding explicit annotations to the interface declaration (these are already available for .NET 4.0 types). For example, the declaration of IEnumerable<T> has the out annotation meaning that it supports covariance:

public interface IEnumerable<out T> : IEnumerable { /* ... */ }

Now, the compiler will allow you to write:

IEnumerable<IState> states = ...
IEnumerable<ITask> tasks = states;
Tomas Petricek
Thanks by the answer. What defines what is a truly read-only type in the eyes of C#? What is the mechanism behind allowing IEnumerable<T> and not IList<T>?
devoured elysium
It's explicit (defining `in` or `out` notation on the type parameter), not implicit. Google returns a bunch of great results for the following query: http://www.google.ch/search?q=covariant+contravariant+in+out
Lucero
Sorry, I don't understand what you mean. Is it something new to C# 4.0? Thanks
devoured elysium
Yes, older C# versions did not allow co- and/or contravariance on anything but arrays and type references (even tough the .NET Framework runtime is said to have been supporting it since the introduction of generics V2).
Lucero
I added some more details - I wasn't sure if you're already talking about C# 4.0 or not (since your question explicitly mentioned variance - which is generally quite unfamiliar concept!)
Tomas Petricek