Нові методи LINQ у .NET 6–.NET 9

pic

У цьому дописі ми розглянемо деякі з останніх вдосконалень 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

Leave a Reply

Your email address will not be published. Required fields are marked *