У цьому дописі ми розглянемо деякі з останніх вдосконалень LINQ, що були представлені у проміжку між .NET 6 та .NET 9. Усі ці методи дійсно вражають і значно скорочують обсяг коду, який доводилося писати раніше.
Давайте розберемося.
На моєму вебсайті я ділюсь найкращими практиками .NET та архітектури.
Підписуйтеся, щоб стати кращим розробником.
Завантажте вихідний код для цього допису безкоштовно.
1. Chunk
Розбиття великих колекцій на менші підколекції (або "частини") раніше вимагало чимало коду. Ймовірно, ви писали цикли, використовуючи Skip
і Take
повторно, лише щоб розбити список на рівномірно розмірені частини.
var fruits = new List { "Banana", "Pear", "Apple", "Orange" };
var chunkSize = 2;
var chunks = new List();
for (var i = 0; i < fruits.Count; i += chunkSize)
{
chunks.Add(fruits.Skip(i).Take(chunkSize).ToArray());
}
foreach (var chunk in chunks)
{
Console.WriteLine($"[{string.Join(", ", chunk)}]");
}
У .NET 6 метод Chunk
надає простий спосіб розбиття колекції на частини за допомогою одного рядка коду:
var fruits = new List { "Banana", "Pear", "Apple", "Orange" };
var chunks = fruits.Chunk(2);
foreach (var chunk in chunks)
{
Console.WriteLine($"[{string.Join(", ", chunk)}]");
}
Як це працює:
Метод Chunk
розділяє колекцію на підмасиви заданого розміру. Якщо останній підмасив не має достатнього розміру, щоб заповнити вказану довжину, він просто повертає меншу частину.
2. Zip
Метод розширення Zip
існує вже певний час, але у .NET 6 його покращили, дозволивши комбінувати три (і більше) послідовності без зайвих складнощів.
Ось як раніше можна було поєднувати три (або більше) колекції:
public record FruitData(string Name, string Color, string Origin);
var fruits = new List { "Banana", "Pear", "Apple" };
var colors = new List { "Yellow", "Green", "Red" };
var origins = new List { "Ecuador", "France", "Canada" };
var combined = new List();
for (var i = 0; i < fruits.Count; i++)
{
// Припустимо, що всі списки мають однакову довжину
var name = fruits[i];
var color = colors[i];
var origin = origins[i];
combined.Add(new FruitData(name, color, origin));
}
foreach (var data in combined)
{
Console.WriteLine($"{data.Name} is {data.Color} and comes from {data.Origin}");
}
Замість того, щоб робити кілька проходів або писати потрійні цикли, тепер ви можете об'єднати їх одразу:
var fruits = new List { "Banana", "Pear", "Apple" };
var colors = new List { "Yellow", "Green", "Red" };
var origins = new List { "Ecuador", "France", "Canada" };
var zipped = fruits.Zip(colors, origins);
Console.WriteLine("Now in .NET 6 (Zip with three sequences):");
foreach (var (fruit, color, origin) in zipped)
{
Console.WriteLine($"{fruit} is {color} and comes from {origin}");
}
Як це працює:
Метод Zip
може об'єднувати дві, три або більше послідовностей у єдину послідовність кортежів.
Кожен елемент у результатуючій послідовності є кортежем (Tuple), який містить елементи з кожного вихідного списку на тому ж індексі.
3, 4: MinBy, MaxBy
Вибір мінімального або максимального об'єкта з послідовності на основі певної властивості часто вимагав сортування або написання власних агрегацій:
var raceCars = new List
{
new RaceCar("Mach 5", 220),
new RaceCar("Bullet", 180),
new RaceCar("RoadRunner", 250)
};
// Пошук максимального або мінімального означав сортування або використання Aggregate
var fastestCar = raceCars
.OrderByDescending(car => car.Speed)
.First();
var slowestCar = raceCars
.OrderBy(car => car.Speed)
.First();
Console.WriteLine($"Fastest Car: {fastestCar.Name}, Speed: {fastestCar.Speed}");
Console.WriteLine($"Slowest Car: {slowestCar.Name}, Speed: {slowestCar.Speed}");
Зверніть увагу, що сортування використовується лише для отримання першого результату, що може бути менш ефективним для великих списків.
З MinBy
і MaxBy
це можна зробити без додаткових накладних витрат.
var raceCars = new List
{
new RaceCar("Mach 5", 220),
new RaceCar("Bullet", 180),
new RaceCar("RoadRunner", 250)
};
var fastestCar = raceCars.MaxBy(car => car.Speed);
var slowestCar = raceCars.MinBy(car => car.Speed);
Console.WriteLine($"Fastest Car: {fastestCar.Name}, Speed: {fastestCar.Speed}");
Console.WriteLine($"Slowest Car: {slowestCar.Name}, Speed: {slowestCar.Speed}");
Як це працює:
MaxBy
повертає елемент із найбільшим значенням на основі вашого селектора (Selector).MinBy
повертає елемент із найменшим значенням.
5. Підтримка діапазонів для Take
Якщо ви використовували вирази діапазонів collection[start..end]
у C#, ви знаєте, наскільки зручно робити нарізки.
Але у випадку з LINQ доводилося використовувати як Skip
, так і Take
методи:
var fruits = new List { "Banana", "Pear", "Apple", "Orange", "Plum" };
// Нарізка з індексу 2 до 3
var slice = fruits.Skip(2).Take(2);
foreach (var fruit in slice)
{
Console.WriteLine(fruit);
}
Тепер, починаючи з .NET 8, ви можете застосовувати нарізку на основі діапазонів безпосередньо у LINQ, використовуючи метод Take
із підтримкою діапазонів:
var fruits = new List { "Banana", "Pear", "Apple", "Orange", "Plum" };
var slice = fruits.Take(2..4);
foreach (var fruit in slice)
{
Console.WriteLine(fruit);
}
Як це працює:
Take(2..4)
означає, що ви хочете отримати елементи, починаючи з індексу 2 до (але не включаючи) індекс 4.
6. CountBy
Якщо ви виконуєте групове підрахування, наприклад, "порахувати кількість замовлень за назвою", доводилося писати GroupBy(...).Count()
або щось подібне.
public record Order(string Name, string Category, int Quantity, double Price);
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("Bread", "Groceries", 5, 2.49),
new Order("Milk", "Groceries", 2, 1.99)
};
var countByName = orders
.GroupBy(p => p.Name)
.ToDictionary(
g => g.Key,
g => g.Count()
);
foreach (var item in countByName)
{
Console.WriteLine($"Name: {item.Key}, Count: {item.Value}");
}
CountBy(...)
спрощує підхід, автоматично виконуючи групування та підрахунок за один крок.
Ця функція була представлена в .NET 9:
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("Bread", "Groceries", 5, 2.49),
new Order("Milk", "Groceries", 2, 1.99)
};
var countByName = orders.CountBy(p => p.Name);
foreach (var item in countByName)
{
Console.WriteLine($"Name: {item.Key}, Count: {item.Value}");
}
Як це працює:
CountBy
автоматично групує елементи за ключем і підраховує, скільки разів цей ключ зустрічається.- Повертає структуру, схожу на словник, із ключами та їх кількістю.
7. AggregateBy
Агрегація значень за групами — це ще одна поширена задача: підсумовування загальної ціни за категорією, обчислення середнього балу за відділом тощо.
public record Order(string Name, string Category, int Quantity, double Price);
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("Bread", "Groceries", 5, 2.49),
new Order("Milk", "Groceries", 2, 1.99)
};
var totalPricesByCategory = orders
.GroupBy(x => x.Category)
.ToDictionary(
g => g.Key,
g => g.Sum(x => x.Quantity * x.Price)
);
foreach (var item in totalPricesByCategory)
{
Console.WriteLine($"Category: {item.Key}, Total Price: {item.Value}");
}
З методом AggregateBy
, ви можете групувати та агрегувати дані в один крок із мінімальним використанням синтаксису. Цей метод було додано у .NET 9:
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99),
new Order("Bread", "Groceries", 5, 2.49),
new Order("Milk", "Groceries", 2, 1.99)
};
var totalPricesByCategory = orders.AggregateBy(
keySelector: x => x.Category,
seed: 0.0,
func: (total, order) => total + order.Quantity * order.Price
);
foreach (var item in totalPricesByCategory)
{
Console.WriteLine($"Category: {item.Key}, Total Price: {item.Value}");
}
Як це працює:
- Подібно до
GroupBy
, але ви надаєтеseedFactory
(початкове значення) і функцію агрегатора (Aggregator), яка накопичує значення. - Цей підхід в один крок простіший, ніж написання
GroupBy(…)
із подальшим викликом.ToDictionary(…)
і.Sum(…)
.
8. Index
Іноді вам потрібен індекс кожного елемента під час ітерації послідовності. Ви могли використовувати зовнішній лічильник індексів або ітератори.
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99)
};
var index = 0;
foreach (var item in orders)
{
Console.WriteLine($"Order #{index}: {item}");
index++;
}
Новий метод Index
створює пару кожного елемента з його індексом, спрощуючи доступ:
var orders = new List
{
new Order("Phone", "Electronics", 2, 299.99),
new Order("TV", "Electronics", 1, 499.99)
};
foreach (var (index, item) in orders.Index())
{
Console.WriteLine($"Order #{index}: {item}");
}
Як це працює:
Index
автоматично додає до кожного елемента його індекс, починаючи з нуля.- Значно спрощує код, коли потрібен індекс для відображення, логування або додаткової логіки.
Підсумок
Нові методи LINQ зменшують потребу в шаблонному коді, ручному групуванні або кастомній обробці індексів.
Ось перелік нових методів:
- Chunk (.NET 6) дозволяє розбивати великі колекції на менші підколекції (chunks).
- Zip (три послідовності) (.NET 6) об'єднує кілька послідовностей в одну.
- MinBy і MaxBy (.NET 6) дозволяють швидко отримати об'єкти з найменшим або найбільшим значенням властивості.
- Підтримка діапазонів для Take (.NET 8) усуває необхідність комбінувати Skip і Take для нарізки.
- CountBy (.NET 9) об'єднує групування та підрахунок в один крок.
- AggregateBy (.NET 9) дозволяє групувати та виконувати будь-яку кастомну агрегацію з мінімальною кількістю кроків.
- Index (.NET 9) дозволяє повертати елемент разом із його індексом.
Сподіваюся, цей блог був для вас корисним. Успіхів у програмуванні!
Оригінально опубліковано на https://antondevtips.com 28 січня 2025 року.
На моєму вебсайті я ділюся найкращими практиками .NET і архітектури.
Підписуйтесь, щоб стати кращим розробником.
Завантажте вихідний код для цього допису безкоштовно.
Перекладено з: The New LINQ Methods from .NET 6 to .NET 9