LINQ und ArcObjects
- by Marko Apfel
LINQ und ArcObjects Motivation LINQ1 (language integrated query) ist eine Komponente des Microsoft .NET Frameworks seit der Version 3.5. Es erlaubt eine SQL-ähnliche Abfrage zu verschiedenen Datenquellen wie SQL, XML u.v.m. Wie SQL auch, bietet LINQ dazu eine deklarative Notation der Problemlösung - d.h. man muss nicht im Detail beschreiben wie eine Aufgabe, sondern was überhaupt zu lösen ist. Das befreit den Entwickler abfrageseitig von fehleranfälligen Iterator-Konstrukten. Ideal wäre es natürlich auf diese Möglichkeiten auch in der ArcObjects-Programmierung mit Features zugreifen zu können. Denkbar wäre dann folgendes Konstrukt: var largeFeatures =
from feature in features
where (feature.GetValue("SHAPE_Area").ToDouble() > 3000)
select feature;
bzw. dessen Äquivalent als Lambda-Expression:
var largeFeatures =
features.Where(feature =>
(feature.GetValue("SHAPE_Area").ToDouble() > 3000));
Dazu muss ein entsprechender Provider zu Verfügung stehen, der die entsprechende Iterator-Logik managt. Dies ist leichter als man auf den ersten Blick denkt - man muss nur die gewünschten Entitäten als IEnumerable<IFeature> liefern.
(Anm.: nicht wundern - die Methoden GetValue() und ToDouble() habe ich nebenbei als Erweiterungsmethoden deklariert.)
Im Hintergrund baut LINQ selbständig eine Zustandsmaschine (state machine)2 auf deren Ausführung verzögert ist (deferred execution)3 - d.h. dass erst beim tatsächlichen Anfordern von Entitäten (foreach, Count(), ToList(), ..) eine Instanziierung und Verarbeitung stattfindet, obwohl die Zuweisung schon an ganz anderer Stelle erfolgte. Insbesondere bei mehrfacher Iteration durch die Entitäten reibt man sich bei den ersten Debuggings verwundert die Augen wenn der Ausführungszeiger wie von Geisterhand wieder in die Iterator-Logik springt.
Realisierung
Eine ganz knappe Logik zum Konstruieren von IEnumerable<IFeature> lässt sich mittels Durchlaufen eines IFeatureCursor realisieren. Dazu werden die einzelnen Feature mit yield ausgegeben. Der einfachen Verwendung wegen, habe ich die Logik in eine Erweiterungsmethode GetFeatures() für IFeatureClass aufgenommen:
public static IEnumerable GetFeatures(this IFeatureClass featureClass,
IQueryFilter queryFilter, RecyclingPolicy policy)
{
IFeatureCursor featureCursor =
featureClass.Search(queryFilter, RecyclingPolicy.Recycle == policy);
IFeature feature;
while (null != (feature = featureCursor.NextFeature()))
{
yield return feature;
}
//this is skipped in unit tests with cursor-mock
if (Marshal.IsComObject(featureCursor))
{
Marshal.ReleaseComObject(featureCursor);
}
}
Damit kann man sich nun ganz einfach die IEnumerable<IFeature> erzeugen lassen:
IEnumerable features =
_featureClass.GetFeatures(RecyclingPolicy.DoNotRecycle);
Etwas aufpassen muss man bei der Verwendung des "Recycling-Cursors". Nach einer verzögerten Ausführung darf im selben Kontext nicht erneut über die Features iteriert werden. In diesem Fall wird nämlich nur noch der Inhalt des letzten (recycelten) Features geliefert und alle Features sind innerhalb der Menge gleich.
Kritisch würde daher das Konstrukt
largeFeatures.ToList().
ForEach(feature => Debug.WriteLine(feature.OID));
weil ToList() schon einmal durch die Liste iteriert und der Cursor somit einmal durch die Features bewegt wurde. Die Erweiterungsmethode ForEach liefert dann immer dasselbe Feature.
In derartigen Situationen darf also kein Cursor mit Recycling verwendet werden.
Ein mehrfaches Ausführen von foreach ist hingegen kein Problem weil dafür jedes Mal die Zustandsmaschine neu instanziiert wird und somit der Cursor neu durchlaufen wird – das ist die oben schon erwähnte Magie.
Ausblick
Nun kann man auch einen Schritt weiter gehen und ganz eigene Implementierungen für die Schnittstelle IEnumerable<IFeature> in Angriff nehmen. Dazu müssen nur die Methode und das Property zum Zugriff auf den Enumerator ausprogrammiert werden. Im Enumerator selbst veranlasst man in der Reset()-Methode das erneute Ausführen der Suche – dazu übergibt man beispielsweise ein entsprechendes Delegate in den Konstruktur:
new FeatureEnumerator(
_featureClass, featureClass =>
featureClass.Search(_filter, isRecyclingCursor));
und ruft dieses beim Reset auf:
public void Reset()
{
_featureCursor = _resetCursor(_t);
}
Auf diese Art und Weise können Enumeratoren für völlig verschiedene Szenarien implementiert werden, die clientseitig restlos identisch nach obigen Schema verwendet werden. Damit verschmelzen Cursors, SelectionSets u.s.w. zu einer einzigen Materie und die Wiederverwendbarkeit von Code steigt immens.
Obendrein lässt sich ein IEnumerable in automatisierten Unit-Tests sehr einfach mocken - ein großer Schritt in Richtung höherer Software-Qualität.4
Fazit
Nichtsdestotrotz ist Vorsicht mit diesen Konstrukten in performance-relevante Abfragen geboten. Dadurch dass im Hintergrund eine Zustandsmaschine verwalten wird, entsteht einiges an Overhead dessen Verarbeitung zusätzliche Zeit kostet - ca. 20 bis 100 Prozent. Darüber hinaus ist auch das Arbeiten ohne Recycling schnell ein Performance-Gap.
Allerdings ist deklarativer LINQ-Code viel eleganter, fehlerfreier und wartungsfreundlicher als das manuelle Iterieren, Vergleichen und Aufbauen einer Ergebnisliste. Der Code-Umfang verringert sich erfahrungsgemäß im Schnitt um 75 bis 90 Prozent! Dafür warte ich gerne ein paar Millisekunden länger.
Wie so oft muss abgewogen werden zwischen Wartbarkeit und Performance - wobei für mich Wartbarkeit zunehmend an Priorität gewinnt. Zumeist ist sowieso nicht der Code sondern der Anwender die Bremse im Prozess.
Demo-Quellcode
support.esri.de
[1] Wikipedia: LINQ
http://de.wikipedia.org/wiki/LINQ
[2] Wikipedia: Zustandsmaschine
http://de.wikipedia.org/wiki/Endlicher_Automat
[3] Charlie Calverts Blog: LINQ and Deferred Execution
http://blogs.msdn.com/b/charlie/archive/2007/12/09/deferred-execution.aspx
[4] Clean Code Developer - gelber Grad/Automatisierte Unit Tests
http://www.clean-code-developer.de/Gelber-Grad.ashx#Automatisierte_Unit_Tests_8