Top 10 Fehler–#7: Collection-Klassen

25. Juli 2014

Dies ist Teil 6 der kleinen Serie, die ich als Kommentar zum Blog-Beitrag Top 10 Mistakes that C# Programmers Make begonnen habe.

Heute zum Thema… (immer noch ein bisschen LINQ…)

 

Common Mistake #7: Using the wrong type of collection for the task at hand

Yup, das ist eine Sache, die ich auch immer wieder ärgerlich finde. Allerdings aus anderen Gründen, als Patrick:

C# provides a large variety of collection objects, with the following being only a partial list:
Array, ArrayList, BitArray, BitVector32, Dictionary<K,V>, HashTable, HybridDictionary, List<T>, NameValueCollection, OrderedDictionary, Queue, Queue<T>, SortedList, Stack, Stack<T>, StringCollection, StringDictionary.

Dabei hat er sogar Typen unterschlagen, die regelmäßig Probleme machen: CollectionBase und ICollection bzw. Collection<T> und ICollection<T>.

Und er scheint diese Inflation für eine gute Sache zu halten:

While there can be cases where too many choices is as bad as not enough choices, that isn’t the case with collection objects. The number of options available can definitely work to your advantage.  Take a little extra time upfront to research and choose the optimal collection type for your purpose.  It will likely result in better performance and less room for error.

Und kommt zu einer Empfehlung, mit der ich mich nicht anfreunden kann:

If there’s a collection type specifically targeted at the type of element you have (such as string or bit) lean toward using that one first. The implementation is generally more efficient when it’s targeted to a specific type of element.

Offen gestanden ist mir das etwas zu kurzsichtig.

 

Das Thema Collections krankt an zwei Phänomenen:

  • Historischer Wildwuchs
  • Vermischung von Konzepten

Das hat u.a. dazu geführt, dass das .NET Framework – sonst immer ein guter Indikator – und die Empfehlungen von Microsoft selbst zu dieser Frage nur Chaos als Antwort bieten.

Historischer Wildwuchs

.NET 1.0 hatte keine Unterstützung für Generics. Trotzdem brauche man natürlich Collections, und so entstanden Klassen, die mit Objects arbeiten, sowie spezialisierte Ergänzungen für bestimmte Typen und Anforderungen.

Mit .NET 2.0 kamen Generics, einschließlich der entsprechenden Typen für Collections. Und damit auch die Empfehlung, diesen den Vorzug zu geben:

If you’re using the .NET Framework 2.0 or later, using the old non-generic collections is strongly discouraged. (MSDN Magazine)

Natürlich waren zu diesem Zeitpunkt schon wesentliche Teile des .NET Frameworks in Stein gemeißelt. Ergo kommt man auch heute oft nicht an NameValueCollection oder anderen Klassen vorbei.

Vermischung von Konzepten

Von Anfang an gab es zwei konzeptionelle Zusammenhänge in denen Collections eine Rolle spielen:

  1. Als Implementierung von Datenstrukturen
  2. Als Schnittstelle zu den Datenstrukturen

Diese Trennung ist nun nicht gerade neu. Die Collection-Klassen in der Collection Class Library von IBM (Teil der IBM Open Class Library) haben Schnittstelle (Stack, Queue, ec.) und die eigentliche Implementierung (Array, Linked List, etc.) über Templates zusammengebaut – vor über einem viertel Jahrhundert. Die STL erreicht eine Trennung der Konzepte über Container-Klassen und Iteratoren. 

Der Versuch, gleiches im .NET Framework zu verankern ist nur teilweise gelungen. Einerseits gab es von Anfang an ICollection als Schnittstelle und CollectionBase als Standard-Implementierung, die auch oft verwendet wurden (CollectionBase Ableitungen, Roles.Providers, …). Andererseits findet man aber auch genügend Stellen, an denen das zugunsten andere Datenstrukturen, etwa NameValueCollection oder Arrays, nicht passiert. Und auch CollectionBase (die man als “Schnittstellenklasse” bezeichnen könnte) delegiert die eigentliche Datenhaltung zwar weiter, ist aber so eng an ArrayList gekoppelt, dass an einen Austausch kaum zu denken ist.

Trotzdem versucht Microsoft das Konzept der Trennung weiter zu forcieren, zum Beispiel auch mit FxCop-Regeln für gegen Arrays und List<T>. Für wiederverwendbare Klassen, die relativ eigenständig entwickelt werden und in unterschiedlichen Kontexten eingesetzt werden sollen macht das durchaus Sinn.

Andererseits gilt wie so oft: “One size fits all” funktioniert nicht!
Zum einen macht es keinen Sinn, die konzeptionelle Trennung von Schnittstelle und Implementierung gleichmacherisch auf alles zu übertragen. Zum anderen wurden die Karten mit LINQ neu gemischt.

Wenn es nicht um wiederverwendbaren Code geht, sondern um Schnittstellen in meiner Anwendungsarchitektur, dann interessiert nicht die Abstraktion der Datenstruktur, sondern Effizienz und Features.

Je nach Architekturansatz und Gusto kommen dann oft LINQ-Interfaces (IEnumerable<T> oder IQueryable<T>) zum Einsatz, oder die tatsächlichen Container – Dank der Unterstützung von Enumerable.ToArray() und Enumerable.ToList() üblicherweise Arrays oder List<T>. Sobald man sich der Oberfläche nähert mag auch mal eine ObservableCollection<T> im Spiel sein.

ICollection und Konsorten spielen hier definitiv keine Rolle. Dagegen ist auch prinzipiell nichts zu sagen, solange man konsistent bleibt. Auch wenn FxCop sich beschwert. FxCop-Regeln kann man abschalten!

Fazit

Alles zusammengenommen – gewürzt mit veralteten Empfehlungen – ist das eine manchmal etwas unverdauliche Suppe, die wir da auslöffeln müssen.

Meine Empfehlung? Sich bewusst sein, dass es nicht die Antwort gibt, und dass unterschiedliche Kontexte unterschiedliche Lösungen brauchen. Bei wiederverwendbare Komponenten würde ich “konzeptioneller Reinheit” den Vorzug geben, in der Anwendungsentwicklung dem pragmatischen Ansatz.

 

Weitere Informationen und Diskussionen zum Thema:

  • “Collections (C# and Visual Basic)” (msdn)
  • “Selecting a Collection Class” (msdn)
  • “Why we don’t recommend using List<T> in public APIs” (Krzysztof Cwalina)
  • “FAQ: Why does DoNotExposeGenericLists recommend that I expose Collection<T> instead of List<T>?” (Code Analysis Team Blog)
  • stackoverflow: 1, 2, 3