IComparer vs. IComparable

  • VB.NET

Es gibt 35 Antworten in diesem Thema. Der letzte Beitrag () ist von VaporiZed.

    Jetzt habe ich das getan, was ich schon längst hätte tun sollen: ich habe ein TestProject aufgesetzt, um dem ICOMPARER auf die Schliche zu kommen.

    Eine datagridview DGVTEST mit zwei Spalten ... COLUMN1 und COLUMN2 ... Beide Spalten haben Sortmode PROGRAMATIC.

    COLUMN2 wird (zur Kontrolle) ganz normal mit dgvTest.Sort(column, direction) sortiert.

    COLUMN1 solll mit einem ICOMPARER case insensitive sortiert werden.

    Per Default soll ASCENDING sortiert werden ... wenn die Reihenfolge aber schon ascending ist, dann soll DESCENDING sortiert werden.

    Klickt man den Header von COLUMN2 an, dann wird die DGV korrekt aufsteigend (case sensitive) sortiert ... klickt man den Header noch einmal an, dann wird absteigend sortiert. Genau so wie gewünscht. Das sieht man an den letzen drei angehängten Screenshots.

    Macht man das jetzt aber mit COLUM1, dann ist die Sortierreihenfolge vollkommen chaotisch ! Das sieht man an den ersten beiden Screenshots.

    Ich habe inzwischen gravierende Zweifel daran, ob die COMPARE Routine richtig konzipiert ist !

    VB.NET-Quellcode

    1. Public Function Compare(ByVal KeyOld As Object, ByVal KeyNew As Object) As Integer Implements IComparer.Compare 'Ignore case
    2. 'ignore case
    3. Dim myKeyOld = KeyOld.ToString.ToUpper
    4. Dim myKeyNew = KeyNew.ToString.ToUpper
    5. Dim retval = myKeyNew.CompareTo(myKeyOld)
    6. Debug.Print("KeyOld - KeyNew: " & KeyOld.ToString & " - " & KeyNew.ToString & " --> " & retval.ToString)
    7. Return retval
    8. End Function


    KeyOld und KeyNew werden mit ToString in einen STRING umgewandelt (was immer dabei jetzt heraus kommt !) ... Das ist notwendig, weil COMPARETO strings als Operanden verlangt. Was um alles in der Welt macht dieser Vegleich ? Und warum sollte er die Spalte COLUM1 in Betracht ziehen !

    Dass hier etwas gravierend im Argen liegt, sieht man, wenn man einfach mal spaßeshalber die Reihenfolge der Operanden vertauscht:

    VB.NET-Quellcode

    1. Dim retval = myKeyOld.CompareTo(myKeyNew))


    Jetzt bleibt die Reihenfolge der DGV einfach unverändert !

    Also .... so kann das nicht funktionieren. Hat jemand eine Idee, wie man das Ding zum Laufen kriegen kann ?

    Das Projekt habe ich angehängt ... es hat gerade mal 100 Zeilen Code ...

    LG
    Peter
    Bilder
    • s 2022-06-04 15-25-149.jpg

      79,4 kB, 624×624, 30 mal angesehen
    • s 2022-06-04 15-24-384.jpg

      79,33 kB, 624×624, 28 mal angesehen
    • s 2022-06-04 15-24-118.jpg

      34,25 kB, 624×624, 27 mal angesehen
    • s 2022-06-04 15-23-351.jpg

      34,39 kB, 624×624, 28 mal angesehen
    • s 2022-06-04 15-23-094.jpg

      28,11 kB, 624×624, 27 mal angesehen
    Dateien

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Peter329“ ()

    Mal in's Blaue und wurde auch schon angesprochen:
    Schau' dir mal die Screens 1+2 an. Das, was du da als "String" erhälst ist ja die DGV-Row, du brauchst aber doch wenn ich das richtig verstanden hab jeweils die Value aus den Zellen - sonst
    ist doch logisch, dass da nix ordentlich sortiert werden kann.

    Also da müsste im Debugger folgendes stehen:
    Statt: KeyOld - KeyNew: DataGridViewRow { Index=0 } - DataGridViewRow { Index=3 } --> 1
    müsste da stehen:
    KeyOld - KeyNew: aaa1 - aaa4 oder sowas. Die Value eben und nicht die Row.
    Also musst du dem Vergleich folgendes übergeben: dgv.Item(ColumnIndex, RowIndex).Value.ToString
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Jau ... vollkommen richtig GENAU DIESE Frage habe ich schon zu Beginn dieses langen Threads gestellt ! s. Posting #4

    Die Frage ist halt nur, wie ich an den ZeilenIndex komme ?

    Genau deshalb meine ich ja, dass die Konzeption der Lösung gemäß Spoiler im Posting #2 nicht in Ordnung ist.

    Trotzdem bin ich happy, dass endlich jemand mein Problem verstanden zu haben scheint !

    LG
    Peter

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Peter329“ ()

    @Peter329: Dass da falsche Sachen miteinander verglichen werden, nämlich die Textdarstellung der Row, aber nicht deren Inhalt, habe ich bereits in Post#19 geschrieben. Auch dass mein Testprojekt das Problem umgeht. Ich weiß nicht, ob ich was überlese oder jemand anderes hier. Daher kann ich nicht mehr dazu beitragen … ;(
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Peter329 schrieb:

    Die Frage ist halt nur, wie ich an den ZeilenIndex komme ?


    Du müsstest ja vermutlich alle inhalte der Spalte auf einmal vergleichen und dann irgendwie an's DGV zur Anzeige zurückgeben.
    Ich kenn mich mit dem Comparer-Gedöns nicht aus, aber in der .Sort Eigenschaft des DGV wird das angeboten: dgv.Sort(comparer As Collections.IComparer)
    D.h. deine Funktion muss eine Collection von IComparer zurückgeben, woran sich das DGV zur Sortierung orientieren kann. Mit For..Each kommt man eigentlich an alle Rows und deren Index,
    irgendwie musst du ja auch den ColIndex noch angeben...
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Nein, man muss eine Instanz von einer Klasse dort einfügen, welche IComparer implementiert. Danach sortiert das DGV sich mithilfe dieses Comparers selbst. Oder besser: Das DGV gibt die Rows nacheinander dem Comparer und der vergleicht und gibt das Vergleichsergebnis an das DGV zurück und das DGV entscheidet, welche der beiden Rows zuerst kommt. Wahrscheinlich BubbleSort, aber das ist wurscht. Da muss man aber sonst gar nix machen.

    ##########

    Der Comparer legt also die Sortierung fest und entscheidet, warum welches Objekt vor dem anderen kommt.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VaporiZed“ ()

    @Vaporizel: Also, ich will ja gern zugestehen, dass du das alles schon in deinen früheren Posts angemerkt hast, dass hier eine DatagridRow verglichen wird und nicht der Inhalt der zu sortierenden Spalte. Das ist mir jetzt auch klar wie Kloßbrühe geworden. Auch wenn ich dazu einige Zeit gebraucht habe. Und ich will auch gern zugestehen, dass ich (sträflicherweise) deine Anmerkungen nicht so eingehend gelesen, gewürdigt und honoriert habe, wie das eigentlich angemessen gewesen wäre. Mea Culpa ! Ich schätze deine Ratschläge sehr.

    Aber jetzt mal "Butter bei de Fisch" ... Wie um alles in der Welt kriege ich jetzt diese Kiste vom Eis ! Ich weiß ich bin nervig ... aber ich hätte halt gern eine konkrete Handlungsanweisung (wenn man das so sagen kann), um mein Problem zu lösen ! Sag jetzt hoffentlich nicht, dass das alles schon in deinen Posts steht ... :) Möglicherweise muss man ja wirklich einen vollkommen neuen Ansatz wählen ... dann bitte ich um ein offenes Wort.

    LG an alle Ratgeber
    Peter
    Bau Dir einen untypisierten Comparer, also einen, der Collections.IComparer implementiert - siehe mein DgvComparer. Einfach eine neue Klasse und hinter dem Namen nen Doppelpunkt und die zu implementierende Schnittstelle. Danach wird die notwendige Compare-Methode automatisch generiert.

    VB.NET-Quellcode

    1. Friend Class DeinSpeziellerComparer : Implements System.Collections.IComparer
    2. Public Function Compare(x As Object, y As Object) As Integer Implements System.Collections.IComparer.Compare
    3. Throw New NotImplementedException()
    4. End Function
    5. End Class

    Die nimmt zwei Objects entgegen. Prüfe, ob das DgvRows sind (mit TypeOf). Wenn beides DgvRows sind, dann musst Du Dir aus den Rows die Werte ziehen, die Du verarbeiten willst. Wenn Du also in Spalte 1 die zu sortierenden Werte hast, dann nimm also Row1.Cells(0).Value.ToString und Row2.Cells(0).Value.ToString her. Natürlich kannst Du jetzt schon mit dem Vergleich beginnen. Aber ich hab eben noch einen typisierten Comparer kreiert, der Collections.Generic.IComparer(Of String) implementiert. Damit ich den auch anderweitig nutzen kann. Wie Du willst. Der Vergleich selbst ist langweilig. Ich hab einige Vorprüfungen drin und wenn dann eben Sonderzeichen (aus deutscher Sicht) dazukommen, dann wird ein Zeichen-pro-Zeichen-Vergleich gemacht und die selbstangelegten Prioritätsreihenfolgen beachtet.
    Es ginge unter Umständen sogar so, dass der untypisierte Comparer an die Rohdaten von Deinem Typ rankommt. Aber dann wird die Darstellung wieder im DGV ein Problem. Und wenn Du die Daten sauber in einer List(Of T) hättest und diese mittels BindingSource im DGV darstellst, dann wäre das noch einfacher. ABER! Dann kannst Du das DGV nicht mehr mit DGV.Sort(DeinComparer) sortieren, weil das vom Compiler abgelehnt wird. Zurecht! Sortiert wird dann in der BindingSource oder notfalls die Rohdaten selbst.

    ##########

    Da Dein Comparer eine sonst ganz normale Klasse ist, kannst Du natürlich da auch alles mögliche an Zusatzmethoden einbauen. So kann es eine Methode geben, bei der Du festlegst, welche DGV-Spalte sortiert wird. Diese Methode wird aufgerufen, bevor Du die DGV-Sort-Methode aufrufst. Und dann natürlich noch auch zuvor die speziellen Reihenfolgen angeben, also "aäáàâ", damit Du dann später im Compiler diese Reihenfolge hernehmen kannst, um zu prüfen, ob Fännes vor Fànnes kommt oder nicht.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „VaporiZed“ ()

    Also, erst mal ganz herzlichen Dank für deine Erklärungen !

    Ich starte einen vollkommen neuen Anlauf und versuche jetzt deine Lösung gemäß Beispielprojekt umzusetzen. Leider sind meine Kenntnisse begrenzt und deshalb fällt es mir schwer dein Coding nachzuvollziehen. Ich hoffe, du hast Geduld mit mir:

    Schon das Befüllen deiner DGV verstehe ich nicht:

    VB.NET-Quellcode

    1. DataGridView1.Rows.Add(New Foo With {.Bar = "Foo"})


    VB.NET-Quellcode

    1. Friend Class Foo
    2. Property Bar As String
    3. Public Overrides Function ToString() As String
    4. Return Bar
    5. End Function
    6. End Class


    Rows.Add() ... fügt der DGV eine neue Zeile hinzu, die hat genau eine Spalte FOOS (was bedeutet eigentlich "foo").

    Den Wert dieser Spalte instanzierst du mit der Klasse FOO ... und die "with" Klausel macht daraus die property FOO.BAR = "..."

    Aber was hat das mit dem Override "ToString()" zu tun ? Wo wird denn ToString() aufgerufen ?

    Hätte man nicht einfach

    DataGridView1.Rows.Add("Foo")

    kodieren können ? Wozu braucht es den "Umweg" über die Class ?

    Es wäre nett, wenn du mir das erklären könntest, damit ich weiterkomme.

    LG
    Peter
    Foo ist ein typischer Default-Name für eine irgendwas-Variable. Das kann man für alles Mögliche verwenden. Dem Leser soll damit klargemacht werden: Das ist n Beispielcode, wo man seine eigenen Namen einsetzen muss. Die Reihenfolge fängt an mit Foo, Bar, Baz, Buzz. Aber das ist zweitrangig.

    Ich pack in das DGV eine Klasseninstanz rein. Wenn ich da nix weiter dazu schreiben würde, würde mir im DGV für jede! Foo-Instanz im DGV angezeigt werden MeinProjektname+Foo, weil der Compiler nicht weiß, wie er meine Foo-Instanzen sonst anzeigen sollte. Er ruft einfach für die Foo-Zeilen ToString auf und zeigt das Ergebnis im DGV an. Dann kommt eben MeinProjektname+Foo raus. Wenn ich aber die ToString-Methode überschreibe und festlege, dass stattdessen Bar angezeigt werden soll, wird eben der Wert dieser Property angezeigt.

    Wenn Du mit DataGridView1.Rows.Add("Foo") arbeiten willst, dann gar kein Problem. Tu das. Das macht den Comparer einfacher. Ich arbeite aber nicht gern mit Rohdaten im DGV, sondern habe Rohdaten in einer Klasse und verwende das DGV nur zur Anzeige derselbigen.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Ok ... das verstehe ich. Ich wollte halt sicherstellen, dass hier nicht irgendeine Funktionalität enthalten ist, die auf die Logik des ICOMPARER Einfluss hat.

    Jetzt rufe ich den ICOMPARER auf.

    VB.NET-Quellcode

    1. Public Function Compare(Row1 As Object, Row2 As Object) As Integer Implements IComparer.Compare
    2. Dim TypedCustomStringComparer As New TypedCustomStringComparer()
    3. CustomOrders.ForEach(Sub(x) TypedCustomStringComparer.AddCustomOrder(x))
    4. Return TypedCustomStringComparer.Compare(DirectCast(Row1, DataGridViewRow).Cells(0).Value.ToString, DirectCast(Row2, DataGridViewRow).Cells(0).Value.ToString)
    5. End Function


    Zunächst wird wohl ein neuer TypedCustomStringComparer instanziert. Das mit dem CustomOrders.ForEach(Sub(x) ... ) ist wohl so zu verstehen, dass x der Reihe nach alle Elemente der StringListe CustomOrders durchläuft ? Aber warum wird CustomOrders zweimal definiert ? Und wieso muss ich das jetzt kopieren ? Könnte man das nicht einmal anlegen und dann darauf zugreifen ?

    Einen Fehler hat die Routine noch .... wenn keine Sonderzeichen vorhanden sind, wird konstant der Vergleichswert 0 zurückgeliefert ...

    Brainfuck-Quellcode

    1. Button1 Click ------------------------------------------------
    2. Compare( aaa1, Aaa5) --> 0
    3. Compare( aaa1, aaa4) --> 0
    4. Compare( Aaa5, aaa4) --> 0
    5. Compare( Aaa4, Aaa5) --> 0
    6. Compare( Aaa5, aaa3) --> 0
    7. Compare( aaa2, Aaa5) --> 0
    8. Compare( Aaa5, aaa5) --> 0
    9. Compare( aaa1, aaa3) --> 0
    10. Compare( aaa1, aaa5) --> 0
    11. Compare( aaa3, aaa5) --> 0
    12. Compare( aaa2, Aaa4) --> 0
    13. Compare( aaa2, aaa4) --> 0
    14. Compare( Aaa4, aaa4) --> 0


    Trotzdem sieht das jetzt doch schon ganz manierlich aus. Ich denke, ich muss diese Routine noch korrigieren:

    VB.NET-Quellcode

    1. Private Function GetCharByCharCheckFor(BarOfFoo1 As String, BarOfFoo2 As String) As Integer
    2. For i = 0 To BarOfFoo1.Length - 1
    3. If i = BarOfFoo2.Length Then Return 1
    4. Dim j = i
    5. For Each CustomOrder In CustomOrders.Where(Function(x) x.Contains(BarOfFoo1(j)) AndAlso
    6. BarOfFoo1(j) <> BarOfFoo2(j))
    7. Dim retval = Math.Sign(CustomOrder.IndexOf(BarOfFoo1(j)) - CustomOrder.IndexOf(BarOfFoo2(j)))
    8. Return retval
    9. Next
    10. Next
    11. Return 0
    12. End Function


    Das sollte ich hinbekommen. Problem gelöst ! Herzlichen Dank für deine unendliche Geduld. Du hast meinen Tag gerettet ...

    LG
    Peter

    Peter329 schrieb:

    VB.NET-Quellcode

    1. Dim retval = Math.Sign(CustomOrder.IndexOf(BarOfFoo1(j)) - CustomOrder.IndexOf(BarOfFoo2(j)))
    2. Return retval
    Hatten wir doch schon in Post #5:

    VB.NET-Quellcode

    1. Return CustomOrder.IndexOf(BarOfFoo1(j)).CompareTo(CustomOrder.IndexOf(BarOfFoo2(j)))
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Peter329 schrieb:

    Aber warum wird CustomOrders zweimal definiert ? Und wieso muss ich das jetzt kopieren ? Könnte man das nicht einmal anlegen und dann darauf zugreifen ?
    CustomOrders ist einmal im untypisierten und einmal im typisierten Comparer definiert. Das sind also zwei unabhängig voneinander nutzbare Klassen. Daher das Rüberkopieren, weil der eine den anderen ja für die eigenen Ergebnisse nutzt.

    Du hast recht, am Ende von GetCharByCharCheckFor darf nicht stehen Return 0, sondern Return BarOfFoo1.CompareTo(BarOfFoo2)
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VaporiZed“ ()

    Nochmals vielen Dank an alle Helfer ! Meine Routine schnurrt jetzt so wie sie soll !

    Die Diskussion war lang ... und deshalb ist es mir ein Bedürfnis, das Ergebnis noch einmal auf den Punkt zu bringen:

    Der erste Ansatz hat einen untypisierten Comparer verwendet. Der liefert zwei Objekte, hinter denen sich dgv rows verbergen. Der fatale Fehler war, diese Objekte einfach mit ToString zu "überbügeln" und dann mit CompareTo zu vergleichen. Das KANN nicht funktionieren. Da hätte ich mich lange abstrampeln können ... und insbesondere der Hinweis, dass man die Objekte debuggen kann, in dem man den Cursor über das Feld im Code schweben lässt, war nicht so ganz hilfreich. Weil dadurch eben genau die String Darstellung angezeigt wird, die ich schon mit Debug.Print angezeigt hatte. Und die hilft nun mal nicht weiter. Mit solchen Ratschlägen kann man den Fragesteller dann ganz schön blöde aussehen lassen ... :)

    Man muss die beiden Objekt mit DIRECTCAST in eine DatagridRow umwandeln und dann kann man mit Cell(Index).value auf den Inhalt der zu sortierenden Spaltenelemente zugreifen. Das war der entscheidende Hinweis. Und schon klappt die Sache.

    Das hatte mir gefehlt. Aber jetzt hab ich das ja gelernt. Na, vielleicht kann diese Diskussion ja wenigstens den einen oder anderen davon abhalten, den gleichen Reinfall zu erleben.

    Nochmals vielen Dank an die geduldigen Helfer, Daumen hoch und Problem gelöst!

    LG
    Peter

    Peter329 schrieb:

    Der erste Ansatz hat einen untypisierten Comparer verwendet.

    kann man nicht einen typisierten Comparer verwenden?
    Vielleicht wäre dann der Hassel mit dem Object-Object - Vergleich ausgeblieben.

    Nur Frage, weil mit DatagridViewRows sortieren kenn ich mich nicht aus - ich sortiere immer schon die Daten (typisiert).
    (Oder verwende gar die BindingSource.Sort-Property)
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.