Модул 8

LINQ (Language-Integrated Query)

LINQ превръща обработката на колекции в едноредови заявки — Where за филтриране, Select за трансформация, OrderBy за сортиране. Плюс ламбда изразите (=>) и отложеното изпълнение.

Теория

LINQ (Language-Integrated Query) е функционалност на C#, която позволява да филтрираме, сортираме и търсим в колекции (масиви, списъци) с много малко код — подобно на заявки към база данни, но направо в езика. Изисква using System.Linq;.

LINQ разчита на ламбда изрази: x => x > 5 се чете "вземи елемент x, такъв че x да е по-голямо от 5". Лявата страна на => е параметърът (текущият елемент), дясната — изразът, който се изчислява за него. Ламбдата е малка анонимна функция, която подаваме на LINQ метода.

Методите се навързват във верига (method chaining): products.Where(...).OrderBy(...).Select(...) — резултатът от единия е вход за следващия. Така сложна обработка се чете отгоре надолу като изречение.

Отложено изпълнение (Deferred Execution): повечето LINQ заявки не се изпълняват на момента, а чак когато обходим резултата (с foreach) или извикаме .ToList() / .ToArray(). Заявката е "рецепта", не "готово ястие" — изпълнява се при поискване.

Златни правила

Какво е LINQ?

Функционалност на C#, която ви позволява да филтрирате, сортирате и търсите в колекции с много малко код, подобно на заявки към база данни. Не забравяйте using System.Linq;!

Ламбда изрази (=>)

LINQ разчита силно на ламбда изрази. => се чете "отива в" или "такова че". Например: x => x > 5 означава "вземи елемент x, такъв че x да е по-голямо от 5".

Отложено изпълнение

Повечето LINQ заявки не се изпълняват на момента, а чак когато обходите резултата (например с foreach) или изрично извикате .ToList() / .ToArray().

Where филтрира, Select трансформира

Where отговаря на въпроса "кои елементи?" (връща по-малко елементи от същия тип), Select — на "какво от всеки елемент?" (връща същия брой, но трансформирани).

Бърз справочник

Основни LINQ методи

LINQ методЗа какво служи?Пример
.Where()Филтрира колекцията по зададено условие.list.Where(x => x % 2 == 0)
.Select()Трансформира / извлича конкретна част от данните.students.Select(s => s.Name)
.OrderBy()Сортира във възходящ ред.list.OrderBy(x => x)
.OrderByDescending()Сортира в низходящ ред.list.OrderByDescending(x => x)
.FirstOrDefault()Връща първия елемент, отговарящ на условие, или null/0.list.FirstOrDefault(x => x == 100)
.Count()Връща броя на елементите, отговарящи на дадено условие.list.Count(x => x > 50)
.Sum() / .Average()Сума / средна стойност на елементите.grades.Average()
.Min() / .Max()Най-малък / най-голям елемент.prices.Max()
.ToList()Изпълнява заявката и връща резултата като List<T>.var result = query.ToList();

Още полезни LINQ методи

LINQ методЗа какво служи?Пример
.Any()Има ли поне един елемент, отговарящ на условието (bool).numbers.Any(n => n > 20)
.All()Всички ли елементи отговарят на условието (bool).numbers.All(n => n > 0)
.Distinct()Премахва дубликатите.numbers.Distinct()
.Take(n)Взима първите n елемента.sorted.Take(3) — топ 3
.Skip(n)Прескача първите n елемента.sorted.Skip(3) — всичко без топ 3
.ThenBy()Второ ниво на сортиране след OrderBy.people.OrderBy(p => p.Age).ThenBy(p => p.Name)

Код за анализ и пренаписване

Where, OrderByDescending и Select върху списък с продукти

Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace ModuleEight
{
    class Product
    {
        public string Name { get; set; }
        public double Price { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Product> products = new List<Product>
            {
                new Product { Name = "Лаптоп", Price = 1500.00 },
                new Product { Name = "Мишка", Price = 45.50 },
                new Product { Name = "Монитор", Price = 450.00 }
            };

            // 1. Where: Намиране на продукти с цена под 200 лв.
            var cheapProducts = products.Where(p => p.Price < 200).ToList();

            Console.WriteLine("--- Евтини продукти ---");
            foreach (var p in cheapProducts)
            {
                Console.WriteLine($"{p.Name} - {p.Price} лв.");
            }

            // 2. OrderBy & Select: Сортиране по цена низходящо и взимане само на имената
            var sortedNames = products
                .OrderByDescending(p => p.Price)
                .Select(p => p.Name)
                .ToList();

            Console.WriteLine("\n--- Продукти от най-скъп към най-евтин ---");
            foreach (var name in sortedNames)
            {
                Console.WriteLine(name);
            }
        }
    }
}

Какво се случва тук?

  • Списъкът е създаден с инициализатор на колекция — обектите се описват направо в { }, с инициализатор на обект (new Product { Name = ..., Price = ... }) вместо конструктор.
  • products.Where(p => p.Price < 200) — ламбдата се изпълнява за всеки продукт p; остават само тези, за които изразът е true (тук — само Мишката).
  • .ToList() "материализира" заявката веднага — без него cheapProducts би бил само рецепта, изпълнявана при всяко обхождане (отложено изпълнение).
  • Във втората заявка методите са навързани: първо OrderByDescending(p => p.Price) сортира по цена от най-скъп към най-евтин, после Select(p => p.Name) взима само името на всеки продукт — резултатът е List<string>, не List<Product>.
  • Записването на веригата на отделни редове (по един метод на ред) е стандартният четим стил за LINQ.

Статистика върху числа: Any, All, Distinct, Take

Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace ModuleEightExtra
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> numbers = new List<int> { 12, 5, 8, 21, 5, 16, 3, 8 };

            // Филтриране + string.Join за красив печат
            var evens = numbers.Where(n => n % 2 == 0).ToList();
            Console.WriteLine($"Четни: {string.Join(", ", evens)}");

            // Any и All - въпроси с да/не отговор
            Console.WriteLine($"Има ли число над 20? {numbers.Any(n => n > 20)}");
            Console.WriteLine($"Всички ли са положителни? {numbers.All(n => n > 0)}");

            // Distinct - без дубликати
            Console.WriteLine($"Уникални стойности: {numbers.Distinct().Count()}");

            // Верига: сортирай, махни дубликатите, вземи топ 3
            var top3 = numbers
                .OrderByDescending(n => n)
                .Distinct()
                .Take(3)
                .ToList();
            Console.WriteLine($"Топ 3: {string.Join(", ", top3)}");

            // Агрегации
            Console.WriteLine($"Сума: {numbers.Sum()}, Средно: {numbers.Average():F2}");
        }
    }
}

Какво се случва тук?

  • string.Join(", ", evens) слепва списъка в красив низ "12, 8, 16, 8" — двойката Join + LINQ е стандартът за отпечатване на резултати.
  • Any пита "има ли поне един?", All — "всички ли?". И двата връщат bool и спират да проверяват веднага щом отговорът стане ясен.
  • Distinct() маха повторенията (тук двете петици и двете осмици се броят по веднъж).
  • Веригата за топ 3 се чете отгоре надолу като рецепта: сортирай низходящо → премахни дубликати → вземи първите 3 → материализирай. Всеки метод подава резултата си на следващия.
  • Average() връща double дори за списък от int — LINQ е помислил за капана с целочисленото делене вместо вас.

Внимавай! Чести грешки

Грешките, които почти всеки прави в тази тема — виж разликата между грешния и правилния код.

Забравен using System.Linq

Грешно

using System;
using System.Collections.Generic;

// CS1061: 'List<int>' does not contain
// a definition for 'Where'

Правилно

using System;
using System.Collections.Generic;
using System.Linq;

LINQ методите са разширения и "се появяват" върху колекциите само с using System.Linq;. Грешката CS1061 за Where/Select/OrderBy почти винаги значи точно това.

>= вместо =>

Грешно

var big = numbers.Where(n >= 5);
// объркан ламбда оператор

Правилно

var big = numbers.Where(n => n >= 5);

=> е ламбда операторът ("такова че"), >= е сравнение "по-голямо или равно". В ламбдата първо идва параметърът, после =>, после условието — което може да съдържа >=.

First върху празен резултат

Грешно

var found = products.First(p => p.Price > 9999);
// InvalidOperationException, ако няма такъв

Правилно

var found = products.FirstOrDefault(p => p.Price > 9999);
if (found != null)
{
    Console.WriteLine(found.Name);
}

First гърми, когато няма съвпадение. FirstOrDefault връща null (или 0 за числа) — но тогава задължително проверете резултата, преди да го ползвате, иначе ви чака NullReferenceException.

Заявка без ToList, ползвана многократно

Грешно

var expensive = products.Where(p => p.Price > 100);
Console.WriteLine(expensive.Count()); // изпълнява заявката
foreach (var p in expensive) { ... }  // изпълнява я ПАК

Правилно

var expensive = products.Where(p => p.Price > 100).ToList();
Console.WriteLine(expensive.Count);
foreach (var p in expensive) { ... }

Без .ToList() заявката е "рецепта" и се изпълнява наново при всяко обхождане (отложено изпълнение). Ще ползвате резултата повече от веднъж? Материализирайте го веднъж с ToList().

Задачи за самостоятелна работа

Опитай първо сам — подсказката е там само ако наистина закъсаш.

Задача 1: Филтриране на числа

Създайте списък с числата от 1 до 20. Използвайте LINQ, за да създадете нов списък, който съдържа само числата, които се делят на 3 без остатък, и ги отпечатайте.

Задача 2: Търсене на студенти

Създайте списък от обекти от клас Student (съдържащи Име и Възраст). Използвайте LINQ, за да намерите и отпечатате само студентите, които са пълнолетни (на 18 или повече години), сортирани по име по азбучен ред.

Задача 3: Обработка на думи

Даден е списък с думи. С една LINQ верига: вземете само думите с дължина поне 5 букви, превърнете ги в главни букви и ги подредете по дължина (от най-късата към най-дългата). Отпечатайте резултата.

Мини-тест за проверка

Отговори си наум (или на глас!) и чак тогава разкрий отговора.

1. Какво означава символът => и как се нарича?

2. Каква е разликата между .Where() и .Select()?

3. Какво ще върне .FirstOrDefault(), ако списъкът е празен или търсеният елемент не съществува?

4. Какво правят .Any() и .All() и какво връщат?

Речник на термините

Виж пълния речник на курса
LINQ
Language-Integrated Query — методи за филтриране, сортиране и трансформиране на колекции направо в езика. Изисква using System.Linq;.
Ламбда израз
Малка анонимна функция: x => x > 5 — "вземи x, такова че x > 5". Лявата страна е параметърът, дясната — изразът.
Предикат
Ламбда, която връща bool — условието, подавано на Where, Any, All, Count.
Верижност (chaining)
Навързване на методи: list.Where(...).OrderBy(...).Select(...) — резултатът от единия е вход за следващия.
Отложено изпълнение
LINQ заявката се изпълнява чак при обхождане или при .ToList() — дотогава е само "рецепта".
Материализация
Принудително изпълнение на заявката и запазване на резултата: .ToList(), .ToArray().
Агрегация
Свеждане на колекция до една стойност: Sum, Average, Min, Max, Count.

Провери знанията

Бърз тест с избор от отговори върху термините на модула — с точки и моментална обратна връзка.

Провери знанията си

7 въпроса върху термините от модула. Показваме определение — ти избираш правилния термин.

Препоръки за този модул

  • Четете заявката на глас като изречение: "от продуктите, където цената < 200, взимам имената" — ако изречението звучи логично, заявката е вярна.
  • Решете една задача и по двата начина — с цикли и с LINQ — и сравнете кода; така ще усетите какво точно ви спестява LINQ.
  • Завършвайте с .ToList(), когато искате резултата веднага и многократно — иначе заявката се изпълнява наново при всяко обхождане.
  • Ползвайте смислени имена в ламбдите при по-сложни заявки: products.Where(product => ...) е по-четимо от p => ... за начинаещи.

Начален курс по C# — учебен наръчник и бърз справочник за студенти.

Натисни Ctrl K или / отвсякъде, за да търсиш.