Interfaces anstatt immer templates



  • Raptor schrieb:

    Jetzt zu meiner Frage: Ist die template Definition nicht dadurch hinfaellig? Man kann ja gar nicht jeden beliebigen Typ in Container einfuegen, sondern nur solche mit einer clone Funktion.

    Ja, aber wenn Du per Interface arbeitest, dann zwingst Du den Typen nicht nur eine (implizite) öffentliche Schnittstelle auf sondern auch eine explizite Vererbungshierarchie.

    Sorgfältig angewandt kann das natürlich durchaus ein Vorteil (oder zumindest kein Nachteil) sein. Wie man am .NET-Framework sieht, kann aber genau so etwas ziemlich in die Hose gehen. Stell Dir mal vor, jemand will Deine Methode einbinden, verwendet aber eine Klasse aus einer anderen Bibliothek, die zwar die 'clone'-Methode definiert, aber nicht von Deinem Interface ableitet. Da steht er blöd da.

    Abhilfe schafft das Adapter-Entwufsmuster aber das ist natürlich ne Krücke.



  • Raptor schrieb:

    Container(const Container& c)
        {
            for(int i=0; i<len; ++i)
                data[i] = c.data[i].clone();
        }
    private:
        int len;
        T* data;
    };
    

    Solchen Code sieht man, denke ich recht haeufig.

    nö.
    man hat zwar einige container, die zeiger auf basisklassen enthalten, aber diese container sind nicht besitzer der objekte, sondern nur indextabellen. ne kleine queue, wer noch drankommen soll, oder sowas. sichergestellt wird anderenorts, daß die queue kurzlebiger ist ale der container, der die objekte besitzt und der sie deshalb auch löscht, wenn er verschwindet.



  • Konrad Rudolph schrieb:

    Wie man am .NET-Framework sieht, kann aber genau so etwas ziemlich in die Hose gehen. Stell Dir mal vor, jemand will Deine Methode einbinden, verwendet aber eine Klasse aus einer anderen Bibliothek, die zwar die 'clone'-Methode definiert, aber nicht von Deinem Interface ableitet. Da steht er blöd da.

    Wieso, was ist dann das Problem?



  • Optimizer schrieb:

    Konrad Rudolph schrieb:

    Wie man am .NET-Framework sieht, kann aber genau so etwas ziemlich in die Hose gehen. Stell Dir mal vor, jemand will Deine Methode einbinden, verwendet aber eine Klasse aus einer anderen Bibliothek, die zwar die 'clone'-Methode definiert, aber nicht von Deinem Interface ableitet. Da steht er blöd da.

    Wieso, was ist dann das Problem?

    Och, da gibt es so einige. Beispielsweise, dass die Datentypen für Zahlen zwar die 'IComparable'-Schnittstelle implementieren, nicht jedoch eine vereinheitlichte Schnittstelle für Rechenarten. Auf diese Weise ist es unmöglich, folgenden Quellcode zum Laufen zu bringen:

    class Complex<T>
        : INumeric
        where T: INumeric {
    // ...
    }
    

    Also eine parametrisierte Klasse für komplexe Zahlen, wie in C++, weil es keine einheitliche Schnittstelle für das Rechnen mit Zahlen gibt.

    Das ist natürlich nur ein Beispiel von vielen. Die Container in .NET sind ebenfalls eine einzige Katastrophe, weil sie keine logischen Schnittstellen abstrahieren, anhand derer man generische Algorithmen bauen kann, die allgemeingültig mit Containern arbeiten. In C++ klappt dies viel besser. Aus diesem Grund entwickle ich gerade eine Containerklassen-Bibliothek für .NET. Aber das gehört nicht hierher.



  • STL.NET ist jetzt als Beta oder so verfügbar. Kannste im VC++ Team Blog nachlesen.



  • Artchi schrieb:

    STL.NET ist jetzt als Beta oder so verfügbar. Kannste im VC++ Team Blog nachlesen.

    Schade, dass der Artikel an gerechter Kritik an den .NET-Container spart. Wie dem auch sei, ich programmiere VB.NET/C#, nicht C++ CLI. Daher ist dieses Werkzeug leider keine Ersatz für meine Container-Lib.



  • Achso... hatte gedacht es geht um C++/CLI.



  • Konrad Rudolph schrieb:

    Optimizer schrieb:

    Konrad Rudolph schrieb:

    Wie man am .NET-Framework sieht, kann aber genau so etwas ziemlich in die Hose gehen. Stell Dir mal vor, jemand will Deine Methode einbinden, verwendet aber eine Klasse aus einer anderen Bibliothek, die zwar die 'clone'-Methode definiert, aber nicht von Deinem Interface ableitet. Da steht er blöd da.

    Wieso, was ist dann das Problem?

    Och, da gibt es so einige. Beispielsweise, dass die Datentypen für Zahlen zwar die 'IComparable'-Schnittstelle implementieren, nicht jedoch eine vereinheitlichte Schnittstelle für Rechenarten. Auf diese Weise ist es unmöglich, folgenden Quellcode zum Laufen zu bringen:

    class Complex<T>
        : INumeric
        where T: INumeric {
    // ...
    }
    

    Also eine parametrisierte Klasse für komplexe Zahlen, wie in C++, weil es keine einheitliche Schnittstelle für das Rechnen mit Zahlen gibt.

    Was du gerade ansprichst, ist eine (zu recht kritisierte) Schwachstelle der Generics. Um mit Typparametern bestimmte Operationen durchführen zu können, muss man diese Operationen vorher als Constraint verlangen - ein Prinzip, dass ich außerordentlich gelungen finde und dass seitenlange Template-Fehlermeldungen vermeidet. Es gibt jedoch zur Zeit in der Sprache noch keinen Operator-Constraint - mit der Betonung auf "noch".

    Ich hätte bitte gerne ein Beispiel dafür, was du mit dem Clone() gemeint hast - wenn ich dich da richtig verstanden habe, irrst du nämlich IMHO. Falls ich dich falsch verstanden habe, kannst du ja noch ein Beispiel bringen.

    Anscheinend meinst du, dass es Typen geben kann, die eine bestimmte Schnittstelle erfüllen und tauglich sind, in einem bestimmten Kontext verwendet zu werden, aber nicht das entsprechende Interface angeben - sowas sollte es nicht geben. Das schöne an Interfaces ist ja gerade, dass man nicht einfach eine Toilette anstatt eines Streams übergeben kann, nur weil sie eine Flush()-Methode hat - etwas übertrieben, aber das ist genau die Denke: Man muss sagen "Ich will diesen Vertrag erfüllen".

    Und ich glaube dir auch nicht, dass es sich zufällig ergibt, dass du eine Klasse in einem bestimmten Kontext verwenden könntest und es dann nicht darfst, weil du das Interface nicht ausdrücklich implementierst. Das ergibt sich auch in C++ nicht zufällig so, dass es in jedem Container ein begin() und end() gibt, sondern das macht man so. Und in C# macht man es so, indem man ein Interface implementiert. Man kann sogar ein IToilette.Flush() und ein IStream.Flush() gleichzeitig implementieren, geht alles geil. Und wieder eine Vorgehensweise, die seitenlange Compilermeldungen verhindert. Weil der Compiler nicht irgendwann tief drinnen eine Methode vermisst sondern gleich am Anfang nur den Typ checken muss.

    Das ist natürlich nur ein Beispiel von vielen. Die Container in .NET sind ebenfalls eine einzige Katastrophe, weil sie keine logischen Schnittstellen abstrahieren, anhand derer man generische Algorithmen bauen kann, die allgemeingültig mit Containern arbeiten. In C++ klappt dies viel besser. Aus diesem Grund entwickle ich gerade eine Containerklassen-Bibliothek für .NET. Aber das gehört nicht hierher.

    Doch, tun sie. Es gibt haufenweise Interfaces im Namespace System.Collections.Generic. Und mich würde echt mal interessieren, was du da noch vermissen kannst.

    Es gibt interfaces für Container, vom schlichten IEnumerable<T>, über ICollection<T>, bis hin zu IList<T>, IDictionary<T> und mehr. Und für Algorithmen gibt es IEquatable<T>, IComparable<T>, IComparer<T>, ...
    dann gibt es haufenweise Delegate-Typen, die sich für Collection-Algorithmen einigen wie Predicate<T>, Action<T>, Comparer<T>, ... was bitte fehlt dir da? 😕 Gib mal ein Beispiel. 🙂

    Mein einziger Kritikpunkt an den Collections ist, dass ein sortiertes und unsortiertes Set wirklich fehlen. Aber in C++ fehlt das unsortierte auch, und die unsortierte Map und noch mehr. Und das ist eh kein Design-Fehler, dass könnte man immer noch hinzufügen.



  • Optimizer schrieb:

    Das gibt jedoch zur Zeit in der Sprache noch keinen Operator-Constraint - mit der Betonung auf "noch".

    Meinen Informationen nach soll das nicht geändert werden. Allerdings werden implizite Interfaces eingeführt (dann allerdings über late binding), die zumindest Teilaspekte des Problems lösen.

    Ich hätte bitte gerne ein Beispiel dafür, was du mit dem Clone() gemeint hast - wenn ich dich da richtig verstanden habe, irrst du nämlich IMHO.

    Das sicher nicht. Allerdings bin ich ein Fan von Duck Typing, wohingegen Du dessen Nachteile beleuchtest. Was ich meinte, war folgendes:

    struct IchKannMichKlonen {
        IchKannMichKlonen clone() { return /* Kopie von mir */; }
    }
    

    Voilà, dies implementiert die verlangte Syntax und Semantik, ist aber mit dem ursprünglichen Code leider nicht verwendbar.

    Anscheinend meinst du, dass es Typen geben kann, die eine bestimmte Schnittstelle erfüllen und tauglich sind, in einem bestimmten Kontext verwendet zu werden, aber nicht das entsprechende Interface angeben - sowas sollte es nicht geben. Das schöne an Interfaces ist ja gerade, dass man nicht einfach eine Toilette anstatt eines Streams übergeben kann, nur weil sie eine Flush()-Methode hat

    Das ist so zwar zutreffend, aber nur wenn Du von einer definierten Interface erbst, bedeutet das noch lange nicht, dass Du die erforderliche Semantik korrekt implementierst (siehe .NETs IDisposable, die Semantik des VB-Beispiels aus der MSDN ist falsch ;-)).

    Das heißt: Die Interface bietet hier nur einen Scheinvorteil.

    Es gibt interfaces für Container, vom schlichten IEnumerable<T>, über ICollection<T>, bis hin zu IList<T>, IDictionary<T> und mehr. Und für Algorithmen gibt es IEquatable<T>, IComparable<T>, IComparer<T>, ...
    dann gibt es haufenweise Delegate-Typen, die sich für Collection-Algorithmen einigen wie Predicate<T>, Action<T>, Comparer<T>, ... was bitte fehlt dir da? 😕 Gib mal ein Beispiel. 🙂

    Ok, implementier mir in .NET eine 'Sort'-Methode, die (effizient) sowohl Arrays als auch 'List's als auch 'ArrayList's (und den ganzen Rest) sortieren kann.

    Die Schnittstellen sind einfach unzureichend feinkörnig (z.B. 'IList' -- selten ist es sinnvoll, diese Schnittstelle wirklich für einen Container zu implementieren. Andere Schnittstellen wären hier besser geeignet.

    Abgesehen davon sind die Namen der Container doof (da sie nicht den gängigen Namen entsprechen: Die 'List'-Klasse z.B. ist in Wahrheit ein Vektor) und außerdem bieten sie eine überdicke Schnittstelle mit unnötigen Methoden, die den wahren Einsatz der jeweiligen Container verschleiern, weil nicht klar wird, welche Operationen effizient sind (im Gegensatz zu C++, wo ein Container nur Operationen implementiert, die auch effizienz durchführbar sind, weswegen der List-Iterator z.B. bidirektional statt beliebig ist).

    Mein einziger Kritikpunkt an den Collections ist, dass ein sortiertes und unsortiertes Set wirklich fehlen.

    Unsortiertes Set: Dictionary. Verwendet intern eine Hashtabelle.
    Sortiertes Set: SortedDictionary. Verwendet intern einen Binärbaum.



  • Konrad Rudolph schrieb:

    Optimizer schrieb:

    Das gibt jedoch zur Zeit in der Sprache noch keinen Operator-Constraint - mit der Betonung auf "noch".

    Meinen Informationen nach soll das nicht geändert werden.

    Das ganze ist dann aber von daher obsolet, dass es Extension Methods geben wird. D.h. man könnte der Struct System.Int32 einfach eine Methode 'Add' injizieren und dann über implizite Interfaces ("dynamic interfaces") gehen. Das würde klappen, ist aber natürlich nicht ganz so schön, als wenn man Operatoren in Interfaces definieren könnte.



  • Konrad Rudolph schrieb:

    Optimizer schrieb:

    Das gibt jedoch zur Zeit in der Sprache noch keinen Operator-Constraint - mit der Betonung auf "noch".

    Meinen Informationen nach soll das nicht geändert werden. Allerdings werden implizite Interfaces eingeführt (dann allerdings über late binding), die zumindest Teilaspekte des Problems lösen.

    So weit ich weiß, ist da noch nichts entschieden. Es gibt einige Anregungen (siehe Microsoft Connect) dafür, die Constraint-Syntax zu erweitern. Auf meiner Wunschliste stehen ganz oben:
    - special types wie delegate, enum constrainen zu können
    - operator constraints (oder die möglichkeit, operatoren in interfaces definieren zu können)
    - new(beliebige parameter) constraint - im Moment geht nur new()

    Man kann für diese Anregungen auch voten, tu es, ich würde mich freuen.

    Ich hätte bitte gerne ein Beispiel dafür, was du mit dem Clone() gemeint hast - wenn ich dich da richtig verstanden habe, irrst du nämlich IMHO.

    Das sicher nicht. Allerdings bin ich ein Fan von Duck Typing, wohingegen Du dessen Nachteile beleuchtest. Was ich meinte, war folgendes:

    struct IchKannMichKlonen {
        IchKannMichKlonen clone() { return /* Kopie von mir */; }
    }
    

    Voilà, dies implementiert die verlangte Syntax und Semantik, ist aber mit dem ursprünglichen Code leider nicht verwendbar.

    Aber was ist das Problem damit? Wenn du es in deinem API für richtig hältst, den Clients dein eigenes Cloneable-Interface aufzuzwingen, kannst du das natürlich tun.

    Es gibt doch immer die eine Seite (die die Algorithmen bereitstellt), die stellt die Bedingungen und definiert die Interfaces. Die anderes Seite implementiert diese und kann dafür den Algorithmus nutzen. Das ist in C++ genauso, nur dass dort üblicherweise die Absicht nicht angegeben wird, warum du eine Methode begin() schreibst. Wenn du IEnumerable<T> implementierst, weiß ich, was du damit bezweckst. Ich finde es nicht schlecht, explizit angeben zu müssen, welche Verträge mein Typ erfüllen will.

    Anscheinend meinst du, dass es Typen geben kann, die eine bestimmte Schnittstelle erfüllen und tauglich sind, in einem bestimmten Kontext verwendet zu werden, aber nicht das entsprechende Interface angeben - sowas sollte es nicht geben. Das schöne an Interfaces ist ja gerade, dass man nicht einfach eine Toilette anstatt eines Streams übergeben kann, nur weil sie eine Flush()-Methode hat

    Das ist so zwar zutreffend, aber nur wenn Du von einer definierten Interface erbst, bedeutet das noch lange nicht, dass Du die erforderliche Semantik korrekt implementierst (siehe .NETs IDisposable, die Semantik des VB-Beispiels aus der MSDN ist falsch ;-)).

    Das heißt: Die Interface bietet hier nur einen Scheinvorteil.

    Das ist richtig, das Problem hast du aber überall. Die Vorgehensweise mit Interfaces und Constraints hat für mich mindestens 3 konkrete Vorteile:
    - Du kannst damit klar sagen "ich will diesen Vertrag erfüllen" und es auch zur Laufzeit erfragen. Wenn du nur weißt eine Methode Flush() zu haben, dann kennst du die Absicht dahinter nicht. Mit dem Interface ist klar, welche Absicht damit verfolgt wird.
    - Der Compiler prüft, ob der statische Typ einer Variablen oder eines Typparameters passt. Er compiliert nicht 100 Zeilen Quelltext, instanziert dabei Templates und sieht schon völlig anderen Code vor sich als du und findet dann irgendne Methode plötzlich nicht. Das finde ich nicht sehr geil.
    - Du kannst zwei verschiedene Methoden mit gleichem Namen haben. Was passiert wenn du zwei begin() brauchst? Einmal weil etwas aufzählbar sein soll und einmal, weil ein anderes API von deinem Typ verlangt, dass man etwas beginnen kann. Mit Interfaces in C# kein Problem.

    Es gibt interfaces für Container, vom schlichten IEnumerable<T>, über ICollection<T>, bis hin zu IList<T>, IDictionary<T> und mehr. Und für Algorithmen gibt es IEquatable<T>, IComparable<T>, IComparer<T>, ...
    dann gibt es haufenweise Delegate-Typen, die sich für Collection-Algorithmen einigen wie Predicate<T>, Action<T>, Comparer<T>, ... was bitte fehlt dir da? 😕 Gib mal ein Beispiel. 🙂

    Ok, implementier mir in .NET eine 'Sort'-Methode, die (effizient) sowohl Arrays als auch 'List's als auch 'ArrayList's (und den ganzen Rest) sortieren kann.

    Die Schnittstellen sind einfach unzureichend feinkörnig (z.B. 'IList' -- selten ist es sinnvoll, diese Schnittstelle wirklich für einen Container zu implementieren. Andere Schnittstellen wären hier besser geeignet.

    IList<T> ist aber genau das, was hier alles kann, was du brauchst. IList<T> wird auch von einem T[] implementiert, von daher kein Problem. Etwas, was keine Liste ist, willst du doch gar nicht sortieren. Was soll das sein, ein Heap, ein Baum, ein schwarzes Loch? Auch im Java API wird ein sort nur für Listen angeboten: http://java.sun.com/j2se/1.5.0/docs/api/java/util/Collections.html#sort(java.util.List)
    Was heißt "Liste"? Liste ist ein abstraktes Konzept, die Hauptaussage ist wohl: Die Elemente haben Positionen und ich kann sie immer in dieser Reihenfolge aufzählen. Nur hier macht sortieren überhaupt Sinn.

    Dann kommt noch erschwerend hinzu, dass man eine LinkedList schon wieder anders sortieren sollte, weil der random access eh schon wieder langsam ist. Für ArrayList also Quicksort und Heapsort, für LinkedList MergeSort, ... selbst IList<T> ist noch zu unspezifisch.

    Abgesehen davon sind die Namen der Container doof (da sie nicht den gängigen Namen entsprechen: Die 'List'-Klasse z.B. ist in Wahrheit ein Vektor)

    Die Konvention ist wie folgt: Es gibt Interfaces, die Verhalten definieren und eine Standardimplementierung dazu. Zum Beispiel zu IList<T> die List<T> mit der Implementierung als Array. Als etwas weniger gewöhnlicher Fall gibt es dann noch eine LinkedList<T>. Bzw. IDictionary<K, V> mit Dictionary<K, V> und SortedDictionary<K, V>. Aber ich stimme dir zu, zumindest die List<T> hätte man ArrayList<T> nennen sollen. Vector finde ich nicht so geil, weil ich dabei an was anderes denke. Bei Dictionary<K, V> macht es finde ich Sinn, das unsortierte einfach als "das" Dictionary darzustellen.

    Und wie sind die Namen in der STL? Ich würde niemals eine linked list einfach "list" nennen. Das ist der selbe Fehler, wie eine ArrayList einfach List zu nennen. Eine Liste ist nur ein abstraktes Konzept.

    und außerdem bieten sie eine überdicke Schnittstelle mit unnötigen Methoden, die den wahren Einsatz der jeweiligen Container verschleiern, weil nicht klar wird, welche Operationen effizient sind (im Gegensatz zu C++, wo ein Container nur Operationen implementiert, die auch effizienz durchführbar sind, weswegen der List-Iterator z.B. bidirektional statt beliebig ist).

    Ich finde das gut. Mir ist eigentlich immer klar, welche Operationen effizient sind, aber oft kann es mir krass egal sein. Ich nehme dann gerne eine List (=ArrayList) mit vielleicht 5 Elementen drin, auch wenn ich mal aus der Mitte ein Element rausnehmen muss - das kann ja der std::vector auch, obwohl es ineffizient ist. Es ist sehr oft gar nicht wichtig und die ArrayList braucht dann einfach am wenigsten Platz und ist von der Struktur her das simpelste.

    Mein einziger Kritikpunkt an den Collections ist, dass ein sortiertes und unsortiertes Set wirklich fehlen.

    Unsortiertes Set: Dictionary. Verwendet intern eine Hashtabelle.
    Sortiertes Set: SortedDictionary. Verwendet intern einen Binärbaum.

    Jo, meine Set-Klasse nutzt intern das Dictionary. Aber der Value wird halt verschwendet, das ist schade und müsste nicht sein.



  • Optimizer schrieb:

    So weit ich weiß, ist da noch nichts entschieden. Es gibt einige Anregungen (siehe Microsoft Connect)

    Jaja, das Feedbackprogramm. Hat mich in der Vergangenheit enttäuscht. Vorschläge fließen so gut wie gar nicht ein. Aber gut, das ist MS' Bier. Was mich ärgert, sind die "won't fix"-Anworten auf knallharte Bug-Rückmeldungen. Das ist ärgerlich.

    - special types wie delegate, enum constrainen zu können

    Kenne ich kein Anwendungsbeispiel für. Selbst 'class' und 'struct' sind eigentlich nur bedingt brauchbar. Wie dem auch sei, ich konzentriere mich bei Connect eigentlich auf Bugs und nicht einmal die findet Microsoft beachtenswert (stimmt nicht ganz, sie reagieren drauf, aber nur, um "nein" zu sagen). Das Einbringen von Vorschlägen habe ich schon lange aufgegeben. Und Constraints finde ich da echt nebensächlich, erstmal muss Microsoft das Typensystem neu aufziehen. Das ist zum Henker, wenn man sich Delegates und Type inference anschaut. Aber hier schweifen wir wirklich ab.

    Ich finde es nicht schlecht, explizit angeben zu müssen, welche Verträge mein Typ erfüllen will.

    Wie gesagt, beide Vorgehen haben Vor- und Nachteile. Am .NET-Framework sieht man nunmal die Nachteile recht schön. An Haskell sieht man die Vorteile. Dort ist ja nun wirklich jede öffentliche Schnittstelle über einen Vertrag geregelt. Das macht das ganze sehr erweiterbar.

    Aber auch das Konzept von C++ finde ich eben gut. Mit C++0x wird es noch besser, wenn C++ Typenkonzepte kennt, die Kontrakte abbilden. Dann entfällt nämlich der von Dir genannte Kritikpunkt mit den kryptischen Fehlermeldungen (obwohl man sagen muss, dass moderne Headerdateien das eh schon umsetzen. In VC++ 8 bekomme ich saubere, 1-Zeilen-lange Fehlermeldungen, wenn ein generischer Typ eine Schnittstelle nicht erfüllt. Das klappt super).


Anmelden zum Antworten