Skriva C#LINQ-frågor för att fråga efter data

De flesta frågor i linq-dokumentationen (Introductory Language Integrated Query) skrivs med hjälp av linq-deklarativ frågesyntax. Frågesyntaxen måste dock översättas till metodanrop för .NET common language runtime (CLR) när koden kompileras. Dessa metodanrop anropar standardfrågeoperatorerna, som har namn som Where, Select, GroupBy, Join, Maxoch Average. Du kan anropa dem direkt med hjälp av metodsyntax i stället för frågesyntax.

Frågesyntax och metodsyntax är semantiskt identiska, men frågesyntaxen är ofta enklare och enklare att läsa. Vissa frågor måste uttryckas som metodanrop. Du måste till exempel använda ett metodanrop för att uttrycka en fråga som hämtar antalet element som matchar ett angivet villkor. Du måste också använda ett metodanrop för en fråga som hämtar elementet som har det maximala värdet i en källsekvens. Referensdokumentationen för standardfrågeoperatorerna i System.Linq namnområdet använder vanligtvis metodsyntax. Du bör bekanta dig med hur du använder metodsyntax i frågor och i själva frågeuttrycken.

Standardmetoder för frågeoperatortillägg

I följande exempel visas ett enkelt frågeuttryck och den semantiskt likvärdiga frågan som skrivits som en metodbaserad fråga.

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 + " ");
}

Utdata från de två exemplen är identiska. Du kan se att frågevariabelns typ är samma i båda formulären: IEnumerable<T>.

För att förstå den metodbaserade frågan ska vi undersöka den närmare. Till höger om uttrycket ser du att where satsen nu uttrycks som en instansmetod för numbers objektet, som har en typ av IEnumerable<int>. Om du är bekant med det allmänna IEnumerable<T> gränssnittet vet du att det inte har någon Where metod. Men om du anropar intelliSense-slutförandelistan i Visual Studio IDE ser du inte bara en Where metod, utan många andra metoder som Select, SelectMany, Joinoch Orderby. Dessa metoder implementerar standardfrågeoperatorerna.

Skärmbild som visar alla standardfrågeoperatorer i Intellisense.

Även om det ser ut som om IEnumerable<T> det innehåller fler metoder, så gör det inte det. Standardfrågeoperatorerna implementeras som tilläggsmetoder. Tilläggsmetoder "utökar" en befintlig typ. de kan anropas som om de vore instansmetoder för typen. Standardfrågeoperatorerna utökas IEnumerable<T> och därför kan du skriva numbers.Where(...).

Om du vill använda tilläggsmetoder tar du med dem i omfånget med using direktiv. Från programmets synvinkel är en tilläggsmetod och en vanlig instansmetod densamma.

Mer information om tilläggsmetoder finns i Tilläggsmetoder. Mer information om vanliga frågeoperatorer finns i Översikt över vanliga frågeoperatorer (C#). Vissa LINQ-leverantörer, till exempel Entity Framework och LINQ till XML, implementerar sina egna standardfrågeoperatorer IEnumerable<T>och tilläggsmetoder för andra typer förutom .

Lambda-uttryck

I föregående exempel ser du att villkorsuttrycket (num % 2 == 0) skickas som ett infogat argument till Enumerable.Where metoden: Where(num => num % 2 == 0). Det här infogade uttrycket är ett lambda-uttryck. Det är ett bekvämt sätt att skriva kod som annars skulle behöva skrivas i mer besvärlig form. Till num vänster om operatorn finns indatavariabeln som motsvarar num i frågeuttrycket. Kompilatorn kan härleda typen av num eftersom den vet att det numbers är en allmän IEnumerable<T> typ. Brödtexten i lambda är precis samma som uttrycket i frågesyntaxen eller i något annat C#-uttryck eller -uttryck. Den kan innehålla metodanrop och annan komplex logik. Returvärdet är bara uttrycksresultatet. Vissa frågor kan bara uttryckas i metodsyntax och vissa av dessa kräver lambda-uttryck. Lambda-uttryck är ett kraftfullt och flexibelt verktyg i din LINQ-verktygslåda.

Sammansättning av frågor

I föregående kodexempel Enumerable.OrderBy anropas metoden med hjälp av punktoperatorn i anropet till Where. Where skapar en filtrerad sekvens och sorterar sedan Orderby sekvensen som skapas av Where. Eftersom frågor returnerar en IEnumerableskriver du dem i metodsyntaxen genom att länka ihop metodanropen. Kompilatorn gör den här kompositionen när du skriver frågor med hjälp av frågesyntax. Eftersom en frågevariabel inte lagrar resultatet av frågan kan du ändra den eller använda den som grund för en ny fråga när som helst, även efter att du har kört den.

I följande exempel visas några enkla LINQ-frågor med hjälp av varje metod som angavs tidigare.

Kommentar

Dessa frågor fungerar på enkla minnesinterna samlingar. Den grundläggande syntaxen är dock identisk med den som används i LINQ till entiteter och LINQ till XML.

Exempel – frågesyntax

Du skriver de flesta frågor med frågesyntax för att skapa frågeuttryck. I följande exempel visas tre frågeuttryck. Det första frågeuttrycket visar hur du filtrerar eller begränsar resultat genom att tillämpa villkor med en where sats. Den returnerar alla element i källsekvensen vars värden är större än 7 eller mindre än 3. Det andra uttrycket visar hur du beställer de returnerade resultaten. Det tredje uttrycket visar hur du grupperar resultat enligt en nyckel. Den här frågan returnerar två grupper baserat på ordets första bokstav.

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

Typen av frågor är IEnumerable<T>. Alla dessa frågor kan skrivas med hjälp av var det som visas i följande exempel:

var query = from num in numbers...

I varje tidigare exempel körs frågorna inte förrän du itererar över frågevariabeln i en instruktion eller annan foreach instruktion.

Exempel – metodsyntax

Vissa frågeåtgärder måste uttryckas som ett metodanrop. De vanligaste metoderna är de metoder som returnerar numeriska singleton-värden, till exempel Sum, Max, Min, Averageoch så vidare. Dessa metoder måste alltid anropas sist i en fråga eftersom de returnerar ett enda värde och inte kan fungera som källa för en extra frågeåtgärd. I följande exempel visas ett metodanrop i ett frågeuttryck:

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

Om metoden har System.Action eller System.Func<TResult> parametrar tillhandahålls dessa argument i form av ett lambda-uttryck, som du ser i följande exempel:

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

I föregående frågor körs endast fråga nr 4 omedelbart eftersom den returnerar ett enda värde och inte en allmän IEnumerable<T> samling. Själva metoden använder foreach eller liknande kod för att beräkna dess värde.

Var och en av de tidigare frågorna kan skrivas med implicit inmatning med "var", som du ser i följande exempel:

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

Exempel – Syntax för blandade frågor och metoder

Det här exemplet visar hur du använder metodsyntax för resultatet av en frågesats. Omslut bara frågeuttrycket inom parenteser och tillämpa sedan punktoperatorn och anropa metoden. I följande exempel returnerar fråga 7 ett antal tal vars värde är mellan 3 och 7. I allmänhet är det dock bättre att använda en andra variabel för att lagra resultatet av metodanropet. På så sätt är det mindre troligt att frågan förväxlas med resultatet av frågan.

// 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();

Eftersom Fråga nr 7 returnerar ett enda värde och inte en samling körs frågan omedelbart.

Den tidigare frågan kan skrivas med implicit inmatning med var, enligt följande:

var numCount = (from num in numbers...

Den kan skrivas i metodsyntaxen på följande sätt:

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

Den kan skrivas med explicit inmatning enligt följande:

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

Ange predikatfilter dynamiskt vid körning

I vissa fall vet du inte förrän i körningen hur många predikat du måste tillämpa på källelement i where -satsen. Ett sätt att dynamiskt ange flera predikatfilter är att använda Contains metoden, som du ser i följande exempel. Frågan returnerar olika resultat baserat på värdet id för när frågan körs.

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

Du kan använda kontrollflödesinstruktioner, till exempel if... else eller switch, för att välja bland fördefinierade alternativa frågor. I följande exempel studentQuery använder en annan where sats om körningsvärdet oddYear för är true eller 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
 */

Hantera null-värden i frågeuttryck

Det här exemplet visar hur du hanterar möjliga null-värden i källsamlingar. En objektsamling, till exempel en IEnumerable<T> kan innehålla element vars värde är null. Om en källsamling är null eller innehåller ett element vars värde är null, och frågan inte hanterar null värden, utlöses en NullReferenceException när du kör frågan.

Du kan koda defensivt för att undvika ett null-referensfel enligt följande exempel:

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
    };

I föregående exempel where filtrerar satsen bort alla null-element i kategorisekvensen. Den här tekniken är oberoende av null-kontrollen i kopplingssatsen. Villkorsuttrycket med null i det här exemplet fungerar eftersom Products.CategoryID är av typen int?, vilket är en förkortning för Nullable<int>.

Om bara en av jämförelsenycklarna är en nullbar värdetyp i en kopplingssats kan du omvandla den andra till en nullbar värdetyp i frågeuttrycket. I följande exempel antar du att det EmployeeID är en kolumn som innehåller värden av typen 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 };

I vart och ett av exemplen används frågenyckelordet equals . Du kan också använda mönstermatchning, som innehåller mönster för is null och is not null. Dessa mönster rekommenderas inte i LINQ-frågor eftersom frågeprovidrar kanske inte tolkar den nya C#-syntaxen korrekt. En frågeprovider är ett bibliotek som översätter C#-frågeuttryck till ett inbyggt dataformat, till exempel Entity Framework Core. Frågeprovidrar implementerar System.Linq.IQueryProvider gränssnittet för att skapa datakällor som implementerar System.Linq.IQueryable<T> gränssnittet.

Hantera undantag i frågeuttryck

Det är möjligt att anropa valfri metod i kontexten för ett frågeuttryck. Anropa inte någon metod i ett frågeuttryck som kan skapa en bieffekt, till exempel att ändra innehållet i datakällan eller utlösa ett undantag. Det här exemplet visar hur du undviker att skapa undantag när du anropar metoder i ett frågeuttryck utan att bryta mot de allmänna .NET-riktlinjerna för undantagshantering. Dessa riktlinjer anger att det är acceptabelt att fånga ett specifikt undantag när du förstår varför det genereras i en viss kontext. Mer information finns i Metodtips för undantag.

Det sista exemplet visar hur du hanterar dessa fall när du måste utlösa ett undantag under körningen av en fråga.

I följande exempel visas hur du flyttar undantagshanteringskod utanför ett frågeuttryck. Den här refaktoreringen är bara möjlig när metoden inte är beroende av några variabler som är lokala för frågan. Det är enklare att hantera undantag utanför frågeuttrycket.

// 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());
    }
}

I blocket catch (InvalidOperationException) i föregående exempel hanterar du (eller hanterar inte) undantaget på det sätt som är lämpligt för ditt program.

I vissa fall kan det bästa svaret på ett undantag som genereras från en fråga vara att stoppa frågekörningen omedelbart. I följande exempel visas hur du hanterar undantag som kan genereras inifrån en frågetext. Anta att SomeMethodThatMightThrow det kan orsaka ett undantag som kräver att frågekörningen stoppas.

Blocket try omsluter loopen foreach och inte själva frågan. Loopen foreach är den punkt där frågan körs. Körningsfel utlöses när frågan körs. Därför måste de hanteras i loopen 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.
 */

Kom ihåg att fånga det undantag som du förväntar dig att skapa och/eller göra nödvändig rensning i ett finally block.

Se även