Laden...

Generic Probleme bei Vererbung

Erstellt von cshw89 vor einem Jahr Letzter Beitrag vor einem Jahr 1.001 Views
C
cshw89 Themenstarter:in
4 Beiträge seit 2022
vor einem Jahr
Generic Probleme bei Vererbung

Hallo zusammen,

ich experimentiere seit kurzem mit Unity, stoße aber immer wieder auf Probleme mit Generics, da ich aus meiner längeren Java-Vergangenheit andere Möglichkeiten gewohnt bin. Folgendes Problem habe ich gerade. Ich habe eine Klasse ElementAmount für verschiedene Typen, alle von der Basis-Klasse ElementType. Nun bekomme ich in "Test.Method" eine Liste mit ElementAmounts von "Test.GetList". Hier würde ich in Java schon "IList<ElementAmount<? extends ElementType>>" schreiben. Jetzt will ich aber für alle "ElementAmount<SpecialType>" eine spezielle Methode aufrufen. Beim Aufruf von "DoSomethingWithSpecial" kann ich aber nicht mal "auf eigene Gefahr" casten. Kann ich den Cast vielleicht doch erzwingen, oder ggf. den Rückgabewert von "GetList" ändern? Oder gibt es eine andere Möglichkeit, das Problem zu umgehen?


using System.Collections.Generic;

public class ElementAmount<T> where T : ElementType {
    public readonly T Type;
    public readonly int Number;
    public ElementAmount(T type, int number) {
        Type = type;
        Number = number;
    }
}

public abstract class ElementType {
}
public class SpecialType : ElementType {
}

public class Test {
    public void Method() {
        var list = GetList();
        foreach (var amount in list) {
            if (amount.Type is SpecialType specialType) {
                DoSomethingWithSpecial((ElementAmount<SpecialType>) amount);
            }
        }
    }
    
    public void DoSomethingWithSpecial(ElementAmount<SpecialType> amount) {
    }
    
    public IList<ElementAmount<ElementType>> GetList() {
        // ... fill list ...
        return new List<ElementAmount<ElementType>>();
    }
}

Vielen Dank
Kevin

16.807 Beiträge seit 2008
vor einem Jahr

Man sieht Deine Java-Vergangenheit am Code. Deutlich 😉

Generell kannst Du nicht pauschal einfach so von einer Parent in eine Child Class casten, das geht nirgends.
Wenn Deine abstrakte Basisklasse Tier ist und Du zwei vererbte Klassen Katze und Hund hast, dann kannst Du nicht pauschal alles in Hund casten, weil eine List<Tier> eben auch Katzen beinhalten könnte.

Aus Flow-sicht musst Du durch die abstrakte Liste iterieren und den Typ prüfen.
Du kannst dazu zB https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.oftype?view=net-7.0 verwenden.


Gerade bei Software Design unterscheiden sich Java und .NET zur Runtime schon.
Auch wenns Dir evtl schwer fällt, Du darfst C#/.NET nicht behandeln als sei es Java.

Auf Deinen Code bezogen:

  • Handhabung von Typsicherheit ist doch etwas anders als zu Java. Aus einer sehr strengen Typ-Sicherheitssicht wundert man sich manchmal wirklich, was man sich da bei Java gedacht hat...
  • Code Aufbau
  • In C# gibts für Deinen Zweck Eigenschaften. Felder haben andere Zwecke (=> Properties - C# Programming Guide) und sollten primär für den internen Gebrauch einer Klasse verwendet werden.
C
cshw89 Themenstarter:in
4 Beiträge seit 2022
vor einem Jahr

Danke erstmal Abt. Properties kenne ich schon. Ich benutze sie auch schon an einigen Stellen, hier bisher aber noch nicht. Kann ich hier noch machen, das stimmt.

Java ist streng typsicher. Ich weiß nicht genau, was du meinst. Durch Upper/Lower-Bounds kann man den Typ in der Collection halt "verweichlichen", hat dafür aber auch weniger Möglichkeiten auf die Collection zuzugreifen. Hinzufügen von Elementen geht z.B. nicht. Durch die Bounds ist allerdings ein Cast auf einen Subtyp erlaubt. Wie könnte ich das folgende in C# umsetzen?


public static class Tier {
}
public static class Katze extends Tier {
}
public static class Hund extends Tier {
}

public static void main(String[] args) {
    List<List<? extends Tier>> lists = new ArrayList<>();
    lists.add(new ArrayList<Katze>());
    lists.add(new ArrayList<Hund>());
    
    List<? extends Tier> inner = lists.get(0);
    // inner.add(new Katze());  das geht natürlich nicht, aber...
    List<Katze> katzen = (List<Katze>) inner; // dieser Cast geht
    katzen.add(new Katze());
}

Bezüglich meinem Code, funktioniert die Lösung Enumerable.OfType leider nicht. Die Liste in Test.GetList wird gar nicht von mir befüllt, sondern von Unity. Dadurch habe ich nicht gemerkt, dass alle Einträge vom Typ "ElementAmount<ElementType>" sind, selbst wenn ich ein "SpecialType" dort auswähle. Dadurch kann ich die Elemente gar nicht vom Typ unterscheiden, sondern nur durch den Typ von "ElementAmount.Type".

16.807 Beiträge seit 2008
vor einem Jahr
List&lt;Katze&gt; katzen = (List&lt;Katze&gt;) inner; // dieser Cast geht

Das ist prinzipiell mal ein völlig anderer Code, der mit der Problematik aus dem ersten Beitrag nicht mehr viel gemeinsam hat, ausser, dass Generics verwendet werden.

Schön, dass dieser Cast geht (würde auch in C#) gehen, trotzdem würde man das nie tun, weil das die Typsicherheit untergräbt.
Das ist "reines Glück", dass das funktioniert, weil halt an erster Position eine Liste ist, die nur Katzen enthält.
Es bleibt trotzdem richtig übles Software Design - gehört ins Horror Kabinett 😉

Die Liste in Test.GetList wird gar nicht von mir befüllt, sondern von Unity.

Ja, leider ein Unity-spezifisches Thema, für das C#/.NET nur wenig kann.
Unity hat einige Eigenheiten, auch bei Generics, die sich von üblicher C# Code-Anwendung unterscheidet.
Damit muss man leider leben.

C
cshw89 Themenstarter:in
4 Beiträge seit 2022
vor einem Jahr

Es ist so ziemlich das gleich Problem, wie im Eingangspost. Ersetze nur die inneren Listen durch ElementAmount. Dadurch kann ich zusätzlich sicherstellen, dass der Cast wirklich immer funktioniert. Auch wieder in Java:


public static void main(String[] args) {
    List<ElementAmount<? extends ElementType>> lists = new ArrayList<>();
    lists.add(new ElementAmount<SpecialType>(mySpecialType, 1));
    lists.add(new ElementAmount<OtherType>(myOtherType, 2));

    ElementAmount<? extends ElementType> inner = lists.get(0);
    if (inner.Type instanceof SpecialType) {
        ElementAmount<SpecialType> special = (ElementAmount<SpecialType>) inner;
        DoSomethingWithSpecial(special);
    }
}

Unterschied zum Eingangspost, ich iteriere nicht über die komplette äußere Liste, sondern ermittle nur den ersten Eintrag.

Zum Thema Horror Kabinett. Es war ein simples Beispiel. Ein konkreteres Konstrukt, welches ich häufig benutze:


public static class TierHolder {
    Map<Class<? extends Tier>, List<? extends Tier>> tierListenAnhaengigVomTyp;
    
    public <T extends Tier> void addTier(T tier) {
        List<T> list = (List<T>) tierListenAnhaengigVomTyp.get(tier.getClass());
        if (list == null) {
            list = new ArrayList<>();
            tierListenAnhaengigVomTyp.put(tier.getClass(), list);
        }
        list.add(tier);
    }
    
    public <T extends Tier> List<T> getTiere(Class<T> clazz) {
        return (List<T>) tierListenAnhaengigVomTyp.get(clazz);
    }
    
    public static void main(String[] args) {
        TierHolder holder = new TierHolder();
        holder.addTier(new Katze());
        List<Katze> katzen = holder.getTiere(Katze.class);
    }
}

Durch die innere Struktur, kann ich bei TierHolder.getTiere immer davon ausgehen, dass mir eine konkrete Liste zurückgegeben wird. Natürlich habe ich hier zwei "gefährliche" Casts. Aber solange meine Klasse intern korrekt arbeitet, habe ich außen kein Problem.

16.807 Beiträge seit 2008
vor einem Jahr

Ohne Wertung, aber das Konstrukt erinnert mich an die frühen 2000er Jahre - und eigentlich genau das, was man heutzutage, in einer modernen, typisierten Umgebung nicht mehr haben will; völlig egal ob nun in Java oder .NET.

Klar, Du hast hier eine Ausgangssituation, die eine Unity-Einschränkung darstellt, aber solch ein "Holder" ist heutzutage eines der ineffizientesten und "gefährlichsten" Dinge, die man tun kann. Sowohl eben in der programmatischen Umsetzung (der Dev braucht konkrete Kenntnis) wie auch eben zur Laufzeit 🙂
Die Idee ist es ja, dass die Überführung in eine typisierte Umgebung zu früh wie möglich stattfindet. Hier hingegen ist der Bestand dauerhaft "nicht bekannt" sondern erst bei Zugriff - und das jedes mal über potentiell gefährliche Wege.

Unterschied zum Eingangspost, ich iteriere nicht über die komplette äußere Liste, sondern ermittle nur den ersten Eintrag.

Es ist technisch wie auch aus Architektursicht schon ein riesen Unterschied, ob ich nun eine Liste mit konkreten Typen einer gemeinsamen Basis habe, oder ein Sub-Listen-Konstrukt mit künstlichen Indizes 😉

Dadurch habe ich nicht gemerkt, dass alle Einträge vom Typ "ElementAmount<ElementType>" sind, selbst wenn ich ein "SpecialType" dort auswähle. Dadurch kann ich die Elemente gar nicht vom Typ unterscheiden, sondern nur durch den Typ von "ElementAmount.Type".

Versteh ich nicht ganz, was Du damit sagen willst.
Die Instanz kann ja nicht vom Typ ElementType sein, wenn sie abstrakt ist. Kann also nicht erzeugt werden werden.


Tier tier = new(); // new() geht nicht wenn Tier abstract
Katze katzeVonTier = (Katze)tier; // wird in InvalidCastException enden, weil Upcasting

Tier katze = new Katze("Batman");
Katze katzeVonTKatze = (Katze)katze; // geht

Tier hund = new Hund("Robin");
Katze katzeVonHund = (Katze)hund; // wird in InvalidCastException enden, weil Hund keine Katze

public record class Tier();
public record class Katze(string Name) : Tier();
public record class Hund(string Name) : Tier();

Dadurch kann ich die Elemente gar nicht vom Typ unterscheiden, sondern nur durch den Typ von "ElementAmount.Type".

Wenn es sich wirklich um ElementType-Instanzen handelt, brauchst ohnehin - weil sonst Upcasting - einen Converter, der neue Instanzen der Zieltyps erstellt.

2.078 Beiträge seit 2012
vor einem Jahr

Der Unterschied ist, dass die Generics bei C# im Gegensatz zu Java "zuende" implementiert sind.
Bei Java ist der Datentyp der generischen Variablen, Parameter, etc. immer noch object und der Compiler passt zusätzlich auf.
Bei C# gehen die Generics bis runter auf die IL-Ebene (das Gegenstück zum Java ByteCode) und sind sowohl in den Metdadaten, in den Binaries und zur Laufzeit existent.
Dadurch muss/kann (ich empfinde das als riesigen Vorteil) C# bei dem Thema aber auch deutlich strenger sein und erlaubt solche Casts nicht.

Wenn wir nun dein Tier-Beispiel betrachten:


List<List<Tier>> lists = new List<List<Tier>>();

// Angenommen, das hier wäre erlaubt, ...
lists.Add(new List<Katze>());
lists.Add(new List<Hund>());

List<Tier> katzen = lists[0];
List<Tier> hunde = lists[1];

// ... was passiert dann hier?
// Eigentlich müsste es doch erlaubt sein, da die Variable ja den Typ List<Tier> hat.
// Aber was passiert zur Laufzeit, wenn sich herausstellt, dass die Instanz einen nicht passenden Typ hat?
katzen.Add(new Hund());
hunde.Add(new Katze());

Die inneren Listen sind vom Typ Tier, demnach müsste man ja auch jede Tier-Instanz hinzufügen dürfen, oder?
Wenn die konkrete innere Liste aber vom Typ Katze ist, wie soll da dann Hund hinzugefügt werden?
Diese Einschränkung ist also durchaus sinnvoll, sie verhindert Cast-Fehler zur Laufzeit.

Folgendes geht allerdings schon:


List<IReadOnlyList<Tier>> lists = new List<IReadOnlyList<Tier>>();
lists.Add(new List<Katze>());
lists.Add(new List<Hund>());

Oder Du bleibst beim Basis-Typ Tier:


List<IReadOnlyList<Tier>> lists = new List<IReadOnlyList<Tier>>();
lists.Add(new List<Tier>());
lists.Add(new List<Tier>());

// Hier geht alles, Du musst nur jedes Mal prüfen, ob Du Hund oder Katze hast.
// Meiner Erfahrung nach deutet aber genau das (dass man immer auf den konkreten Typ prüfen muss) auf grobe Architektur-Fehler an anderer Stelle.

Das geht, weil der Datentyp IReadOnlyList<T> den generischen Datentyp nur "raus gibt", da kann Katze oder Hund natürlich immer nach Tier gecastet werden.
IReadOnlyList<T> kann aber nicht verändert werden, der generische Datentyp kann nicht "rein gegeben" werden, denn da hättest Du wieder das Problem, dass Tier nicht immer nach Katze oder Hund gecastet werden kann.

Das Thema heißt Covariance and Contravariance in Generics

4.931 Beiträge seit 2008
vor einem Jahr

Hallo,

so ganz verstehe ich nicht, warum du unbedingt casten möchtest (aus einem ElementAmount<ElementType> wird [in C#] niemals ein ElementAmount<SpecialType>).
Reicht dir denn nicht einfach:


{
   // ...
   if (amount.Type is SpecialType /*specialType */)
       DoSomethingWithSpecial(amount);
}

public void DoSomethingWithSpecial(ElementAmount<ElementType> amount)
{
  // z.B.
  Console.WriteLine(amount.Number);
}

Oder willst du auf die Eigenschaften bzw. Methoden von SpecialType darin zugreifen. Dafür hast du ja den Cast schon durchgeführt und kannst specialType als Parameter weitereichen (oder nochmals in der Methode entsprechend casten).

Aber gerade so Methoden wie DoSomethingSpecial sollten doch besser mit OOP gelöst werden, d.h. eine abstrakte (bzw. virtuelle) Methode, welche dann aufgerufen wird.

2.078 Beiträge seit 2012
vor einem Jahr

Aber gerade so Methoden wie DoSomethingSpecial sollten doch besser mit OOP gelöst werden, d.h. eine abstrakte (bzw. virtuelle) Methode, welche dann aufgerufen wird.

... oder ein Interface mit dieser Methode, das man dann nach Bedarf implementiert oder eben nicht.

C
cshw89 Themenstarter:in
4 Beiträge seit 2022
vor einem Jahr

@Palladin007: Ich denke, das war der entscheidene Hinweis, danke. Ich verstehe nur nicht, warum ich nicht auch Generics von Klassen als covariant definieren kann, da auch hier alles vom Typ T nur zurückgegeben wird, aber nicht geändert. Aber naja, mit einem zusätzlichen Interface funktioniert es dann auch mit OfType. Leider habe ich dadurch allerdings ein neues Problem mit Unity, da ich das Erstellen der Liste (GetList) nicht ohne weiteres im Unity Editor erledigen kann. Dann muss ich da mal weiterschauen. Trotzdem danke 🙂


using System.Linq;
using System.Collections.Generic;

public interface IElementAmount<out T> where T : ElementType {
	public T GetElementType();
}

public class ElementAmount<T> : IElementAmount<T> where T : ElementType {
    public readonly T Type;
    public readonly int Number;
    public ElementAmount(T type, int number) {
        Type = type;
        Number = number;
    }
    public T GetElementType() {
        return Type;
    }
}

public abstract class ElementType {
}
public class SpecialType : ElementType {
}
public class OtherType : ElementType {
}

public class Test {
    private SpecialHolder holder;
    
    public void Method() {
        var list = GetList();
        foreach (var amount in list.OfType<ElementAmount<SpecialType>>()) {
            holder.AddSpecialAmount(amount);
        }
    }
    
    public IList<IElementAmount<ElementType>> GetList() {
        var list = new List<IElementAmount<ElementType>>();
        list.Add(new ElementAmount<SpecialType>(new SpecialType(), 1));
        list.Add(new ElementAmount<OtherType>(new OtherType(), 1));
        return list;
    }
}

public class SpecialHolder {
    public void AddSpecialAmount(ElementAmount<SpecialType> amount) {
        /// add to list
    }		
}

@Th69: Normalerweise gebe ich dir recht. Allerdings möchte ich hier nicht ein Verhalten von ElementAmount ausführen, sondern Instanzen eines bestimmten Typs sammeln (siehe auch meine Lösung oben). SpecialHolder soll nur Instanzen vom Typ "ElementAmount<SpecialType>" sammeln. Im ursprünglichen Code wollte ich es nur so einfach wie möglich halten.

C
55 Beiträge seit 2020
vor einem Jahr

Korrigiert mich, aber wäre hier ein Decorator empfehlenswert? Wirkt halt so auf mich, dass das hier durchaus Sinnvoll sein könnte bei deinen Problem @cshw89. Hier sind ein Paar Infos zu dem Design Pattern:
https://www.dofactory.com/net/decorator-design-pattern

T
73 Beiträge seit 2004
vor einem Jahr

using System;
using System.Collections.Generic;
using static System.Console;

List<Tier> tiere = new ()
{
	new Katze { name = "Freddy" },
	new Katze { name = "Fibi"   },
	new Hund  { name = "Bello"  },
	new Katze { name = "Isi"    },
	new Hund  { name = "Happy"  },
};

foreach (var tier in tiere)
{
	var prefix = tier switch
	{
		Katze => "Die Katze sagt ",
		Hund  => "Der Hund macht ",
		_	  => ""
	};
	
	WriteLine ($"{prefix} {tier.sprich()}");
}

WriteLine (DateTime.Now);

interface Tier
{ 
	string name {get;set;}
	string sprich();
}

class Katze: Tier
{
	public string name {get;set;}
	public string sprich() => "miau";
}

class Hund : Tier
{
	public string name {get;set;}
	public string sprich() => "wau wau";
}

https://dotnetfiddle.net/ZBMNTX