Проблема иерархии классов (с дисперсией общего типа!) - PullRequest
2 голосов
/ 27 апреля 2010

Проблема:

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>.
    }
}

Предполагая, что возвращенный IList будет только для чтения, я знаю, что то, что я пытаюсь достичь, безопасно (или нет?). Есть ли способ, которым я могу добиться того, что я пытаюсь? Я не хотел бы пытаться реализовать сам алгоритм TasksChain (снова!), Так как он был бы подвержен ошибкам и приводил бы к дублированию кода. Может быть, я мог бы просто определить абстрактную цепочку, а затем реализовать оттуда и TasksChain, и StatesChain? Или, может быть, реализует класс Chain<T>?

Как бы вы подошли к этой ситуации?

Подробности: Я определил ITask интерфейс:

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

и IState интерфейс, который наследуется от ITask:

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

Я также определил IHasTasksList интерфейс:

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

и IHasStatesList:

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

Теперь я определил TasksChain, это класс с некоторой кодовой логикой, которая будет манипулировать цепочкой задач (помните, что TasksChain сам по себе является своего рода ITask!):

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

    ...

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

    ...
}

Я реализую State следующим образом:

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; }
    }
}

, который, как вы можете видеть, использует явные реализации интерфейса, чтобы "скрыть" FailureTask и вместо этого показать FailureState свойство.

Проблема заключается в том, что я также хочу определить StatesChain, который наследует от IState и IHasStateList (и это также подразумевает ITask и IHasTaskList, реализованные как явные интерфейсы) и Я хочу, чтобы он также скрывал IHasTaskList 1054 * и показывал только IHasStateList 1056 *. (То, что содержится в разделе «Проблема», действительно должно быть после этого, но я подумал, что поставить его на первое место будет гораздо удобнее для читателей).

(пф .. длинный текст) Спасибо!

Ответы [ 2 ]

2 голосов
/ 27 апреля 2010

Короче говоря, нет, это небезопасно, поскольку IList<> "только для чтения" не существует (по контракту). Только реализация будет отклонять записи, но это слишком поздно, поскольку сам вызов потребует, чтобы параметр типа интерфейса был одновременно и контравариантным одновременно.

Однако можно вместо этого вернуть IEnumerable<>, что является ковариантным в C # 4. Поскольку этого достаточно для использования LINQ, это не должно быть слишком большим недостатком и лучше выражать природу только для чтения.

1 голос
/ 27 апреля 2010

В строке, где вы получаете сообщение об ошибке, вы пытаетесь вернуть IList<IStates>, как если бы это был экземпляр типа IList<ITask>. Это не работает автоматически, потому что эти два типа различны (независимо от того, связаны ли общие параметры).

В C # 3.0 или старше нет способа достичь этого автоматически. В C # 4.0 добавлена ​​поддержка ковариации и контравариантности, которая служит именно этой цели. Но, как вы заметили, это работает только тогда, когда возвращаемая коллекция доступна только для чтения. Тип IList<T> не гарантирует этого, поэтому он не аннотируется как ковариантный в .NET 4.0.

Чтобы сделать это с помощью C # 4.0, вам нужно будет использовать действительно доступный только для чтения тип , который имеет ковариантную аннотацию в структуре - лучший вариант в вашем случае - IEnumerable<T> (хотя вы можно определить свой собственный, используя модификатор out T).

Чтобы добавить больше деталей, в C # 4.0 вы можете объявить интерфейс либо ковариантным, либо противоположным вариантом. Первый случай означает, что компилятор позволит вам выполнить преобразование, необходимое для вашего примера (другой случай полезен для классов только для записи). Это делается путем добавления явных аннотаций к объявлению интерфейса (они уже доступны для типов .NET 4.0). Например, объявление IEnumerable<T> имеет аннотацию out, означающую, что оно поддерживает ковариацию:

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

Теперь компилятор позволит вам написать:

IEnumerable<IState> states = ...
IEnumerable<ITask> tasks = states;
...