Moderne Programmierung mit C#

Florian Rappl, Universität Regensburg

C# entfesselt:
Moderne Programmierung

Agenda

  1. 08:00 - 12:30 Vortrag
  2. 12:30 - 13:00 Mittagspause
  3. 13:00 - 16:00 Übungen
  4. 16:00 - 17:00 Offener Teil

Bitte Fragen direkt einbringen und bei Verständnisproblemen sofort nachfassen.

Die Geschichte von C#

  • 1.0 (2002): Managed Code, grundlegende Syntax
  • 2.0 (2005): Generics, erweiterte Syntax
  • 3.0 (2007): LINQ, Anonyme Objekte und Methoden
  • 4.0 (2010): Dynamics, Task Parallel Library
  • 5.0 (2012): Asynchrone Programmierung

Der Vergleich mit C/C++

C/C++
  • Speicherverwaltung durch Benutzer
  • Klare Trennung Adressen / Werte
  • Begrenzte objektorientierte Fähigkeiten
  • Starke Präprozessorfähigkeiten
C#
  • Automatische Speicherverwaltung
  • Interne Trennung Pointer / Wert
  • Vollständig Objektorientiert
  • Eingeschränkte Präprozesorfähigkeiten

→ 01 - Basics

Managed Code: Automatische Speicherverwaltung

  • Destruktoren werden nicht benötigt
  • Objekte werden automatisch aufgeräumt
  • Der Speicher wird dynamisch verschoben
  • Pointerarithmetik (außerhalb unsafe) nicht durchführbar
  • Bei Bedarf IDisposable implementieren

Der Garbage Collector (GC)

  • Schafft für uns Speicher
  • Löscht nicht mehr benötigte Objekte
  • Wird automatisch ausgeführt
  • Kann von uns auch (teilweise) gesteurt werden
  • GC.Collect() sammelt Leichen ein

Vereinfachter GC Zyklus

GC Zyklus

→ 02 - Klassen

Wichtige Definitionen

  • Heap
    • Dynamischer (vom System zugewiesener) Speicher
    • Hier werden Referenztypen (d.h. Klassen, class) abgelegt
    • Speicher skaliert mit notwendiger Größe nach oben
  • Stack
    • Fest zugewiesener Stapelspeicher (skaliert nicht!)
    • Hier werden Werttypen (d.h. Strukturen, struct) abgelegt
    • Wird für Adressen der Funktionsrücksprünge verwendet

Aus C++ bekannt: Generics (Templates)

  • Generics sind parametrisierte Typen
  • Idee besteht aus zusätzlichen Variablen für Typen
  • Repräsentieren zum Zeitpunkt der Implementierung unbekannte Typen
  • Müssen erst bei der Verwendung der Klasse angegeben werden
  • Reduzieren Casts und optimieren Code
  • Gibt signifikante Unterschiede zwischen C++ und C#
  • Generics in C# sind weniger flexibel, dafür aber einfacher als Templates

Möglichkeiten mit Generics

  • Neben Vererbung der zweite Weg für wiederverwendbaren Code
  • In vielen Fällen muss man von keiner Basisklasse mehr erben
  • Keine Typchecks oder Casts mehr notwendig
  • Vermeiden Code-Duplikate
  • Überlassen dem Compiler die Verantwortung für Typensicherheit
  • Die neuen Typen können überall in der Klasse verwendet werden

→ 03 - Generics

Einsatz für generische Methoden

  • Eine generische Methode beinhaltet Parameter in ihrer Signatur
  • Generische Methoden können meistens bereits vom Compiler ohne Typenangaben aufgelöst werden
  • Daher spart man sich hier oft die explizite Angabe der Parameter
  • Methoden und Klassen sind die einzigen Orte für Typen-Parameter

Generische Methoden schreiben und verwenden

  • Dies erstellt eine Funktion zur Vertauschung von zwei Variablen
    static void Swap<T>(ref T a, ref T b) {
    	T temp = a; a = b; b = temp;
    }
    
  • Wir können diese Methode so ansprechen:
    int x = 10, y = 20;
    Swap<int>(ref x, ref y);
    
  • Aber geschickt wäre es, die Methode ohne den Parameter anzusprechen
    	Swap(ref x, ref y); //Wird vom Compiler korrekt aufgelöst
    

→ 04 - GenericMethods

Was geht noch mit Generics?

  • Wir können die möglichen Übergaben einschränken
  • Benötigen wir einen (leeren) Konstruktor, so schreiben wir
    class MyClass<T> where T : new()
    {
    	/* Inhalt hier */
    }
    
  • Ansonsten gibt es noch class, struct, interface und die Möglichkeit einen (Basis) Typ anzugeben

Erweiterungsmethoden

  • Müssen den Namensraum der Methode(n) über using einbinden
  • Vorteil: Extension Methoden sind sehr leicht austauschbar
  • Ansonsten können wir die Ext. Methods wie normale statische Methoden verwenden
  • Nicht nur Klassen, sondern auch Interfaces können erweitert werden

Datentypen vom Compiler bestimmen lassen

  • Mit dem Schlüsselwort var entscheidet der Compiler über den Typen
  • Dadurch ersparen wir uns viel Schreibarbeit
  • Außerdem sorgt dies im Regelfall für weniger Fehler
  • Einige Dinge gehen dann jedoch nicht mehr, z.B. Deklaration mehrerer Variablen in einer Anweisung (durch Komma getrennt)
  • Die dadurch entstandenen Zuweisungen sind statisch

Lambda Methoden (anonymone Funktionen)

  • Sind entweder Delegaten oder vom Typ Expression<T>
  • Es gibt zwei Arten von Lambda-Ausdrücken
  • Ausdrücke mit Rückgabewert - z.B.
    var lambda = x => x * x - x;
    
  • Ausdrücke ohne Rückgabewert - hier schreibt man
    var lambda = (x, y) => { var z = x * y - x; Console.WriteLine(z); }
    

Unterschied zwischen var und object

  • object ist ein echter Datentyp - jeder Typ erbt von Object
  • var ist nur ein Platzhalter, der vom Compiler aufgelöst wird
    int a = 20;
    var b = 30;
    object c = 40;
    Console.WriteLine(a + b);//Geht - liefert 50
    Console.WriteLine(a + c);//Geht nicht - Addition nicht definiert für object und int
    
  • var ist notwendig bei der Erstellung anonymer Objekte

Anonyme Objekte erstellen und übergeben

  • Ein anonymes Objekt ist eine spontan erstellte Klasse, um eine Sammlung von Werten zu speichern
  • Die Syntax ist hierbei folgendermaßen aufgebaut,
    var a = new { Name = "Florian", Age = 28 };
    
  • Nachdem der Name des Types Compiler-generiert ist, muss man var verwenden um auf die (read-only) Eigenschaften zuzugreifen

→ 05 - ExtensionLambdas

Language Integrated Query (LINQ)

  • LINQ erlaubt uns typensichere Abfragen an lokale Liste und externe Quellen (z.B. ein Datenbanksystem) zu stellen
  • Abfragen an Listen sind über das Interface IEnumerable<T> möglich
  • LINQ basiert auf Sequenzen und deren Elementen
  • Sequenzen implementieren das Interface IEnumerable<T>
  • LINQ Operatoren verformen Eingabesequenzen in Ausgabesequenzen
  • Alle Operatoren wurde als statische Erweiterungsmethode eingebaut

SQL ähnliche Syntax

  • Um LINQ analog zu SQL schreiben zu können wurde die Syntax übernommen
    var query = from n in names where n.Contains("a") orderby n.Length select n.ToUpper();
    
  • Dies entspricht der sog. Query Expression
  • Startet immer mit einer from Anweisung
  • Hört mit einer select oder group Anweisung auf
  • Neue Variablen können mit let eingeführt werden

Verwendung von LINQ über Methoden

  • Alternativ können wir LINQ über die Erweiterungsmethoden ansprechen
  • Wichtig hierzu ist die Verwendung des System.Linq Namespaces
  • Das Statement von eben sieht nun folgendermaßen aus
    var query = names.Where(n => n.Contains("a")).OrderBy(n => n.Length).Select(n => n.ToUpper());
    
  • Wir erhalten immer eine (LINQ) Liste zurück
  • Ausnahme: wir verwenden einen Operator wie First(), ToList(), ...

Reihenfolge von LINQ Anweisungen

LINQ Zyklus

Besonderheiten von LINQ

  • Verzögerte Ausführung (bis Ergebnis benötigt wird, z.B. Last())
  • Vorsicht bei Verwendung von Sum(), Max() etc. mit leeren Datenquellen - Fehlergefahr!
  • Statt die Liste auf Elemente mit Count() zu überprüfen, sollte Any() verwendet werden
  • Operatoren wie First() sollten bei Unklarheit über den Zustand der Liste nicht verwendet werden, sondern nur Elemente wie FirstOrDefault() mit anschließender Abfrage auf null

LINQ Statements testen

  • Zum Ausprobieren von LINQ Abfragen eignet sich das Tool LINQPad
  • Dies generiert uns auch noch den IL Code
  • Das Programm unterstützt LINQ to Objects, LINQ to SQL, das Entity Framework, sowie LINQ to XML
  • Ebenfalls unterstützt wird das Schreiben von SQL Anweisungen
  • Die Adresse zum Download ist linqpad.net

Komplexe Abfragen mit LINQ

  • Wie kann man z.B. Duplikate mit LINQ entfernen oder finden?
  • Viele Wege führen nach Rom - z.B.
    // Ganz allgemeine Duplikate entfernen
    var distinctItems = items.Distinct();
    // Duplikate durch verschiedene Eigenschaften identifizieren, z.B. nach Id
    var distinctItems = items.GroupBy(x => x.Id).Select(y => y.First());
    // Umgedreht, wir wollen alle Duplikate
    var duplicates = items.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key);
    
  • Zwischen mehreren Listen kann man z.B. mit Intersect() arbeiten

Venn Diagramme mit LINQ

LINQ mit Mengen

→ 06 - LINQ

LINQ und Datenquellen

  • Prinzipiell stellt jede Liste eine Datenquelle dar
  • LINQ für jede Datenquelle verfügbar durch Implementierung von IEnumerable<T> (In-Memory Daten)
  • Durch Implementierung von IQueryable<T> sind auch Remote Zugriffe möglich (baut nur eine Abfrage)
  • Die Abfrage wird erst bei Operationen wie ToArray() ausgeführt, was letztlich In-Memory Daten erzeugt

Objekte sezieren mit Reflection

  • Jede Typ kann mit typeof() inspiziert werden
  • Jede Instanz besitzt hierzu auch die Methode GetType()
  • Liefert uns eine Instanz vom Typ Type
  • Darüber kann man Informationen über Objekte erhalten
  • z.B. liefert die Methode GetProperties() alle Eigenschaften des Typs
  • Anwendung: Einfache Erweiterbarkeit, Plugins, Auslesen von Objekten

Essentielle Typen

Reflection Cycle Types

Wann Reflection verwenden?

  • Reflection ist ansich relativ teuer
  • Bevorzugt sollten Konzepte aus der OOP verwendet werden
  • Reflection auf Objekte loszulassen ist nur bei vollständig unbekannten Objekten sinnvoll
  • Das Ergebnis bei Reflection sollte immer gecached werden

→ 07 - Reflection

Metadaten einer Assembly

Reflection

Die Geschwindigkeit von Reflection im Detail

  • Reflection erzeugt viel Overhead und sollte daher iterativ vermieden werden (Startzeit ist sehr hoch)
  • Ein beispielhafter Vergleich: Reflection gegen direkte Erzeugung oder einen direkten Aufruf
  • Reflection ist in etwa 3 mal langsamer beim Erzeugen von Objekten
  • Außerdem ist Reflection ungefähr 4x langsamer bei Methodenaufrufen

Die Typoperatoren is und as einsetzen

  • Mit is können wir auf Verwandtschaft abfragen
  • Dieser Operator stellt die Kurzform von instanz.GetType() == typeof(Klasse) dar
  • Eine sichere Konvertierung von Referenztypen erhalten wir durch as
  • Ist eine Kurzschreibweise von:
    instanz is Klasse ? (Klasse)instanz : null
  • Über den ?? Operator können wir sehr schnell auf null überprüfen

→ 08 - Benchmark

Die DLR nutzen mit dynamic

  • dynamic ist ein vollständig dynamischer Typ (zur Laufzeit!)
  • Nicht zu verwechseln mit var oder object
  • Dient zur Kommunikation mit dynamischen Sprachen (PHP, Python, Ruby, JavaScript, ...)
  • Oder auch zur Vereinfachung für COM-Interop
  • Können die DLR nutzen indem wir von DynamicObject erben

Unterschied: dynamic, object, var

  • object ist das (allgemeinste) Objekt in C# - jede Instanz ist vom Typ object und besitzt deren Methoden (z.B. ToString())
  • var wird vom Compiler aufgelöst und ist an einen Typen gebunden - erleichtert uns nur die Schreibarbeit
  • dynamic ist dynamisch zur Laufzeit und verfügt über keine IntelliSense Unterstützung im Visual Studio

→ 09 - DLR

Multithreading mit C#

  • In C# gibt es die Möglichkeit über den Namespace System.Threading neue Threads aufzumachen
  • Dabei übergibt man dem Konstruktor der Thread Klasse lediglich eine Methode ohne Rückgabetyp
  • Die Interaktion zwischen verschiedenen Threads ist keineswegs einfach
  • Vorsicht: Neue Threads können keine Änderungen an Steuerelementen durchführen

Vom einfachen Thread zum komplexen Task

  • Das Thread Konzept ist bereits sehr alt und umständlich
  • Viel eleganter ist ein Aufgaben basiertes Konzept, wo verschiedene Aufgaben in Relation gebracht werden
  • Jede Aufgabe stellt dabei einen elemantaren Thread dar
  • Durch die angegebenen Relationen können Threads nun kontrolliert nacheinander, nebeneinander und in entsprechenden Kontexten (z.B. UI Thread) ausgeführt werden

Einordnung von Tasks

Tasks and Threads

→ 10 - Tasks

Cross-Threading Exceptions vermeiden

  • Mehrere Möglichkeiten: Entweder durch BackgroundWorker
  • Oder durch abschließenden Task im UI Thread
  • Oder durch einen delegate
  • Wird immer benötigt wenn InvokeRequired Eigenschaft true ist
  • In WPF muss hier mit einem Dispatcher gearbeitet werden
  • Die Probleme entstehen immer beim Setzen von Werten, da hier Race Conditions entstehen können

Objektorientierte Funktionspointer: Delegates

  • Das Problem mit Funktionszeigern wurde in C# elegant gelöst
  • Hierbei wurde ein objektorientierter Datentyp namens delegate erstellt
  • Delegaten legen die Signatur der Funktion(en) fest, die dahinter stecken können
  • Festlegt werden muss der Rückgabetyp, sowie die Übergabetypen
  • Im Gegensatz zu C/C++ sind Delegaten sicher

Die Fähigkeiten der Task Parallel Library (TPL)

  • Die TPL ist Task-basiert - reduziert den Over-Head für Thread Erstellung
  • Im Prinzip handelt es sich um einen optimierten ThreadPool
  • Tasks können voneinander abhängig sein, z.B. ContinueWith()
  • Neben dem neuen Task Konzept wurden weitere Verbesserungen wie z.B. PLINQ (Parallel LINQ) eingeführt
  • Datenparallelität und Aufgabenparallelität sind die zwei unterschiedlichen Regionen

Parallelisierung von Schleifen

  • Die TPL enthält noch einige Helfer in der Parallel Klasse
  • Wichtige Methoden sind z.B. For() und ForEach()
  • Vergleiche eine serielle for-Schleife
    for(var i = 0; i < 10; i++)
    	Console.WriteLine(i);
    
  • Mit der parallelen Version
    Parallel.For(0, 10, i => { 
    	Console.WriteLine(i); });
    

→ 11 - ParallelFor

Verbesserungen in C# 5: async und await

  • Können in Methoden nun explizit asynchronen Code verwenden
  • Die Methode muss dazu vor dem Rückgabetyp das Schlüsselwort async haben
  • In dieser Methode geben wir an der Stelle, die Zeit braucht await an
  • Der Rest (und die Rückgabe) werden dann vom Compiler in einen Callback ausgelagert
  • Der (verstecke) Callback ist bereits wieder im UI Thread

→ 12 - Async

Viel Erfolg!

MVP

Florian Rappl, Universität Regensburg