Saschas Weblog

ESCde Developer Blog

  Home :: Kontakt :: RSS Feed
  30 Posts :: 0 Artikel :: 10 Kommentare :: 32 Trackbacks

Archiv

Post Kategorien

ESCde

ESCde Blogger

Im letzten Post hatte ich eine List<T> mit Hilfe von IComparer<T> und Predicate<T> sortiert bzw. gefiltert. Das hatte so weit auch funktioniert. Das einzige, was mir an dieser Lösung noch nicht gefällt: Wenn ich mich dafür entscheiden sollte, meine MP3-Dateien jetzt nach einem Artist statt nach einem Titel zu filtern, muss ich zuerst eine entsprechende Funktion dazu erstellen, dann das Prädikat erzeugen, was auf diese Funktion zeigt, und dieses dann beim Aufruf der FindAll-Methode verwenden. Das gleiche beim Sortieren. Der vorhandene Mp3Comparer sortiert nur nach Artist. Neue Sortierung, neuer Comparer. Das muss doch einfacher gehen...

Die Lösung bietet Reflection. Zumindest in meinem Fall. Reflection ist Gift für die Performance, so dass man bei großen Listen evtl. ein Problem mit den Laufzeiten der Sort-Methode bekommt, wenn bei der Erstellung der Sortierordnung jedes Listenelement mehrfach mittels Reflection untersucht werden muss.

An den Comparer wird der Name der Eigenschaft übergeben, in der Compare-Methode wird die Eigenschaft und deren Werte per Reflection aus den zu vergleichenden Elementen ermittelt und das Ergebnis des Vergleichs zurückgeliefert. 

Der bisherige MP3Comparer ist speziell auf den Vergleich von Instanzen des Typs ID3Tag ausgelegt, da er auf die Eigenschaften Titel und Artist angewiesen ist. Daher war er auch vom Typ IComparer<ID3Tag>. Der neue Comparer, den ich ReflectionComparer getauft habe, ist hingegen für alle Typen geeignet, daher vom Typ IComparer<T>. Der Comparer funktioniert aber nur, wenn der Typ der Eigenschaften, deren Werte verglichen werden sollen, selbst das IComparable Interface implementiert. Für die Standarddatentypen wie String, Int32, etc. ist dies erfüllt.

Der komplette Code für den neuen Comparer sieht nun folgendermaßen aus: 

public class ReflectionComparer<T> : IComparer<T> { SortType sortType; string sortPropertyName; public ReflectionComparer(string sortPropertyName) { this.sortPropertyName = sortPropertyName; this.sortType = SortType.Ascending; } public ReflectionComparer(string sortPropertyName, SortType sortType) { this.sortPropertyName = sortPropertyName; this.sortType = sortType; } #region IComparer<T> Members public int Compare(T x, T y) { // Mittels Reflection die Werte //der Eigenschaften aus x und y bestimmen object valueX = x.GetType().GetProperty( sortPropertyName).GetValue(x, null); object valueY = y.GetType().GetProperty( sortPropertyName).GetValue(y, null); // Der Typ der Eigenschaft muss IComparable implementieren IComparable comparable = (IComparable)valueX; // Wert x mit Wert y vergleichen return (int)sortType * comparable.CompareTo(valueY); } #endregion }

Auf sämtliche Fehlerbehandlung wurde in diesen Beispielen verzichtet. Gerade bei Reflection ist das aber wichtig, weil der Kompiler keinen Fehler meldet, wenn wir nach einem Feld sortieren wollen, das es gar nicht gibt. In der Form kann ich den IComparer wie gehabt an die Sort-Methode von List<T> anhängen. Ich möchte den Aufruf jedoch etwas intuitiver kapseln, so dass ich die Liste mit

id3Tags.Sort("Artist", SortType.Ascending); id3Tags.Sort("Album", SortType.Descending);

entsprechend sortieren kann. Das heißt, ich muss eine eigene Generic Collection erzeugen. Es wäre jetzt naheliegend, diese CustomGenericCollection von System.Collections.Generic.List abzuleiten. Dies sollte man nach den Empfehlungen von Microsoft jedoch nicht tun, da List<T> eine große Anzahl von öffentlichen Methoden und Eigenschaften besitzt, die in den meisten Szenarien nicht relevant sind. Das heißt nicht, dass man nicht von List<T> erben könnte. Wenn man z.B. nur eine weitere Methode hinzufügen möchte, dann kann es schon Gründe geben, es auf diesem Weg machen. Es ist aber nicht möglich, sich in die Eventkette der Liste einzuklinken. Muss z.B. beim Löschen eines Elements aus der Liste ein Logeintrag erstellt werden, ist List<T> als Basisklasse nicht zu verwenden. Überhaupt bietet List<T> keine überschreibbaren Member, was auch ein Zeichen dafür ist, dass diese Klasse nicht zum vererben gemacht wurde. Auch sollten keine List<T> Objekte als Eigenschaften von Klassen nach außen gegeben werden.

FXCop meldet einen DoNotExposeGenericLists Fehler mit dem Hinweis: "System.Collections.Generic.List<T> is a generic collection designed for
performance not inheritance and, therefore, does not contain any virtual members. The following generic collections are designed for inheritance and
should be exposed instead of System.Collections.Generic.List<T>.
* System.Collections.ObjectModel.Collection<T>
* System.Collections.ObjectModel.ReadOnlyCollection<T>
* System.Collections.ObjectModel.KeyedCollection<TKey, TItem>"

Krzysztof Cwalina (Program Manager .Net Framework team at MS)  hat in einem Blogpost "System.Collections vs. System.Collection.Generic and System.Collections.ObjectModel" die verschiedenen Collection-Klassen und -Interfaces in den verschiedenen Namespaces in einer Übersicht zusammengestellt. Auch sehr hilfreich war ein Auszug des Wrox Buches "Professional C# 2005"

Anders die Klasse Collection<T>, die in der MSDN als "base class for a generic collection" beschrieben wird. Diese gehört aber nicht zum Namensraum System.Collections.Generic, sondern findet sich in System.Collections.ObjectModel. Grund dafür ist eine gleichnamige, nichtgenerische Collection Klasse aus dem Microsoft.VisualBasic Namensraum, der bei Visual Basic Klassen, ebenso wie System.Collections.Generic, standardmäßig importiert wird und es dadurch zu Namenskonflikten kommen kann.
Nach aussen sind bei der Collection<T> Klasse nur die nötigsten Eigenschaften und Methoden verfügbar und intern kann die innere Liste über die Items-Eigenschaft abgegriffen werden. Zudem bietet Collection<T> überschreibbare Methoden zur Benachrichtigung bei Änderungen an der Liste, d.h. bei Hinzufügen, Aktualisieren und Löschen von Elementen. Das hört sich doch gut an, eine CustomGenericCollection ist dann schnell erstellt. 

public class CustomGenericCollection<T> : System.Collections.ObjectModel.Collection<T> { protected override void InsertItem(int index, T item) { // Aktion, wenn Item hinzugefügt wird base.InsertItem(index, item); } public T[] ToArray() { return ((List<T>)base.Items).ToArray(); } public void Sort(string sortPropertyName) { Sort(sortPropertyName, SortType.Ascending); } public void Sort(string sortPropertyName, SortType sortType) { ReflectionComparer<T> comparison = new ReflectionComparer<T>(sortPropertyName, sortType); ((List<T>)base.Items).Sort(comparison); } }

Neben den zwei Sort-Methoden, die die innere Liste mit dem ReflectionComparer von oben aufrufen, wird zusätzlich noch die ToArray-Methode der inneren Liste nach außen geführt, die in Collection<T> der Schlankheit zuliebe eingespart wurde. In der Insert-Methode könnte man das Hinzufügen eines neuen Elements zur Liste protokollieren oder auch unterbinden.

Die Filterung sollte auch noch etwas einfacher vonstatten gehen. Das Vorgehen ist wieder ähnlich. Die bisherige Filtermethode, die mittels Prädikat an die FindAll-Methode der List<T> übergeben wurde, wollte ein Objekt vom Typ ID3Tag übergeben bekommen. Da der neue GenericFilter ebenfalls mit Reflection arbeitet, ist der Typ der generischen Filtermethode egal. Wenn eine Instanz von GenericFilter erzeugt wird, muss ihr im Konstruktor der Name der zu filternden Eigenschaft, der Vergleichswert sowie optional ein Operator mitgegeben werden. Der Code der Klasse GenericFilter sieht dann so aus:

internal class GenericFilter { string filterPropertyName = ""; string filterValue = ""; FilterOperator filterOperator; public GenericFilter(string filterPropertyName, string filterValue) { this.filterPropertyName = filterPropertyName; this.filterValue = filterValue; this.filterOperator = FilterOperator.Contains; } public GenericFilter(string filterPropertyName, string filterValue, FilterOperator filterOperator) { this.filterPropertyName = filterPropertyName; this.filterValue = filterValue; this.filterOperator = filterOperator; } public bool Filter<T>(T x) { // Mittels Reflection die Werte // der Eigenschaften aus x und y bestimmen object valueX = x.GetType().GetProperty( filterPropertyName).GetValue(x, null); switch (filterOperator) { case FilterOperator.Contains: if (Convert.ToString(valueX).Contains(filterValue)) return true; break; case FilterOperator.Equals: if (Convert.ToString(valueX).Equals(filterValue)) return true; break; case FilterOperator.GreaterThan: if (Convert.ToInt32(valueX).CompareTo(Convert.ToInt32(filterValue)) > 0) return true; break; case FilterOperator.LessThan: if (Convert.ToInt32(valueX).CompareTo(Convert.ToInt32(filterValue)) > 0) return true; break; default: return false; } return false; } }

Der Filter kann für Texteigenschaften die Operatoren Contains und Equals anwenden und für ganzzahlige Eigenschaften einen GreaterThan oder LessThan Vergleich anstellen. Auch hier wieder eine ziemlich laxe Fehlerbehandlung ;-)

Wie dem auch sei, jetzt muss nur noch die FindAll-Methode meiner CustomGenericCollection das Prädikat mit dem Verweis auf die neue Filter-Funktion erstellen und das dann an die FindAll-Methode der inneren Liste weiterreichen. Als Ergebnis wird ein Array vom Typ T nach außen gegeben.

public T[] FindAll(string filterPropertyName, string filterValue, FilterOperator filterOperator) { GenericFilter genericFilter = new GenericFilter(filterPropertyName, filterValue, filterOperator); // Prädikat vorbereiten Predicate<T> filterGeneric = new Predicate<T>(genericFilter.Filter<T>); // Filtern und als Array zurückgeben return ((List<T>)base.Items).FindAll(filterGeneric).ToArray(); } public T[] FindAll(string filterPropertyName, string filterValue) { return FindAll(filterPropertyName, filterValue, FilterOperator.Contains); }

Die Liste von ID3Tags muss noch auf den neuen Typ CustomGenericCollection umgestellt werden.

//List<ID3Tag> id3Tags = new List<ID3Tag>(); CustomGenericCollection<ID3Tag> id3Tags = new CustomGenericCollection<ID3Tag>();

Nun lässt sie sich anständig filtern:

ID3Tag[] filteredItemsArray; filteredItemsArray = id3Tags.FindAll("Year", "1990", FilterOperator.GreaterThan); filteredItemsArray = id3Tags.FindAll("Title", "Love", FilterOperator.Contains);

Ich hätte die neuen Filter- und Sortiermethoden nicht unbedingt generisch halten müssen. Nimmt man den geringen Mehraufwand in Kauf, wird man mit hoher Wiederverwendbarkeit belohnt, wie der folgende und letzte Codefetzen zeigt. Was er tut, erahnen Sie sicherlich. 

public class Person { string nachname; string vorname; public Person(string nachname, string vorname) { this.nachname = nachname; this.vorname = vorname; } public string Nachname { get { return nachname; } } public string Vorname { get { return vorname; } } public override string ToString() { return String.Format("{0}, {1}", nachname, vorname); } } ... CustomGenericCollection<Person> persColl = new CustomGenericCollection<Person>(); persColl.Add(new Person("Frietsch", "Sascha")); persColl.Add(new Person("Schimmel", "Jochen")); persColl.Add(new Person("Janczik", "Sebastian")); persColl.Sort("Nachname", SortType.Descending); Person[] persArray = persColl.ToArray(); persArray = persColl.FindAll("Vorname", "Sascha", FilterOperator.Equals);
veröffentlicht am 01.03.2007 18:58