Warum beschwert sich der C # -Compiler darüber, dass sich "Typen vereinheitlichen" können, wenn sie von verschiedenen Basisklassen stammen?

76

Mein aktueller nicht kompilierter Code ähnelt dem folgenden:

public abstract class A { }

public class B { }

public class C : A { }

public interface IFoo<T>
{
    void Handle(T item);
}

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Der C # -Compiler weigert sich, dies zu kompilieren, und führt die folgende Regel / den folgenden Fehler an:

'MyProject.MyFoo <TA>' kann nicht sowohl 'MyProject.IFoo <TA>' als auch 'MyProject.IFoo <MyProject.B>' implementieren, da sie möglicherweise für einige Typparameter-Ersetzungen vereinheitlicht werden

Ich verstehe, was dieser Fehler bedeutet; Wenn TAes überhaupt etwas geben könnte, könnte es auch technisch gesehen ein BProblem sein, das zu Unklarheiten über die beiden verschiedenen HandleImplementierungen führen würde.

Aber TA kann nichts sein. Basierend auf der Typhierarchie TA kann es nicht sein B- zumindest glaube ich nicht, dass es das kann. TAmuss ableiten von A, was nicht abgeleitet ist B, und offensichtlich gibt es in C # /. NET keine Vererbung mehrerer Klassen.

Wenn ich die generischen Parameter entfernen und zu ersetzen TAmit C, oder sogar A, kompiliert.

Warum erhalte ich diesen Fehler? Ist es ein Fehler oder eine allgemeine Unintelligenz des Compilers oder fehlt mir noch etwas?

Gibt es eine Problemumgehung oder muss ich die MyFoogenerische Klasse nur als separate nicht generische Klasse für jeden möglichen TAabgeleiteten Typ erneut implementieren ?

Aaronaught
quelle
2
Ich denke, TItem sollte TA lesen, nein?
Jeff
Es ist unwahrscheinlich, dass es sich um einen Fehler im Compiler handelt. Um fair zu sein, verwendet die Fehlermeldung die Wörter "kann vereinheitlichen". Ich vermute, dass dies daran liegt, dass Sie beide Schnittstellen verwenden.
Sicherheitshund
Edit: Nevermind, ich habe gelesen, dass B ein Typparameter ist. <strike> Was hindert Sie daran, dasselbe an den Parameter zu übergeben, an Bdas Sie übergeben TA? </ strike>
Josh
1
@JoshEinstein: Bist kein Typparameter, sondern ein tatsächlicher Typ. Der einzige Typparameter ist TA.
Aaronaught
1
@ Ramhound Ich verstehe nicht, warum es "unwahrscheinlich" ist, ein Compiler-Fehler zu sein. Ich kann wirklich keine andere Erklärung sehen. Es scheint ein leichter Fehler zu sein und eine Situation, die nicht sehr oft auftritt.
kmkemp

Antworten:

49

Dies ist eine Folge von Abschnitt 13.4.2 der C # 4-Spezifikation, in dem es heißt:

Wenn ein aus C erstellter möglicher konstruierter Typ nach dem Ersetzen von Typargumenten in L dazu führen würde, dass zwei Schnittstellen in L identisch sind, ist die Deklaration von C ungültig. Einschränkungsdeklarationen werden bei der Bestimmung aller möglichen konstruierten Typen nicht berücksichtigt.

Beachten Sie den zweiten Satz dort.

Es ist daher kein Fehler im Compiler; Der Compiler ist korrekt. Man könnte argumentieren, dass es sich um einen Fehler in der Sprachspezifikation handelt.

Im Allgemeinen werden Einschränkungen in fast jeder Situation ignoriert, in der eine Tatsache über einen generischen Typ abgeleitet werden muss. Einschränkungen werden meistens verwendet, um die zu bestimmen effektive Basisklasse eines generischen Typparameters zu bestimmen, und sonst wenig.

Leider führt dies manchmal zu Situationen, in denen die Sprache unnötig streng ist, wie Sie festgestellt haben.


Es ist im Allgemeinen ein schlechter Codegeruch, "dieselbe" Schnittstelle zweimal zu implementieren, was sich in gewisser Weise nur durch generische Typargumente unterscheidet. Es ist bizarr, zum Beispiel zu haben class C : IEnumerable<Turtle>, IEnumerable<Giraffe>- was ist C , dass es sowohl eine Folge von Schildkröten ist, und eine Folge von Giraffen, zugleich ? Können Sie beschreiben, was Sie hier eigentlich versuchen? Es könnte ein besseres Muster geben, um das eigentliche Problem zu lösen.


Wenn Ihre Benutzeroberfläche genau so ist, wie Sie es beschreiben:

interface IFoo<T>
{
    void Handle(T t);
}

Dann stellt die Mehrfachvererbung der Schnittstelle ein weiteres Problem dar. Sie könnten sich vernünftigerweise dafür entscheiden, diese Schnittstelle kontravariant zu machen:

interface IFoo<in T>
{
    void Handle(T t);
}

Angenommen, Sie haben

interface IABC {}
interface IDEF {}
interface IABCDEF : IABC, IDEF {}

Und

class Danger : IFoo<IABC>, IFoo<IDEF>
{
    void IFoo<IABC>.Handle(IABC x) {}
    void IFoo<IDEF>.Handle(IDEF x) {}
}

Und jetzt wird es richtig verrückt ...

IFoo<IABCDEF> crazy = new Danger();
crazy.Handle(null);

Welche Implementierung von Handle wird aufgerufen? ???

Weitere Gedanken zu diesem Thema finden Sie in diesem Artikel und in den Kommentaren:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

Eric Lippert
quelle
void IFoo<IABC>.Handle(IABC x)und ich würde vermuten, dass interne Implementierungsdetails herausschauen?
Asawyer
2
Auf jeden Fall macht es keinen Sinn, wenn Schnittstellen die Typparameter sind; Ich hatte fälschlicherweise angenommen, dass die Verwendung konkreter Klassen das Problem umgehen würde. Für das Szenario ist die Klasse eine Saga, die mehrere Nachrichtenhandler implementieren muss (einen für jede Nachricht, die Teil der Saga ist). Es gibt ungefähr 10 nahezu identische Sagen, deren einziger Unterschied der genaue konkrete Typ einer der Nachrichten ist, aber ich kann keinen abstrakten Nachrichtenhandler haben, daher dachte ich, ich würde versuchen, eine generische Basisklasse zu verwenden und nur eine zu verwenden Bündel von Stub-Klassen; Die einzige Alternative ist viel Kopieren und Einfügen.
Aaronaught
22
@Eric: Die Situation, dieselbe generische Schnittstelle mit mehr als einmal zu implementieren, ist wahrscheinlicher, wenn Sie eine Schnittstelle wie IComparable<T>oder IEquatable<T>eher als betrachten IEnumerable<T>. Es ist durchaus plausibel, ein Objekt zu haben, das mit mehr als einer Art von Typ verglichen werden kann. Tatsächlich bin ich in diesem Fall mehrmals auf Probleme mit der Typvereinigung gestoßen.
LBushkin
4
@ Eric, LBushkin ist richtig. Dass einige Verwendungen unsinnig sind, bedeutet nicht, dass alle Verwendungen unsinnig sind. Dieses Problem hat mich auch nur gebissen, weil ich eine abstrakte Parsing-Schnittstelle IFoo <T0, T1, ..> implementiert habe, die eine stückweise IParseable <T0>, IParseable <T1>, ... mit einer Reihe von Erweiterungsmethoden implementiert, die auf IParseable definiert sind , aber jetzt scheint das unmöglich. C # / CLR hat viele frustrierende Eckfälle wie diesen.
Naasking
1
@EricLippert Bin ich zu Recht davon ausgegangen, dass Funktionen hinzugefügt wurden, um unter "bestimmten" Bedingungen Mehrdeutigkeiten zuzulassen? Es scheint, dass Sie jetzt (.NET 4.5) das obige Beispiel ausdrücken dürfen, während eine generische Version immer noch nicht erlaubt ist, dh class Danger : IFoo<IABC>, IFoo<DEF> {...}erlaubt ist, während sie class Danger<T1,T2> : IFoo<T1>, IFoo<T2>immer noch nicht erlaubt ist . Es scheint, dass die Disambiguierung durch ein deterministisches Schiedsverfahren erfolgt, bei dem die erste definierte, spezifischste Implementierung verwendet wird (wo mehrere anwendbar sind).
Micdah
8

Anscheinend war es beabsichtigt, wie bei Microsoft Connect beschrieben:

Um dieses Problem zu umgehen, definieren Sie eine andere Schnittstelle wie folgt:

public interface IIFoo<T> : IFoo<T>
{
}

Implementieren Sie dies stattdessen wie folgt:

public class MyFoo<TA> : IIFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Es kompiliert jetzt gut, von Mono .

Nawaz
quelle
Upvoted für den Connect-Link; Leider lässt sich die Problemumgehung nicht mit dem Microsoft-Compiler kompilieren.
Aaronaught
@Aaronaught: Versuchen Sie, IIFooein abstract classdann.
Nawaz
Das kompiliert, obwohl es mich offensichtlich daran hindert, von einer anderen Basisklasse abzuleiten. Ich denke, ich könnte tatsächlich in der Lage sein, diese Arbeit zu machen, obwohl es eine andere Zwischenstufe (hackisch / nutzlos) in der Klassenhierarchie vorschreibt.
Aaronaught
Dies verstößt tatsächlich gegen die Spezifikation, wenn ich mich nicht irre. Der Abschnitt, den Eric zitiert hat, gibt an, dass es nicht möglich sein sollte, nicht wahr?
Konfigurator
2
@Aaronaught: Es ist legal, dass ein Typ dieselbe Schnittstelle zweimal über seine Vererbungskette implementiert (um die spröde Basisklasse auszublenden und zu mildern). Es ist jedoch nicht legal, dass derselbe Typ dieselbe Schnittstelle zweimal am selben Ort implementiert - da es keine "bessere" gibt.
Konfigurator
4

Sie können es unter das Radar schleichen, wenn Sie einer Basisklasse eine Schnittstelle hinzufügen.

public interface IFoo<T> {
}

public class Foo<T> : IFoo<T>
{
}

public class Foo<T1, T2> : Foo<T1>, IFoo<T2>
{
}

Ich vermute, dass dies funktioniert, denn wenn die Typen "vereinheitlichen", ist klar, dass die Implementierung der abgeleiteten Klasse gewinnt.

Colin
quelle
2

Meine Antwort auf im Grunde dieselbe Frage finden Sie hier: https://stackoverflow.com/a/12361409/471129

Bis zu einem gewissen Grad kann dies getan werden! Ich verwende eine Differenzierungsmethode, anstatt Qualifizierer, die die Typen einschränken.

Es vereinheitlicht sich nicht, in der Tat ist es möglicherweise besser als wenn, weil Sie die separaten Schnittstellen auseinander ziehen können.

Siehe meinen Beitrag hier mit einem voll funktionsfähigen Beispiel in einem anderen Kontext. https://stackoverflow.com/a/12361409/471129

Grundsätzlich fügen Sie IIndexer einen weiteren Typparameter hinzu , damit dieser wird IIndexer <TKey, TValue, TDifferentiator>.

Wenn Sie es dann zweimal verwenden, übergeben Sie "Erste" an die erste Verwendung und "Zweite" an die zweite Verwendung

Klasse Test wird also: Klasse Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

So können Sie tun new Test<int,int>()

wo erstens und zweitens trivial sind:

interface First { }

interface Second { }
Erik Eidt
quelle
1

Ich weiß, dass es eine Weile her ist, seit der Thread veröffentlicht wurde, aber für diejenigen, die über die Suchmaschine auf diesen Thread zugreifen, um Hilfe zu erhalten. Beachten Sie, dass 'Base' unten für die Basisklasse für TA und B steht.

public class MyFoo<TA> : IFoo<Base> where TA : Base where B : Base
{
    public void Handle(Base obj) 
    { 
       if(obj is TA) { // TA specific codes or calls }
       else if(obj is B) { // B specific codes or calls }
    }

}
Yohan Chung
quelle
0

Jetzt raten ...

Könnten A, B und C nicht in externen Assemblys deklariert werden, in denen sich die Typhierarchie nach der Kompilierung von MyFoo <T> ändern kann, was die Welt verwüstet?

Die einfache Problemumgehung besteht darin, nur Handle (A) anstelle von Handle (TA) zu implementieren (und IFoo <A> anstelle von IFoo <TA> zu verwenden). Mit Handle (TA) können Sie ohnehin nicht viel mehr tun als auf Zugriffsmethoden von A (aufgrund der A: TA-Einschränkung).

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}
sisve
quelle
Nein dazu >> Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo<T>, bringing havoc into the world?.
Nawaz
Das Ändern der Typhierarchie würde fast jedes Programm ungültig machen . es nicht eine ganze Menge Sinn für mich , dass der Compiler andere seine Entscheidungen auf etwas stützen würde als das, was die Art Hierarchie richtig ist jetzt (obwohl ich denke , es ist für mich nicht weniger Sinn macht , als der Fehler selbst im Moment. ..) Die Problemumgehung wird zwar kompiliert, ist aber nicht mehr generisch, sodass sie nicht wirklich viel hilft.
Aaronaught
0

Hmm, was ist damit:

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    void IFoo<TA>.Handle(TA a) { }
    void IFoo<B>.Handle(B b) { }
}
luqui
quelle
2
Nein, der Fehler liegt in der Klasse selbst. Dies hängt nicht davon ab, wie die Schnittstelle implementiert ist.
Qwertie