2013-03-25 19 views
8

Mam pewne kłopoty ze zrozumieniem, dlaczego następujący fragment nie daje mi błądGeneric przeszkodą dla działania nie działa zgodnie z oczekiwaniami

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

ale ten, który spodziewałbym do pracy ze względu na ogólny Typ ograniczenia

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 
} 

daje ten błąd

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

Używam VS2012 sP1 i .NET 4.5.

Może ktoś wyjaśnić, dlaczego ograniczenie to nie pozwala, aby skompilować?

+1

Dlaczego twoja lista jest tylko do odczytu i dlaczego "nowy ILIST"? Czy to jest prawdziwa deklaracja? –

+1

Klasy i delegaci to nie to samo. 'System.Action ' reprezentuje funkcję z jednym parametrem typu 'MyInterface' podczas gdy' System.Action 'reprezentuje metodę z parametrem typu' T: MyInterface'. Sygnatury funkcji nie są kompatybilne, nie jest istotne, że 'T' jest pochodną' MyInterface', sygnatura byłaby kompatybilna, gdyby 'T' było dokładnie' MyInterface'. –

+0

@PaoloTedesco przeprasza, jest zrekonstruowany i uproszczony na podstawie innego kodu. Kopiuj/Wklej błąd –

Odpowiedz

3

Klasy i delegaci nie to samo. System.Action<MyInterface> stanowi funkcja z jednym parametrem typu MyInterface podczas gdy System.Action<T> przedstawia metodę z parametrem typu T : MyInterface. Podpisy funkcyjne nie są zgodne, to nie dowiemy się z T że jest pochodną MyInterface, podpis będzie kompatybilny tylko jeśli T było dokładnie MyInterface.

0

Jeśli T jest ograniczona do pewnego interfejsu tak, można po prostu użyć tego interfejsu w jego miejsce:

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 
} 

będzie działać i skompilować i jest praktycznie takie samo jak to, co masz teraz.

Generics są użyteczne, jeśli chcesz wykonać tę samą operację, NIEZALEŻNIE od typu, jeśli ograniczysz typ do jakiegoś interfejsu, któremu udało się pokonać cel ogólny, i prawdopodobnie powinieneś po prostu użyć tego interfejsu.

+0

Chyba chce mieć możliwość przekazywania podtypów tego konkretnego interfejsu .. np. MyOtherInterface: MyInterface –

+0

@Roger, który może być prawdziwy, nie dostałem tego z OP, ale przyznaję, że to podejście, którego też nie brałem pod uwagę. – Bazzz

4

Jest to kwestia kontrawariancja - Action<MyInterface> powinny być w stanie podjąć żadnej instancji MyInterface jako argument, jednak staramy się przechowywać Action<T> gdzie T jakiś podtyp MyInterface, który nie jest bezpieczny.

Na przykład jeśli miał:

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

można przypisać tylko do pewnego Action<U>Action<T> jeśli typ T jest „mniejsza” niż typu U. Na przykład:

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

jest bezpieczny, ponieważ argument łańcucha jest zawsze poprawny, gdy argumentem jest obiekt.

+0

+1 dla wyjaśnienia na przykładzie. – nawfal

1

where T: MyInterface ograniczenie oznacza „każdy wystąpienie Każda klasa lub struktura, która realizuje MyInterface”.

Więc co próbujesz zrobić można uprościć następująco:

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

Który nie ma pracy, a jeszcze IList : IEnumerable.Więcej szczegółów można znaleźć tutaj:

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

Więc jeśli naprawdę trzeba użyć generycznych, a nie tylko interfejs - można zrobić to tak, jakby to dodaje zagadnienia złożoności i drobne wydajności:

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

Zajęcia i delegaci zachowują się nieco inaczej. Zobaczmy prosty przykład:

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

W tej metodzie można zakładać, że T byłoby przynajmniej MyInterface, więc można zrobić coś takiego MyInterface e = arg; ponieważ args zawsze mogłoby być oddane do MyInterface.

Teraz zobaczmy, jak delegaci zachowują:

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

Teraz we'r dodanie DerivedClass oddzwanianie do myActionList a potem gdzieś wywołać delegatów:

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

Ale nie można tego zrobić, ponieważ jeśli masz wywołanie zwrotne DerivedClass, musisz przekazać mu DerivedClass jako parametr.

To pytanie odnosi się do Covariance and contravariance. Możesz przeczytać o wariancji z artykułu this, także Eric Lippert ma bardzo ciekawe artykuły na temat wariancji, this to pierwszy artykuł, resztę można znaleźć na swoim blogu.

P.S. Edytowane zgodnie z komentarzem Lee.

+0

Klasy nie mogą być kowariancyjne - tylko delegaci i interfejsy mogą mieć adnotacje różnicujące.Błędne jest również mówienie, że delegaci mają "contrawariancję" - typy delegatów "Func" są sprzeczne w swoich argumentach i kowariantne w swoich typach powrotów. Kontrawariancja nie jest ograniczona do delegowania typów, zobacz na przykład interfejs 'IObserver ', który jest sprzeczny z 'T'. – Lee

+0

Edytowałem mój wpis, dziękuję. – Andrew

2

Uważam, że w takich sytuacjach pomocne jest rozważenie, co jest nie tak, jeśli pozwolisz na zachowanie. Rozważmy to.

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

Co się dzieje? Tworzymy delegata, który bierze tygrysa i rzuca, przekazuje to do Subscribe<Tiger>, zamienia to na Action<IAnimal> i przekazuje żyrafę, która następnie rzuca się.

Oczywiście to musi być nielegalne. Jedynym miejscem, w którym rozsądne jest uczynienie tego nielegalnym, jest konwersja z Action<Tiger> na Action<IAnimal>. To tam jest nielegalne.