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/2014
Erklä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 2
Abstract 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 4
Inhaltsverzeichnis 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 5
Inhaltsverzeichnis 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 6
Abbildungsverzeichnis 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 7
Listings 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 8
Listings 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 9
Tabellenverzeichnis 3.1. Vorbelegung der Parameterliste . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.1. Überblick der Ergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 A.1. Überblick der Ergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 10
1. 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 11
1. 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 12
1. 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- 13
1. 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. 14
Glossar 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. 15
1. 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. 16
2. 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 17
2. 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 18
2. 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). 19
2. 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 20
2. 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. 21
2. 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. 22
2. 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. 23
2. 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- 24
2. 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- 25
2. 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. 26
2. 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 27
2. 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 28
2. 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 } 29
2. Grundlagen Abbildung 2.1.: Darstellung der Klasse Person als Syntaxbaum 30
2. 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 31
2. 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- 32
2. 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. 33
3. 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. 34
3. 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. 35
3. 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- 36
3. 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 37
Sie können auch lesen