Udostępnij za pomocą


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) używa składni zapytania deklaratywnego LINQ. Kompilator języka C# tłumaczy składnię zapytania na wywołania metod. Te wywołania metod implementują standardowych operatorów zapytań. Mają takie nazwy jak Where, , Select, GroupByJoin, , Maxi Average. Wywołaj je 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. Należy wyrazić niektóre zapytania 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. Dowiedz się, jak używać składni metody w zapytaniach i w wyrażeniach 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. Typ zmiennej kwerendy jest taki sam w obu formularzach: IEnumerable<T>.

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. Członkowie rozszerzenia "rozszerzają" istniejący typ; mogą być wywoływani tak, jakby byli członkami typu. Standardowe operatory zapytań rozszerzają metodę IEnumerable<T>i dlatego można napisać polecenie numbers.Where(...). Wprowadzasz rozszerzenia do zakresu za pomocą dyrektyw using przed ich wywołaniem.

Aby uzyskać więcej informacji na temat członków rozszerzenia, zobacz członkowie rozszerzenia. 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 elementy członkowskie rozszerzeń dla innych typów oprócz IEnumerable<T>.

Wyrażenia lambda

W poprzednim przykładzie wyrażenie warunkowe (num % 2 == 0) jest przekazywane jako argument wbudowany do metody Enumerable.Where: 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 wynikiem wyrażenia. W składni metody można wyrazić tylko niektóre zapytania, a niektóre z tych zapytań wymagają wyrażeń lambda. Wyrażenia lambda to zaawansowane i elastyczne narzędzie w przyborniku LINQ.

Komponowanie zapytań

W poprzednim przykładzie kodu metoda Enumerable.OrderBy jest wywoływana za pomocą operatora kropki na wywołaniu 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 podstawowych zapytań LINQ przy użyciu każdego z wymienionych wcześniej metod.

Uwaga

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

Przykład — składnia zapytania

Pisanie większości zapytań przy użyciu składni zapytania w celu utworzenia wyrażeń zapytań. 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

Musisz wyrazić niektóre z operacji zapytań w formie wywołania metody. Najbardziej typowe metody to metody, które zwracają pojedyncze wartości liczbowe, takie jak Sum, Max, Min, Averagei tak dalej. Wywołaj te metody na końcu w dowolnym zapytaniu, ponieważ zwracają jedną wartość i nie mogą służyć jako źródło do dalszych 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 parametry System.Action lub System.Func<TResult>, podaj te argumenty 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ć, stosując niejawne typowanie z pomocą 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 zapytań i metod

W tym przykładzie pokazano, jak używać składni metody w wynikach klauzuli zapytania. Umieść wyrażenie zapytania w nawiasach, a następnie zastosuj operator kropki i wywołaj metodę. W poniższym przykładzie zapytanie #7 zwraca liczbę liczb, których wartość wynosi od 3 do 7.

// 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ć, używając niejawnego typowania z var, 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
 */

Uwaga

W tym przykładzie użyto następującego źródła danych i danych:

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)])
];

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 studentQuery klauzuli, where 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.

W poniższym przykładzie użyto następujących typów i statycznych tablic danych:

record Product(string Name, int CategoryID);
record Category(string Name, int ID);
static Category?[] categories =
[
    new ("brass", 1),
    null,
    new ("winds", 2),
    default,
    new ("percussion", 3)
];

static Product?[] products =
[
    new Product("Trumpet", 1),
    new Product("Trombone", 1),
    new Product("French Horn", 1),
    null,
    new Product("Clarinet", 2),
    new Product("Flute", 2),
    null,
    new Product("Cymbal", 3),
    new Product("Drum", 3)
];

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ń

Dowolną metodę można wywołać 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. Refaktoryzację można przeprowadzić tylko wtedy, gdy metoda nie zależy od żadnych zmiennych lokalnych 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 catch (InvalidOperationException) w poprzednim przykładzie obsłuż (lub nie obsługuj) wyjątek w sposób odpowiedni dla aplikacji.

W niektórych przypadkach najlepszą odpowiedzią na wyjątek zgłaszany z poziomu zapytania 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 obsłuż je w pętli foreach.

// 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.
 */

Przechwyć dowolny wyjątek, który ma zgłosić, i wykonaj wszelkie niezbędne oczyszczanie w finally bloku.

Zobacz też