Grundlagen zu Abfrageausdrücken

In diesem Artikel werden die grundlegenden Konzepte für Abfrageausdrücke in C# vorgestellt.

Was ist eine Abfrage, und welche Funktion hat sie?

Eine Abfrage ist ein Satz von Anweisungen, der beschreibt, welche Daten aus einer bestimmten Datenquelle (oder Quellen) abgerufen werden sollen, und welche Form und Organisation die zurückgegebenen Daten haben sollen. Eine Abfrage unterscheidet sich von den Ergebnissen, die sie erzeugt.

Im Allgemeinen werden die Quelldaten logisch als Sequenz von Elementen der gleichen Art organisiert. Eine SQL-Datenbanktabelle enthält z. B. eine Sequenz von Zeilen. In einer XML-Datei gibt es eine „Sequenz“ von XML-Elementen (auch wenn XML-Elemente hierarchisch in einer Baumstruktur organisiert sind). Eine Auflistung im Arbeitsspeicher enthält eine Sequenz von Objekten.

Aus Sicht einer Anwendung sind der spezifische Typ und die Struktur der ursprünglichen Datenquelle nicht wichtig. Die Anwendung sieht die Quelldaten immer als eine IEnumerable<T>- oder IQueryable<T>-Auflistung an. In LINQ to XML werden die Quelldaten z. B. als IEnumerable<XElement> sichtbar gemacht.

Wenn diese Quellsequenz vorliegt, kann eine Abfrage eine der folgenden drei Aktionen durchführen:

  • Abrufen einer Teilmenge der Elemente zum Erstellen einer neuen Sequenz ohne die einzelnen Elemente zu verändern. Die Abfrage kann die zurückgegebenen Sequenzen dann auf verschiedene Arten sortieren oder gruppieren, wie im folgenden Beispiel gezeigt wird (Annahme: scores ist int[]):

    IEnumerable<int> highScoresQuery =
        from score in scores
        where score > 80
        orderby score descending
        select score;
    
  • Abrufen einer Sequenz von Elementen wie im vorherigen Beispiel, aber mit Transformation der Elemente in einen neuen Objekttyp. Eine Abfrage kann z. B. nur die Nachnamen aus bestimmten Kundendatensätzen in einer Datenquelle abrufen. Sie kann möglicherweise auch den vollständigen Datensatz abrufen und ihn zum Erstellen eines anderen Objekttyps im Arbeitsspeicher oder sogar von XML-Daten vor dem Generieren der endgültigen Ergebnissequenz verwenden. Im folgenden Beispiel wird eine Projektion von int in string veranschaulicht. Beachten Sie den neuen Typ von highScoresQuery.

    IEnumerable<string> highScoresQuery2 =
        from score in scores
        where score > 80
        orderby score descending
        select $"The score is {score}";
    
  • Abrufen eines Singleton-Werts zu den Quelldaten, z.B.:

    • Die Anzahl der Elemente, die eine bestimmte Bedingung erfüllen

    • Das Element, das den größten oder den niedrigsten Wert hat

    • Das erste Element, das einer Bedingung entspricht oder die Summe bestimmter Werte in einer angegebenen Menge von Elementen Die folgende Abfrage gibt z.B. die Anzahl von Ergebnissen aus dem scores-Ganzzahlarray zurück, die höher als 80 sind:

      var highScoreCount = (
          from score in scores
          where score > 80
          select score
      ).Count();
      

      Beachten Sie im vorherigen Beispiel die Verwendung von Klammern um den Abfrageausdruck vor dem Aufruf der Enumerable.Count-Methode. Sie können auch eine neue Variable verwenden, um das konkrete Ergebnis zu speichern.

      IEnumerable<int> highScoresQuery3 =
          from score in scores
          where score > 80
          select score;
      
      var scoreCount = highScoresQuery3.Count();
      

Im vorherigen Beispiel wird die Abfrage im Aufruf von Count ausgeführt, da Count die Ergebnisse durchlaufen muss, um die Anzahl der von highScoresQuery zurückgegebenen Elemente zu bestimmen.

Was ist ein Abfrageausdruck?

Ein Abfrageausdruck ist eine in der Abfragesyntax ausgedrückte Abfrage. Ein Abfrageausdruck ist ein erstklassiges Sprachkonstrukt. Sie verhält sich wie jeder andere Ausdruck und kann in jedem Kontext verwendet werden, in dem ein C#-Ausdruck gültig ist. Ein Abfrageausdruck besteht aus einem Satz von in einer deklarativen Syntax geschriebenen Klauseln, ähnlich wie SQL oder XQuery. Jede Klausel umfasst wiederum einen oder mehrere C#-Ausdrücke. Diese Ausdrücke sind möglicherweise selbst Abfrageausdrücke oder enthalten einen Abfrageausdruck.

Ein Abfrageausdruck muss mit einer from-Klausel beginnen und mit einer select- oder group-Klausel enden. Zwischen der ersten from-Klausel und der letzten select- oder group-Klausel kann ein Abfrageausdruck eine oder mehrere der folgenden optionalen Klauseln enthalten: where, orderby, join, let und sogar zusätzliche from-Klauseln. Sie können auch das Schlüsselwort into verwenden, um zuzulassen, das das Ergebnis einer join- oder group-Klausel als Quelle für weitere Abfrageklauseln im selben Abfrageausdruck dient.

Abfragevariable

In LINQ ist eine Abfragevariable eine beliebige Variable, die eine Abfrage anstatt des Ergebnisses einer Abfrage speichert. Genauer gesagt ist eine Abfragevariable immer ein aufzählbarer Typ, der eine Sequenz von Elementen erzeugt, wenn er in einer foreach-Anweisung oder einem direkten Aufruf der IEnumerator.MoveNext()-Methode durchlaufen wird.

Hinweis

In den Beispielen in diesem Artikel werden die folgende Datenquelle und Beispieldaten verwendet.

record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000),
    new City("Mumbai", 20_412_000),
    new City("Beijing", 20_384_000),
    new City("Cairo", 18_772_000),
    new City("Dhaka", 17_598_000),
    new City("Osaka", 19_281_000),
    new City("New York-Newark", 18_604_000),
    new City("Karachi", 16_094_000),
    new City("Chongqing", 15_872_000),
    new City("Istanbul", 15_029_000),
    new City("Buenos Aires", 15_024_000),
    new City("Kolkata", 14_850_000),
    new City("Lagos", 14_368_000),
    new City("Kinshasa", 14_342_000),
    new City("Manila", 13_923_000),
    new City("Rio de Janeiro", 13_374_000),
    new City("Tianjin", 13_215_000)
];

static readonly Country[] countries = [
    new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
    new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
    new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
    new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
    new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
    new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
    new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
    new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];

Das folgende Codebeispiel zeigt einen einfachen Abfrageausdruck mit einer Datenquelle, einer Filtering-Klausel, einer Ordering-Klausel und ohne Transformationen der Quellelemente. Die Klausel select beendet die Abfrage.

// Data source.
int[] scores = [90, 71, 82, 93, 75, 82];

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (var testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

// Output: 93 90 82 82

Im vorherigen Beispiel ist scoreQuery eine Abfragevariable, die manchmal auch einfach als Abfrage bezeichnet wird. Die Abfragevariable speichert keine tatsächlichen Ergebnisdaten, die in der foreach-Schleife erzeugt werden. Wenn die foreach-Anweisung ausgeführt wird, werden die Ergebnisse der Abfrage nicht über die Abfragevariable scoreQuery zurückgegeben. Stattdessen werden sie über die Iterationsvariable testScore zurückgegeben. Die scoreQuery-Variable kann in einer zweiten foreach-Schleife durchlaufen werden. Die gleichen Ergebnisse werden erzeugt, solange weder die Variable noch die Datenquelle geändert wurde.

Eine Abfragevariable kann eine Abfrage speichern, die in einer Abfragesyntax oder Methodensyntax oder einer Kombination aus beiden ausgedrückt wird. In den folgenden Beispielen sind sowohl queryMajorCities als auch queryMajorCities2 Abfragevariablen:

City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000)
];

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;

// Execute the query to produce the results
foreach (City city in queryMajorCities)
{
    Console.WriteLine(city);
}

// Output:
// City { Population = 120000 }
// City { Population = 112000 }
// City { Population = 150340 }

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

Andererseits zeigen die beiden nächsten Beispiele Variablen, die keine Abfragevariablen sind, obwohl beide mit einer Abfrage initialisiert werden. Sie sind keine Abfragevariablen, da sie Ergebnisse speichern:

var highestScore = (
    from score in scores
    select score
).Max();

// or split the expression
IEnumerable<int> scoreQuery =
    from score in scores
    select score;

var highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
var largeCitiesList = (
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city
).ToList();

// or split the expression
IEnumerable<City> largeCitiesQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;
var largeCitiesList2 = largeCitiesQuery.ToList();

Explizite und implizite Typisierung von Abfragevariablen

Diese Dokumentation enthält normalerweise den expliziten Typ der Abfragevariablen, um die Typbeziehung zwischen der Abfrage und der select-Klausel darzustellen. Sie können aber auch das Schlüsselwort var verwenden, um den Compiler anzuweisen, den Typ einer Abfragevariable (oder eine andere lokale Variable) zur Kompilierzeit abzuleiten. Das Beispiel einer Abfrage, das vorher in diesem Artikel gezeigt wurde, kann beispielsweise auch durch implizierte Typisierung ausgedrückt werden:

var queryCities =
    from city in cities
    where city.Population > 100000
    select city;

Im vorherigen Beispiel ist die Verwendung von „var“ optional. queryCities ist mit impliziter oder expliziter Typisierung IEnumerable<City>.

Starten eines Abfrageausdrucks

Ein Abfrageausdruck muss mit einer from-Klausel beginnen. Er gibt eine Datenquelle zusammen mit einer Bereichsvariablen an. Die Bereichsvariable stellt jedes darauffolgende Element in der Quellsequenz dar, wenn das Quellelement durchsucht wird. Die Bereichsvariable ist, basierend auf den Typen des Elements in der Datenquelle, stark typisiert. Im folgenden Beispiel ist die Bereichsvariable auch als Country typisiert, da countries ein Array von Country-Objekten ist. Da die Bereichsvariable stark typisiert ist, können Sie den Punktoperator verwenden, um auf verfügbare Member des Typs zuzugreifen.

IEnumerable<Country> countryAreaQuery =
    from country in countries
    where country.Area > 500000 //sq km
    select country;

Die Bereichsvariable befindet sich im Geltungsbereich, bis die Abfrage entweder mit einem Semikolon oder einer continuation-Klausel beendet wird.

Ein Abfrageausdruck enthält möglicherweise mehrere from-Klauseln. Verwenden Sie weitere from-Klauseln, wenn jedes Element in der Quellsequenz selbst eine Auflistung ist oder eine Auflistung enthält. Nehmen wir beispielsweise an, dass Sie über eine Auflistung von Country-Objekten verfügen, von der jedes eine Auflistung von City-Objekten mit dem Namen Cities enthält. Verwenden Sie zwei from-Klauseln, um die City-Objekte in jedem Country abzufragen, wie hier gezeigt:

IEnumerable<City> cityQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

Weitere Informationen finden Sie unter from-Klausel.

Beenden eines Abfrageausdrucks

Ein Abfrageausdruck muss entweder mit einer group- oder einer select-Klausel enden.

group-Klausel

Verwenden Sie die group-Klausel, um eine Sequenz von Gruppen zu erzeugen, die von einem von Ihnen angegebenen Schüssel organisiert wird. Der Schlüssel kann ein beliebiger Datentyp sein. Die folgende Abfrage erstellt z. B. eine Sequenz von Gruppen, die ein oder mehrere Country-Objekte enthalten und deren Schlüssel vom Typ char ist und als Wert den ersten Buchstaben von Ländern enthält.

var queryCountryGroups =
    from country in countries
    group country by country.Name[0];

Weitere Informationen zum Gruppieren finden Sie unter group-Klausel.

select-Klausel

Verwenden Sie die select-Klausel, um alle anderen Typen von Sequenzen zu erzeugen. Eine einfache select-Klausel erzeugt nur eine Sequenz von Objekten desselben Typs wie die Objekte, die in der Datenquelle enthalten sind. In diesem Beispiel enthält die Datenquelle Country-Objekte. Die orderby-Klausel sortiert die Elemente in eine neue Reihenfolge, und die select-Klausel erzeugt eine Sequenz der neu angeordneten Country-Objekte.

IEnumerable<Country> sortedQuery =
    from country in countries
    orderby country.Area
    select country;

Die select-Klausel kann zum Transformieren von Quelldaten in Sequenzen neuer Typen verwendet werden. Diese Transformation wird auch als Projektion bezeichnet. Im folgenden Beispiel projiziert die select-Klausel eine Sequenz anonymer Typen, die nur eine Teilmenge der Felder im ursprünglichen Element enthält. Die neuen Objekte werden mit einem Objektinitialisierer initialisiert.

var queryNameAndPop =
    from country in countries
    select new
    {
        Name = country.Name,
        Pop = country.Population
    };

In diesem Beispiel ist var erforderlich, da die Abfrage einen anonymen Typ erzeugt.

Weitere Informationen zu allen Verfahren, in denen eine select-Klausel zum Transformieren von Daten verwendet werden kann, finden Sie unter select-Klausel.

Fortsetzungen mit into

Sie können das Schlüsselwort into in einer select- oder group-Klausel verwenden, um einen temporären Bezeichner zu erstellen, der eine Abfrage speichert. Verwenden Sie die into-Klausel, wenn Sie zusätzliche Abfragevorgänge nach einem Gruppierungs- oder Auswahlvorgang für eine Abfrage ausführen müssen. Im folgenden Beispiel werden countries gemäß der Bevölkerung in Bereiche von 10 Millionen gruppiert. Nachdem diese Gruppen erstellt wurden, filtern weitere Klauseln einige Gruppen heraus und sortieren die Gruppen dann in aufsteigender Reihenfolge. Um diese zusätzlichen Vorgänge durchzuführen, wird die von countryGroup dargestellte Fortsetzung benötigt.

// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
    from country in countries
    let percentile = (int)country.Population / 10_000_000
    group country by percentile into countryGroup
    where countryGroup.Key >= 20
    orderby countryGroup.Key
    select countryGroup;

// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
    Console.WriteLine(grouping.Key);
    foreach (var country in grouping)
    {
        Console.WriteLine(country.Name + ":" + country.Population);
    }
}

Weitere Informationen finden Sie unter into.

Filtern, Sortieren und Verknüpfen

Zwischen der from-Klausel am Anfang und der select- oder group-Klausel am Ende sind alle anderen Klauseln (where, join, orderby, from, let) optional. Eine der optionalen Klauseln kann entweder überhaupt nicht oder mehrfach in einem Abfragetext verwendet werden.

where-Klausel

Verwenden Sie die Klausel where zum Herausfiltern von Elementen aus den Quelldaten basierend auf einem oder mehreren Prädikatausdrücken. Im folgenden Beispiel verfügt die Klausel where über ein Prädikat mit zwei Bedingungen.

IEnumerable<City> queryCityPop =
    from city in cities
    where city.Population is < 200000 and > 100000
    select city;

Weitere Informationen finden Sie unter where-Klausel.

orderby-Klausel

Verwenden Sie die orderby-Klausel zum Sortieren der Ergebnisse in auf- oder absteigender Reihenfolge. Sie können auch eine sekundäre Sortierreihenfolge angeben. Im folgenden Beispiel wird mit der Eigenschaft Area eine primäre Sortierung der country-Objekte durchgeführt. Anschließend wird eine sekundäre Sortierung mit der Eigenschaft Population durchgeführt.

IEnumerable<Country> querySortedCountries =
    from country in countries
    orderby country.Area, country.Population descending
    select country;

Das Schlüsselwort ascending ist optional. Wenn keine andere Reihenfolge angegeben ist, ist dies die Standardreihenfolge. Weitere Informationen finden Sie unter orderby-Klausel.

join-Klausel

Verwenden Sie die join-Klausel zum Zuordnen und/oder Kombinieren von Elementen aus einer Datenquelle mit Elementen aus einer anderen Datenquelle basierend auf einem Gleichheitsvergleich zwischen angegebenen Schlüsseln in jedem Element. In LINQ werden Verknüpfungsvorgänge für Sequenzen von Objekten ausgeführt, deren Elemente unterschiedliche Typen haben. Nachdem Sie zwei Sequenzen verknüpft haben, müssen Sie eine select- oder group-Anweisung verwenden, um anzugeben, welches Element in der Ausgabesequenz gespeichert werden soll. Sie können auch einen anonymen Typ verwenden, um Eigenschaften aus jedem Satz der zugewiesenen Elemente in einem neuen Typ für die Ausgabesequenz zu kombinieren. Im folgenden Beispiel werden prod-Objekte, deren Category-Eigenschaft einer der Kategorien im categories-Zeichenfolgearray entspricht, zugewiesen. Produkte, deren Category-Element keiner Zeichenfolge in categories entspricht, werden herausgefiltert. Die select-Anweisung projiziert einen neuen Typ, dessen Eigenschaften aus cat und prod stammen.

var categoryQuery =
    from cat in categories
    join prod in products on cat equals prod.Category
    select new
    {
        Category = cat,
        Name = prod.Name
    };

Sie können auch eine Gruppenverknüpfung durchführen, indem Sie die Ergebnisse des join-Vorgangs mithilfe des Schlüsselworts into in eine temporäre Variable speichern. Weitere Informationen finden Sie unter join-Klausel.

let-Klausel

Verwenden Sie die let-Klausel zum Speichern der Ergebnisse eines Ausdrucks, z.B. eines Methodenaufrufs, in einer neuen Bereichsvariable. Im folgenden Beispiel speichert die Bereichsvariable firstName das erste Elemente eines Arrays von Zeichenfolgen, das von Split zurückgegeben wird.

string[] names = ["Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia"];
IEnumerable<string> queryFirstNames =
    from name in names
    let firstName = name.Split(' ')[0]
    select firstName;

foreach (var s in queryFirstNames)
{
    Console.Write(s + " ");
}

//Output: Svetlana Claire Sven Cesar

Weitere Informationen finden Sie unter let-Klausel.

Unterabfragen in einem Abfrageausdruck

Eine Abfrageklausel kann selbst einen Abfrageausdruck enthalten, der manchmal als Unterabfrage bezeichnet wird. Jede Unterabfrage beginnt mit ihrer eigenen from-Klausel, die nicht unbedingt auf die gleiche Datenquelle in der ersten from-Klausel verweist. Die folgende Abfrage zeigt z.B. einen Abfrageausdruck, der in der Select-Anweisung zum Abrufen der Ergebnisse eines Gruppierungsvorganges verwendet wird.

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.ExamScores.Average()
        ).Max()
    };

Weitere Informationen finden Sie unter Ausführen einer Unterabfrage für einen Gruppierungsvorgang.

Siehe auch