Когда следует или не следует использовать ограничения общего типа?

StackOverflow https://stackoverflow.com/questions/1219573

Вопрос

У меня есть базовый класс:

public abstract class StuffBase
{
    public abstract void DoSomething();
}

И два производных класса

public class Stuff1 : StuffBase
{
    public void DoSomething()
    {
        Console.WriteLine("Stuff 1 did something cool!");
    }
    public Stuff1()
    {
        Console.WriteLine("New stuff 1 reporting for duty!");
    }
}

public class Stuff2 : StuffBase
{
    public void DoSomething()
    {
        Console.WriteLine("Stuff 2 did something cool!");
    }
    public Stuff1()
    {
        Console.WriteLine("New stuff 2 reporting for duty!");
    }
}

Хорошо, теперь предположим, что у меня есть список элементов:

var items = new List<StuffBase>();
items.Add(new Stuff1());
items.Add(new Stuff2());

и я хочу, чтобы все они вызвали свой метод DoSomething().Я мог бы рассчитывать на то, что просто пройдусь по списку и вызову их метод DoSomething(), так что, скажем, у меня есть метод для этого, называемый AllDoSomething(), который просто перебирает список и выполняет задание:

public static void AllDoSomething(List<StuffBase> items)
{
    items.ForEach(i => i.DoSomething());
}

В чем практическое отличие следующего метода?

public static void AllDoSomething<T>(List<T> items) where T: StuffBase
{
    items.ForEach(i => i.DoSomething());
}

Оба метода на самом деле, хотя и синтаксически различны, выполняют одно и то же.

Это просто разные способы сделать одно и то же?Я понимаю дженерики и ограничения типов, но не понимаю, почему в данном случае мне следует использовать один способ вместо другого.

Это было полезно?

Решение

Это связано с тем, что C# пока не поддерживает Ковариация.

Более формально, в C# v2.0, если T является подтипом u, то T [] является подтипом u [], но G не является подтипом G (где G является каким -либо общим типом).В терминологии теории типа мы описываем это поведение, говоря, что типы массивов C# являются «коваритными», а общие типы являются «инвариантными».

Ссылка: http://blogs.msdn.com/rmbyers/archive/2005/02/16/375079.aspx

Если у вас есть следующий метод:

public static void AllDoSomething(List<StuffBase> items)
{
    items.ForEach(i => i.DoSomething());
}

var items = new List<Stuff2>();
x.AllDoSomething(items); //Does not compile

Если вы используете ограничение универсального типа, так оно и будет.

Для получения дополнительной информации о ковариации и контравариантности посетите сайт Серия постов Эрика Липперта.


Другие посты, которые стоит прочитать:

Другие советы

Предположим, у вас есть список:

List<Stuff1> l = // get from somewhere

Теперь попробуйте:

AllDoSomething(l);

С общей версией это будет разрешено. С неуниверсальным это не будет. Это существенная разница. Список Stuff1 не является списком StuffBase. Но в общем случае вам не требуется, чтобы он был точно списком <=>, поэтому он более гибкий.

Вы можете обойти это, сначала скопировав свой список <=> в список <=>, чтобы сделать его совместимым с неуниверсальной версией. Но тогда предположим, что у вас есть метод:

List<T> TransformList<T>(List<T> input) where T : StuffBase
{
    List<T> output = new List<T>();

    foreach (T item in input)
    {
        // examine item and decide whether to discard it,
        // make new items, whatever
    }

    return output;
}

Без обобщений вы могли бы принять список <=>, но затем вам пришлось бы вернуть список <=>. Вызывающий должен будет использовать приведение, если он знает, что предметы действительно были производного типа. Таким образом, дженерики позволяют вам сохранить фактический тип аргумента и направить его через метод к возвращаемому типу.

В приведенном вами примере нет никакой разницы, но попробуйте следующее:

List<Stuff1> items = new List<Stuff1>();
items.Add(new Stuff1());
AllDoSomething(items);
AllDoSomething<StuffBase>(items);

Первый вызов работает хорошо, но второй не компилируется из-за общей ковариации

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top