Rubrik: Variablen/Strings · Array/ArrayList | VB-Versionen: VB.NET | 30.01.06 |
Instanzen in einer ArrayList sortieren II Wie man Instanzen eigener Klassen sortiert, die in einer ArrayList stehen | ||
Autor: DaveS | Bewertung: | Views: 11.978 |
ohne Homepage | System: WinNT, Win2k, WinXP, Win7, Win8, Win10, Win11 | Beispielprojekt auf CD |
Bezugnehmend auf den Tipp Instanzen in einer ArrayList sortieren (VB.NET) möchten wir Ihnen heute eine verbesserte Version vorstellen, bei der die Basisrichtlinien für .NET Code berücksichtig werden:
Design Guidelines oder Cwalina und Abrams "Framework Design Guidelines" (Adison Wesley).
Es wird sehr empfohlen solche Klassen mit Properties zu versehen, und nicht mit Public Fields. Das erfordert, dass man nach Properties sucht. Aber hier sollten wir nicht ausser Acht lassen, dass Properties auch virtuell sein können (etwa ICustomTypeDescriptor ist implementiert). Das ist nicht völlig irrelevant, weil Windows Forms DataBinding überall mit Properties arbeitet, und man durchaus eigene Klassen mit ArrayLists und DataBinding verwenden könnte. Das verlangt aber, dass man nicht Reflection, sondern das Component Model PropertyDescriptor Mechanismus verwendet. Aber lassen wir das, und bleiben wir der Einfachkeit halber bei einfachen Properties und Reflection.
Dann hätten wir immerhin eine saubere Klasse:
Public Class Schiff Implements IComparable Private _name As String Private _heimathafen As String Private _baujahr As Integer Private _kaufpreis As Single Private _stapellauf As New Date Public Property Name() As String Get Return _name End Get Set(ByVal Value As String) _name = Value End Set End Property Public Property Heimathafen() As String Get Return _heimathafen End Get Set(ByVal Value As String) _heimathafen = Value End Set End Property Public Property Baujahr() As Integer Get Return _baujahr End Get Set(ByVal Value As Integer) _baujahr = Value End Set End Property Public Property Kaufpreis() As Single Get Return _kaufpreis End Get Set(ByVal Value As Single) _kaufpreis = Value End Set End Property Public Property Stapellauf() As DateTime Get Return _stapellauf End Get Set(ByVal Value As DateTime) _stapellauf = Value End Set End Property Public Shared SortierFeld As String = "Heimathafen" Public Shared Sortierung As SortOrder = SortOrder.Ascending Public Sub New(ByVal Name As String, ByVal Heimathafen As String, _ ByVal Baujahr As Integer, ByVal Kaufpreis As Single, ByVal Stapellauf As Date) Me.Name = Name Me.Heimathafen = Heimathafen Me.Baujahr = Baujahr Me.Kaufpreis = Kaufpreis Me.Stapellauf = Stapellauf End Sub Public Sub New() End Sub ' IComparable... End Class
Die CompareTo() Routine ist sehr viel komplizierter als notwendig. Weil alles was man vernünftig vergleichen will (oder kann) IComparable unterstützt (Integer, String, Single usw) müssen wir die Parameter nur zu IComparable casten, und wenn das nicht klappt wird eine Exception ausgelöst, was sinnvoll ist weil wir die Werte dann eh nicht vergleichen können. So auszig Zeilen Code wird einfach
Public Function CompareTo(ByVal obj As Object) As Integer Implements System.IComparable.CompareTo Dim s As Schiff s = CType(obj, Schiff) Dim f1 As Reflection.PropertyInfo f1 = Me.GetType.GetProperty(Me.SortierFeld) Dim f2 As Reflection.PropertyInfo f2 = s.GetType.GetProperty(Me.SortierFeld) Dim Wert1 As IComparable = CType(f1.GetValue(Me, Nothing), IComparable) Dim Wert2 As Object = f2.GetValue(obj, Nothing) Return Wert1.CompareTo(Wert2) End Function
Aber so sind wir längst nicht fertig, zweimal wird ein PropertyInfo (ursprünglich FieldInfo) mittels Reflection geholt (eine teure Operation) obwohl wir wissen, dass wir es in beiden Fällen mit einem Objekt vom Typ Schiff zu tun haben. Wenn wir diese Schwäche auch ausbügeln haben wir:
Public Function CompareTo(ByVal obj As Object) As Integer Implements System.IComparable.CompareTo Dim s As Schiff s = CType(obj, Schiff) Dim f1 As Reflection.PropertyInfo f1 = Me.GetType.GetProperty(Me.SortierFeld) Dim Wert1 As IComparable = CType(f1.GetValue(Me, Nothing), IComparable) Dim Wert2 As Object = f1.GetValue(obj, Nothing) Return Wert1.CompareTo(Wert2) End Function
Allerdings, damit sind wir mit der Kritik auch längst nicht fertig. Reflection bei jedem Vergleich zwischen zwei Elementen des Arrays zu verwenden ist extrem ineffizient, und es ist sehr ungeschickt Parameter an CompareTo() mittels statischen (Shared) Variablen zu übergeben, und nicht nur ungeschickt sondern umständlich und fehleranfällig. Und reverse Sort zu implementieren indem wir das ganze Array umschaufeln ist auch nicht gerade elegent. Wesentlich sinnvoller wäre es ein IComparer Objekt bei arraylist.Sort() zu übergeben, wo die notwendigen Parameter im Klassenobjekt kapsuliert sind. Wir sind dann gleich alle drei Schwächen los, weil wir Reflection nur im Klassenkonstruktor benutzen müssen, und eine Sortoption auch übergeben können. Dann hätten wir die Klasse:
Public Class Schiff ' Fields, Properties, Constructors...(Shared Felder entfallen) Public Class Comparer Implements IComparer Private _sortProperty As Reflection.PropertyInfo Private _sortDescending As Boolean Public Sub New(ByVal sortFieldName As String, ByVal sortDescending As Boolean) _sortProperty = GetType(Schiff).GetProperty(sortFieldName) _sortDescending = sortDescending End Sub Public Function Compare(ByVal obj1 As Object, ByVal obj2 As Object) As Integer Implements IComparer.Compare Dim Wert1 As IComparable = CType(_sortProperty.GetValue(obj1, Nothing), IComparable) Dim Wert2 As Object = _sortProperty.GetValue(obj2, Nothing) Dim res As Integer = Wert1.CompareTo(Wert2) If _sortDescending Then Return -res Return res End Function End Class End Class
Der Aufruf im Hauptcode sieht denn so aus:
AL.Sort(New Schiff.Comparer("Baujahr", False))
AL.Reverse() entfällt ebenfalls.
Ein etwas erweitertes Beispiel
Dieser Code benutzt PropertyDescriptor. Die Daten werden im DataGrid dargestellt, was wesentlich übersichtlicher ist, und uns erlaubt die Sortreihenfolge mittels Mausklick auf Spaltenkopf zu setzen, eine etwas bessere Demonstration, denke ich.
Public Class Form1 Inherits System.Windows.Forms.Form #Region " Windows Form Designer generated code " Public Class Schiff Private _name As String Private _heimathafen As String Private _baujahr As Integer Private _kaufpreis As Single Private _stapellauf As New Date Public Property Name() As String Get Return _name End Get Set(ByVal Value As String) _name = Value End Set End Property Public Property Heimathafen() As String Get Return _heimathafen End Get Set(ByVal Value As String) _heimathafen = Value End Set End Property Public Property Baujahr() As Integer Get Return _baujahr End Get Set(ByVal Value As Integer) _baujahr = Value End Set End Property Public Property Kaufpreis() As Single Get Return _kaufpreis End Get Set(ByVal Value As Single) _kaufpreis = Value End Set End Property Public Property Stapellauf() As DateTime Get Return _stapellauf End Get Set(ByVal Value As DateTime) _stapellauf = Value End Set End Property Public Sub New(ByVal Name As String, ByVal Heimathafen As String, _ ByVal Baujahr As Integer, ByVal Kaufpreis As Single, ByVal Stapellauf As Date) Me.Name = Name Me.Heimathafen = Heimathafen Me.Baujahr = Baujahr Me.Kaufpreis = Kaufpreis Me.Stapellauf = Stapellauf End Sub Public Sub New() End Sub Public Class Comparer Implements IComparer Private Shared _sortProperties As System.ComponentModel.PropertyDescriptorCollection Private _sortProperty As System.ComponentModel.PropertyDescriptor Private _sortDescending As Boolean Public Sub New(ByVal sortFieldName As String, ByVal sortDescending As Boolean) If _sortProperties Is Nothing Then _sortProperties = System.ComponentModel.TypeDescriptor.GetProperties(GetType(Schiff)) End If _sortProperty = _sortProperties(sortFieldName) _sortDescending = sortDescending End Sub Public Sub New(ByVal sortFieldNumber As Integer, ByVal sortDescending As Boolean) If _sortProperties Is Nothing Then _sortProperties = System.ComponentModel.TypeDescriptor.GetProperties(GetType(Schiff)) End If _sortProperty = _sortProperties(sortFieldNumber) _sortDescending = sortDescending End Sub Public Function Compare(ByVal obj1 As Object, ByVal obj2 As Object) As Integer Implements IComparer.Compare Dim Wert1 As IComparable = CType(_sortProperty.GetValue(obj1), IComparable) Dim Wert2 As Object = _sortProperty.GetValue(obj2) Dim res As Integer = Wert1.CompareTo(Wert2) If _sortDescending Then Return -res Return res End Function End Class End Class Dim AL As New ArrayList Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load ' Erstellen einiger Instanzen Dim s1 As New Schiff("Albatros", "Rostock", 1864, 492.9, New Date(1863, 3, 5)) Dim s2 As New Schiff("Ubena von Bremen", "Bremen", 1380, 482.5, New Date(1379, 8, 27)) Dim s3 As New Schiff("Adler von Lübeck", "Lübeck", 1565, 285.7, New Date(1563, 12, 9)) Dim s4 As New Schiff("Rath von Riga", "Riga", 1341, 450.34, New Date(1340, 11, 24)) ' Füllen der ArrayList mit den Schiffen AL.Add(s1) AL.Add(s2) AL.Add(s3) AL.Add(s4) ' Show data DataGrid1.DataSource = AL End Sub Dim currentSortColumn As Integer = -1 Private Sub DataGrid1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles DataGrid1.Click Dim mp As Point = DataGrid1.PointToClient(MousePosition) Dim hti As DataGrid.HitTestInfo = DataGrid1.HitTest(mp.X, mp.Y) If hti.Type = DataGrid.HitTestType.ColumnHeader Then Dim sd As Boolean = hti.Column = currentSortColumn AL.Sort(New Schiff.Comparer(hti.Column, sd)) If sd Then currentSortColumn = -1 Else currentSortColumn = hti.Column End If End If DataGrid1.DataSource = AL End Sub End Class