PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : [.NET] Kovarianz


Monger
2010-10-06, 10:42:17
Hallo,

ich hab da gerade ein Problem, von dem ich vermute dass es was mit Kovarianz zu tun hat, aber ich krieg das einfach nicht in meinen Kopf rein...

Also: ich hab zwei Klassen die voneinander erben, ich nenn sie mal "Vater" und "Kind". Eine Eigenschaft von "Vater" ist, dass er eine Methode hat, deren Rückgabewert ebenfalls vom Typ Vater ist(ich schreib jetzt mal in VB.NET Syntax, obwohl es natürlich in C# genau das selbe Problem gibt):


Class Vater
Function doSth() as Vater
End Class


Die Unterklasse "Kind" soll hier dagegen den Rückgabewert "Kind" zurückliefern.
Mein erster Gedanke war, diese Methode als virtual/overridable zu kennzeichnen, und in der Unterklasse zu überschreiben:


Class Kind
Overrides Function doSth as Kind ' geht nicht!
End Class


Mein zweiter Gedanke war, dies als Generics zu lösen.


Class Vater(of T)
Overridable Function doSth as T
End Class

Class Kind(of T)
overrides Function doSth as T
End Class

Problem hier ist: ich kann für T keine Constraints angeben. T müsste eigentlich mindestens vom Typ "Vater" sein.


Class Vater(of T as Vater) ' geht nicht!!
Overridable Function doSth as T
End Class


Irgendeine Idee wie ich das am geschicktesten lösen kann? :confused:

RattuS
2010-10-06, 11:13:15
Mein erster Gedanke war, diese Methode als virtual/overridable zu kennzeichnen, und in der Unterklasse zu überschreiben:


Class Kind
Overrides Function doSth as Kind ' geht nicht!
End Class

Es geht, du musst nur die Methode "umlaufen":

class Vater
{
protected virtual Vater Create()
{
return new Vater();
}

public Vater doSth ()
{
Vater result = Create();

return result;
}
}

class Kind : Vater
{
protected override Vater Create() { return new Kind(); }
}

Zwar ist die Rückgabe von Vater, das Objekt ist aber von Kind.


Mein zweiter Gedanke war, dies als Generics zu lösen.


Class Vater(of T)
Overridable Function doSth as T
End Class

Class Kind(of T)
overrides Function doSth as T
End Class

Problem hier ist: ich kann für T keine Constraints angeben. T müsste eigentlich mindestens vom Typ "Vater" sein.


Class Vater(of T as Vater) ' geht nicht!!
Overridable Function doSth as T
End Class

Mit einer abstrakten Klasse geht das:

public abstract class Vater<T> where T : Vater<T>, new()
{
public abstract T doSth();
}

public class Kind : Vater<Kind>
{
public override Kind doSth()
{
return new Kind();
}
}

Eine andere Lösung für dein Vorhaben gibt es afaik nicht, da es sich, wie du schon richtig festgestellt hast, um Kovarianz handelt.

Expandable
2010-10-06, 13:38:59
class Vater
{
public Vater doSth()
{ return new Vater(); }
}
class Kind : Vater
{
public new Kind doSth()
{ return new Kind(); }
}


Ist es das, was du haben willst? Dann kannst du schreiben


Vater vater = new Vater().doSth();
Kind kind = new Kind().doSth();

Monger
2010-10-06, 15:39:06
Danke Rattus, so funktioniert es tatsächlich. Dass die Generic Deklaration so syntaktisch gültig ist, hätte ich ehrlich gesagt nicht erwartet! :ugly:

Ich hab jetzt noch bei Unter-Unterklassen ein Problem, und wie ich das vernünftig mit Collections als Rückgabewert verheirate, aber das bringt mich erstmal weiter.

Monger
2010-10-06, 15:46:32
class Vater
{
public Vater doSth()
{ return new Vater(); }
}
class Kind : Vater
{
public new Kind doSth()
{ return new Kind(); }
}


Ist es das, was du haben willst?

Nein, das wäre eine überladene Methode, und nicht etwa eine überschriebene. Ich brauch aber den Polymorphismus:

List<Vater> list = new List<Vater>;
list.Add(new Vater());
list.Add(new Kind());

foreach(var item in list){
item.doSth(); // Gibt grundsätzlich NUR die Methode der Vaterklasse zurück!!
}

Das geht nur, wenn ich eine Methode überschreibe (was aber nicht geht, weil ich den Rückgabewert verändern würde), oder die Methode per generischem Parameter variabel mache (was dann genau das ist was RattuS beschrieben hat). "Shadowing" reicht hier eben nicht.

Yavion
2010-10-06, 19:38:13
Problem hier ist: ich kann für T keine Constraints angeben. T müsste eigentlich mindestens vom Typ "Vater" sein.


Class Vater(of T as Vater) ' geht nicht!!
Overridable Function doSth as T
End Class


Irgendeine Idee wie ich das am geschicktesten lösen kann? :confused:

Keine Ahnung, ob ich das jetzt in 2min richtig verstanden habe aber:
Was spricht dagegen, hier ein Interface für den Rückgabetypen zu definieren?

public class Vater :IFamily
{
public virtual IFamily DoSth()
{..}

}

public class Child :Vater
{
public override IFamily doSth()
{..}

}

Monger
2010-10-06, 20:57:08
Keine Ahnung, ob ich das jetzt in 2min richtig verstanden habe aber:
Was spricht dagegen, hier ein Interface für den Rückgabetypen zu definieren?

Mir gehts hier um Typsicherheit. Kind.DoSth() darf eben NICHT den Vater zurückgeben - und auch kein anderes Familienmitglied.

Ich hab absichtlich ein paar Details weggelassen damit es nicht zu komplex wird. Im Endeffekt ist das eine Baumimplementierung mit mehreren Vererbungsebenen, so à la: A -> B -> C , wobei A Unterknoten und einen Oberknoten vom Typ A hat, B Unter- und Oberknoten vom Typ B, C dagegen Knoten von Typ B...

Die Collection die die Unterknoten verwaltet, ist selbst generisch, und hat selber Methoden, um unterlagerte Knoten zu suchen. Dafür ist es notwendig, dass diese selbst eben "SubNodes" auf den jeweiligen Knoten aufrufen kann, und tatsächlich die richtige Implementierung zurückkriegt.

Schwer zu beschreiben, aber wegen Typsicherheit hier ist ein Interface keine Alternative.

Gast
2010-10-07, 10:12:22
Mir gehts hier um Typsicherheit. Kind.DoSth() darf eben NICHT den Vater zurückgeben - und auch kein anderes Familienmitglied.


Verstehe ich nicht, dann kannst du auch nicht die Variante von Rattus verwenden. Das ist auch nicht mehr oder weniger Typsicher als über ein Interface bzw. ist das letztendlich eh dasselbe. Das Bsp. von Rattus würde ja auch nicht funktionieren, wenn Kind nicht von Vater erben würde.

del_4901
2010-10-07, 12:53:01
Mir gehts hier um Typsicherheit. Kind.DoSth() darf eben NICHT den Vater zurückgeben - und auch kein anderes Familienmitglied.

Ich hab absichtlich ein paar Details weggelassen damit es nicht zu komplex wird. Im Endeffekt ist das eine Baumimplementierung mit mehreren Vererbungsebenen, so à la: A -> B -> C , wobei A Unterknoten und einen Oberknoten vom Typ A hat, B Unter- und Oberknoten vom Typ B, C dagegen Knoten von Typ B...

Die Collection die die Unterknoten verwaltet, ist selbst generisch, und hat selber Methoden, um unterlagerte Knoten zu suchen. Dafür ist es notwendig, dass diese selbst eben "SubNodes" auf den jeweiligen Knoten aufrufen kann, und tatsächlich die richtige Implementierung zurückkriegt.

Schwer zu beschreiben, aber wegen Typsicherheit hier ist ein Interface keine Alternative.

Ich hab mir jetzt nicht die Muehe gemacht das komplett mit zu durchdenken, aber evtl. hilft dir ein Visitor oder Double Dispatch weiter.
Und evtl. kannst du den Rueckgabewert auch ueber die Signatur fuellen?

Monger
2010-10-07, 14:23:56
Verstehe ich nicht, dann kannst du auch nicht die Variante von Rattus verwenden. Das ist auch nicht mehr oder weniger Typsicher als über ein Interface bzw. ist das letztendlich eh dasselbe.
Dann mach doch mal bitte ein Beispiel. Kann mir das aktuell nicht vorstellen. Theoretisch könnte ich das Interface einmal auf "Vater" und einmal auf "Kind" implementieren, jeweils mit unterschiedlichen generischen Parametern... aber was bringt mir das? Sinn der Geschichte ist ja nunmal, keine Fallunterscheidung machen zu müssen.

Gast
2010-10-07, 14:44:10
Dann mach doch mal bitte ein Beispiel. Kann mir das aktuell nicht vorstellen. Theoretisch könnte ich das Interface einmal auf "Vater" und einmal auf "Kind" implementieren, jeweils mit unterschiedlichen generischen Parametern... aber was bringt mir das? Sinn der Geschichte ist ja nunmal, keine Fallunterscheidung machen zu müssen.

Was für ne Fallunterscheidung? Im Bsp. von Rattus könntest du auch jedes x beliebige andere Objekt zurückliefern, dass von Vater erbt oder anders gesagt im obigen Beispiel ist es für den Consumer Code alles andere als sicher, dass auch wirklich ein Kind Objekt zurückgeliefert wird. Es wird lediglich ein Vater Objekt zurückgeliefert, dass du im obigen Beispiel zu einem Kind Objekt casten kannst. Du könntest aber auch das Objekt Oma zurückliefern, dass zufälligerweise Vater implementiert, aber beim casten zu Kind dann vielleicht eine Exception wirft.

Gast
2010-10-07, 17:40:41
Hallo,

ich hab da gerade ein Problem, von dem ich vermute dass es was mit Kovarianz zu tun hat, aber ich krieg das einfach nicht in meinen Kopf rein...

Also: ich hab zwei Klassen die voneinander erben, ich nenn sie mal "Vater" und "Kind". Eine Eigenschaft von "Vater" ist, dass er eine Methode hat, deren Rückgabewert ebenfalls vom Typ Vater ist(ich schreib jetzt mal in VB.NET Syntax, obwohl es natürlich in C# genau das selbe Problem gibt):


Ich bleibe auch einmal bei VB.NET, da ich mir hier leichter tue. Lässt sich aber sicher 1:1 umlegen.



Class Vater
Function doSth() as Vater
End Class


Die Unterklasse "Kind" soll hier dagegen den Rückgabewert "Kind" zurückliefern.
Mein erster Gedanke war, diese Methode als virtual/overridable zu kennzeichnen, und in der Unterklasse zu überschreiben:


Class Kind
Overrides Function doSth as Kind ' geht nicht!
End Class



Der Rückgabetyp muss immer ident sein. Du kannst aber sehr wohl eine Instanz des Typs Kind zurückgeben:


Class Kind
Overrides Function doSth as Vater
Return New Kind()
End Function
End Class


Das führt dazu, dass du eine Kind Instanz zurückgeben kannst. Die Referenz ist jedoch vom Typ Vater.


Mein zweiter Gedanke war, dies als Generics zu lösen.


Class Vater(of T)
Overridable Function doSth as T
End Class

Class Kind(of T)
overrides Function doSth as T
End Class

Problem hier ist: ich kann für T keine Constraints angeben. T müsste eigentlich mindestens vom Typ "Vater" sein.


Du müsstest es so schreiben können:
Class Kind(of T As Vater)
overrides Function doSth as T
End Class

Das Problem mit generischen Klassen ist, dass du dann keinen Konstruktor mehr aufrufen kannst:


Public Function GetFamilyMember(Of T As Vater)
Return New T() 'Haut nicht hin
End Function


Ich habe das bisher immer so gelöst:

Public Class Vater
Public Overidable Function DoSomething() as Vater
Return New Vater()
End Function
End Class

Public Class Kind
Inherits Vater

Public Overrides Function DoSomething() As Vater
Return New Kind
End Function
End Class

Wenn ich jetzt eine Referenz vom Typ Vater habe, dann ist es ja OK, dass die Referenz, die ich zurück bekomme auch vom Typ Vater ist (Instanz ist ein Kind), da der Programmteil ja die Klasse Kind gar nicht kennt.

Wenn ich eine Referenz vom Typ Kind habe, dann bekomme ich zwar eine Referenz vom Typ Vater zurück, aber ich weiß, dass die Instanz ein Kind ist und kann mit CType die Instanz einfach casten:

Public Sub Test()
Dim Vater As New Vater()
Dim Vater1 = Vater.DoSomething() 'Erster Vater von 10 wird zugewiesen.

Dim Kind As New Kind()
Dim Kind1 As Kind = CType(Kind.DoSomething(),Kind) 'Geht auch auf jeden Fall gut, da ich ja weiß, dass da nur ein Kind zurückkommen kann.
End Sub

Ein Problem bekommst du nur dann, wenn du nicht ein Kind zurückgeben willst, sondern eine Array oder eine Collection von Kindern:


Public Class Vater
Public Overidable Function DoSomething() as Vater()
Dim Ret(5-1) As Vater
Return Ret
End Function
End Class

Public Class Kind
Inherits Vater

Public Overrides Function DoSomething() As Vater()
Dim Ret(5-1) As Kind
Return Ret
End Function
End Class


Ein Array von Vätern kannst du nicht so einfach auf eine Array von Kindern casten oder umgekehrt.

In diesem Fall habe ich es bisher immer so gelöst, dass ich jedes Element für sich gecastet habe. Das erzeugt aber dann eine neue Collection/Array, was zu weitern Problemen führen kann.

Bietchiebatchie
2010-10-07, 18:04:54
Mir gehts hier um Typsicherheit. Kind.DoSth() darf eben NICHT den Vater zurückgeben - und auch kein anderes Familienmitglied.

Ich hab absichtlich ein paar Details weggelassen damit es nicht zu komplex wird. Im Endeffekt ist das eine Baumimplementierung mit mehreren Vererbungsebenen, so à la: A -> B -> C , wobei A Unterknoten und einen Oberknoten vom Typ A hat, B Unter- und Oberknoten vom Typ B, C dagegen Knoten von Typ B...

Die Collection die die Unterknoten verwaltet, ist selbst generisch, und hat selber Methoden, um unterlagerte Knoten zu suchen. Dafür ist es notwendig, dass diese selbst eben "SubNodes" auf den jeweiligen Knoten aufrufen kann, und tatsächlich die richtige Implementierung zurückkriegt.

Schwer zu beschreiben, aber wegen Typsicherheit hier ist ein Interface keine Alternative.


Ich weiss jetzt absolut nicht ob ich dich richtig verstanden habe, aber wenn Interfaces keine Alternative sind willst du wahrscheinlich (ja ich rate gerade) allen Typen explizite Typenparameter hinzufügen. Folgendes Beispiel soll zeigen was ich meine und könnte dir u.U. bei deinem Problem weiterhelfen:

Prinzipiell kann es ja sinn machen eine Menge von Webseiten (die verlinkt sind) als Graphen zu betrachten wo die Knoten eben die Htmlseiten und die Kanten die Links zwischen den Seiten sind.
In C# kann ein Graph (mit expliziter paramterisierung über die Typen von Knoten und Kanten) folgendermaßen beschrieben sein (die where-Klauseln machen das ganze nur typsicherer; sind nicht nötig):

interface INode<TNode, TEdge>
where TNode : INode<TNode, TEdge>
where TEdge : IEdge<TNode, TEdge>
{
IEnumerable<TEdge> Edges { get; }
}

interface IEdge<TNode, TEdge>
where TNode : INode<TNode, TEdge>
where TEdge : IEdge<TNode, TEdge>
{
TEdge Source { get; }
TEdge Target { get; }
}

interface IGraph<TNode, TEdge>
where TNode : INode<TNode, TEdge>
where TEdge : IEdge<TNode, TEdge>
{
IEnumerable<TNode> Nodes { get; }
IEnumerable<TEdge> Edges { get; }
}


Dann kann man diese eine Menge von Webseiten (im folgenden Webgraph genannt) folgendermaßen modellieren:
class Link : IEdge<HtmlPage, Link>
{
public Link Source { get { return null; } }
public Link Target { get { return null; } }
// zusätzliche Methoden und Properties
}

class HtmlPage : INode<HtmlPage, Link>
{
public IEnumerable<Link> Edges { get { return null; } }
// zusätzliche Methoden und Properties, z.B. Url
public string Url { get { return null; } }
}

class Webgraph : IGraph<HtmlPage, Link>
{
public IEnumerable<HtmlPage> Nodes { get { return null; } }
public IEnumerable<Link> Edges { get { return null; } }
// zusätzliche Methoden und Properties
}


Das hat den Vorteil dass man diesen Webgraph als abstraken Graphen behandeln kann (d.h. Algorithmen die die konkreten Typen von IGraph vollkommen ignorieren) aber ebenso auf die Objekte innerhalb dieses Graphen zugreifen kann ohne casten zu müssen (z.B. if (webgraph.Links[10].Source.Url == "http://...") ... ).

][immy
2010-10-07, 18:41:20
Nur um das richtig zu verstehen, du willst mit Klassenvererbung arbeiten, dabei aber wissen das es eine Methode gibt, die sich allerdings beim rückgabewert unterscheidet.

Also beim Vater

private Vater GibElement(..)


und beim Kind

private Kind GibElement(..)


und beim Kind vom Kind (Kind2)

private Kind2 GibElement(..)


Aber auf gar keinen Fall darf Kind etwas zurückgeben was nur vom Typ Vater ist und Kind2 nichts was nur vom Typ Kind ist.

also dies wirst du nicht über vererbung oder Interfaces schaffen, damit diese Methoden automatisch zur verfügung stehen, weil du hierüber Verträge schließt wie sich die Methode nach außen hin präsentiert.
Dir bleibt hier eigentlich nichts anderes übrig (da die anderen Klassen ja wissen sollen das Kind und Vaterelemente ebenfalls diese Methode haben) dies manuell zu überprüfen.
Z.B. durch

if (returnValue is Vater)
{

}


Ansonsten gäbe es aber noch den einfacheren weg dieses in der Ursprungsmethode direkt für alle unterklassen mit zu hinterlegen.


Type baseType = this.GetType();
Type currentType = returnValue.GetType();
while(currentType != typeof(object) && currentType != baseType)
{
currentType = currentType.BaseType;
}
if (currentType != baseType)
{
throw new Exception(...);
}


Auf diese weise ließe es sich vielleicht realisieren, wenn ich dich richtig verstanden habe.

PS:
Sry, aber VB ist auf jeden Fall nicht meine Stärke, daher in C#

Monger
2010-10-09, 10:19:53
Was ich eigentlich suche, ist das was man in Java unter Kovariante Rückgabewerte kennt (Link (http://www.java-tips.org/java-se-tips/java.lang/covariant-return-types.html)).

Also: eine Unterklasse kann den Rückgabewert einer Methode der Oberklasse mit einem spezifischeren Typ überschreiben.

In .NET geht das nur leider nicht, und ich versuche irgendwie das selbe Verhalten zu simulieren.

Mittlerweile habe ich festgestellt, dass Generics da auch keine Lösung sind. Spätestens ab der Enkel Klasse führt das zu enorm aufgeblasener Syntax. Noch dazu kann ich nichts von der Implementierung verbergen, weil die Oberklasse mindestens so sichtbar sein muss wie die Unterklasse.

Wie ich mittlerweile gesehen habe, bin ich auch nicht der einzige mit solchen Problemen:

http://connect.microsoft.com/VisualStudio/feedback/details/90909/need-covariant-return-types-in-c-all-net-langage


http://bytes.com/topic/c-sharp/answers/494582-covariant-return-types

del_4901
2010-10-10, 20:36:57
Warum willst du denn auch unbedingt den Rueckgabetyp ueberschreiben? Wenn du es einfach simulieren moechtest, dann uebergib eine Referenz an die Methode und quallifizier die mit dem out keyword.

Monger
2010-10-10, 21:18:15
Noch einmal: das ist eine Art Baumimplementierung. Die Methode heißt eigentlich "SubNodes" und "Parent". Ich kann der nix übergeben, weil diese Methode die Knotenobjekte selber erst erzeugt.

Bestimmte Knoten innerhalb dieses Baum überschreiben die SubNodes Eigenschaft, und erweitern ihre Oberklasse mit bestimmten Methoden. Damit der Anwender nicht ziellos rumcasten muss, würde ich gerne genau den Typus zurückgeben der hier auch tatsächlich erzeugt wird.

Expandable
2010-10-11, 07:22:33
Ich kann der nix übergeben, weil diese Methode die Knotenobjekte selber erst erzeugt.

Wieso schließt das out-Parameter aus? Genau dafür sind die doch da!

Monger
2010-10-11, 07:57:57
Wieso schließt das out-Parameter aus? Genau dafür sind die doch da!
Ja, okay. Nicht nachgedacht.

Ändert aber nix daran, dass out-Parameter genauso wenig kovariant überschrieben werden können wie Rückgabewerte.

del_4901
2010-10-12, 00:40:59
Ja, okay. Nicht nachgedacht.

Ändert aber nix daran, dass out-Parameter genauso wenig kovariant überschrieben werden können wie Rückgabewerte.
Wiso sollte man die nicht ueberladen/ueberdecken koennen? Wenn du eh nicht casten moechtest, dann hast du doch explizit den Rueckgabetyp gegeben. Wenn du den nicht expliziet angeben moechtest, dann musst du frueher oder spaeter eh casten, oder du machst das implizit ueber die vtable oder noch impliziter ueber dynamic typen. Und rein theoretisch muesste man bei Ueberdeckung auch mit var als ausgabetyp auskommen.