Marko Apfel - Afghanistan/Belgium/Germany

Management, Architecture, Programming, QA, Coach, GIS, EAI

  Home  |   Contact  |   Syndication    |   Login
  187 Posts | 2 Stories | 201 Comments | 4 Trackbacks

News



Twitter | LinkedIn | Xing

Article Categories

Archives

Post Categories

BizTalk

C#

Enterprise Library

SAP

SQL Server

Technologie

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 auch SQL 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 sehr 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<IFeature> 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"4. Nach einer verzögerten Ausführung darf im selben Kontext nicht erneut über die Features iteriert werden. In diesem Fall würde nämlich nur noch der Inhalt des letzten (recycelten) Features geliefert, und alle Features sind innerhalb der Menge gleich.

Kritisch wäre 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 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 Konstruktor:
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 obigem 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 mocken5 - ein großer Schritt in Richtung höherer Software-Qualität.6

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. In Zeiten von Mehrkernprozessoren wird die Abarbeitungsdauer der meisten Geschäftsprozesse sowieso nicht durch Code-Ausführung sondern durch das Warten auf Benutzereingaben dominiert.

Demo-Quellcode

Den Quellcode für diesen Prototypen mit diversen Unit-Tests können Sie gerne hier oder unter http://support.esri.de downloaden.

LinqToArcObjects.zip

 

[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] How to use cursors in the geodatabase
http://resources.esri.com/help/9.3/arcgisengine/dotnet/bdb9558a-d78d-446c-a9d8-f35f9eb44a5b.htm#Recycling

[5] Martin Fowler: Mocks Aren't Stubs
http://martinfowler.com/articles/mocksArentStubs.html

[6] Clean Code Developer - gelber Grad/Automatisierte Unit Tests
http://www.clean-code-developer.de/Gelber-Grad.ashx#Automatisierte_Unit_Tests_8

posted on Friday, March 18, 2011 12:52 AM