Baumstrukturen mit LINQ abfragen (de-DE)
http://c.statcounter.com/9186659/0/1ecc522a/1/Dieser Artikel ist die deutsche Übersetzung meines zuerst auf Englisch erschienen Artikels How to Query Trees Using LINQ.
(Gerichtete) Bäume gehören zu den häufigsten Datenstrukturen in der Informatik. Wie sie mit LINQ genutzt werden, ist aber weniger offensichtlich. Dieser Artikel definiert eine generische LINQ-Extension, die dieses Problem löst, gibt Beispiele an und diskutiert Alternativen.
Motivation
Ausgangspunkt dieses Artikels war eine einfache Frage im LINQ-Forum, die einige Jahre unbeantwortet geblieben ist ...
Wie kann Knoten eines Baums unabhängig von ihrer Position abfragen?
Es scheint sich also zu lohnen, diese Aufgabe näher zu betrachten. Fangen wir mit einem Beispiel an! Eine einfache Klasse TreeNode ist so implementiert:
public class TreeNode
{
public string Id { get; set; }
public int Data { get; set; }
public List<TreeNode> Children { get; private set; }
public TreeNode(string id, int data)
: this(id, data, new TreeNode[0])
{ }
public TreeNode(string node, int data, params TreeNode[] children)
{
Id = node;
Data = data;
Children = new List<TreeNode>(children);
}
}
Dazu passen folgende Testdaten:
TreeNode rootNode =
new TreeNode("node-0", 35,
new TreeNode("node-1", 17,
new TreeNode("node-2", 20)),
new TreeNode("node-3", 19),
new TreeNode("node-4", 5,
new TreeNode("node-5", 25),
new TreeNode("node-6", 40)));
Lösung
Implementiere eine generische Extension-Klasse, die beliebige Baumstrukturen traversieren kann. Die einzige zu erfüllende Vorbedingung ist, dass die Kinder eines Knoten als *IEnumerable *gespeichert werden. Die Wahl eines Lambda-Ausdruck als Selektor vermeidet, den Zugriffspfads fest zu kodieren und ermöglicht, die Wiederverwendung der Extension.
public static class LinqTreeExtension
{
public static IEnumerable<T> SelectNestedChildren<T>
(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
{
foreach (T item in source)
{
yield return item;
foreach (T subItem in SelectNestedChildren(selector(item), selector))
{
yield return subItem;
}
}
}
}
Die Extension-Methode SelectNestedChildren traversiert den Baum per Tiefensuche von links nach rechts.
Beispiel
Beispiel 1: Einebnen einer Baumstruktur (ohne die Wurzel des Baums)
var flattenedTreeWithoutRoot
= rootNode.Children
.SelectNestedChildren(t => t.Children).ToList();
Die Ergebnisliste enthält die Knoten mit den IDs "note-1" bis "note-6". Hinweis: Die Wurzel des Baums ist in der Ergebnisliste nicht enthalten. Wir korrigieren das im nächsten Beispiel.
Beispiel 2: Einebnen einer Baumstruktur (inklusive Wurzel des Baums)
var flattenedTreeIncludingRoot
= new TreeNode[] {rootNode }
.SelectNestedChildren(t => t.Children).ToList();
Die Abfrage wird auf einem Array ausgeführt, das die Wurzel des Baums als einziges Element enthält. Die Ergebnisliste enthält - wie erwartet - die Knoten mit den IDs "note-0" bis "note-6".
Beispiel 3: LINQ-Abfrage über alle Knoten eines Baums.
var result
= new TreeNode[] { rootNode }
.SelectNestedChildren(t => t.Children)
.Where(t => t.Data > 30).ToList();
Die Ergebnisliste enthält zwei Knoten mit den IDs "node-0" und "node-6".
Gegenanzeigen und Alternativen
Die vorgestellte LINQ-Erweiterung hat entscheidende Stärken:
- Die Extension ist auf alle Baumstrukturen anwendbar, die ihre Kinder als IEnumerable speichern.
- Sie integriert sich nahtlos in andere LINQ-Operationen.
In einigen Fällen muss jedoch auf Alternativen zurückgegriffen werden:
Zyklischer Graph statt eines Baums
Problem: Die Extension wird nicht terminieren, wenn sie in einen Zyklus des Graphen läuft.
Lösung: Modifikation der Extension:
- Füge einen HashSet<T> zu der Extension hinzu, die über die bereits besuchten Knoten Buch führt.
- Die Klasse T sollte GetHashCode() und Equals() überschreiben.
- Die Extension muss bereits besuchte Knoten überspringen.
Persistenter Baum in der Datenbank
Problem: Wenn der Baum in einer Datenbank (und nicht im Speicher) vorliegt, ist die Extension unnötig aufwändig.
Lösung: Alle Knoten des Baums liegen in einer Datenbanktabelle vor, die direkt abgefragt werden kann. Dabei ist es unerheblich, ob es sich um eine logische Baumstruktur oder sogar einen zyklischen Graphen handelt. Die Abfrage erfolgt mit dem Entity Framework:
var databaseResult = context.TreeNodes
.Where(t => t.Data > 30).ToList();
Im Falle dieses Beispiels sollten jedoch zwei kleine Änderungen an der Klasse TreeNode vorgenommen werden:
- Das Property Children sollte den modifier virtual erhalten, um Lazy Loading zu ermöglichen.
- Das Entity Framework benötigt einen weiteren parameterlosen Konstruktor.
XML-Datenstruktur
Eine XML-Datenstruktur ist ein Spezialfall eines gerichteten Baums.
**Problem: ** LINQ to XML ist auf XML-Documente spezialisiert.
Lösung: LINQ to XML verwenden. Dieser Ansatz reduziert den Codierungsaufwand und nutzt einen etablierten Standard. Außerdem sind weitere Features von LINQ to XML nutzbar. Beispiel:
var xmlElements = XDocument.Load("myfile.xml").Descendants();
var result = xmlElements.Where(x => ...);
XDocument.Descendents() gibt ein IEnumerable<XElement> zurück, das direkt abgefragt werden kann.
Fazit:
Die Nutzung von LINQ ist einfach. Es gibt jedoch nicht die Lösung, die für alle Aufgabenstellungen passt.
Links
101 LINQ Samples
Erläutert alle wesentlichen Aspekte von *LINQ *mit anschaulichen Beispielen.LINQ (Language Integrated Query)
LINQ-Dokumentation in der MSDN-Library.LINQ to XML
LINQ to XML-Dokumentation in der MSDN-Library.
Siehe auch
- User Page: Carsten Siemens
Weitere TechNet-Wiki-Artikel von Carsten Siemens.
Andere Sprachen
Dieser Artikel ist auch in folgenden Sprachen verfügbar