Skip to content

Latest commit

 

History

History
720 lines (564 loc) · 23.8 KB

File metadata and controls

720 lines (564 loc) · 23.8 KB

Vorlesung Softwareentwicklung - 25 - Language-Integrated Query (LINQ)


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.


Kontrollfragen

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?


LINQ

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.

OOPGeschichte LINQEbenen

Grundlagen

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)

Hinter den Kulissen

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)

Filtern

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.

Gruppieren

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)

Sortieren

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)

Ausgaben

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

Anwendungsbeispiel

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

Anhang

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