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

2

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

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

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

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

Теги:
generics
type-constraints
c#-2.0

3 ответа

6
Лучший ответ

Это связано с тем, что С# не поддерживает Covariance.

Более формально, в С# v2.0, если T является подтипа U, то T [] является подтипом U [], но G не является подтипом G (где G - любой общий тип). В терминологию типа теории, мы описываем это поведение, сказав, что массив С# типы являются "ковариантными" и универсальными типы являются "инвариантными".

Ссылка: 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

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

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


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

  • 0
    Поэтому, если я правильно понимаю: в моем примере я не смог передать экземпляр List <Stuff1> или List <Stuff2> без ограничения типа, и поэтому должен был бы передать экземпляр List <StuffBase> при использовании ограничения типа , Я мог бы?
  • 0
    т.е. AllDoSomething <T> (List <T> items), где T: StuffBase позволит мне передавать экземпляры List <Stuff1> или List <Stuff2>
Показать ещё 2 комментария
1

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

List<Stuff1> l = // get from somewhere

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

AllDoSomething(l);

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

Вы можете обойти это, предварительно скопировав список 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;
}

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

  • 0
    Итак, существенное отличие состоит в том, что мой список не может быть определен как список моего производного типа - в то время как список моего базового класса может содержать мои производные типы?
  • 0
    Следует помнить, что переменной типа List<B> не может быть присвоен объект типа List<D> , даже если переменная типа B может принимать D Это потому, что у класса, подобного List, может быть метод типа Add . Если переменная представляет собой List<D> , вы не должны иметь возможность молча преобразовать ее в List<B> , потому что это позволило бы добавить в нее объекты B (когда это на самом деле List<D> ).
0

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню