У меня есть базовый класс:
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());
}
Оба метода выглядят в реальном выражении, хотя и синтаксически различны, чтобы делать то же самое.
Это просто разные способы сделать то же самое? Я понимаю обобщения и ограничения типов, но не могу понять, почему я должен использовать один путь в другом случае в этом случае.
Это связано с тем, что С# не поддерживает 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
Где, как если бы вы использовали ограничение общего типа, оно будет.
Для получения дополнительной информации о ковариации и контравариантности см. серии статей Эрика Липперта.
Другие сообщения, которые стоит прочитать:
Предположим, у вас есть список:
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
. Вызывающему нужно будет использовать отбрасывания, если они знают, что элементы действительно имеют производный тип. Таким образом, дженерики позволяют сохранить фактический тип аргумента и передать его методу в возвращаемый тип.
List<B>
не может быть присвоен объект типа List<D>
, даже если переменная типа B
может принимать D
Это потому, что у класса, подобного List, может быть метод типа Add
. Если переменная представляет собой List<D>
, вы не должны иметь возможность молча преобразовать ее в List<B>
, потому что это позволило бы добавить в нее объекты B
(когда это на самом деле List<D>
).
В приведенном примере нет никакой разницы, но попробуйте следующее:
List<Stuff1> items = new List<Stuff1>();
items.Add(new Stuff1());
AllDoSomething(items);
AllDoSomething<StuffBase>(items);
Первый вызов работает хорошо, но второй не компилируется из-за общей ковариации