Extract Closure für C# - WS 2013/2014 - Fakultät für Informatik und Mathematik Lehrgebiet Programmiersysteme Masterarbeit im Studiengang ...
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Fakultät für Informatik und Mathematik
Lehrgebiet Programmiersysteme
Masterarbeit
im Studiengang
Praktische Informatik
eingereicht von
Alexander Ennen
Matrikelnummer : 8548226
Betreut durch
Prof. Dr. Friedrich Steimann
Extract Closure für C#
WS 2013/2014Erklärung
Hiermit erkläre ich, dass ich die vorliegende Arbeit selbstständig verfasst, noch nicht ander-
weitig für Prüfungszwecke vorgelegt, keine anderen als die angegebenen Quellen oder Hilfs-
mittel verwendet, sowie wörtliche und inhaltliche Zitate als solche gekennzeichnet habe.
Hilpoltstein, den 10. März 2014
Alexander Ennen
2Abstract Deutsch In der objektorientierten Programmierung (OOP) werden oftmals Funktionen in Form von Parametern anderen Funktionen übergeben. Diese Funktionen werden zumeist in Form von anonymen Funktionen realisiert. Dies hat den Vorteil, dass die benötigten Anweisungen als Inlineanweisung formuliert werden können und somit keine Instanzmethode im eigentlichen Sinne benötigt wird. Gerade durch die Integration von Konstrukten aus der funktionalen Pro- grammierung wird diese Technik in modernen Frameworks immer häufiger eingesetzt. Besitzt die entstandene anonyme Funktion dabei Referenzen auf ihren Erstellungskontext, so spricht man von einem Closure. Im Rahmen dieser Arbeit entstand ein Werkzeug, welches die Erzeu- gung dieser Closures ermöglicht. Es extrahiert dabei vom Anwender selektierte Anweisungen unter Berücksichtigung einer Typkonfiguration in ein eigenständiges Closure. Englisch In todays object-oriented programming, functions are often given to other functions as parame- ters. These functions are most often implemented as anonymous functions. This is beneficial because these functions can be declared inline without the need to create an actual instance method. Especially in the course of the integration of functional programming structures to object-oriented programming this feature is used more and more frequently. If such a function keeps references to the scope it was first declared in, it is called a Closure. During this work a refactoring tool was developed which performs the extraction of user selected statements and creates the responding Closure out of it while considering a specific type configuration.
Inhaltsverzeichnis
Inhaltsverzeichnis 4
Abbildungsverzeichnis 7
Quellcodeverzeichnis 8
Tabellenverzeichnis 10
1. Einführung 11
1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2. Problemstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3. Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2. Grundlagen 17
2.1. .NET Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2. Delegate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3. Closures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3.1. Anonyme Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3.2. Lambda-Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.3.3. Erfasste Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4. Microsoft Roslyn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.4.1. Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.4.2. Syntaxbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4.3. Manipulation des Syntax Trees . . . . . . . . . . . . . . . . . . . . . 31
2.5. Microsoft Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.5.1. Erweiterbarkeit von Visual Studio . . . . . . . . . . . . . . . . . . . 32
3. Lösungsansatz 34
3.1. Rahmenbedingungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4Inhaltsverzeichnis
3.2. Grundsätzlicher Ablauf der Refaktorisierung . . . . . . . . . . . . . . . . . . 35
3.3. Vorbedingungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.4. Konfigurationsmöglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.5. Vorgehensweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.5.1. Analyse des Quellcodes . . . . . . . . . . . . . . . . . . . . . . . . 40
3.5.2. Erstellung des Closures . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.6. Manipulation des Syntaxbaumes . . . . . . . . . . . . . . . . . . . . . . . . 44
3.7. Zusatzbeobachtungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.7.1. Namenskonflikte . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.7.2. Erfasste Schleifenvariablen . . . . . . . . . . . . . . . . . . . . . . . 49
4. Implementierung 51
4.1. Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.2. Integration in Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.3. Realisierung von Extract Closure . . . . . . . . . . . . . . . . . . . . . . . . 55
4.3.1. Überprüfung der Vorbedingungen . . . . . . . . . . . . . . . . . . . 56
4.3.2. Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.3.3. Erzeugung des Delegattyps . . . . . . . . . . . . . . . . . . . . . . . 58
4.3.4. Manipulation der Anweisungen . . . . . . . . . . . . . . . . . . . . 60
5. Ergebnisse 63
5.1. Evaluation durch Stichprobe . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.1.1. Vorgehensweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.1.2. Ergebnisse der Stichprobe . . . . . . . . . . . . . . . . . . . . . . . 66
5.2. Grenzfälle der Extraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.2.1. Erfassen der Schleifenvariablen . . . . . . . . . . . . . . . . . . . . 67
5.2.2. Einfluss der Parametrisierbarkeit auf den Erhalt von Variablenbindungen 68
6. Diskussion 72
6.1. Bewertung der Ergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.2. Vergleich mit ähnlichen Arbeiten . . . . . . . . . . . . . . . . . . . . . . . . 73
6.2.1. Lambdaficator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
6.2.2. ReLooper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
6.3. Schlussbetrachtung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
7. Zusammenfassung 79
5Inhaltsverzeichnis
8. Ausblick 81
Literatur 83
A. Installation des Plugins 85
A.1. Voraussetzungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
A.2. Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
A.3. Ausführung des Plugins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
A.4. Inhalt der CD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6Abbildungsverzeichnis
2.1. Darstellung der Klasse Person als Syntaxbaum . . . . . . . . . . . . . . . . 30
3.1. Darstellung der ursprünglichen return-Anweisung . . . . . . . . . . . . . . . 46
3.2. Resultierende lokale Variablendeklaration . . . . . . . . . . . . . . . . . . . 47
4.1. Struktur der in Extract Closure verwendeten Elemente . . . . . . . . . . . . 52
4.2. Benutzerdialog mit beispielhafter Konfiguration . . . . . . . . . . . . . . . . 57
A.1. Das korrekt installierte Package wird im Manager angezeigt . . . . . . . . . 86
A.2. Die ausführbare Refaktorisierung im Kontextmenü . . . . . . . . . . . . . . 87
7Listings
2.1. Instanziierung mit benannter, nicht statischer Methode . . . . . . . . . . . . 20
2.2. generischer Delegattyp Func . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3. Instanziierung mit anonymer Methode . . . . . . . . . . . . . . . . . . . . . 22
2.4. Ausschnitt aus dem erzeugten IL Code von Listing 2.3 . . . . . . . . . . . . 23
2.5. Instanziierung einer anonymen Funktion mit Hilfe eines Anweisungslambdas 24
2.6. Instanziierung einer anonymen Funktion mittels implizierter Typisierung . . . 24
2.7. Einfaches Beispiel einer erfassten Variablen innerhalb einer anonymen Methode 25
2.8. Einfache Implementierung einer Klasse Person zur Visualisierung des Syntax-
baumes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.1. Beispielhafte, nicht eindeutige Ausgangssituation für extract Closure . . . . . 38
3.2. Mögliche Closures aus 3.1 mittels generischer Delegattypen . . . . . . . . . 39
3.3. equivalente delegate, explizit und generisch . . . . . . . . . . . . . . . . . . 42
3.4. Umwandeln von expliziten zu generischen Delegattypen . . . . . . . . . . . 42
3.5. Simple return-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.6. Anpassung der ursprünglichen return-Anweisung . . . . . . . . . . . . . . . 45
3.7. Ursprüngliche Bindung an die globale Variable x . . . . . . . . . . . . . . . 48
3.8. Änderung der Variablenbindung innerhalb der anonymen Funktion . . . . . . 49
4.1. Implementierung der Statusabfrage im Visual Studio . . . . . . . . . . . . . 54
4.2. Überprüfung der Anweisungen auf Sprunganweisungen . . . . . . . . . . . . 57
4.3. Erzeugung einzelner Parameter für explizite Delegattypen mittels Roslyn . . . 60
4.4. Erzeugung des expliziten Delegattyps mittels Roslyn . . . . . . . . . . . . . 60
4.5. Rückgabe des neu erstellten BlockSyntax . . . . . . . . . . . . . . . . . . . . 62
5.1. Ausgangssituation zur Untersuchung der Verhaltensänderung bei der Erfas-
sung von Schleifenvariablen . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8Listings
5.2. Ausgangssituation zur Untersuchung der Aufrechterhaltung von
Variablenbindungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.3. Erstellte anonyme Funktion mit korrekten Bindungen . . . . . . . . . . . . . 70
5.4. Erstellte anonyme Funktion mit manipulierten Bindungen . . . . . . . . . . . 70
6.1. Ausgangszustand For-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.2. Erzeugte Lambda-Anweisung mit Hilfe des Lambdaficators . . . . . . . . . 74
6.3. Ursprüngliche, nicht parallelisierte Ausführung . . . . . . . . . . . . . . . . 76
6.4. Refaktorisierte, parallelisierte Ausführung . . . . . . . . . . . . . . . . . . . 76
6.5. Mögliche Anwendung zur Parallelisierung . . . . . . . . . . . . . . . . . . . 77
9Tabellenverzeichnis
3.1. Vorbelegung der Parameterliste . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.1. Überblick der Ergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
A.1. Überblick der Ergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
101. Einführung
1.1. Motivation
Durch den ständigen Änderungsprozess dem eine Softwarekomponente im Laufe ihres Le-
benszyklus unterliegt, wandelt sich ihre Architektur und ihre Strukturierung. Viele dieser
Änderungen fügen neue Funktionalität hinzu oder passen bereits bestehende an neue An-
forderungen an. Den oftmals damit einhergehenden Verfall der Software bezeichnet man als
Softwareerosion oder Softwarezerfall. Dabei handelt es sich in den seltensten Fällen um tat-
sächliche Programmierfehler innerhalb des Quellcodes. Vielmehr thematisiert der Begriff den
Verfall der Qualität des Quellcodes hinsichtlich seiner Strukturierung. Ein heutzutage als be-
währt geltendes Mittel gegen das Fortschreiten dieser Degeneration stellen Refaktorisierungen
dar.
Bereits 1990 führten Ralph Johnson und William Obdyke (Obdyke und Johnson, 1990) den
Begriff Refactoring (engl., von hier ab soll die deutsche Bezeichnung Refaktorisierung ver-
wendet werden) ein. Unter einer Refaktorisierung versteht man die Änderung von Quellcode,
ohne dessen beobachtetes Verhalten zu verändern (Fowler, 1999). Die Änderung dient somit
ausschließlich der Verbesserung von Lesbarkeit, Wartbarkeit und Erweiterbarkeit und fügt (im
Normalfall) keinerlei neue Funktionalität hinzu und ändert auch keine bestehende ab. Durch
die Verbesserung der Strukturierung des Quellcodes wird dem Softwarezerfall aktiv entgegen-
gewirkt und die Arbeit mit dem Quellcode erleichtert. So können beispielsweise neue Funktio-
nalitäten einfacher und effizienter integriert werden oder ermöglicht durch erhöhte Lesbarkeit,
neue Mitarbeiter schneller in die bestehende Codebasis eingearbeitet werden.
Dabei handelt es sich bei vielen Refaktorisierungen um eine feste Abfolge von Tätigkeiten,
die einen vorhandenen Istzustand in einen gewünschten Sollzustand überführen. Die dabei
zu verrichtenden Aufgaben sind oftmals umfangreich und fehleranfällig. Deshalb ist für die
111. Einführung
Durchführung von Refaktorisierungen grundsätzlich eine Werkzeugunterstützung erstrebens-
wert. Aus diesem Grund bieten die gängigen Entwicklungsumgebungen (wie z.B. Visual Stu-
dio oder Eclipse) und diversen Zusatzwerkzeuge (z.B. ReSharper 1 oder Whole tomato 2 )
bereits eine Vielzahl von Refaktorisierungen als integrierte Operationen an, um die Softwa-
reentwickler bei der Durchführung dieser qualitätserhaltenden Maßnahmen zu unterstützen.
Somit können viele anfallende Arbeitsschritte automatisiert durchgeführt werden, was Fehler-
quellen minimiert.
Ein weiteres Einsatzgebiet von Refaktorisierungen, welches oftmals weniger Aufmerksam-
keit erfährt, ist die unterstützte Verwendung neuer programmiersprachlicher Konstrukte. So
halten beispielsweise immer mehr Konstrukte funktionaler Programmiersprachen Einzug in
objektorientierte Sprachen (beispielsweise Lambda-Ausdrücke in .NET oder JAVA). Diese er-
höhen teilweise deutlich die Lesbarkeit oder verbessern die Erweiterbarkeit von Ausdrücken,
Anweisungen und Abfragen. Die Verwendung neuer Features in bestehenden Quellcode und
somit die Änderung bereits bestehender Funktionalität, erfordert jedoch ein genaues Verständ-
nis beider Varianten. Auch hier wäre eine automatisierte Unterstützung bei der Portierung des
deprecated Codes3 wünschenswert.
Die vorliegende Arbeit versucht dafür einen Grundstein zu legen. In vielen Fällen des Um-
stiegs, vor allem bei der Arbeit mit Auflistungen und dem in .NET eingeführten LINQ (Lan-
guage Integrated Query) (Microsoft, 2014), müssen die Anweisungen, welche innerhalb der
neuen Möglichkeiten genutzt werden sollen, in Methoden überführt werden. Oftmals müsste
für diese Methoden zusätzlich eine kapselnde Klasse erstellt werden, welche alle benötigten
Daten und Eigenschaften verwaltet. Eine zumeist elegantere und direktere Variante ist das
Erstellen einer anonymen Methode, welche zusätzlich Informationen aus ihrem Erstellungs-
kontext referenziert, um alle benötigten Informationen zu erhalten - einem sogenannten Clos-
ure. Die Refaktorisierung Extract Closure soll dabei den Schritt der Erstellung des Closures
übernehmen.
1
http://www.http://www.jetbrains.com/resharper/
2
http://www.wholetomato.com/
3
Als Deprecated Code werden veraltete oder nicht mehr erwünschte Konstrukte bezeichnet
121. Einführung
1.2. Problemstellung
Im Rahmen dieser Abschlussarbeit soll eine Refaktorisierung entstehen, welche es dem An-
wender ermöglicht aus selektierten Anweisungen ein Closure zu erzeugen. Um dies zu er-
reichen, muss zunächst eine Validierung des selektierten Quellcodes stattfinden, um sicher-
zustellen das dieser in ein Closure überführt werden kann. Danach müssen alle notwendigen
Änderungen im Quellcode bestimmt und durchgeführt werden. Da innerhalb der meisten se-
lektierten Anweisungen eine Vielzahl von Möglichkeiten besteht verschiedenste Closures zu
erzeugen, soll zudem ein Benutzerdialog entstehen, der es dem Anwender ermöglicht, einzel-
ne Komponenten des entstehenden Closures zu editieren bzw. zu konfigurieren.
1.3. Aufbau der Arbeit
In Kapitel 2 sollen zunächst alle für die Realisierung relevanten Voraussetzungen erläutert
und vorgestellt werden. Dies beinhaltet, gegeben durch die verwendeten Rahmenbedingungen,
eine kurze Einführung in das .NET Framework und eine kontextspezifische Definition des
Begriffes Closure in C# sowie der dafür notwendigen Begrifflichkeiten.
Als verwendete Werkzeuge sollen außerdem Microsofts Visual Studio und dessen Erweite-
rungsmöglichkeiten kurz erläutert werden. Das zur Durchführung der Refaktorisierung ver-
wendete Framework Roslyn wird ebenfalls kurz in seiner grundlegenden Funktionalität und
Verwendungsweise vorgestellt.
Kapitel 3 stellt den zur Realisierung des Plugins verwendeten Lösungsansatz vor und stellt ei-
ne prinzipielle Erläuterung der Lösung dar. Hier sollen unter anderem für die Implementierung
notwendige Vorbedingungen beschrieben und erläutert werden. Zusätzlich soll der prinzipielle
Ablauf der Refaktorisierung skizziert werden und auf die Notwendigkeit der Konfigurierbar-
keit des Plugins eingegangen werden.
Das darauf folgende Kapitel 4 beschreibt die konkrete Implementierung, die innerhalb die-
ser Abschlussarbeit zur Lösung der Anforderungen erstellt wurde. Sie ist spezifisch für die
verwendeten Rahmenbedienungen und im Gegensatz zum vorhergehenden Kapitel nur inner-
halb dieser verwendbar. Es beinhaltet konkrete Realisierungen der Integration in Visual Stu-
131. Einführung
dio sowie der Anwendung von Roslyn. Des Weiteren erläutert es die Implementierung des zur
Überführung des Quellcodes notwendigen Algorithmus.
Die Evaluation der erstellten Refaktorisierung wird in Kapitel 5 durchgeführt. Da sich die
benötigten Eingangsdaten nur bedingt für eine automatisierte Durchführung der Refaktori-
sierung eignen, wird sie in Form mehrerer Fallstudien durchgeführt. Dabei sollen vor allem
Grenzfälle untersucht werden, welche entweder aufgrund der Konfigurationsmöglichkeiten
oder aufgrund von Uneindeutigkeiten problematisch sind.
Innerhalb der Diskussion in Kapitel 6 soll das entstandene Plugin mit ähnlichen Ansätzen und
Implementierungen verglichen werden. Insbesondere die Arbeiten (Franklin u. a., 2013) und
(Gyori u. a., 2013) sollen innerhalb dieser diskutiert werden.
Abgeschlossen wird die Arbeit durch Kapiel 7 und Kapitel 8, welche eine kurze Zusammen-
fassung der Arbeit sowie einen Ausblick auf mögliche Einstiegsgedanken für eine Weiterfüh-
rung werfen sollen.
14Glossar
Anonyme Funktion
Der Begriff anonyme Funktion wird innerhalb dieser Arbeit als Überbegriff verwendet. Reali-
siert werden kann eine anonyme Funktion entweder durch eine anonyme Methode oder durch
einen Lambda-Ausdruck. Der Begriff wird immer dann verwendet, wenn beide Realisierungs-
möglichkeiten verwendet werden können oder es schlicht keine Rolle spielt.
Anwender
Als Anwender wird, innerhalb dieser Arbeit, der tatsächliche Bediener der entstandenen Re-
faktorisierung bezeichnet. Er wählt die zu extrahierenden Anweisungen aus, startet die Refak-
torisierung und konfiguriert diese.
Delegat
Der im Kontext von C# mehrfach überladene Begriff Delegat wird im Rahmen dieser Arbeit
mehrdeutig verwendet. Zum einen bezeichnet delegate das programmiersprachliche Schlüs-
selwort zur Erstellung von Referenztypen, welche Methoden kapseln. Zum anderen beschreibt
Delegat die tatsächliche Instanz eines Delegat-Objektes.
151. Einführung
Delegattyp
Als Delegattyp wird der tatsächlich aus der Deklaration eines Delegates entstehende Typ be-
zeichnet. Er spezifiziert somit die Signatur der Funktion, welche von ihm gekapselt werden
kann. Jedes tatsächliche Delegat ist dabei vom Typ eines bestimmten Delegattypen.
Refaktorisieren
Als Refaktorisieren wird der Vorgang des Ausführens einer bestimmten Refaktorisierung oder
der Tätigkeit im Allgemeinen verstanden. Der Anwender einer Refaktorisierung soll jedoch
nicht als ”der Refaktorisierende” sondern schlicht als Anwender bezeichnet werden.
Refaktorisierung
Als Refaktorisierung soll einerseits die allgemeine Tätigkeit der Änderung von Quellcode oh-
ne ihr beobachtetes Verhalten zu manipulieren bezeichnet werden, andererseits eine tatsächli-
che, bestimmte Refaktorisierung wie beispielsweise Extract Closure.
Plugin
Als Plugin wird eine Erweiterung für ein bestehendes Softwareprodukt bezeichnet. Zumeist
unterliegen diese Plugins bestimmten Anforderungen denen sie gerecht werden müssen, um
in eine gewünschte Anwendung integriert werden zu können. Das ”entstandene Plugin” be-
zeichnet weiterhin die im Rahmen dieser Arbeit entstandene Erweiterung für Visual Studio im
Allgemeinen.
162. Grundlagen
Das folgende Kapitel soll einen kurzen Überblick über die im Rahmen dieser Arbeit verwen-
deten Frameworks, Werkzeuge und Entwicklungsumgebungen geben. Es sollen vor allem die
Zusammenhänge der einzelnen Faktoren und deren Einfluss auf die erzeugte Lösung erläutert
werden.
2.1. .NET Framework
Das .NET Framework stellt eine aus dem Hause Microsoft stammende Programmierplatt-
form dar, welche zusammen mit Visual Studio.NET und der Programmiersprache C# erstmals
2002 veröffentlicht wurden. Die .NET Plattform selbst besteht dabei aus der Laufzeitumge-
bung Common Language Runtime (CLR) und der Klassenbibliothek Framework Class Library
(FCL).
Die Laufzeitumgebung CLR ist für die Ausführung von .NET Programmen zuständig. Sie
übersetzt die Assemblies in ausführbaren Maschinencode und übergibt diesen an das Betriebs-
system, welches diesen ausführt. Die Übersetzung in ausführbaren Maschinencode erfolgt da-
bei zur Laufzeit, man spricht deswegen von einem Just in time Compiler (JIT). Die CLR bein-
haltet zusätzlich eine Vielzahl von Grundlegender Funktionalitäten wie beispielsweise einer
automatischen Speicherbereinigung, Debugdiensten oder die Behandlung von Ausnahmen.
Die FCL ist eine objektorientierte Bibliothek, welche eine Vielzahl von Klassen für verschie-
denste Zwecke beinhaltet. Sie bietet unter anderem Basisfunktionalität für die Datenerfassung,
Datenbankanbindung oder die Entwicklung und Erstellung von Benutzungsschnittstellen.
Bei der Entwicklung von Programmen für das .NET Framework wird der erzeugte Quellcode
172. Grundlagen
folglich nicht direkt in ausführbaren Maschinencode übersetzt. Vielmehr übersetzen die .NET
sprachabhängigen Compiler den Quellcode in eine sprachunabhängige Zwischensprache, die
Common Intermediate Language (CIL). Unter der Voraussetzung, das eine betreffende Pro-
grammiersprache CIL-Konformen Code erzeugen kann, ermöglicht diese Vorgehensweise den
Einsatz einer Vielzahl von Programmiersprachen zum Erstellen von .NET Komponenten. So
können unter anderem die Programmiersprachen Visual Basic, Smalltalk oder Ruby eingesetzt
werden um CIL Code zu erzeugen. Die am weitesten verbreitete .NET Sprache ist dabei C#.
Durch Verwendung der CIL wird es ermöglicht Programmbibliotheken verschiedener .NET
Programmiersprachen untereinander zu verwenden.
Ein weiterer Vorteil des Einsatzes der Zwischensprache ist die grundsätzlich mögliche Platt-
formunabhängigkeit. Durch den starken Fokus Microsofts auf hauseigene Betriebssysteme
wird dies allerdings vor allem durch die Projekte Mono1 und DotGNU2 verfolgt und vorange-
trieben.
Die Ursprungsidee des .NET Frameworks ist dabei die Realisierung der Common Language
Infrastructure (CLI), eines von Microsoft initiierten ISO/IEC/ECMA Standards. Inhalt des
Standards ist die Definition Sprach- und Plattform unabhängiger Anwendungsentwicklung
und -ausführung. Das .NET Framework ist dabei eine vollständige Implementierung dieses
Standards.
Im Rahmen dieser Arbeit wird die Programmiersprache C# in der von Microsoft 2012 her-
ausgegebenen Version 5.0 verwendet. Alle Ergebnisse und Implementierungen stützen sich
auf die Version 4.5 des .NET Frameworks. Ebenfalls sind alle in der Arbeit aufgeführten
Quellcodeausschnitte innerhalb dieser Rahmenbedingungen erzeugt. Zum Ausführen des bei-
gelegten Quellcodes wird Visual Studio 20123 sowie die CTP Version des Roslyn Frameworks
vom September 20124 benötigt.
1
http://www.mono-project.com
2
http://www.gnu.org/software/dotgnu
3
http://www.visualstudio.com/
4
http://msdn.microsoft.com/de-DE/roslyn
182. Grundlagen
2.2. Delegate
Seit der Framework Version 1.1 aus dem Jahr 2003 bietet Microsoft Delegate als Teil des
Sprachumfanges von .NET an. Ein Delegat ist dabei ein Referenztyp, welcher eine Metho-
densignatur definiert. Diesem Referenztyp kann dann eine tatsächliche Methode zugewiesen
werden. Das Objekt vom Typ des Delegates kapselt somit eine Methode und ermöglicht es
grundsätzlich, die in der Methode enthaltenen Anweisungen beim Aufrufen des Delegates aus-
zuführen. Durch die Verwendung von Delegaten ist es unter anderem möglich Methoden (in
Form eines Delegates) als Rückgabewert von anderen Methoden zu erhalten, sie als Funktions-
parameter anderen Methoden zu übergeben oder sie an Variablen (vom Typ eines zulässigen
Delegattyps) zu binden. Man spricht davon, dass Methoden als First-Class-Objekt behandelt
werden können und somit First-Class-Funktionen darstellen. Delegate erlauben es außerdem,
Methodenaufrufe zur Laufzeit zu ändern und somit beispielsweise neue Funktionalität in be-
reits bestehende Klassen zu integrieren.
Im Gegensatz zu Methodensignaturen, beinhaltet die Signatur eines Delegattyps auch seinen
Rückgabewert. Somit ist darauf zu achten, dass bei der Zuweisung einer Methode an einen
Delegattypen neben Typ und Art ihrer Methodenparameter auch der Rückgabewert zulässig
und gültig ist.
Delegate in C# sind somit typsicher und unterscheiden sich deshalb stark von dem Prinzip der
Funktionszeiger, wie man diese z.B. aus der Programmiersprache C++ kennt. Seit der .NET
Framework Version 3.5 realisiert C# zudem Ko- und Kontravarianz für Methodensignaturen
und Delegattypen. Es ist somit möglich Methoden an ein Delegat zu binden deren Rückgabe-
wert Kindklasse des Delegat Rückgabetypes ist (Kovarianz), sowie Parameter zu übergeben
die Elternklasse des Delegat Parametertypes sind (Kontravarianz).
Erzeugt wird die Instanz eines Delegates dabei entweder durch die Zuweisung einer statischen
Methode oder unter Angabe des tatsächlichen Objektes (das Objekt auf dem die Methode
später aufgerufen wird), anhand einer Instanzmethode (siehe Listing 2.1).
192. Grundlagen
Listing 2.1: Instanziierung mit benannter, nicht statischer Methode
1 public class Converter
2 {
3 private delegate double ConvertIntToDouble(int parameter);
4
5 private double ConvertMethod(int parameter)
6 {
7 return System.Convert.ToDouble(parameter);
8 }
9
10 public double Convert(int parameter)
11 {
12 ConvertInToDouble converter = this.ConvertMethod;
13 return converter.Invoke(parameter);
14 }
15 }
Im Listing 2.1 wird dem Delegattypen ConvertIntToDouble unter Verwendung des Delegates
converter die Instanzmethode ConverterMethod zugewiesen. Das Ziel des Aufrufes ist dabei
die erstellende Instanz selbst (this).
Aufgerufen und somit ausgeführt werden kann die im Delegat gebundene Methode mittels
der Invoke Funktion, die jedes .NET Delegat-Objekt zur Verfügung stellt. Diese nimmt die
im Delegattypen festgelegten Parameter entgegen und liefert den spezifizierten Rückgabewert
zurück. Als zusätzliche Vereinfachung ist es möglich auf Variablen des Delegattypen (in den
Beispielen converter) die Funktion auszuführen, als ob das Objekt die Methode selbst wäre.
Somit kann auf den expliziten Aufruf der Methode Invoke verzichtet werden.
Generische Delegattypen
Um zu verhindern, einer Klasse für jede gekapselte Methode (oder jeder gekapselten Me-
thode mit eigener Signatur) einen eigenen Delegattypen hinzufügen zu müssen, stellt .NET
verschiedene, generische Delegattypen zur Verfügung.
Die beiden am häufigsten verwendeten Delegattypen sind dabei Action und Func. Das Acti-
on-Delegat kapselt eine Methode, welche über bis zu 16 Parameter verfügt und keinen Rück-
gabewert liefert. Es wird beispielsweise im Eventhandling der gängigen C# GUI Frameworks
202. Grundlagen
verwendet.
Das Func-Delegat kapselt eine Methode mit bis zu 16 Parametern welche außerdem einen
Rückgabewert liefert. Es findet überwiegend als Parameter für anderer Methoden und in
Microsofts LINQ Verwendung. Listing 2.2 zeigt Ausschnitte der Deklaration von Func.
Listing 2.2: generischer Delegattyp Func
1 TResult Func()
2 TResult Func (T arg)
3 {...}
4 TResult Func (T arg)
2.3. Closures
Unter einem Closure versteht man in der Informatik allgemein eine Methode, die Zugriff auf
den Kontext hat, aus welchem heraus sie erstellt wurde. Im Falle von C# wird dies durch
das Erfassen von Variablen in anonymen Funktionen realisiert. Eine anonyme Funktion wird
dabei entweder durch eine anonyme Methode oder durch die Verwendung eines Lambda-
Ausdruckes repräsentiert. Im Folgenden sollen die drei Begrifflichkeiten anonyme Methode,
Lambda-Ausdrücke und erfasste Variable erklärt und damit verbundene Möglichkeiten erläu-
tert werden.
2.3.1. Anonyme Funktionen
Unter einer anonymen Funktion versteht man in .NET eine Funktion ohne Namen. Sie wird
nicht, wie bei Instanzmethoden üblich, innerhalb der umschließenden Klasse erstellt, sondern
direkt an einen Delegattypen gebunden. Dies kann in Form eines benannten Delegates gesche-
hen (Vergleich mit Listing 2.3) oder indem die Funktion als Parameter einer anderen Methode
übergeben wird. Listing 2.3 zeigt die Zuweisung einer anonymen Methode an die Variable
converter vom expliziten Delegattypen ConvertToDouble.
212. Grundlagen
Listing 2.3: Instanziierung mit anonymer Methode
1 public class Converter
2 {
3 private delegate double ConvertToDouble(int parameter);
4
5 public double Convert(int parameter)
6 {
7 ConvertToDouble converter = delegate(int arg)
8 {
9 return System.Convert.ToDouble(arg);
10 };
11 return converter(parameter);
12 }
13 }
Anonyme Methoden bieten die Möglichkeit eine explizite Methode durch eine Inlineanwei-
sung zu ersetzen. Dies ist besonders dann vorteilhaft, wenn der einzige Zweck dieser Methode
die Bindung an das Delegat war. Grundsätzlich können anonyme Methoden überall dort ver-
wendet werden wo ein konkreter Delegattyp gefordert ist. Sie können unter anderem direkt als
Parameter einer anderen Methode übergeben werden, welche als Parameter einen speziellen
Delegattypen erwartet.
Für jede in der Klasse enthaltene anonyme Methode erstellt der Compiler beim Erstellen des
IL Codes eine tatsächliche Methode innerhalb der Klasse. Dennoch kann diese Methode, im
Gegenteil zu gewöhnlichen Instanzmethoden, nicht direkt aufgerufen werden. Dies wird ver-
hindert indem für die automatisch generierten anonymen Methoden Namen vergeben werden,
die nur innerhalb der IL gültig sind, jedoch nicht innerhalb der verwendeten Programmier-
sprache. Ein Ausführen des an das Delegat gebundenen Funktionsblockes ist somit nur durch
den Aufruf des Delegates selbst möglich.
Listing 2.4 zeigt einen Ausschnitt aus dem erzeugten IL Code. Die automatisch generierte,
kryptisch benannte Methode b__0 befindet sich dabei, zusätzlich zu den Instanz-
methoden, im generierten IL Code. Der Compiler unterscheidet bei der Ausführung des IL
Codes also nicht zwischen normalen und automatisch generierten anonymen Methoden.
222. Grundlagen
Listing 2.4: Ausschnitt aus dem erzeugten IL Code von Listing 2.3
1
2 .class public auto ansi beforefieldinit Converter
3 extends [mscorlib]System.Object
4 {
5 {...}
6 .method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
7 {
8 }
9
10 .method private hidebysig static float64 b__0(int32
arg) cil managed
11 {
12 {...}
13 }
14 {...}
15 }
2.3.2. Lambda-Ausdrücke
Seit der Version 3.5 des .NET Frameworks besteht neben der Möglichkeit anonyme Funk-
tionen mit anonymen Methoden zu erstellen die Möglichkeit dies durch sogenannte Lamb-
da-Ausdrücke zu realisieren. Lambda-Ausdrücke können als Weiterentwicklung anonymer
Methoden verstanden werden. Der größte Benefit, der durch die Verwendung von Lambda-
Ausdrücken erzielt werden kann, ist eine enorme Verkürzung und Vereinfachung des notwen-
digen Syntax zum Erstellen anonymer Funktionen (Microsoft, 2012).
Zum Erstellen eines Lambda-Ausdruckes wird der Lambda Operator => verwendet. Er trennt
die Eingangsparameter der linken von den definierten Anweisungen auf der rechten Seite.
Listing 2.5 zeigt die Realisierung von Listing 2.3 unter Zuhilfenahme eines Anweisungslamb-
das.
232. Grundlagen
Listing 2.5: Instanziierung einer anonymen Funktion mit Hilfe eines Anweisungslambdas
1 public class Converter
2 {
3 private delegate double ConvertToDouble(int parameter);
4
5 public double Convert(int parameter)
6 {
7 ConvertToDouble converter = (int arg) =>
8 { return System.Convert.ToDouble(arg); };
9 return converter(parameter);
10 }
11 }
Ein weiterer Vorteil der Verwendung von Lambda-Ausdrücken ist die Möglichkeit in vielen
Fällen Parameterlisten und Rückgabetyp des Ausdruckes implizit statt explizit zu typisieren.
Der Typ der Eingangsparameter kann im Falle eines nicht generischen Delegattyps aus dessen
Signatur übernommen werden und muss nicht explizit angegeben werden. Somit kann im
obigen Beispiel auf die Typisierung des Eingangsparameters verzichtet werden.
Zusätzlich kann bei allen Lambda-Ausdrücken, welche ausschließlich aus einer return-
Anweisung bestehen, auf die Verwendung des Schlüsselwortes return und die geschweiften
Klammern, welche den Ausdruck umschließen, verzichtet werden. In diesen Fällen spricht
man von sogenannten Ausdrucklambdas.
Die Erstellung des Delegates kann somit auf 2.6 reduziert werden.
Listing 2.6: Instanziierung einer anonymen Funktion mittels implizierter Typisierung
1 ConvertToDouble converter = arg =>
2 System.Convert.ToDouble(arg);
Der impliziten Typisierung der Parameterliste sind jedoch einige Grenzen gesetzt. So ist es
nicht möglich eine Mischung von implizit und explizit typisierten Parametern zu verwenden.
Alle verwendeten Parameter müssen entweder ausschließlich explizit oder ausschließlich im-
plizit typisiert sein. Zusätzlich ist die implizite Typisierung ausgeschlossen wenn es sich bei
den verwendeten Parametern um out oder ref Parameter handelt.
Lambda-Ausdrücke werden überwiegend in Fällen eingesetzt, in denen die anonyme Metho-
242. Grundlagen
de einen Rückgabewert besitzt. Dies wird eingesetzt um eine Aneinanderreihung von Lamb-
da-Ausdrücken zu ermöglichen und somit z.B. geschachtelte Ausdrücke zu vereinfachen. In
diesen Fällen wird selten mit expliziten Delegattypen gearbeitet sondern fast ausschließlich
generische Delegattypen eingesetzt.
2.3.3. Erfasste Variablen
Von einer erfassten Variablen spricht man immer dann, wenn eine anonyme Funktion eine
Variable verwendet, die in der gleichen Methode deklariert wurde wie die anonyme Methode
selbst, diese Variable aber kein Parameter des Delegates ist (Skeet, 2011).
Listing 2.7: Einfaches Beispiel einer erfassten Variablen innerhalb einer anonymen Methode
1 public void CaptuerVariable()
2 {
3 int legalAge = 18;
4 List Persons = new List() {
5 new Person("Marry", 20),
6 new Person("Peter", 16),
7 new Person("Gilbert", 25)};
8
9 Action ageFilter = delegate (Person p)
10 {
11 if (p.Age < legalAge)
12 {
13 Console.WriteLine(p.Name);
14 }
15 };
16 Persons.ForEach(ageFilter);
17 }
Listing 2.7 zeigt ein einfaches Beispiel einer erfassten Variablen. Während dem Delegat age-
Filter eine Person als Parameter zur Verfügung steht, greift das Delegat zusätzlich auf die
äußere Variable legalAge zu. Das Delegat kann somit nur ausgeführt werden wenn es seine
Bindung zu dieser Variablen behält. Da die Möglichkeit besteht, dass das so eben erstellte
Delegat den Gültigkeitsbereich der Methode verlässt und weiter existiert, selbst wenn die Me-
252. Grundlagen
thode zurückgekehrt ist, muss sicher gestellt werden, dass eine erfasste lokale Variable ihren
eigentlichen Gültigkeitsbereich überleben kann. Ermöglicht wird dies, wie bereits im Falle der
anonymen Methoden, mit Hilfe der IL. Für jede erfasste Variable erstellt der Compiler im IL
Code eine Klasse, welche diese Variable hält. Die das Delegat umgebende Methode, sowie
das Delegat selbst, halten jeweils eine Referenz auf die gleiche Instanz dieser Klasse. Daraus
folgt das der anonymen Methode die tatsächliche Variable übergeben wird und nicht der Wert
der Variablen zum Zeitpunkt der Erstellung des Delegates. Dies führt dazu das die erfass-
te Variable nicht, wie sonst üblich, beim Verlassen der Methode zerstört wird. Dies ist nicht
möglich da noch eine Referenz des Delegates vorhanden sein könnte. Somit verlängert sich
die Lebenszeit der Variablen bis die letzte Referenz zerstört wurde. Der Garbage Collector 5
tritt also erst in Aktion wenn auch das referenzierende Delegat zerstört wurde.
Ein häufig in diesem Zusammenhang auftretende Problem wird als outer variable trap be-
zeichnet. (Lippert, 2009) Es tritt genau dann ein, wenn erwartet wird, dass der Wert der Va-
riablen übergeben wird, anstatt der Variablen selbst. Dies ist besonders beim Erfassen von
Schleifenvariablen oftmals der Fall. Eine Betrachtung dieses Problems wird in Abschnitt 3.7.2
durchgeführt.
2.4. Microsoft Roslyn
Im Rahmen dieser Arbeit wurde mit der im September 2012 erschienen Community Technolo-
gy Preview (CTP) Version von Microsoft Roslyn gearbeitet. Die entstandene Refaktorisierung
und somit auch das implementierte Visual Studio Plugin stützen sich auf dieses Framework. In
diesem Kapitel sollen kurz die grundlegenden Funktionalitäten dieses Frameworks vorgestellt
werden und zusätzlich alle benötigten Begrifflichkeiten erläutert werden.
2.4.1. Einführung
Mit dem Roslyn Framework stellt Microsoft eine Programmierschnittstelle (API - Application
Programming Interface) zur Verfügung, die einen tiefen Einblick in die vom Compiler erzeug-
ten Daten gewährt. Dabei beschränkt sich das Framework nicht auf die reine Bereitstellung
5
Der Garbage Collector stellt eine Form automatischer Speicherbereinigung dar.
262. Grundlagen
dieser Daten. Mit Hilfe des Frameworks ist es möglich Quellcode Transformationen und Ana-
lysen auf Basis tatsächlicher programmiersprachlicher Konstrukte durchzuführen. Durch die
gebotene Funktionalität unterstützt es die Erstellung von Quellcode erzeugenden oder Quell-
code manipulierenden Softwaretools.
2.4.2. Syntaxbaum
Um Modifikationen am Quellcode zu ermöglichen stellt das Roslyn Framework diesen in Form
eines Syntaxbaumes zur Verfügung. Der Syntaxbaum ist dabei eine vollständige Darstellung
des Quellcodes als Baumstruktur. Durch die verkettete Darstellungsform können die Bezie-
hungen zwischen programmiersprachlichen Konstrukten leichter dargestellt und analysiert
werden. Innerhalb des Baumes werden Attribute als Variablen der Knotenelemente verwaltet
während Beziehungen zwischen einzelnen Knotenelementen durch Referenzen repräsentiert
werden.
Im Gegensatz zum Abstract Syntax Tree (AST) enthält der Syntaxbaum alle im Quellcode vor-
handenen Informationen. Er repräsentiert also neben dem gesamten, sprachabhängigen Syn-
tax auch alle Leerzeichen, Kommentare und Formatierungseigenschaften, die keinen direkten
Einfluss auf die Ausführung des Programmes nehmen.
Der Syntaxbaum selbst ist dabei aus einer Kombination von verschiedenen Syntaxelemen-
ten zusammengesetzt. Die einzelnen Elemente Syntax Node, Syntax Token und Syntax Trivia
sollen im Folgenden kurz erläutert werden.(Ng u. a., 2012)
Syntax Nodes
Syntax Nodes bilden den Hauptbestandteil eines Syntaxbaumes. Durch sie werden alle im
Quellcode vorhandenen syntaktischen Bestandteile repräsentiert. So werden beispielsweise
Deklarationen, Anweisungen oder Ausdrücke als Syntax nodes dargestellt. Jede Syntax No-
de selbst, verwaltet über die Eigenschaften Parent und ChildNodes den jeweiligen Vorgänger
bzw. ihre nachfolgenden Nodes. Somit kann, ausgehend von jeder beliebigen Syntax Node,
ein Unter -oder Teilbaum abgeleitet werden. Für jede mögliche Form von Deklarationen, An-
weisungen oder Ausdrücken existiert eine tatsächliche Implementierung in Roslyn. So wird
272. Grundlagen
beispielsweise konkret zwischen PropertyDeclarationSyntax und FieldDeclarationSyntax un-
terschieden.
Syntax Tokens
Syntax Tokens stellen den kleinsten Bestandteil des Syntaxbaumes dar. Sie repräsentieren un-
ter anderem Schlüsselwörter oder Bezeichner. Da sie, im Gegensatz zu Syntax Nodes, ein
atomarer Bestandteil des Quellcodes sind, verwalten sie keine Kind Knoten. Sie selbst werden
jedoch in exakter Reihenfolge, in der sie im Quellcode vorkommen, dem zugehörigen Syntax
Node als Kinder zugeordnet. Da nicht für jeden benötigten Syntax Token eine konkrete Klasse
existiert, wird die dem Token zugehörige Information über die Eigenschaft Value wiederge-
geben. Diese beinhaltet im Falle des PublicKeywords beispielsweise die Zeichenkette public,
während im IdentifierToken der tatsächliche Name des Bezeichners beinhaltet ist.
Syntax Trivia
Syntax Trivia repräsentieren die im Quellcode vorkommenden Leerzeichen und Kommentare.
Jeder geparste Token beinhaltet eine vorgehende und eine nachfolgende Syntax Trivia. Durch
die ebenfalls als Trivia erfassten Leerzeichen und Zeilenumbrüche wird unter anderem auch
die Formatierung des geparsten Quellcodes erfasst und editiert.
Listing 2.8 zeigt eine einfache Implementierung einer Klasse Person. Abbildung 2.1 zeigt
die daraus resultierende Darstellung in Form des Syntaxbaumes. In der mit Hilfe des Roslyn
Syntax Visualizer 6 erzeugten Darstellung als Syntaxbaum sind alle Syntax Nodes blau, alle
Syntax Token grün und alle Formen von Syntax Trivia in rot dargstellt.
6
http://blogs.msdn.com/b/visualstudio/archive/2011/10/19/
roslyn-syntax-visualizers.aspx
282. Grundlagen
Listing 2.8: Einfache Implementierung einer Klasse Person zur Visualisierung des Syntaxbau-
mes
1 using System;
2
3 public namespace Roslyn
4 {
5 public class Person
6 {
7 public int Age { get; set; }
8 public string Name { get; set; }
9
10 public Person(string name, int age)
11 {
12 this.Name = name;
13 this.Age = age;
14 }
15 }
16 }
292. Grundlagen
Abbildung 2.1.: Darstellung der Klasse Person als Syntaxbaum
302. Grundlagen
2.4.3. Manipulation des Syntax Trees
Die Modifikation von Quellcode ermöglicht Roslyn über die Manipulation des daraus er-
stellten Syntaxbaumes. Eine explizite Änderung wird dabei durch das Austauschen, Hinzu-
fügen oder Löschen von einzelnen Syntaxelementen realisiert. Dabei muss ein Syntaxelement
nicht durch ein gleichartiges ersetzt werden. Das Zusammenfassen einzelner Anweisungen zu
Blocksyntax ist genauso möglich wie eine einfache Änderungen des Bezeichners. Elemente
können mit Roslyn grundsätzlich auf zwei unterschiedliche Arten und Weisen erzeugt wer-
den. Die erste Möglichkeit ist das Verwenden von Factory Methoden. Diese sind für die meis-
ten mögliche Syntaxelemente vorhanden und erlauben eine stark parametrisierte Erzeugung
einzelner Elemente. Die zweite Möglichkeit ist die Generierung von Syntaxelementen aus
Zeichenketten. Dabei werden die Syntaxelemente direkt durch, von der Compiler API gepars-
te, Zeichenketten erzeugt. Besonders bei der Erstellung verschachtelter Anweisungen erweist
sich diese Vorgehensweise als vorteilhaft. Mit ihr können vollständige Teilbäume direkt und
unter Berücksichtigung verschiedener Faktoren, wie z.B. der Zielversion des Frameworks,
erzeugt werden.
Da der Syntaxbaum in der Lage ist, nicht kompilierbaren Quellcode zu verwalten, muss bei der
Erstellung und Manipulation von Syntaxelementen auf die semantische Korrektheit des Quell-
codes geachtet werden. Weder bei der Erstellung einzelner Elemente noch beim Erstellen des
editierten Syntaxbaumes findet eine semantische Verifizierung statt. Es ist also unter anderem
möglich ungültige Typbindungen zu erzeugen, ungültige Methodenaufrufe zu generieren oder
nicht kompilierbare Anweisungen in den Editor zu schreiben.
Da alle Syntaxelemente und Syntaxbäume in Roslyn grundsätzlich unveränderlich sind, kann
ein Editieren eines bestehenden Elementes nur über die Erzeugung eines neuen Elementes
erfolgen. Um die manipulierten Elemente in den Syntaxbaum einzufügen muss dieser folglich
ebenfalls neu erstellt werden.
2.5. Microsoft Visual Studio
Visual Studio ist eine von Microsoft entwickelte, integrierte Entwicklungsumgebung (IDE -
integrated development environment). Sie unterstützt die Entwicklung von Programmen mit
312. Grundlagen
verschiedensten verwalteten und nicht verwalteten Programmiersprachen. Darunter beispiels-
weise C#, Visual Basic .NET, C++ und HTML.
Visual Studio beinhaltet des Weiteren eine Vielzahl von Werkzeugen zur Steigerung der Pro-
duktivität von Softwareentwicklern. So beinhaltet es unter anderem eine IntelliSense, zusätz-
liche grafische Editoren zum Erstellen von Windows Forms oder Windows Presentation Foun-
dation (WPF) Benutzungsschnittstellen und einen integrierten Debugger.
Im Rahmen dieser Arbeit wird Visual Studio vor allem als Basis für die zu entwickelnde
Erweiterung verwendet. Aus diesem Grund soll im Folgenden kurz auf die Möglichkeiten der
Erweiterung von Visual Studio eingegangen werden.
2.5.1. Erweiterbarkeit von Visual Studio
Grundsätzlich bietet Visual Studio eine Vielzahl von Möglichkeiten Erweiterungen zu inte-
grieren, welche unter dem Begriff Visual Studio Extensibility (VSX) zusammengefasst wer-
den.
Die Möglichkeiten zur Erweiterung reichen von einfachen Makros zur Vereinfachung oder
Automatisierung von häufig auftretenden Aufgaben über das Hinzufügen von Code Snippets
bis hin zu Visual Studio Add-Ins und Integrated Packages (VSPackages).
Makros stellen dabei die einfachste Form der Erweiterung dar. Sie ermöglichen die Erstellung
einer Abfolge von Anweisungen zum Erfüllen verschiedenster Aufgaben. Makros können
entweder durch den, direkt in die IDE eingebetteten Service aufgenommen und gespeichert
werden oder in einer, an Visual Basic angelehnten, Skriptsprache händisch erstellt werden.
Da Makros weder vollen Zugriff auf die IDE haben, noch die Möglichkeit beinhalten neue
Funktionalität wie beispielsweise Kontextmenü-Einträge zu integrieren, sind sie für die zu
realisierende Aufgabe nicht ausreichend.
Visual Studio Add-Ins stellen eine weitaus mächtigere Variante der Erweiterung von Visual
Studio dar. Ihnen steht die Möglichkeit offen auf das Visual Studio automation model zuzu-
greifen. Dieses ermöglicht einen breiten Zugriff auf die Funktionalitäten der IDE. So können
beispielsweise die Funktionalitäten anderer Add-Ins genutzt werden, Funktionen zu bestehen-
den Editoren wie dem Text Editor hinzugefügt werden oder vollständig eigenständige Werk-
322. Grundlagen
zeuge integriert werden. Im Gegensatz zu Makros werden Add-Ins über die Implementierung
einer COM (Component objekt model - eine Vorgänger-Technologie von .NET) Schnittstelle
eingebunden. Sie werden als kompilierte DLL eingebunden. Mit dem Erscheinen von Visu-
al Studio 2013 wurden Visual Studio Add-Ins von Microsoft als veraltet gekennzeichnet, die
empfohlene Alternative sind die Visual Studio Packages.
Unter Visual Studio Packages versteht man die wohl mächtigsten Erweiterungsmöglichkeiten
der Visual Studio IDE. Sie bieten neben dem Zugriff auf das automation model zusätzlich
eine Vielzahl von Funktionalitäten der IDE an. Die Möglichkeiten der Packages sind so weit-
reichend, dass selbst Teile der Kernkomponenten des Visual Studio selbst, als Packages im-
plementiert wurden. Darunter fallen beispielsweise der Texteditor oder der Debugger. (Novak,
2008) Um die Funktionalität des zu erstellenden Plugins auch in Visual Studio 2013 prinzipiell
zu ermöglichen, wurde es als Visual Studio Package realisiert.
333. Lösungsansatz
Das folgende Kapitel soll den grundsätzlichen Lösungsansatz darlegen, welcher zur Realisie-
rung der Refaktorisierung Extract Closure verwendet wurde. Dabei sollen zuerst die Rahmen-
bedingungen skizziert werden, innerhalb welcher diese Arbeit durchgeführt wurde. Nachfol-
gend sollen die Anforderungen an das Werkzeug selbst sowie an die Konfigurierbarkeit der
Lösung vollständig dargelegt werden. Außerdem wird die zur Extraktion des Closures ver-
wendete Vorgehensweise vorgestellt. Abschließend werden zusätzliche Einflüsse auf die ent-
standene Lösung betrachtet, welche außerdem in Kapitel 5 bewertet werden sollen.
3.1. Rahmenbedingungen
Im Folgenden sollen, in aller Kürze, die zur Realisierung von Extract Closure verwendeten
Rahmenbedingungen vorgestellt werden. Wie bereits erwähnt soll das Plugin in der Program-
miersprache C# entwickelt werden. Zur Realisierung der Datenflussanalyse und zur Mani-
pulation des Syntaxbaumes wird das Roslyn Framework verwendet. Das Plugin wird für die
Entwicklungsumgebung Visual Studio 2012 entwickelt.
Eine weitere Rahmenbedingung bezieht sich auf den Quellcode, aus welchem heraus die Re-
faktorisierung gestartet werden soll. Da der von Roslyn verwaltete Syntaxbaum in der Lage ist,
nicht kompilierbaren Quellcode darzustellen, kann die Refaktorisierung prinzipiell auch auf
Quellcode ausgeführt werden, welcher syntaktische Fehler beinhaltet und somit nicht kompi-
liert.
Da die reine Erstellung eines Closures bzw. einer anonymen Methode diese nicht ausführt,
muss die neu erstellte Funktion anstelle des früheren Quellcodes ausgeführt werden.
343. Lösungsansatz
3.2. Grundsätzlicher Ablauf der Refaktorisierung
Der grundsätzliche Ablauf der Extract Closure Refaktorisierung lehnt sich an die bereits in
Visual Studio integrierten Refaktorisierungen an. So wird nach der Selektion des Quellcodes
über das Kontextmenü die betreffende Refaktorisierung gestartet. Dabei wird überprüft ob
alle Vorbedingungen zur Ausführung der gewählten Refaktorisierung erfüllt sind. Die zusätz-
lich anzugebenden Benutzerinformationen werden in einem Benutzungsdialog abgefragt und
stellen die Konfiguration des zu erstellenden Closures dar. Sind alle notwendigen Informatio-
nen vorhanden, kann die Refaktorisierung ausgeführt werden. Der stichpunktartige Ablauf der
Refaktorisierung lässt sich wie folgt beschreiben:
1. Der zu refaktorisierende Quellcode wird vom Benutzer selektiert
2. Eine erste Überprüfung stellt fest, ob die Refaktorisierung auf dem selektierten Quell-
code ausführbar ist
3. Die auszuführende Refaktorisierung wird vom Benutzer gestartet
4. Vom Benutzer anzugebende, zusätzliche Konfigurationsmöglichkeiten werden behan-
delt
5. Die angestrebten Änderungen im Quellcode werden berechnet
6. Falls die Änderungen durchführbar waren, wird das Ergebnis in den Texteditor zurück-
geschrieben
Dabei kann nach der Ausführung der Refaktorisierung der neue erstellte Quellcode mit den
Undo-Funktionen der Entwicklungsumgebung rückgängig gemacht werden. Sollte die Refak-
torisierung auf den ausgewählten Quellcode nicht ausführbar sein, wird die entsprechende
Option im Kontextmenü ausgeblendet und kann somit nicht ausgeführt werden.
353. Lösungsansatz
3.3. Vorbedingungen
Um die vom Benutzer selektierten Anweisungen zu extrahieren müssen eine Reihe von Vor-
bedingungen erfüllt sein, die eine gültige Auswahl von Anweisungen eruieren. Diese sollen
sicherstellen, dass die Refaktorisierung korrekt durchgeführt werden kann und sich innerhalb
zulässiger Grenzen bewegt. Die im Rahmen dieser Arbeit verwendeten Vorbedingungen für
die Refaktorisierung Extract Closure sind dabei:
1. Es muss mindestens eine Anweisung selektiert sein
2. Alle selektierten Anweisungen müssen sich innerhalb derselben Methode befinden
3. Die erste und die letzte selektierte Anweisung müssen sich zusätzlich innerhalb des
gleichen Gültigkeitsbereiches befinden
4. Die Selektion darf keine Variablendeklaration beinhalten
5. Die selektierten Anweisungen dürfen eine Reihe von Ausdrücken nicht enthalten
6. Mindestens eine der selektierten Anweisungen muss eine lokale Variable beinhalten
Die aufgeführten Vorbedingungen sind Ausschlusskriterien für die Durchführung der Refak-
torisierung und werden noch vor der Freigabe des Anwenderdialoges zur Konfiguration der
Refaktorisierung überprüft. Um die Refaktorisierung durchführen zu können, muss mindes-
tens eine gültige Anweisung selektiert sein. Dabei muss die Auswahl die Anweisung nicht
vollständig umschließen. Wird nur ein Teil einer gültigen Anweisung selektiert, so wird die
Auswahl auf die vollständige, berührte Anweisung erweitert. Dies soll einer Extraktion un-
vollständiger und damit fehlerhafter Anweisungen entgegenwirken.
Bei allen Anweisungen, die selbst einen eigenen Gültigkeitsbereich erstellen, wird explizit
unterschieden ob die Selektion die beinhalteten Anweisungen betrifft oder die Anweisung
(inklusive aller beinhalteten Anweisungen) selbst. Somit ist es beispielsweise möglich An-
weisung innerhalb von Schleifen oder die gesamte Schleifenanweisung zu extrahieren.
Für die semantische Analyse der Anweisungen und für die Erstellung einer anonymen Funk-
tion ist es zwingend notwendig, dass alle selektierten Anweisung innerhalb des gleichen Gül-
363. Lösungsansatz
tigkeitsbereiches liegen. So ist es unter anderem nicht möglich über Methodengrenzen hinweg
Anweisungen zu selektieren oder Teile von geschachtelten Gültigkeitsbereichen zu extrahie-
ren. Wäre dies möglich, würde die Schachtelung der Gültigkeitsbereiche durch den Abschluss
der anonymen Funktion (oder der Anweisungen innerhalb des Anweisungslambdas) die be-
stehende Strukturierung des Quellcodes verändert. Dies würde im besten Fall zu nicht kompi-
lierbarem Code führen und somit den Fehler offenlegen, im schlimmsten Fall unbemerkt das
Verhalten ändern.
Eine weitere Vorbedingung ist, dass sich innerhalb der zu extrahierenden Anweisungen keine
Variablendeklaration befinden darf. Da durch die anonyme Funktion ein neuer Gültigkeits-
bereich erstellt wird, wäre die vorher im umgebenden Gültigkeitsbereich verfügbare Varia-
ble, nur noch lokal innerhalb der anonymen Funktion verfügbar. Grundsätzlich bestünde die
Möglichkeit die Variablendeklaration vor die Erstellung der anonymen Funktion zu stellen.
Somit könnte die Bindung der Variablen aufrecht erhalten werden und zusätzlich die Extrak-
tion ausgeführt werden. Da die Verschiebung der Variablendeklaration selbst jedoch an einige
Vorbedingungen geknüpft ist, soll im Rahmen dieser Arbeit auf diese Möglichkeit verzichtet
werden.
Zusätzlich sind eine Reihe von Anweisungen innerhalb anonymer Funktionen nicht oder nur
unter Einschränkungen verfügbar. Bei nicht erlaubten Anweisungen handelt es sich zumeist
um Sprunganweisungen 1 (Microsoft, 2012). Diese stellen eine Übergabe der Programmsteue-
rung dar. Durch die Sprunganweisungen goto, break und continue kann eine anonyme Metho-
de nicht verlassen werden. Sie verhindern somit eine Ausführung von Extract Closure auf den
ausgewählten Anweisungen.
Ein elementarer Bestandteil eines Closures ist der Bezug auf dessen Erstellungskontext (siehe
Kapitel 2.3). Daher kann ein Closure nur extrahiert werden, wenn innerhalb der selektierten
Anweisungen entweder schreibend oder lesend auf mindestens eine lokale Variable oder einen
lokalen Parameter zugegriffen wird. In allen anderen Fällen wird die Refaktorisierung nicht
ausgeführt, da eine resultierende anonyme Funktion kein Closure darstellen könnte.
1
http://msdn.microsoft.com/de-de/library/d96yfwee.aspx
37Sie können auch lesen