Pisanie zapytań LINQ języka C# w celu wykonywania zapytań dotyczących danych

Większość zapytań w dokumentacji zintegrowanych zapytań języka wprowadzającego (LINQ) jest zapisywana przy użyciu składni zapytania deklaratywnego LINQ. Jednak składnia zapytania musi zostać przetłumaczona na wywołania metody dla środowiska uruchomieniowego języka wspólnego platformy .NET (CLR), gdy kod jest kompilowany. Te wywołania metody wywołują standardowe operatory zapytań, które mają nazwy, takie jak Where, , SelectGroupBy, Join, , Maxi Average. Można je wywoływać bezpośrednio przy użyciu składni metody zamiast składni zapytania.

Składnia zapytań i składnia metody są semantycznie identyczne, ale składnia zapytań jest często prostsza i łatwiejsza do odczytania. Niektóre zapytania muszą być wyrażone jako wywołania metody. Na przykład należy użyć wywołania metody, aby wyrazić zapytanie, które pobiera liczbę elementów pasujących do określonego warunku. Należy również użyć wywołania metody dla zapytania, które pobiera element, który ma maksymalną wartość w sekwencji źródłowej. Dokumentacja referencyjna standardowych operatorów zapytań w System.Linq przestrzeni nazw zwykle używa składni metody. Musisz zapoznać się ze sposobem używania składni metody w zapytaniach i samych wyrażeń zapytań.

Standardowe metody rozszerzenia operatora zapytania

W poniższym przykładzie pokazano proste wyrażenie zapytania i zapytanie semantycznie równoważne napisane jako zapytanie oparte na metodzie.

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

Dane wyjściowe z dwóch przykładów są identyczne. Widać, że typ zmiennej kwerendy jest taki sam w obu formularzach: IEnumerable<T>.

Aby zrozumieć zapytanie oparte na metodzie, przyjrzyjmy się mu dokładniej. Po prawej stronie wyrażenia zwróć uwagę, że where klauzula jest teraz wyrażona jako metoda wystąpienia obiektu numbers , która ma typ IEnumerable<int>. Jeśli znasz interfejs ogólny IEnumerable<T> , wiesz, że nie ma Where metody. Jeśli jednak wywołasz listę uzupełniania funkcji IntelliSense w środowisku IDE programu Visual Studio, zobaczysz nie tylko metodęWhere, ale wiele innych metod, takich jak Select, , SelectManyJoini Orderby. Te metody implementują standardowe operatory zapytań.

Zrzut ekranu przedstawiający wszystkie standardowe operatory zapytań w funkcji IntelliSense.

Chociaż wygląda na to, że IEnumerable<T> zawiera więcej metod, nie. Standardowe operatory zapytań są implementowane jako metody rozszerzenia. Metody rozszerzeń "rozszerzają" istniejący typ; mogą być wywoływane tak, jakby były metodami wystąpienia w typie. Standardowe operatory zapytań rozszerzają i IEnumerable<T> dlatego można napisać .numbers.Where(...)

Aby użyć metod rozszerzeń, należy wprowadzić je do zakresu z dyrektywami using . Z punktu widzenia aplikacji metoda rozszerzenia i zwykła metoda wystąpienia są takie same.

Aby uzyskać więcej informacji na temat metod rozszerzeń, zobacz Metody rozszerzeń. Aby uzyskać więcej informacji na temat standardowych operatorów zapytań, zobacz Omówienie standardowych operatorów zapytań (C#). Niektórzy dostawcy LINQ, tacy jak Entity Framework i LINQ to XML, implementują własne standardowe operatory zapytań i metody rozszerzeń dla innych typów oprócz IEnumerable<T>.

Wyrażenia lambda

W poprzednim przykładzie zwróć uwagę, że wyrażenie warunkowe (num % 2 == 0) jest przekazywane jako argument wbudowany do Enumerable.Where metody: Where(num => num % 2 == 0). to wyrażenie wbudowane jest wyrażeniem lambda. Jest to wygodny sposób pisania kodu, który w przeciwnym razie musiałby zostać napisany w bardziej kłopotliwej formie. Po num lewej stronie operatora jest zmienna wejściowa, która odpowiada num w wyrażeniu zapytania. Kompilator może wywnioskować typ, num ponieważ wie, że numbers jest to typ ogólny IEnumerable<T> . Treść wyrażenia lambda jest taka sama jak wyrażenie w składni zapytania lub w dowolnym innym wyrażeniu lub instrukcji języka C#. Może zawierać wywołania metod i inną złożoną logikę. Wartość zwracana jest tylko wynikiem wyrażenia. Niektóre zapytania można wyrazić tylko w składni metody, a niektóre z nich wymagają wyrażeń lambda. Wyrażenia lambda to zaawansowane i elastyczne narzędzie w przyborniku LINQ.

Komponowanie zapytań

W poprzednim przykładzie Enumerable.OrderBy kodu metoda jest wywoływana przy użyciu operatora kropki w wywołaniu metody .Where Where tworzy filtrowaną sekwencję, a następnie Orderby sortuje sekwencję utworzoną przez Whereprogram . Ponieważ zapytania zwracają element IEnumerable, należy utworzyć je w składni metody, łącząc wywołania metody. Kompilator wykonuje tę kompozycję podczas pisania zapytań przy użyciu składni zapytania. Ponieważ zmienna kwerendy nie przechowuje wyników zapytania, można ją zmodyfikować lub użyć jako podstawy dla nowego zapytania w dowolnym momencie, nawet po jego wykonaniu.

W poniższych przykładach przedstawiono kilka prostych zapytań LINQ przy użyciu każdego z wymienionych wcześniej metod.

Uwaga

Te zapytania działają na prostych kolekcjach w pamięci; Jednak podstawowa składnia jest identyczna z składnią używaną w linQ to Entities i LINQ to XML.

Przykład — składnia zapytania

Większość zapytań pisze się przy użyciu składni zapytań w celu utworzenia wyrażeń zapytania. W poniższym przykładzie przedstawiono trzy wyrażenia zapytania. Pierwsze wyrażenie zapytania pokazuje, jak filtrować lub ograniczać wyniki, stosując warunki z klauzulą where . Zwraca wszystkie elementy w sekwencji źródłowej, których wartości są większe niż 7 lub mniejsze niż 3. Drugie wyrażenie pokazuje, jak uporządkować zwrócone wyniki. Trzecie wyrażenie pokazuje, jak grupować wyniki zgodnie z kluczem. To zapytanie zwraca dwie grupy na podstawie pierwszej litery słowa.

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

Typ zapytań to IEnumerable<T>. Wszystkie te zapytania można napisać przy użyciu, var jak pokazano w poniższym przykładzie:

var query = from num in numbers...

W każdym poprzednim przykładzie zapytania nie są wykonywane do momentu iteracji zmiennej zapytania w foreach instrukcji lub innej instrukcji.

Przykład — składnia metody

Niektóre operacje zapytań muszą być wyrażone jako wywołanie metody. Najbardziej typowe metody to metody, które zwracają pojedyncze wartości liczbowe, takie jak Sum, Max, Min, Averagei tak dalej. Te metody muszą być zawsze wywoływane jako ostatnie w dowolnym zapytaniu, ponieważ zwracają jedną wartość i nie mogą służyć jako źródło dodatkowej operacji zapytania. W poniższym przykładzie pokazano wywołanie metody w wyrażeniu zapytania:

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Jeśli metoda ma System.Action parametry lub System.Func<TResult> , te argumenty są podane w postaci wyrażenia lambda, jak pokazano w poniższym przykładzie:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

W poprzednich zapytaniach zapytanie #4 jest wykonywane natychmiast, ponieważ zwraca jedną wartość, a nie kolekcję ogólną IEnumerable<T> . Sama metoda używa foreach lub podobnego kodu, aby obliczyć jego wartość.

Każde z poprzednich zapytań można napisać przy użyciu niejawnego pisania z " var", jak pokazano w poniższym przykładzie:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Przykład — mieszana składnia zapytania i metody

W tym przykładzie pokazano, jak używać składni metody w wynikach klauzuli zapytania. Wystarczy ująć wyrażenie zapytania w nawiasy, a następnie zastosować operator kropki i wywołać metodę . W poniższym przykładzie zapytanie #7 zwraca liczbę liczb, których wartość wynosi od 3 do 7. Ogólnie rzecz biorąc, lepiej jednak użyć drugiej zmiennej do przechowywania wyniku wywołania metody. W ten sposób zapytanie jest mniej prawdopodobne, aby było mylone z wynikami zapytania.

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Ponieważ zapytanie nr 7 zwraca pojedynczą wartość, a nie kolekcję, zapytanie jest wykonywane natychmiast.

Poprzednie zapytanie można napisać przy użyciu niejawnego pisania za varpomocą polecenia , w następujący sposób:

var numCount = (from num in numbers...

Można go napisać w składni metody w następujący sposób:

var numCount = numbers.Count(n => n is > 3 and < 7);

Można go napisać przy użyciu jawnego pisania w następujący sposób:

int numCount = numbers.Count(n => n is > 3 and < 7);

Dynamiczne określanie filtrów predykatu w czasie wykonywania

W niektórych przypadkach nie wiadomo, ile predykatów należy zastosować do elementów źródłowych w klauzuli where . Jednym ze sposobów dynamicznego określania wielu filtrów predykatu jest użycie Contains metody , jak pokazano w poniższym przykładzie. Zapytanie zwraca różne wyniki na podstawie wartości id po wykonaniu zapytania.

int[] ids = [111, 114, 112];

var queryNames =
    from student in students
    where ids.Contains(student.ID)
    select new
    {
        student.LastName,
        student.ID
    };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [122, 117, 120, 115];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

Możesz użyć instrukcji przepływu sterowania, takich jak if... else lub switch, aby wybrać spośród wstępnie określonych zapytań alternatywnych. W poniższym przykładzie użyto innej where klauzuli, studentQuery jeśli wartość oddYear czasu wykonywania to true lub false.

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

Obsługa wartości null w wyrażeniach zapytań

W tym przykładzie pokazano, jak obsługiwać możliwe wartości null w kolekcjach źródłowych. Kolekcja obiektów, taka jak obiekt IEnumerable<T> , może zawierać elementy, których wartość ma wartość null. Jeśli kolekcja źródłowa jest null lub zawiera element, którego wartość to null, a zapytanie nie obsługuje null wartości, NullReferenceException jest zgłaszany podczas wykonywania zapytania.

Możesz kodować defensywnie, aby uniknąć wyjątku odwołania o wartości null, jak pokazano w poniższym przykładzie:

var query1 =
    from c in categories
    where c != null
    join p in products on c.ID equals p?.CategoryID
    select new
    {
        Category = c.Name,
        Name = p.Name
    };

W poprzednim przykładzie klauzula where filtruje wszystkie elementy o wartości null w sekwencji kategorii. Ta technika jest niezależna od sprawdzania wartości null w klauzuli join. Wyrażenie warunkowe o wartości null w tym przykładzie działa, ponieważ Products.CategoryID jest typu int?, który jest skrótem dla .Nullable<int>

W klauzuli sprzężenia, jeśli tylko jeden z kluczy porównania jest typem wartości dopuszczanej do wartości null, można rzutować drugi na typ wartości dopuszczalnej wartości w wyrażeniu zapytania. W poniższym przykładzie przyjęto założenie, że EmployeeID jest to kolumna zawierająca wartości typu int?:

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

W każdym z przykładów equals jest używane słowo kluczowe zapytania. Można również użyć dopasowywania wzorców, w tym wzorców dla is null i is not null. Te wzorce nie są zalecane w zapytaniach LINQ, ponieważ dostawcy zapytań mogą nie poprawnie interpretować nowej składni języka C#. Dostawca zapytań to biblioteka, która tłumaczy wyrażenia zapytań języka C# na natywny format danych, taki jak Entity Framework Core. Dostawcy zapytań implementują interfejs w System.Linq.IQueryProvider celu utworzenia System.Linq.IQueryable<T> źródeł danych, które implementują interfejs.

Obsługa wyjątków w wyrażeniach zapytań

Można wywołać dowolną metodę w kontekście wyrażenia zapytania. Nie należy wywoływać żadnej metody w wyrażeniu zapytania, które może utworzyć efekt uboczny, taki jak modyfikowanie zawartości źródła danych lub zgłaszanie wyjątku. W tym przykładzie pokazano, jak uniknąć zgłaszania wyjątków podczas wywoływania metod w wyrażeniu zapytania bez naruszania ogólnych wytycznych platformy .NET dotyczących obsługi wyjątków. Wytyczne te stanowią, że dopuszczalne jest przechwycenie określonego wyjątku, gdy rozumiesz, dlaczego jest zgłaszany w danym kontekście. Aby uzyskać więcej informacji, zobacz Najlepsze rozwiązania dotyczące wyjątków.

W ostatnim przykładzie pokazano, jak obsługiwać te przypadki, gdy podczas wykonywania zapytania należy zgłosić wyjątek.

W poniższym przykładzie pokazano, jak przenieść kod obsługi wyjątków poza wyrażeniem zapytania. Ta refaktoryzacja jest możliwa tylko wtedy, gdy metoda nie zależy od żadnych zmiennych lokalnych dla zapytania. Łatwiej jest radzić sobie z wyjątkami poza wyrażeniem zapytania.

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

W bloku w poprzednim przykładzie catch (InvalidOperationException) obsłuż (lub nie obsługujej) wyjątek w sposób odpowiedni dla aplikacji.

W niektórych przypadkach najlepszą odpowiedzią na wyjątek zgłaszany w zapytaniu może być natychmiastowe zatrzymanie wykonywania zapytania. W poniższym przykładzie pokazano, jak obsługiwać wyjątki, które mogą być zgłaszane w treści zapytania. Załóżmy, że SomeMethodThatMightThrow potencjalnie może spowodować wyjątek, który wymaga zatrzymania wykonywania zapytania.

Blok try otacza pętlę foreach , a nie samą kwerendę. Pętla foreach to punkt, w którym jest wykonywane zapytanie. Wyjątki czasu wykonywania są zgłaszane podczas wykonywania zapytania. W związku z tym muszą być obsługiwane w foreach pętli.

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        @"C:\newFolder\" + s;

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery =
    from file in files
    let n = SomeMethodThatMightThrow(file)
    select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

Pamiętaj, aby przechwycić dowolny wyjątek, którego oczekujesz, aby zgłosić i/lub wykonać wszelkie niezbędne czyszczenie w finally bloku.

Zobacz też