Общее ограничение для действия не работает должным образом

У меня возникли проблемы с пониманием того, почему следующий фрагмент не дает мне ошибки

public void SomeMethod<T>(T arg) where T : MyInterface
{
  MyInterface e = arg;
}

Но этот, который, как я ожидаю, будет работать из-за ограничения универсального типа

private readonly IList<Action<MyInterface>> myActionList = new List<Action<MyInterface>>();

public IDisposable Subscribe<T>(Action<T> callback) where T: MyInterface
{
  myActionList.Add(callback); // doesn't compile
  return null
}

Выдает эту ошибку

cannot convert from 'System.Action<T>' to 'System.Action<MyInterface>'

Я использую VS2012 sp1 и .NET 4.5.

Может ли кто-нибудь объяснить, почему ограничение не позволяет это скомпилировать?


person Jim Jeffries    schedule 25.03.2013    source источник
comment
Почему ваш список доступен только для чтения и почему new IList? Это настоящая декларация?   -  person Paolo Tedesco    schedule 25.03.2013
comment
Классы и делегаты — это не одно и то же. System.Action<MyInterface> представляет функцию с одним параметром типа MyInterface, а System.Action<T> представляет метод с параметром типа T : MyInterface. Сигнатуры функций несовместимы, не имеет значения, что T является производным от MyInterface, сигнатура будет совместима только в том случае, если T будет точно MyInterface.   -  person odyss-jii    schedule 25.03.2013
comment
@PaoloTedesco извиняется, он реконструирован и упрощен из какого-то другого кода. Ошибка копирования/вставки   -  person Jim Jeffries    schedule 25.03.2013
comment
@jamesj: нет проблем, спасибо за исправление, кстати, хороший вопрос :)   -  person Paolo Tedesco    schedule 25.03.2013


Ответы (6)


Классы и делегаты — это не одно и то же. System.Action<MyInterface> представляет функцию с одним параметром типа MyInterface, а System.Action<T> представляет метод с параметром типа T : MyInterface. Сигнатуры функций несовместимы, не имеет значения, что T является производным от MyInterface, сигнатура будет совместима только в том случае, если T будет точно MyInterface.

person odyss-jii    schedule 25.03.2013

Это проблема контравариантности - Action<MyInterface> должен иметь возможность принимать любой экземпляр MyInterface в качестве аргумента, однако вы пытаетесь сохранить Action<T>, где T является подтипом MyInterface, что небезопасно.

Например, если у вас было:

public class SomeImpl : MyInterface { }
public class SomeOtherImpl : MyInterface { }
List<Action<MyInterface>> list;

list.Add(new Action<SomeImpl>(i => { }));
ActionMyInterface act = list[0];
act(new SomeOtherImpl());

Вы можете присвоить Action<T> некоторым Action<U> только в том случае, если тип T "меньше", чем тип U. Например

Action<string> act = new Action<object>(o => { });

безопасен, поскольку строковый аргумент всегда действителен там, где находится объектный аргумент.

person Lee    schedule 25.03.2013

Ограничение where T: MyInterface означает "любой экземпляр любого класса или структуры, который реализует MyInterface".

Итак, то, что вы пытаетесь сделать, можно упростить следующим образом:

Action<IList> listAction = null;
Action<IEnumerable> enumAction = listAction;

Который не должен работать, пока еще IList : IEnumerable. Более подробную информацию можно найти здесь:

http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx http://msdn.microsoft.com/en-us/library/dd799517.aspx

Поэтому, если вам действительно нужно использовать общий, а не только интерфейс, вы можете сделать это так, хотя это добавляет сложности и незначительные проблемы с производительностью:

public static IDisposable Subscribe<T>(Action<T> callback) where T : MyInterface
{
    myActionList.Add(t => callback((T)t)); // this compiles and work
    return null;
}
person Lanorkin    schedule 25.03.2013

Я считаю полезным в таких ситуациях подумать, что пойдет не так, если вы позволите такое поведение. Итак, давайте рассмотрим это.

interface IAnimal { void Eat(); }
class Tiger : IAnimal 
{ 
  public void Eat() { ... }
  public void Pounce() { ... } 
}
class Giraffe : IAnimal 
...
public void Subscribe<T>(Action<T> callback) where T: IAnimal
{
   Action<IAnimal> myAction = callback; // doesn't compile but pretend it does.
   myAction(new Giraffe()); // Obviously legal; Giraffe implements IAnimal
}
...
Subscribe<Tiger>((Tiger t)=>{ t.Pounce(); });

Так что же происходит? Мы создаем делегата, который берет тигра и набрасывается, передаем его Subscribe<Tiger>, преобразуем его в Action<IAnimal> и передаем жирафа, который затем набрасывается.

Понятно, что это должно быть незаконно. Единственное место, где разумно сделать его незаконным, — это преобразование из Action<Tiger> в Action<IAnimal>. Так вот где это незаконно.

person Eric Lippert    schedule 25.03.2013

Классы и делегаты ведут себя немного иначе. Давайте посмотрим на простой пример:

public void SomeMethod<T>(T arg) where T : MyInterface
{
  MyInterface e = arg;
}

В этом методе вы можете предположить, что T будет по крайней мере MyInterface, поэтому вы можете сделать что-то вроде этого MyInterface e = arg;, потому что аргументы всегда могут быть приведены к MyInterface.

Теперь давайте посмотрим, как ведут себя делегаты:

public class BaseClass { };
public class DerivedClass : BaseClass { };
private readonly IList<Action<BaseClass >> myActionList = new List<Action<BaseClass>>();

public void Subscribe<T>(Action<T> callback) where T: BaseClass
{
  myActionList.Add(callback); // so you could add more 'derived' callback here Action<DerivedClass>
  return null;
}

Теперь мы добавляем обратный вызов DerivedClass в myActionList, а затем где-то вы вызываете делегатов:

foreach( var action in myActionList ) {
   action(new BaseClass);
}

Но вы не можете этого сделать, потому что если у вас есть обратный вызов DerivedClass, вы должны передать его DerivedClass в качестве параметра.

Этот вопрос относится к ковариантности и контравариантности. Вы можете прочитать о дисперсии из этого, также у Эрика Липперта есть очень интересные статьи о дисперсии, это первая статья, остальные вы можете найти в его блоге.

P.S. Отредактировано в соответствии с комментарием Ли.

person Andrew    schedule 25.03.2013
comment
Классы не могут быть ковариантными — только делегаты и интерфейсы могут иметь аннотации вариантов. Также неверно говорить, что делегаты «имеют» контравариантность — типы делегатов Func контравариантны по своим аргументам и ковариантны по своим возвращаемым типам. Контравариантность не ограничивается типами делегатов, см., например, интерфейс IObserver<T>, который является контравариантным в T. - person Lee; 25.03.2013

Если T в любом случае ограничен определенным интерфейсом, вы можете просто использовать этот интерфейс вместо него:

public void SomeMethod(MyInterface arg)
{
  MyInterface e = arg;
}

private readonly IList<Action<MyInterface>> myActionList = new IList<Action<MyInterface>>();

public IDisposable Subscribe(Action<MyInterface> callback)
{
  myActionList.Add(callback); // does compile
  return null
}

Будет работать и компилироваться и будет практически таким же, как у вас сейчас.

Дженерики полезны, если вы хотите выполнить одну и ту же операцию НЕЗАВИСИМО от типа, если вы затем ограничите тип некоторым интерфейсом, вы победили цель дженериков и, вероятно, должны вместо этого просто использовать этот интерфейс.

person Bazzz    schedule 25.03.2013
comment
Я предполагаю, что он хочет иметь возможность передавать подтипы этого конкретного интерфейса.. например. МойДругойИнтерфейс : МойИнтерфейс - person Roger Johansson; 25.03.2013
comment
@ Роджер, это может быть правдой, я не получил этого из ОП, но я признаю, что это подход, который я тоже не рассматривал. - person Bazzz; 25.03.2013