Link auf die aktuelle Vorlesung im Versionsmanagementsystem GitHub
https://github.com/liaScript/CsharpCourse/blob/master/25_LINQII.md
Die interaktive Form ist unter diese Link zu finden -> LiaScript Vorlesung 25
Wie weit sind wir schon gekommen?
c# Schlüsselwörter:
| abstract | as | base |bool |break |byte |
|case |catch | char |checked |class | const |
|continue |decimal | default | delegate |do |double |
|else |enum | event | explicit | extern |false |
|finally | fixed |float |for |foreach |goto |
|if | implicit | in |int | interface |internal |
| is | lock |long |namespace |new | null |
| object | operator |out | override |params |private |
| protected |public | readonly |ref |return |sbyte |
| sealed |short | sizeof | stackalloc |static |string |
|struct |switch |this |throw |true |try |
| typeof |uint |ulong |unchecked | unsafe |ushort |
|using | virtual |void | volatile |while | |
Auf die Auführung der kontextabhängigen Schlüsselwörter wie where oder
ascending wurde hier verzichtet.
1. Welche Programmierparadigment werden durch C# abgedeckt. In wieweit ist das LINQ Konzept hier eine wichtige Ergänzung?
Programmierparadigmen
┃
┏━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━┓
Imperative Programmierung Deklarative Programmierung
┃ ┃
┏━━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━━━━┓
Prozedural Objektorientiert Funktional Logisch
Strukturierte Programmierung, Aspektorientierte Programmierung,
Generative Programmierung, Generische Programmierung
2. Aus welchen Elementen setzt sich eine LINQ-Anfrage zusammen?
3. Welche Relation hat das LINQ-Konzept zu SQL
4. Welche Vor- und Nachteile sehen Sie bei der Einbettung von LINQ in Ihre Anwendung?
Language Integrated Query (LINQ) umfasst ein Konzept in .NET, dass auf der direkte Integration von Abfragefunktionen abzielt. Dafür definieren die C#, VB.NET und F# eigene Schlüsselwörter sowie eine Menge an vorbestimten LINQ-Methoden. Diese können aber durch den Anwender in der jeweiligen Sprache erweitert werden.
LINQ-Anweisungen sind unmittelbar als Quelltext in .NET-Programme eingebettet. Somit kann der Code durch den Compiler auf Fehler geprüft werden. Andere Verfahren wie ActiveX Data Objects ADO und Open Database Connectivity ODBC hingegen verwenden Abfragestrings. Diese können erst zur Laufzeit interpretiert werden; dann wirken Fehler gravierender und sind schwieriger zu analysieren.
Innerhalb des Quellprogramms in C# oder VB.NET präsentiert LINQ die Abfrage-Ergebnisse als streng typisierte Aufzählungen. Somit gewährleistet es Typsicherheit bereits zur Übersetzungszeit wobei ein minimaler Codeeinsatz zur Realisierung von Filter-, Sortier- und Gruppiervorgänge in Datenquellen investiert wird.
Sie können LINQ zur Abfrage beliebiger aufzählbarer Auflistungen wie List, Array oder Dictionary<TKey,TValue> verwenden. Die Auflistung kann entweder benutzerdefiniert sein oder von einer .NET Framework-API zurückgegeben werden.
Alle LINQ-Abfrageoperationen bestehen aus drei unterschiedlichen Aktionen:
- Abrufen der Datenquelle
- Erstellen der Abfrage
- Ausführen der Abfrage
Für ein einfaches Beispiel, das Filtern einer Liste von Zahlenwerten realisiert sich dies wie folgt:
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Program {
public static void Main(string[] args){
// Spezifikation der Datenquelle
int[] scores = new int[] { 97, 92, 81, 60 };
// Definition der Abfrage
IEnumerable<int> scoreQuery =
from score in scores // Bezug zur Datenquelle
where score > 80 // Filterkriterium
select score; // "Projektion" des Rückgabewertes
// Execute the query.
foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
}
}
}@Rextester.eval(@CSharp)
Datenquellen
| Zugriff | Bedeutung |
|---|---|
| LINQ to Objects | Zugriff auf Objektlisten und -Hierarchien im Arbeitsspeicher |
| LINQ to SQL | Abfrage und Bearbeitung von Daten in MS-SQL-Datenbanken |
| LINQ to Entities | Abfrage und Bearbeitung von Daten im relationalen Modell von ADO.NET |
| LINQ to XML | Zugriff auf XML-Inhalte |
| LINQ to DataSet | Zugriff auf ADO.NET-Datensammlungen und -Tabellen |
| LINQ to SharePoint | Zugriff auf SharePoint-Daten |
Im Rahmen dieser Veranstaltung konzentrieren wir uns auf die LINQ to Objects Variante.
Query Ausdrücke
Insgesamt sind 7 Query-Klauseln vorimplementiert:
| Ausdruck | Bedeutung |
|---|---|
| from | definieren der Laufvariable und einer Datenquelle |
| where | filtert die Daten nach bestimten Kriterien |
| orderby | sortiert die Elemente |
| select | projeziert die Laufvariable auf die Ergebnisfolge |
| group | bildet Gruppen innerhalb der Ergebnismenge |
| join | vereinigt Elemente mehrere Datenquellen |
| let | definiert eine Hilfsvariable |
class Student{
public string Name;
public int Id;
public string Subject{get; set;}
public Student(){}
}
// Collection Initialization
List<Student> students = new List<Student>{
new Student("Max Müller"){Subject = "Technische Informatik", id = 1},
new Student("Maria Maier"){Subject = "Softwareentwicklung", id = 2},
new Student("Martin Morawschek"){Subject = "Höhere Mathematik I", id = 3}
}
// Implizite Typdefinition
var result = from s in students // Spezifikation der Datenquelle
where s.Subject == "Softwarentwicklung"
orderby s.Name
select new (s.Name, s.Id) // Projektion der Ausgabe
// explizite Typdefinition
IEnumerable<Student> result = from s in students
...Im vorangehenden Beispiel ist students die Datenquelle, über der die Abfrage
bearbeitet wird. Der List-Datentyp implementiert das Interface IEnumerable<T>.
Die letzte Zeile bildet das Ergebnis auf die Rückgabe ab, dem Interface
entsprechen auf ein IEnumerable<Student> mit den Feldern Name und Id.
Die Berechnung der Folge wird nicht als Ganzes realisiert sondern bei einer
Iteration durch den Datentyp List<Student>.
Für nicht-generische Typen (die also IEnumerable anstatt IEnumerable unmittelbar) implementieren, muss zusätzlich der Typ der Laufvariable angegeben werden, da diese nicht aus der Datenquelle ermittelt werden kann.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int[] Scores { get; set; }
}
class Program {
public static void Main(string[] args){
//ArrayList StudentList = new ArrayList(); <-- Nicht mehr benutzen
List<Student> StudentList = new List<Student>();
StudentList.Add(
new Student{
FirstName = "Svetlana", LastName = "Omelchenko", Scores = new int[] { 98, 92, 81, 60 }
});
StudentList.Add(
new Student {
FirstName = "Claire", LastName = "O’Donnell", Scores = new int[] { 75, 84, 91, 39 }
});
var query = from student in StudentList
where student.Scores[0] > 95
select student;
foreach (Student s in query)
Console.WriteLine(s.LastName + ": " + s.Scores[0]);
}
}
}@Rextester.eval(@CSharp)
Welche Struktur ergibt sich dabei generell für eine LINQ-Abfrage? Ein Query
beginnt immer mit einer from-Klausel und endet mit einer select oder group-Klausel.
Allgemeingültig lässt sich, entsprechend den Ausführungen in Mössenböck folgende Syntax ableiten:
QueryExpr =
"from" [Type] variable "in" SrcExpr
QueryBody
QueryBody =
{ "from" [Type] variable "in" SrcExpr
| "where" BoolExpr
| "orderby" Expr ["ascending" | "descending"] {"," Expr ["ascending" | "descending"]}
| "join" [Type] variable "in" SrcExpr "on" Expr "equals" Expr ["into" variable]
| "let" variable "=" Expr
}
( "select" ProjectionExpr ["into" variable QueryBody]
| "group" ProjectionExpr "by" Expr ["into" variable QueryBody]
).
Mit der isolierten Definition der Abfragen können diese mehrfach auf die Daten angewandt werden. Man spricht dabei von einer "verzögerten Ausführung" - jeder Aufruf der Ausgabe generiert eine neue Abfrage.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Program {
public static void Main(string[] args){
var numbers = new List<int>() {1,2,3,4};
// Spezifikation der Anfrage
var query = from x in numbers
select x;
Console.WriteLine(query.GetType());
// Manipulation der Daten
numbers.Add(5);
Console.WriteLine(query.Count());
// Manipulation und erneute Anwendung der Abfrage
numbers.Add(6);
Console.WriteLine(query.Count()); // 6
}
}
}@Rextester.eval(@CSharp)
Der Compiler transformiert LINQ-Anfragen in der Abfragesyntax in
Lambda-Ausdrücke, Erweiterungsmethoden, Objektinitializer und anonyme Typen.
Dabei sprechen wir von der Methodensyntax. Abfragesyntax und Methodensyntax sind
semantisch identisch, aber viele Benutzer finden die Abfragesyntax einfacher und
leichter zu lesen. Da aber einige Abfragen nur in der Methodensyntax möglich
sind, müssen sie diese bisweilen nutzen. Beispiele dafür sind Max(), Min(),
oder Take().
Nehmen wir also nochmals eine Anzahl von Studenten an, die in einer generischen Liste erfasst wurden:
List<Student> students = new List<Student>({
new Student{
Id = "123sdf234"
FirstName = "Svetlana",
LastName = "Omelchenko",
Field = "Computer Science",
Scores = new int[] { 98, 92, 81, 60 }
};
//...
});
var result = from s in students
where s.Field == "Computer Science"
orderby s.LastName
select new {s.LastName, s.Id}; Der Compiler generiert daraus folgenden Code:
IEnumerable<Student> result = students
.Where(s => s.Field == "Computer Science" )
.OrderBy(s => s.LastName)
.Select(s => new {s.LastName, s.Id});Wieso hat meine Klasse Student plötzlich eine Methode where? Eine der
Grundlage sind sogenannte Erweiterungsmethoden. Erweiterungsmethoden werden in
C# in einer statischen Klasse als statische Methode definiert. Das Schlüsselwort
this vor dem ersten Parameter definiert den zu erweiternden Typen. Dieser erste
Parameter wird beim Aufruf nicht mit übergeben.
Im Beispiel soll die Klasse System.String um eine weitere Substring-Anweisung ergänzt werden:
public static class MyStringExtensions
{
// zu erweiternder Typ
// |
public static string MySubstring(this string me, int position, int length)
{
//beliebige Logik
return "My" + me.Substring(position, length);
}
}
// Anwendung
string teststring = "test";
teststring.MySubstring(1, 2);Dabei wird die eigentliche Filterfunktion als Delegat übergeben, dies wiederum kann durch eine Lambdafunktion ausgedrückt werden. https://docs.microsoft.com/de-de/dotnet/api/system.linq.enumerable.where?view=netframework-4.8
Dabei beschreiben die Lambdafunktionen sogenannten Prädikate, Funktionen, die eine bestimmte Bedingung prüfen und einen boolschen Wert zurückgeben.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Program {
public static bool filterme(int num){
bool result = false;
if (num > 10) result = true;
return result;
}
public static void Main(string[] args){
int[] numbers = { 0, 30, 20, 15, 90, 85, 40, 75 };
//Func<int, bool> filter = delegate(int num) { return num > 10; };
Func<int, bool> filter = filterme;
IEnumerable<int> query =
numbers.Where(filter);
//IEnumerable<int> query =
// numbers.Where(s => s > 10);
foreach (int number in query)
{
Console.WriteLine(number);
}
}
}
}@Rextester.eval(@CSharp)
Das Beispiel zur Filterung einer Customer-Tabelle wurde der C# Dokumentation unter https://docs.microsoft.com/de-de/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations entnommen.
Die üblichste Abfrageoperation ist das Anwenden eines Filters in Form eines booleschen Ausdrucks. Das Filtern bewirkt, dass im Ergebnis nur die Elemente enthalten sind, für die der Ausdruck eine wahre Aussage liefert.
Das Ergebnis wird durch Verwendung der where-Klausel erzeugt. Faktisch gibt
der Filter an, welche Elemente nicht in die Quellsequenz eingeschlossen werden
sollen. In folgendem Beispiel werden nur die customers zurückgegeben, die eine
Londoner Adresse haben.
var queryLondonCustomers = from customer in customers
where customer.City == "London"
select customer;Sie können die logischen && und || verwenden, um so viele Filterausdrücke wie
benötigt in der where-Klausel anzuwenden.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Program {
public static void Main(string[] args){
var numbers = new List<int>() {-1, 7,11,21,32,42};
var query = from i in numbers
where i < 40 && i > 0
select i;
foreach (var x in query)
Console.WriteLine(x);
}
}
}@Rextester.eval(@CSharp)
Die entsprechenden Operatoren können aber auch um eigenständige Methoden ergänzt werden. Versuchen Sie zum Beispiel die Bereichsabfrage um eine Prüfung zu erweitern, ob der Zahlenwert gerade ist.
Die group-Klausel ermöglicht es, die Ergebnisse auf der Basis eines Merkmals
zusammenzufassen. Die group-Klausel gibt entsprechend eine Sequenz von
IGrouping<TKey,TElement>-Objekten zurück, die null oder mehr Elemente
enthalten, die mit dem Schlüsselwert TKey für die Gruppe übereinstimmen. Der
Compiler leiten den Typ des Schlüssels anhand der Parameter von group her.
IGrouping selbst implementiert das Interface IEnumerable und kann damit
iteriert werden.
var queryCustomersByCity =
from customer in customers
group customer by customer.City;
// customerGroup is an IGrouping<string, Customer> now!
foreach (var customerGroup in queryCustomersByCity) // Iteration 1
{
Console.WriteLine(customerGroup.Key);
foreach (Customer customer in customerGroup) // Iteration 2
{
Console.WriteLine(" {0}", customer.Name);
}
}Dabei können die Ergebnisse einer Gruppierung wiederum Ausgangsbasis für eine
weitere Abfrage sein, wenn das Resultat mit into in einem Zwischenergebnis
gespeichert wird.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Student{
public string Name;
public int id;
public string Subject{get; set;}
public Student(){}
public Student(string name){
this.Name = name;
}
}
class Program {
public static void Main(string[] args){
List<Student> students = new List<Student>{
new Student("Max Müller"){Subject = "Technische Informatik", id = 1},
new Student("Maria Maier"){Subject = "Softwareentwicklung", id = 2},
new Student("Martin Morawschek"){Subject = "Höhere Mathematik I", id = 3},
new Student("Katja Schulz"){Subject = "Technische Informatik", id = 4},
new Student("Karl Tischer"){Subject = "Softwareentwicklung", id = 5},
};
var query = from s in students
group s by s.Subject;
foreach (var studentGroup in query)
{
Console.WriteLine(studentGroup.Key);
foreach (Student student in studentGroup)
{
Console.WriteLine(" {0}", student.Name);
}
}
var query2 = from s in students
group s by s.Subject into sg
select new {Subject = sg.Key, Count = sg.Count()};
Console.WriteLine();
foreach (var group in query2){
Console.WriteLine(group.Count + " students attend in " + group.Subject);
}
}
}
}@Rextester.eval(@CSharp)
Bei einem Sortiervorgang werden die Elemente einer Sequenz auf Grundlage eines oder mehrerer Attribute sortiert. Mit dem ersten Sortierkriterium wird eine primäre Sortierung der Elemente ausgeführt. Sie können die Elemente innerhalb jeder primären Sortiergruppe sortieren, indem Sie ein zweites Sortierkriterium angeben.
In Beispiel unserer customer-Daten sortieren wir diese anhand der Wohnorte in absteigender Reihenfolge. Als zweites Sortierkriterium werden dann die Straßennamen herangezogen.
var queryLondonCustomers = from customer in customers
orderby customer.City, customer.Street descending
select customer;using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Student{
public string Name;
public int id;
public string Subject{get; set;}
public Student(){}
public Student(string name){
this.Name = name;
}
}
class Program {
public static void Main(string[] args){
List<Student> students = new List<Student>{
new Student("Max Müller"){Subject = "Technische Informatik", id = 1},
new Student("Maria Maier"){Subject = "Softwareentwicklung", id = 2},
new Student("Martin Morawschek"){Subject = "Höhere Mathematik I", id = 3},
new Student("Katja Schulz"){Subject = "Technische Informatik", id = 4},
new Student("Karl Tischer"){Subject = "Softwareentwicklung", id = 5},
};
var query = from s in students
orderby s.Subject descending
select s;
foreach (var student in query){
Console.WriteLine("{0,-22} - {1}", student.Subject, student.Name);
}
}
}
}@Rextester.eval(@CSharp)
Die select-Klausel generiert aus den Ergebnissen der Abfrage das Resultat und definiert damit das Format jedes zurückgegebenen Elements. Dies kann
- den vollständigen Datensatz umfassen,
- lediglich eine Teilmenge der Member oder
- einen völlig neuen Datentypen.
Wenn die select-Klausel etwas anderes als eine Kopie des Quellelements erzeugt, wird dieser Vorgang als Projektion bezeichnet.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace Rextester
{
class Student{
public string Name;
public int id;
public string Subject{get; set;}
public Student(){}
public Student(string name){
this.Name = name;
}
}
class Program {
public static void Main(string[] args){
List<Student> students = new List<Student>{
new Student("Max Müller"){Subject = "Technische Informatik", id = 1},
new Student("Maria Maier"){Subject = "Softwareentwicklung", id = 2},
new Student("Martin Morawschek"){Subject = "Höhere Mathematik I", id = 3},
new Student("Katja Schulz"){Subject = "Technische Informatik", id = 4},
new Student("Karl Tischer"){Subject = "Softwareentwicklung", id = 5},
};
var query = from s in students
select new {Surname = s.Name.Split(' ')[0]};
Console.WriteLine(query.GetType());
foreach (var student in query){
Console.WriteLine(student.Surname);
}
}
}
}@Rextester.eval(@CSharp)
Einen guten Überblick zu den Konzequenzen einer Projektion gibt die Webseite https://docs.microsoft.com/de-de/dotnet/csharp/programming-guide/concepts/linq/type-relationships-in-linq-query-operations
Für die Vereinigten Staaten liegen umfangreiche Datensätze zur Namensgebung von Neugeborenen seit 1880 vor. Eine entsprechende csv-Datei (comma separated file) findet sich im Projektordner und /data, sie umfasst 258.000 Einträge. Diese sind wie folgt gegliedert
1880,"John",0.081541,"boy"
1880,"William",0.080511,"boy"
1880,"James",0.050057,"boy"
Die erste Spalte gibt das Geburtsjahr, die zweite den Vornamen, die Dritte den Anteil der mit diesem Vornamen benannten Kinder und die vierte das Geschlecht an.
Der Datensatz steht zum Download unter https://osf.io/d2vyg/ bereit.
Lesen Sie aus den Daten die jeweils am häufigsten vergebenen Vornamen aus und bestimmen Sie deren Anteil innerhalb des Jahrganges.
https://github.com/liaScript/CsharpCourse/tree/master/code/25_LINQII
Referenzen
[DatenbankSchema] Wikipedia "SQL", Nils Boßung, https://de.wikipedia.org/wiki/SQL#/media/Datei:SQL-Beispiel.svg
[Mössenböck] Mössenböck, Hanspeter, "Kompaktkurs C#", dpunkt.verlag, 2019
Autoren
Sebastian Zug, André Dietrich
