Когда следует или не следует использовать ограничения общего типа?
-
10-07-2019 - |
Вопрос
У меня есть базовый класс:
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
Если вы используете ограничение универсального типа, так оно и будет.
Для получения дополнительной информации о ковариации и контравариантности посетите сайт Серия постов Эрика Липперта.
Другие посты, которые стоит прочитать:
- http://www.pabich.eu/blog/archive/2008/02/12/c-generics---parameter-variance-its-constraints-and-how-it.aspx
- http://blogs.msdn.com/rmbyers/archive/2006/06/01/613690.aspx
- http://msdn.microsoft.com/en-us/library/ms228359(VS.80).aspx
- http://www.csharp411.com/convert-between-generic-ienumerablet/
- http://research.microsoft.com/apps/pubs/default.aspx?id=64042
- Почему нельзя List<parent> = List<child>?
Другие советы
Предположим, у вас есть список:
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);
Первый вызов работает хорошо, но второй не компилируется из-за общей ковариации