Laden...

"Type" als generischen Parameter verwenden

Erstellt von timbu42 vor 6 Jahren Letzter Beitrag vor 6 Jahren 6.531 Views
T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren
"Type" als generischen Parameter verwenden

Hallo liebe C#-Gemeinde,

ich habe ein Problem, zu dem ich bisher nichts ergoogeln konnte und keine Lösung gefunden habe.
Es geht um generische Typen und die Klasse Type.

Alles spielt sich in einer Klasse ab, welche die folgende Methode hat:

public T GetWert<T>()
{
    // mache etwas...
}

Ein einfacher Aufruf der Methode wäre ja jetzt:

MyClass m = GetWert<MyClass>();

Soweit so gut, jetzt zum Problem.

Nun kenne ich MyClass im Quelltext nicht direkt, sondern habe sie nur als Variable vom Typ "Type" rumliegen und möchte mit diesem die generische Methode aufrufen, etwa so:

public string Do(Type t)
{
    dynamic wert = GetWert<t>();     // funktioniert nicht! Auch nicht mit typeof(t) statt t.
    return wert.ToString();
}

Hat jemand eine Idee, wie man das machen kann?

Ich sehe noch nicht, dass etwas grundlegend dagegen spricht.

Vielen Dank im Voraus!
Tim

W
955 Beiträge seit 2010
vor 6 Jahren

Du könntest Do() generisch machen.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Hallo,

Danke für die Antwort.

Ich hätte dazu sagen sollen, dass ich das Beispiel vereinfacht habe, um es übersichtlich zu machen. Mein Fehler.

Gehen wir davon aus, dass "t" kein Parameter ist, sondern einfach da ist.

Vielen Dank aber!

public string Do()
{
    Type t = ...;

    dynamic wert = GetWert<t>();     // funktioniert nicht! Auch nicht mit typeof(t) statt t.
    return wert.ToString();
}
849 Beiträge seit 2006
vor 6 Jahren

Hallo,

das funktioniert so nicht weil T zur laufzeit theoretisch jeden Wert annehmen kann. Ein Typparameter muss aber schon zur Compilezeit feststehen.

Du kannst ihn aber als normalen Parameter übergeben.

dynamic solltest Du übrigens nur benutzen, wenn es sich absolut nicht verhindern lässt.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Ok, schade, ich hab mir sowas schon gedacht, dass es am Compiler liegt.

Wenn ich "t" als normalen Parameter übergebe, kann ich z.B. keine "List<t>" mehr erzeugen, da hätte ich dann exakt das gleiche Problem.

dynamic wäre dann die nächste Baustelle, das war jetzt erstmal um zu schauen ob sich das erste Problem überhaupt lösen läßt.

Neue Version des Problems wäre dann:

public void Do()
{
    Type t = GetType();

    List<t> list = new List<t>();     // funktioniert nicht! Auch nicht mit typeof(t) statt t.
}
709 Beiträge seit 2008
vor 6 Jahren

Du könntest das per Reflection lösen.

Die verlinkte Seite zeigt u.a., wie man einen generischen Typen per Reflection erzeugt.
Methodenaufrufe gehen aber auch.

D
985 Beiträge seit 2014
vor 6 Jahren

Natürlich lässt sich das lösen, mit Reflection

How do I use reflection to call a generic method?

Ist nur die Frage, ob man das wirklich will.

849 Beiträge seit 2006
vor 6 Jahren

Hallo nochmals,

leider können wir aus deinem Stück Code nicht sehen, was am Ende herauskommen soll. Ist ein wenig wie zu versuchen in eine Glaskugel zu schauen. Wenn Du noch eine genauere Antwort brauchst, fürchte ich, musst Du etwas weiter Ausholen.

Trotzdem sieht es mir so aus als könnten Interfaces hier nützlich sein.

3.170 Beiträge seit 2006
vor 6 Jahren

Hallo,

Ist nur die Frage, ob man das wirklich will.

Und diese Frage sollte man sich unbedingt auch stellen. Nur weil es geht, heisst das nicht, dass es auch gut ist, das zu tun.

In fast allen Fälle weisen solche Probleme wie hier beschrieben auf Mängel in der Architektur hin und mit einem anderen Ansatz lösen sie sich von selbst.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

T
461 Beiträge seit 2013
vor 6 Jahren

Um es etwas einfacher zu formulieren 😉 teile uns einfach dein Ziel mit, was genau du machen willst.

Meistens gibt es wesentlich einfachere Möglichkeiten, als sich immer weiter in etwas komplexes zu verstricken, bei dem man irgendwann die Lust verliert...

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Hallo Zusammen,

Danke für die vielen guten Antworten!

Ich weiß, dass man meistens mit den "Bordmitteln" gut klar kommt, sprich ohne Dinge wie "dynamic", Reflektion, etc., auch ohne "object".
In dem Projekt, an dem ich arbeite, habe ich allerdings diesen Weg noch nicht gefunden und bin nicht sicher, ob es möglich ist.

Ursprünglich wollte ich der Übersichtlichkeit halber das Problem so gut es geht extrahieren und habe nun auch eine Lösung gefunden (Danke für die Tipps zu Reflektion, das funktioniert). Ist vermutlich ziemlich langsam, ich werde das noch genauer testen.

Für den Fall, dass tatsächlich jemand Lust hat, eine grundlegend andere Lösung zu finden, werde ich das Problem gern etwas ausführlicher beschreiben:

  1. Einer der Hauptbestandteile des anvisierten Programmes ist eine Daten-Klasse, welche Werte verschiedenen Typs dynamisch zur Verfügung stellt. Zum Beispiel können die Daten aus einer CSV-Datei kommen. Die TAB/Komma/Semikolon-getrennten Felder der Datei können z.B. Werte als int, double, string, char, DateTime oder was-auch-immer enthalten.

  2. Für die verschiedenen Datentypen habe ich einen enum angelegt, etwa so:

public enum WertTyp
{
        WT0_ohne,
        WT1_int,
        WT2_long,
        WT3_float,
        WT4_DateTime
}
  1. Das Daten-Objekt hat eine Methode
public T[] GetWerte<T>()
{
    // ...
}

und eine Methode

public string GetWerteAlsString()
{
    WertTyp wertTyp = ...;

    Type t = WertTypToType(wertTyp);
    t[] werte = GetWerte<t>(serie, j);	  // hier taucht die Frage auf.

    //...
}

Ich weiß nicht, ob das an Info reicht, um das grundlegende Konzept eventuell umzuwerfen, aber den enum brauche ich auf jeden Fall (zum Beispiel, um der Daten-Klasse mitzuteilen, als welcher Typ die Daten in einem CSV-Feld vorliegen) und die Methode GetWerte<T>() ebenso.

WertTypToType ist einfach nur etwa sowas:

public static Type WertTypToType (WertTyp wt)
{
    switch (wt)
    {
        case WertTyp.WT0_ohne:
            throw new Exception("Fehler");
        case WertTyp.WT1_int:
            return typeof(int);
         case WertTyp.WT2_long:
             return typeof(long);
         //...
    }
}
F
10.010 Beiträge seit 2004
vor 6 Jahren

Du kommst nicht zufällig von PHP?

Deine ganze Denkweise deutet auf "nicht OOP" konformes scripten hin.

Was Du eigentlich machen willst ist etwas das jeder ORMapper macht, oder ( in deinem Fall ) z.b. die FileHelpers Bibliothek.

Versuche lieber typisiert zu denken, alles andere ist nicht zielführend

3.003 Beiträge seit 2006
vor 6 Jahren

Letzten Endes ist der Ansatz der ORM und auch der Ansatz vom FileHelper auch nix anderes als ein bisschen Reflection-Magie. Und anders geht es auch nicht, wir sind im weitesten Sinn beim Thema Serialisierung und arbeiten mit Typen, die erst zur Laufzeit bekannt werden. Ob man für einen sehr überschaubaren Einsatz sich selbst etwas baut, oder lieber eine fertige Bibliothek nimmt, muss jeder selber wissen. In diesem speziellen Fall würde ich wahrscheinlich auch eher selbst was bauen.((wenn die Alternative FileHelper ist, würde ich sogar ziemlich viel Aufwand betreiben, aber das ist persönlicher Geschmack.)

Wenn die fünf Typen in seiner Enumeration alles sind, liesse sich noch was mit einer abstrakten Fabrik machen, das ohne Reflection auskommt. Wird es flexibler...Pustekuchen.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

@FZelle: ich kenne zwar PHP, Perl, Fortran, QBasic, GWBasic und awk, aber Du kannst mir schon glauben, dass ich sehr objektorientiert denke, viel Programmiererfahrung (auch mit OOP) habe, mit Pattern arbeite, etc. Es geht um ein Projekt, bei dem ich erstmals etwas "dynamisches" mit Datentypen machen muss.
Was genau meinst Du denn deutet auf nicht OOP-konformes Scripten hin?

Eine fertige Bibliothek kommt nicht in Frage, das Gesamtprojekt ist wesentlich komplexer als hier dargestellt und muss viel Spezielleres leisten.

@LaTino: es sind etwas mehr als 5 Typen, aber wahrscheinlich nicht komplett flexibel. Ich kann mir noch nicht vorstellen, inwiefern eine Fabrik hier hilft. Hast Du ein Beispiel?

5.658 Beiträge seit 2006
vor 6 Jahren

Hi timbu42,

Was genau meinst Du denn deutet auf nicht OOP-konformes Scripten hin?

Wenn man versucht, mit Reflection Probleme zu lösen, die man mit ganz normaler OOP lösen könnte, ist das ein guter Hinweis darauf.

Eine fertige Bibliothek kommt nicht in Frage, das Gesamtprojekt ist wesentlich komplexer als hier dargestellt und muss viel Spezielleres leisten.

Das Problem mit den "dynamischen" Datentypen ist schon in anderen Programmen und Bibliotheken gelöst worden. Du mußt die nicht benutzen, aber da kannst du dir anschauen, wie andere bereits vor dir an das Problem herangegangen sind.

Weeks of programming can save you hours of planning

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Ob man die Probleme mit ganz normaler OOP lösen könnte, will ich gerade herausfinden, da ich die Reflection-Lösung auch unbefriedigend finde. Einfach zu behaupten, dass dies möglich ist, finde ich unangemessen.

In anderen Programmen und Bibliotheken habe ich noch nichts gefunden, was mir hilft. Es geht eben nicht einfach um eine Serialisierung, sondern darum, generische Methoden aufzurufen, wenn man den Typ nicht "im Quelltext" hat. Ich glaube nicht, dass man einfach sagen kann: "das Problem ist schon gelöst worden", ohne genau zu verstehen, worum es geht.

T
461 Beiträge seit 2013
vor 6 Jahren

Hm, das was mich etwas irritiert ist das Erkennen der Typen.

Wie machst du das oder wie gehst du vor, um die Daten richtig anwenden zu können? (jetzt im Bsp. zum CSV)

Für was werden die Daten angewendet? Anzeige, Schnittstellen, Datenbank/en..

Sind die Quellen wirklich so dynamisch oder steckt da eine bestimmte Struktur dahinter?

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

2.079 Beiträge seit 2012
vor 6 Jahren

Was mich eher interessiert: Warum ist die Methode überhaupt generisch? Kannst Du das vielleicht umgehen? Ich hab bei dynamischen Dingen bemerkt, dass Generics früher oder später ziemlich oft mit typeof(T) wieder nicht generisch "werden". Die Generics sind dann nur eine Vereinfachung bei der Arbeit, aber nicht zwingend notwendig.

Wenn man so eine flexible Basis baut, dann bietet es sich normalerweise an, da komplett auf Generics zu verzichten, da man sonst früher oder später entweder Klassen oder Methoden mit zig generischen Parametern hat oder umgekehrt gar keinen Parameter, aber Einen braucht.

Man könnte z.B. sagen (ganz sporadisch ein Beispiel aus den Fingern gesaugt):
Wenn Du z.B. CSV lesen willst, wo max. diesen 5 Typen möglich sind, dann schreibst Du dir ein Interface, was eine Methode zum Parsen eines Strings zu Object verspricht.
Irgendwo registrierst Du dann pro Typ eine konkrete Implementierung, die das genau so parst, wie Du es brauchst.

Dann brauchst Du kein Type-Objekt mehr, sondern fragst nach dieser Implementierung über dein enum, gibst der den String und bekommst ein object zurück.
Einziger Nachteil: Du musst alle Implementierungen manuell registrieren - oder Du suchst einmal beim Start per Reflection nach allen Implementierungen. Das Beides finde ich aber immer noch besser.

Wenn die Methode zwingend generisch sein muss, dann kommst Du nicht um Reflection drum herum.
Dann würde ich - wegen der Übersicht - eine Überladung daneben legen, die das Type-Objekt bekommt und die Reflection-Magie macht.
In der Nutzung siehst Du dann nix davon und wenn sie irgendwann doch mal heraus stellen sollte, dass Du die Generics doch nicht zwingend brauchst, dann wird die nicht-generische Methode "führend" und die generische Überladung gibt nur noch typeof(T) weiter.
So hast Du an dieser Stelle die Möglichkeit, die Reflection-Lösung wieder zu verbannen, ohne dass der Rest vom Programm das mit bekommt.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

T
461 Beiträge seit 2013
vor 6 Jahren

Zum Thema CSV ist es doch sehr relativ ohne fixe Vorgaben...

Z.Bsp. eine Nummer
9089789;...

Könnte in einen integer, in ein decimal konvertiert werden aber auch in einen string. Ohne Vorgabe kann es 10x als integer gut gehen und beim 11ten Mal hat man auf einmal folgendes:
9089u789;...

Schon hätte man am selben Index einen string statt integer.
Egal wo man den Wert benötigt (außer Anzeige), find ich es sehr ungenau das Ganze...

In weiterer Folge würde ich, da es nichts genaues ist, einfach bei string bleiben und fertig.. Ist halt immer die Frage was damit gemacht werden soll...

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

D
985 Beiträge seit 2014
vor 6 Jahren

Die CSV-Dateien, die ich liefern/verarbeiten muss, sind niemals irgendwelche, sondern haben eine konkrete Bedeutung und damit gibt es dafür auch eine Spezifikation für den Aufbau.

Wenn dort ein Feld als Integer (Ganzzahl) definiert ist, dann muss da auch etwas kommen, was sich in einen Integer umwandeln lässt, ansonsten ist diese CSV-Datei nicht die, die ich erwarte.

Allerdings ist nicht jeder Feld-Inhalt der aus Ziffern besteht auch automatisch eine Zahl (z.B. GTIN, PLZ, ...) und darf somit auch nicht als Zahl interpretiert werden.

2.079 Beiträge seit 2012
vor 6 Jahren

Wir wissen nicht, was timbu42 da lesen muss (oder ob er überhaupt etwas lesen muss), von daher macht's auch keinen Sinn, zu spekulieren.

Wovon wir denke ich aber aus gehen können:
Er weiß, wann welcher Typ relevant ist, sonst gäbe es kein entsprechendes Enum und keine Methode, die den Typ bekommt.
Wir er zu der Info kommt, ist ja erst mal egal.

Meine Erklärung bezog sich mehr auf eine Möglichkeit, den Teil hinter dieser generischen Methode zu strukturieren mit CSV als Beispiel.
Ob es ein Pattern gibt, was das beschreibt und wie man das nennt, weiß ich nicht, aber vielleicht wird aus meiner Beschreibung heraus ja deutlich, wie ich das meine.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

3.003 Beiträge seit 2006
vor 6 Jahren

@LaTino: es sind etwas mehr als 5 Typen, aber wahrscheinlich nicht komplett flexibel. Ich kann mir noch nicht vorstellen, inwiefern eine Fabrik hier hilft. Hast Du ein Beispiel?

Dafür müsstest du dich von deiner bisherigen Herangehensweise lösen. Du hast zur Zeit, wenn ich es richtig verstanden habe, keine echte Trennung zwischen deinen Daten und der Art, wie sie gefüllt werden, und genau das brauchst du.

Schau dir das Bild auf Wikipedia: Abstrakte Fabrik an, auf das beziehe ich mich.

Abstraktes Produkte: pro Werttyp eins
Abstrakte Fabriken: je eine Methode zum Erstellen jeder Produktart, Implementierungen abh. von der Datenquelle, konkrete Fabriken pro Datenquellenart eine

Ergebnis: pro Fabrik unterschiedliche Familie von Produkten (Daten), die sich ein Interface teilen und typisiert abrufen lassen. Wobei insbesondere die Modellierung der Produkte nicht in Stein gemeißelt ist, ich habe da selbst verschiedene Ideen, die alle nicht bis zum Ende gedacht sind 😉.

Du musst aus meiner Sicht jedenfalls von den Generics weg, denn die sind eine Lösung (Familie von Klassen, die abhängig von einem zur Entwurfszeit bekannten generischen Typ sind), die nicht zu deinem Problem (Familie von Klassen, die abhängig von einem zur Laufzeit bekannten Typ sind) passt. Diese missverstandene Anwendung von Generics findet sich hier im Forum häufiger, du bist also nicht alleine 😉.

LaTino
EDIT: weil's grad passt: "We had a problem we couldn't solve easily. So we threw design patterns at it, and now we've got a problem factory." 😉

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

T
461 Beiträge seit 2013
vor 6 Jahren

Wenn dort ein Feld als Integer (Ganzzahl) definiert ist, dann muss da auch etwas kommen, was sich in einen Integer umwandeln lässt, ansonsten ist diese CSV-Datei nicht die, die ich erwarte.

Allerdings ist nicht jeder Feld-Inhalt der aus Ziffern besteht auch automatisch eine Zahl (z.B. GTIN, PLZ, ...) und darf somit auch nicht als Zahl interpretiert werden.){gray}

Genau das wollt ich ja damit ausdrücken, nur auf eine andere Weise... 😃

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Hui, so viele Antworten, vielen Dank!

Ein paar Worte mehr zum Projekt:

Die Daten-Klasse ist abstrakt. CSV war nur ein Beispiel von vielen für eine Implementierung, also eine Klasse CsvDaten, die von Daten erbt.
Die Werte könnten z.B. auch aus einer Excel-Tabelle kommen, oder aus einer Byte-Datei, oder als Text-Datei mit fester Breite, oder im Speicher liegen als Array, oder als Dictionary, oder...

Diesen konkreten Klassen (CsvDaten, ExcelDaten, ...) muss jeweils mitgeteilt werden, in welcher Weise sie ihre Daten finden und als welchen Typs sie interpretiert werden sollen (wie Sir Rufo ganz richtig angemerkt hat, das ergibt sich nicht immer direkt aus dem Format).

Keine Option ist es, dass der Benutzer für jedes Daten-Objekt eine "RecordClass" anlegt mit Feldern für die Infos, wie z.B. in der FileHelpers Bibliothek.
Die Quellen sind also wirklich "dynamisch" (ThomasE.).

Dann gibt es viele verschiedene Algorithmen, die alle mit der Daten-Klasse arbeiten, d.h. sich von dieser Werte bereitstellen lassen.
Diesen Algorithmen soll es natürlich völlig egal sein, wie die Daten vorliegen (Datei oder Speicher, Format, etc.), daher die abstrakte Klasse.

Um die Algorithmen mit Daten zu versorgen, hat die Daten-Klasse (u.A.) die folgende Methode

public T[] GetWerte<T>(Serie serie)
{
    // ...
}

Auf den Parameter serie dieser Methode muss ich hier nicht weiter eingehen, vielleicht ist es nur gut zu wissen, dass nicht immer alle Werte ausgelesen werden, sondern man konkrete "Serien" anfragt.
T ist der Typ der Werte, den man anfragt.

Diese Methode ist (vielleicht?) unentbehrlich in dieser Form, da sie die Algorithmen mit Werten versorgt. Man könnte hier auf den generischen Typen verzichten, aber
dann kann ich kein generisches Array zurückgeben und das würde mich vermutlich stören, weil ich dann immer casten müsste (Gegenvorschläge höre ich mir aber sehr gern an, hier kenne ich mich nicht so gut aus, da ich in sonstigen Projekten eigentlich immer feste Datentypen habe).
**:::

Teilweise werden auch automatische Konvertierungen durchgeführt, z.B. die Daten-Klasse kennt ihre Werte als int, weiß aber, dass diese zur Basis 100 gespeichert sind (d.h. 15 entspricht 0.15).
Wenn man dann T=int abfragt bekommt man 15, wenn man T=decimal abfragt bekommt man 0.15.

Ich hoffe, das beantwortet alle Fragen. Das mit der Fabrik muss ich nochmal gut durchdenken.

Das Ausgangsproblem meines Posts (mit dem "<t>") taucht dann (bisher) nur in einer Methode auf, welche die Werte als string zurückgeben soll (sie muss dafür GetWerte() aufrufen):

public string[] GetWerteAlsString(Serie serie)
{
    WertTyp wertTyp = serie.WertTyp;
    Type t = WertTypToType(wertTyp);
    t[] werte = GetWerte<t>(serie);		// hier!
}

Ich könnte diese Methode "GetWerteAlsString" wohl auch anders implementieren, habe aber das Gefühl, dass das Prinzip "Klassentyp liegt erst zur Laufzeit vor" noch öfter vorkommen wird und ähnliche Probleme verursachen könnte (passend zum sehr interessanten letzten Satz von LaTino).

Vielen Dank schonmal an alle!

2.079 Beiträge seit 2012
vor 6 Jahren

Diese Methode ist (vielleicht?) unentbehrlich in dieser Form, da sie die Algorithmen mit Werten versorgt. Man könnte hier auf den generischen Typen verzichten, aber
dann kann ich kein generisches Array zurückgeben und das würde mich vermutlich stören, weil ich dann immer casten müsste

Und was hältst Du davon?

public T[] GetWerte<T>(Serie serie)
{
    return GetWerte(typeof(T), serie)
        .Cast<T>()
        .ToArray();
}
public object[] GetWerte(Type type, Serie serie)
{
    // ...
}

Damit hast Du eine generische und eine nicht-generische Variante und das ganz ohne Reflection 😉
Das fordert natürlich, dass die Implementierung auch nicht-generisch arbeiten kann. Aus deiner Erklärung lese ich aber raus, dass das geht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Hi timbu42,

ich würde erst mal hingehen und die Verschiedenen Aspekte Trennen.

Du hast einmal das einlesen einer Datei in Unterschiedlichen Formaten (CSV, EXEL, XML usw.). Schreib dir für die Unterschiedlichen Formate eine eigene Klasse (z.B. CSVProvider). Die Auf die Klasse zugeschnittene Strukturen zurück gibt. Bei CSV wäre es z.B. ein String Array.

Da die Dateien unterschiedlich aufgebaut sein können (wenn ich dich richtig verstanden habe). Z.B. CSV Daten stehen an unterschiedlicher Stelle). Dann kannst du für die Unterschiedlichen CSV Dateine noch eine je Klasse erstellen, die dir die einzelnen "Spalten" passend auf ein Objekt (Konkrete Klasse von dir) mappt. Diese klasse Implementiert dann eine Schnittstelle in der das Objekt Definiert ist was zurück gegeben wird.

Jetzt mal Frei herunter getippt

class CustomerA_CSVMapper() : IMapper //gibt sicher besseren Namen
{

     ICSVProvider _provider;

        public CustomerA_CSVMapper(ICSVProvider provider)
{
       _provider = provider;
}

public MyObject GetObjekt()
{
     var daten = _provider.GetDaten();

     var myObject = new MyObject()

      myObject.Name = daten[0];
      //Anderen Werte alle Passent Mappen und Konvertieren

}


class CustomerB_CSVMapper() : IMapper //gibt sicher besseren Namen
{

     ICSVProvider _provider;

        public CustomerB_CSVMapper(ICSVProvider provider)
{
       _provider = provider;
}

public MyObject GetObjekt()
{
     var daten = _provider.GetDaten();

     var myObject = new MyObject()

      myObject.Name = daten[n];
      //Anderen Werte alle Passent Mappen und Konvertieren

}

Naja oder so ähnlich. Ziel ist es die einzelnen Aspekte zu trennen. Und dann so schnell wie Möglich auf einen Objektorientierten Ansatzt zu Mappen. Mit Konkreten Klassen kannst du auch Anständig Arbeiten.

Die Abhänigkeit welchen Provider und welchen Mapper du brauchst kann dir eine Factory Klasse auflösen. (DI und IoC-Container sind da aber sicher der bessere Ansatz). Und ich würde das ganze in den DAL Packen. Alternative kannst du dir hier mal anschauen wie EF oder Drapper das mit den Unterschiedlichen Providern machen. Und das Repository Pattern kann in den Zusammenhang auch nicht schaden.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Damit hast Du eine generische und eine nicht-generische Variante und das ganz ohne Reflection 😉
Das fordert natürlich, dass die Implementierung auch nicht-generisch arbeiten kann. Aus deiner Erklärung lese ich aber raus, dass das geht.

Sieht erstmal interessant aus. Aber ist das nicht langsam, weil erstens .ToArray() ein neues Array erzeugt und zweitens immer gecastet wird?
Die bei Dir erste Version von GetWerte (die generische) wird sehr viel häufiger verwendet werden, daher wäre es umgekehrt (zweite ruft erste auf) wohl besser, wo man dann wieder das Ausgangsproblem hat.

ich würde erst mal hingehen und die Verschiedenen Aspekte Trennen.

Du hast einmal das einlesen einer Datei in Unterschiedlichen Formaten (CSV, EXEL, XML usw.). Schreib dir für die Unterschiedlichen Formate eine eigene Klasse (z.B. CSVProvider). Die Auf die Klasse zugeschnittene Strukturen zurück gibt. Bei CSV wäre es z.B. ein String Array.

Die Trennung nach Einleseformaten besteht bei mir doch aus den einzelnen Klassen, die von Daten erben (CsvDaten, ExcelDaten, ...). Der Benutzer meiner Bibliothek, der einen Algorithmus auf die Werte eines konkreten Datenobjekts anwenden will (z.B. Csv-Datei mit 10 tab-getrennten Spalten, Spalte 5-8 sollen als int verwendet werden), instanziiert ein Objekt der Klasse CsvDaten mit diesen Infos (z.B. Dateiname, 10, 5, 8, '\t' oder so) und nutzt dieses für den Algorithmus (dem egal ist dass es CsvDaten ist, hauptsache Daten). Wofür brauche ich da noch einen Provider?

P
1.090 Beiträge seit 2011
vor 6 Jahren

Wenn ich dich richtig Verstanden habe sind die einzelnen CSV Dateien schon unterschiedlich Felder (Reihenfolge).
Also zb. die eine hat die Reihenfolge (Name,ID,Anzahl,Einlagerdatum) die Andere (ID,Anzahl,Name).

Der Zugriff auf die CSV Datei ist aber in beiden Fällen der Selbe. Nur das Mapping unterscheidet sich dann.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Wenn ich dich richtig Verstanden habe sind die einzelnen CSV Dateien schon unterschiedlich Felder (Reihenfolge).
Also zb. die eine hat die Reihenfolge (Name,ID,Anzahl,Einlagerdatum) die Andere (ID,Anzahl,Name).

Der Zugriff auf die CSV Datei ist aber in beiden Fällen der Selbe. Nur das Mapping unterscheidet sich dann.

Ja, da stehen komplett unterschiedliche Dinge drin, nicht nur die Reihenfolge unterscheidet sich. Welche das sind und wo sie darin stehen und wie codiert, das teilt der Benutzer der Klasse CsvDaten (die von Daten erbt) mit, z.B. im Konstruktor. Danach weiß das Objekt das und ist von außen nur noch als "Daten" interessant, nicht mehr als "CsvDaten".

Eigentlich ganz einfach, daher verstehe ich nicht, wofür da ein Mapping nötig ist. Die verschiedenen Oberklassen von Daten (CsvDaten, ExcelDaten, ...) sind in ihrer Erzeugung m.E. zu unterschiedlich, um dafür ein einheitliches Konzept zu schaffen wie (mal ganz blöd gesagt):

Daten daten = DatenFactory.Erzeuge(DatenTyp.CSV, ...);  // die Parameter in "..." wären bei einem anderen DatenTyp ganz anders

oder

DatenFactory cf = new CsvFactory();   // oder woher auch immer cf kommt
Daten daten = cf.Erzeuge(...);
P
1.090 Beiträge seit 2011
vor 6 Jahren

Um es mal Grob zu skizzieren:

Interface für den Provider

public interface ICSVProvider
{
String[][] GetData();

}

Abstracte Basis Classe erstellen

public abstract class DACSVBase<TModel>
{

   private ICSVProvider _provider;

 public DACSVBase(ICSVProvider provider)


prodected abstract TModel ToModel(String [] daten)


public IQueryable<TMode>l GetAll() //
{
      var result = new List<TModel>();

      var data = _provider.GetData();

      foreach(var item in data)
      {
            result.add(ToModel(item);
      }

      return result;
}

//Bei Bedarf um weitere Methoden ergänzen


}


Konkrete Klasse daraus


public DAStandartCustomer : DACSVBase<Customer> , ICustomer //Interface kann z.B. vom IoC-Container aufgelöst werden
{

prodected overide CustomerToModel(String [] daten) //ToModel überschreiben mit konkretten Mapping
{
    var kunde = new Customer();

    kunde.ID = date[0];
    kunde.Name = data[1];
   // usw.
}

}

Dann nochmal für einen Kunden nur mit Anderen Mapping

public DAXYZCustomer : DACSVBase<Customer> , ICustomer //Interface kann z.B. vom IoC-Container aufgelöst werden
{

prodected overide CustomerToModel(String [] daten) //ToModel überschreiben mit konkretten Mapping
{
    var kunde = new Customer();

    kunde.ID = date[100];
    kunde.Name = data[10];
   // usw.
}

}

Und jetzt noch mal für ein anderes Objekt

public DAStandartCCar : DACSVBase<Car> , ICar //Interface kann z.B. vom IoC-Container aufgelöst werden
{

prodected overide Car ToModel(String [] daten) //ToModel überschreiben mit konkretten Mapping
{
    var car= new Car();

    car.ID = date[0];
    car.Kenzeichen= data[1];
   // usw.
}

}

Grundlegend könntest du natürlich die Umwandlung auch in der abstrakten Basis Klasse machen. Aber wenn du gegen den ICSVProvider Implementierst. Kannst du da alles Reingeben was auch diese Schnitstelle Implementiert.

Dein ExelProvider könnte z.B. auch die Schnittstelle ICSVProvider implementieren. Wenn dein Kunde als von CSV auf Excel umstellt und die Reihenfolge beibehält, brauchst du keine Zeile Code ändern. Und gegebenen falls nur einen Eintrag in der Config ändern.

p.s. Das ist jetzt einfach nur runter Getippt und man kann sicher noch einiges Optimiern.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

2.079 Beiträge seit 2012
vor 6 Jahren

Sieht erstmal interessant aus. Aber ist das nicht langsam, weil erstens .ToArray() ein neues Array erzeugt und zweitens immer gecastet wird?

Nö, außer das Array ist einige 100k Items groß.

Ich hab auf c# online mal folgenden Code gebastelt:

public static T[] GetWerte<T>()
{
    var werte = GetWerte();
    var watch = Stopwatch.StartNew();
    var result = werte.Cast<T>().ToArray();
    Console.WriteLine(watch.ElapsedMilliseconds + " ms");
    return result;
}
public static object[] GetWerte()
{
    return Enumerable
        .Range(0, 1000000)
        .Cast<object>()
        .ToArray();
}

Mir zeigt er 80ms an.

Das Casten und in ein neues Array schreiben dauert also 80ms. (Mit einem Referenz-Typ sind es 108ms)
Für 1 Million Items kann man das denke ich verkraften.
Und dann sind es immer noch nur 80ms, die der Nutzer sowieso nicht bemerkt, relevant wird es also erst, wenn Du mehrere Million mal castest.

Wenn das später doch als Problem heraus stellen sollte, kannst Du die Implementierung der Methoden immer noch drehen und in der nicht-generischen Variante die Generische aufrufen.
Aber vergiss nicht: Beide Implementierungen messen.
Es kann nämlich auch sein, dass irgendwo durch die generische Implementierung Zeit verloren geht und sich das dann wieder relativiert.

Ich würde mir darum keine Gedanken machen.
Das wird sehr wahrscheinlich nicht relevant sein und die Probleme, die Du dir durch diese "Optimierung" schaffst, wiegen vermutlich schwerer.

PS:

Ich hab mal meinen Test-Code etwas optimiert und Linq raus geworfen.
Das Kopieren in ein Array ist tatsächlich sehr langsam. Wenn ich das Array aber vorher erzeuge und dann nur noch rein schreibe, ist das Thema erst recht irrelevant.

public static T[] GetWerte<T>()
{
    var werte = GetWerte();
    var watch = Stopwatch.StartNew();
    
    var result = new T[werte.Length];
    
    for (var i = werte.Length; i < werte.Length; i++)
        result[i] = (T)werte[i];
    
    Console.WriteLine(watch.ElapsedMilliseconds + " ms");
    return result;
}

Das spuckt mir 0ms aus

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Grundlegend könntest du natürlich die Umwandlung auch in der abstrakten Basis Klasse machen. Aber wenn du gegen den ICSVProvider Implementierst. Kannst du da alles Reingeben was auch diese Schnitstelle Implementiert.

Dein ExelProvider könnte z.B. auch die Schnittstelle ICSVProvider implementieren. Wenn dein Kunde als von CSV auf Excel umstellt und die Reihenfolge beibehält, brauchst du keine Zeile Code ändern. Und gegebenen falls nur einen Eintrag in der Config ändern.

p.s. Das ist jetzt einfach nur runter Getippt und man kann sicher noch einiges Optimiern.

Ich verstehe Dein Konzept und warum es prinzipiell gut ist, allerdings wird es bei mir niemals ein "car.Kennzeichen" geben. Damit meine ich, dass für konkrete Formate (z.B. sowas wie eine CSV-Datei mit Spalte 1 = Kunden-ID, Spalte 2 = Geburtstag, Spalte 3 = Vorname, Spalte 4 = Name) niemals eine Klasse gecodet wird.
Es wird dann nur ein Objekt s der Klasse Serie instanziiert und ihr mitgeteilt, dass es aus 4 Elementen besteht und welche Typen diese haben. Dann wird ein Objekt c der Klasse CsvDaten instanziiert und diesem mitgeteilt, dass es obige Serie s auf Feld 3-6 findet. Eventuell noch sowas wie Formatierung des Datums in Geburtstag. Dann kann man c sagen, dass es über die Zeilen loopen soll (die auch eine Serie z sind) und sich jeweils die konkreten Ausprägungen zur Serie s geben lassen (z.B. 42, 05.08.1970, Hans Meier).

Ich hab mal meinen Test-Code etwas optimiert und Linq raus geworfen.
Das Kopieren in ein Array ist tatsächlich sehr langsam. Wenn ich das Array aber vorher erzeuge und dann nur noch rein schreibe, ist das Thema erst recht irrelevant.

Sehr interessant, vielen Dank! Das ist ja wirklich schnell!
Jetzt muss ich mal prüfen, ob ich GetWerte() nicht-generisch implementieren kann.
Dahinter passiert nämlich noch so einiges.

F
10.010 Beiträge seit 2004
vor 6 Jahren

Du willst also eigentlich nachbauen was MS in 2002 mit der DataTable implementiert hat.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Nein.

Hinweis von MrSparkle vor 6 Jahren

Bitte keine Fullquotes verwenden. Siehe [Hinweis] Wie poste ich richtig?

5.658 Beiträge seit 2006
vor 6 Jahren

Diese Anforderungen sind bisher noch nicht beachtet worden:

Keine Option ist es, dass der Benutzer für jedes Daten-Objekt eine "RecordClass" anlegt mit Feldern für die Infos, wie z.B. in der FileHelpers Bibliothek.

Das Format, in welches die Daten umgewandelt werden, bestimmen offenbar "viele verschiedene Algorithmen" (über deren Aufbau und Funktion wir allerdings nichts wissen):

Dann gibt es viele verschiedene Algorithmen, die alle mit der Daten-Klasse arbeiten, d.h. sich von dieser Werte bereitstellen lassen.
Diesen Algorithmen soll es natürlich völlig egal sein, wie die Daten vorliegen (Datei oder Speicher, Format, etc.), daher die abstrakte Klasse.

Hier müßte man jetzt wissen, wie die Algorithmen das Format bestimmen, in dem sie die die Eingabedaten erwarten. Und warum die Algorithmen sich die Daten nicht selbst in die gewünschte Form konvertieren können.

Teilweise werden auch automatische Konvertierungen durchgeführt, z.B. die Daten-Klasse kennt ihre Werte als int, weiß aber, dass diese zur Basis 100 gespeichert sind (d.h. 15 entspricht 0.15).
Wenn man dann T=int abfragt bekommt man 15, wenn man T=decimal abfragt bekommt man 0.15.

Es geht also nicht (nur) um die Typ-Konvertierung, sondern um eine beliebige Transformation der Daten. Wobei dieses Beispiel auch ohne Typ-Konvertierung auskommen würde, sondern eine einfache Multiplikation ausreichen würde.

Solange aber die Regeln für die Transformationen nicht bekannt sind, können wir nur raten, wie eine Lösung aussehen könnte.

Weeks of programming can save you hours of planning

2.079 Beiträge seit 2012
vor 6 Jahren

Sehr interessant, vielen Dank! Das ist ja wirklich schnell!
Jetzt muss ich mal prüfen, ob ich GetWerte() nicht-generisch implementieren kann.
Dahinter passiert nämlich noch so einiges.

Tipp:
Kümmer dich um Performance erst, wenn es so langsam ist, dass es stört 😄
Das solltest Du aber auch erst dann machen, wenn es wirklich nicht anders geht - oder Du fest stellst, dass das Problem leicht zu lösen ist.

Leider verursachen Performance-Optimierungen gerne schlecht les- und wartbaren Code.
Daher ist es manchmal auch besser, die Zeit in einen schönen Laden-Dialog zu investieren, der dann zwar ein bisschen länger offen ist, der Code dafür aber sehr viel besser wartbar bleibt.

Ist natürlich immer eine Frage der Situation, manchmal geht es ja um jede Sekunde, dann sollte man natürlich auch jede Sekunde raus holen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 6 Jahren

Tipp:
Kümmer dich um Performance erst, wenn es so langsam ist, dass es stört 😄
Das solltest Du aber auch erst dann machen, wenn es wirklich nicht anders geht - oder Du fest stellst, dass das Problem leicht zu lösen ist.

Ich finde es problematisch, wenn man relativ am Anfang des Projektes steht, noch nicht beurteilen kann, ob eine mögliche Lösung mal Performance-Probleme macht (weil man ja noch nichts testen kann), aber schon alles auf diese Lösung zuschneidet. Daher finde ich es sinnvoll, sich vorher darüber Gedanken zu machen.

Klar, im Optimalfall schneidet man nicht speziell auf eine Lösung zu sondern macht das austauschbar. Finde ich in diesem Fall aber noch schwierig.

D
985 Beiträge seit 2014
vor 6 Jahren

Was Palladin007 meint ist der Unterschied zwischen effizientem und hochoptimiertem Code.

Natürlich macht man sich direkt bei der ersten Implementierung Gedanken darum, dass der Code möglichst effizient aber auch sehr gut lesbar ist.

Erst wenn es dort zu unerträglichen Laufzeiten kommt, fängt man an dies zu optimieren.

W
196 Beiträge seit 2008
vor 6 Jahren

Ich komm noch mal kurz zum Ausgangspunkt zurück. Hier meine Lösung zum Aufruf generischer Methoden mittels Type. Ich habe den Ursprungscode verwendet, DeineKlasse meint die Klasse, die GetWert enthält (im Beispielcode ja die gleiche, die ebenfalls Do enthält).


public string Do(Type t)
{
    var methodinfo = typeof(DeineKlasse).GetMethod(nameof(DeineKlasse.GetWert));
    var methodref = methodinfo.MakeGenericMethod(t);
    dynamic wert = methodref.Invoke(this, null); 
    return wert.ToString();
}

Edit: Oh grad gesehen, das SirRufo eine Lösung mittels MakeGeneric bereits verlinkt hat - naja, spart sich ggf. eben einer das klicken auf den Link 😉