LINQ у .NET: технічна реалізація базових методів і суміжні концепції
Всім привіт! Мене звати Олексій, я працюю .NET-розробником в компанії GlobalLogic. Крім цього, періодично проводжу курси та вебінари про .NET, де ділюся своїм досвідом і знаннями. На написання цієї статті мене надихнули студенти, коли вони побачили, як знання, набуті на курсах, застосовуються в LINQ, а точніше — в його внутрішній реалізації.
З практики також можу відмітити, що багато розробників користуються методами LINQ, проте не розуміють, як він влаштований «під капотом». Сподіваюсь, ця стаття трошки привідкриє завісу магії LINQ та дасть розуміння імплементації базових методів.
Проблематика
Перш ніж перейти до реалізації базових методів, пропоную замислитися над однією з проблем, яку вирішує LINQ. Уявімо ситуацію: у нас є клас Student, що має кілька властивостей — Id (унікальний ідентифікатор), Name (ім’я студента) та Age (його вік).
public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
Також маємо колекцію, наприклад List, яка типізована класом Student і містить в собі кілька студентів.
List<Student> students = new List<Student> { new Student { Id = 1, Name = "John", Age = 18 }, new Student { Id = 2, Name = "Alice", Age = 20 }, new Student { Id = 3, Name = "Bob", Age = 22 } };
Тепер, маючи такий набір даних, уявімо, що нам потрібно відфільтрувати студентів за віком. Для цього ми можемо використовувати метод Where из LINQ . Наприклад, якщо ми хочемо знайти студентів старше 18 років, можемо зробити це наступним чином:
var filteredStudents = students.Where(s => s.Age > 18);
Пропоную на деякий час забути про цей метод і загалом про LINQ, та пройти шлях самурая, спробувавши реалізувати метод Where самостійно. Зробімо для цього кілька ітерацій реалізації, поступово покращуючи підхід на кожному етапі. Почнемо з простої задачі — створення методу, який буде відбирати студентів, вік яких перевищує 18 років.
Створення простого методу для фільтрації
Спочатку пропоную визначитися з сигнатурою методу. В ролі першого аргументу метод має приймати колекцію типу List, яка типізована класом Student. Тобто це колекція, яку ми будемо фільтрувати. Другим аргументом будемо передавати вік, за яким хочемо виконати фільтрацію. А повертатиме метод колекцію типу List, тобто відфільтрованих за віком студентів.
public List<Student> FilterStudentsByAge(List<Student> students, int age)
Тіло методу можна реалізувати наступним чином: спочатку створимо порожню колекцію типу List під назвою result, куди будемо додавати студентів, які старші за переданий вік, тобто тих, чий вік більший за другий аргумент методу. Далі створимо цикл foreach, який буде ітерувати вхідну колекцію студентів. У тілі циклу додамо умовну конструкцію if, в якій перевіримо: якщо вік студента перевищує заданий поріг, ми додаємо його до нової колекції, інакше — ігноруємо. Після завершення циклу повернемо нову колекцію як результат роботи методу. І для простоти поки помістимо цей метод у звичайний класс, який назвемо Service. На першому етапі метод виглядатиме так:
public class Service { public List<Student> FilterStudentsByAge(List<Student> students, int ageThreshold) { List<Student> result = new List<Student>(); foreach (var student in students) { if (student.Age > ageThreshold) { result.Add(student); } } return result; } }
Працювати з ним можна наступним чином:
Service service = new Service(); List<Student> filteredStudents = service.FilterStudentsByAge(students, 18);
Тепер ускладнюємо задачу: уявіть, що нам потрібно знайти студентів, вік яких 18 років, або всіх студентів, чиє ім’я починається з певної літери. Як бути в такій ситуації? Простим варіантом, який відразу приходить на думку, було б створити нові методи, змінивши їх назви та умови фільтрації під час ітерації колекції. Але очевидно, що виникне проблема дублювання коду, що не є оптимальним рішенням.
Тому наступним кроком буде впровадження делегатів у нашу реалізацію методу. Маленька примітка: для спрощення виклику методу FilterStudentsByAge пропоную зробити його статичним. Трошки пізніше ми повернемося до цього питання та розглянемо його більш детально.
Делегати в якості аргументу методу
На початку цього пункту хотілося б у двох словах нагадати, що таке делегат і які системні делегати існують у .NET. Перш за все, делегат — це спеціальний тип даних, який зберігає посилання на методи. Делегати також можна передавати як параметри до інших методів або використовувати для реалізації подій. У .NET існують три основні системні делегати: Action, Func та Predicate. Коротко згадаємо про кожен із них:
- Action — це делегат, який представляє методи, що не повертають значення. Він може приймати один або кілька параметрів, що робить його корисним для виконання дій, які не вимагають результату.
- Func — це делегат, який використовується для методів, що повертають значення. Він може приймати від одного до 16 параметрів, при чому останній параметр завжди є типом, що представляє значення, яке повертається.
- Predicate — це делегат, який представляє методи, що повертають логічне значення (true або false). Він приймає один параметр і зазвичай використовується для перевірки певних умов, що робить його корисним у випадках, коли потрібно фільтрувати або шукати дані в колекціях.
Тепер, згадавши теорію, можемо зробити наш метод більш гнучким, щоб він приймав різні умови для фільтрації колекції. Для цього потрібно замінити другий аргумент методу, який раніше представляв вік, на делегат. Тут постає питання: який саме делегат вибрати? Нам потрібен такий делегат, який приймає параметр типу Student та повертає булеве значення залежно від виконання умови. Для цього нам ідеально підійде делегат Func<Student, bool>, де Student — тип аргументу, а bool — тип, що повертається. Тоді сигнатура методу буде виглядати наступним чином:
public List<Student> FilterStudentsByAge(List<Student> students, Func<Student, bool> predicate)
У тілі самого методу нам потрібно внести мінімальні зміни: достатньо викликати метод, сполучений з делегатом. Для цього необхідно на аргументі predicate викликати метод Invoke. Модифікований метод виглядатиме так:
public static List<Student> FilterStudentsByAge(List<Student> students, Func<Student, bool> predicate) { List<Student> result = new List<Student>(); foreach (var student in students) { if (predicate.Invoke(student)) { result.Add(student); } } return result; }
Однак виникає цікаве питання: як тепер викликати метод FilterStudentsByAge? До речі, цей метод можна перейменувати в FilterStudents, оскільки тепер він фільтрує не лише за віком, а за будь-якою умовою, яку ми передамо. Розгляньмо приклад виклику методу з умовою фільтрації за віком.
Func<Student, bool> agePredicate = new Func<Student, bool>(AgeCondition); Service.FilterStudents(students, agePredicate); static bool AgeCondition(Student student) { if (student.Age > 18) { return true; } else { return false; } }
У цьому прикладі ми створили окремий метод AgeCondition, який описує умову фільтрації за віком. Далі створюємо екземпляр делегата Func<Student, bool> під назвою agePredicate, який сполучаємо з цим методом і передаємо його в метод FilterStudents. Такий підхід дозволяє створювати будь-який метод, що описує умову фільтрації, та передавати цю умову в метод FilterStudents. Тобто фактично, при ітерації студентів у циклі foreach, ми будемо викликати метод AgeCondition для кожного студента через виклик Invoke на predicate в методі FilterStudents. Можна сказати, що в FilterStudents ми дозволили розширювати функціонал методу без його модифікації. Фактично ми імплементували принцип відкритості/закритості (OCP) з SOLID-принципів. Наступним етапом ми можемо спростити метод AgeCondition, замінивши логіку умовної конструкції if- else на тернарний оператор, що зробить код більш компактним і читабельним:
static bool AgeCondition(Student student) { return student.Age > 18? true: false; }
Або зробимо ще простіше, залишивши в ролі значення, що повертається, тільки умову:
static bool AgeCondition(Student student) { return student.Age > 18; }
Далі, можна було б використати техніку передбачення делегату, спростивши створення екземпляра делегату та виклик методу:
Func<Student, bool> agePredicate = AgeCondition; Service.FilterStudents(students, agePredicate);
І, нарешті, можна скористатись анонімними методами — це методи без імені, які можна визначити безпосередньо при виклику делегата. Вони використовуються для передачі логіки в делегати без створення окремого методу, що спрощує життя, адже нема потреби створювати окремий метод для кожної логіки. У нашому випадку це виглядатиме так:
Func<Student, bool> agePredicate = delegate (Student student) { return student.Age > 18; }; Service.FilterStudents(students, agePredicate);
Або ще простіше, можна використати лямбда-вирази — це більш компактний і читабельний спосіб визначення анонімних методів. Лямбда-вирази використовують стрілковий оператор => для вказівки параметрів і тіла методу.
Func<Student, bool> agePredicate = student => student.Age > 18; Service.FilterStudents(students, agePredicate);
І останнє спрощення в цьому пункті полягає в тому, що ми можемо видалити змінну agePredicate типу Func<Student, bool> і одразу передавати лямбда-вираз безпосередньо як аргумент методу. На завершення цього пункту весь наш код, включно з викликом методу для фільтрації, виглядатиме наступним чином:
Service.FilterStudents(students, student => student.Age > 18); public class Service { public static List<Student> FilterStudents(List<Student> students, Func<Student, bool> predicate) { List<Student> result = new List<Student>(); foreach (var student in students) { if (predicate.Invoke(student)) { result.Add(student); } } return result; } }
Наступним кроком пропоную зробити цей метод ще більш універсальним, щоб він міг працювати не тільки з колекцією типу List, а й з List, де T — будь-який тип. Для цього нам потрібно використати узагальнення.
Розширення можливостей LINQ за допомогою узагальнених методів
Спочатку знову трішки теорії. Узагальнення або generics — це концепція, яка дозволяє створювати класи, інтерфейси та методи, що працюють з параметризованими типами. Це дає змогу створювати більш гнучкий та повторно використовуваний код, оскільки можна працювати з різними типами даних, не вказуючи їх конкретно до моменту створення екземпляра класу або використання методу. У нашому випадку потрібно просто замість використання типу Student вказати універсальний параметр типу, який можна назвати, наприклад, TSource.
Відповідно, потрібно перейменувати перший аргумент методу, змінивши його зі students на source, а також змінити змінну ітерації з student на element в тілі самого методу. Звісно, потрібно також перейменувати сам метод, оскільки він тепер фільтрує не тільки студентів, а й будь-який список, закритий конкретним типом. Пропоную поки його назвати Filter.
Service.Filter(students, student => student.Age > 18); public class Service { public static List<TSource> Filter<TSource>(List<TSource> source, Func<TSource, bool> predicate) { List<TSource> result = new List<TSource>(); foreach (var element in source) { if (predicate.Invoke(element)) { result.Add(element); } } return result; } }
На цьому етапі ми отримали метод, який може відфільтрувати список, закритий будь-яким типом, за будь-якою умовою, яку ми передаватимемо в якості аргументу. Проте цей метод поки що не є повністю універсальним, оскільки працює лише з однією колекцією — List. Наступним кроком у покращенні пропоную додати можливість використання цього методу з будь-якою колекцією.
Використання інтерфейсів IEnumerable та IEnumerator
На початку варто зазначити, що IEnumerable та IEnumerator — це інтерфейси, які дозволяють ітерувати колекції. Загалом щоб колекція вважалася справжньою колекцією, вона повинна реалізувати обидва інтерфейси. Таким чином, усі колекції в .NET реалізують ці два інтерфейси. Тепер детальніше: фактично, IEnumerable забезпечує доступ до ітератора, повертаючи IEnumerator. IEnumerator, своєю чергою, дозволяє перебирати елементи колекції. Якщо уявити, що в C# немає циклу foreach, для ітерації нам довелося б використовувати ці інтерфейси напряму. Це означає, що ми б отримували доступ до кожного елемента колекції вручну, викликаючи метод MoveNext() та властивість Current на IEnumerator, щоб перейти до наступного елементу.
Розгляньмо приклад. Візьмемо нашу колекцію List і переберемо її, виводячи на консоль інформацію про кожного студента. Перш за все, нам потрібно привести нашу колекцію до базового інтерфейсного типу IEnumerable:
IEnumerable enumerable = (IEnumerable)students;
Тут ми використали UpCast — приведення до базового інтерфейсного типу. Насправді у цьому випадку можна спростити вираз і не вказувати явне приведення, оскільки UpCast виконується автоматично. Остаточний вигляд буде таким:
IEnumerable enumerable = students;
Далі на змінній enumerable ми можемо викликати метод GetEnumerator, який поверне об’єкт типу IEnumerator. До речі, метод GetEnumerator — це єдиний метод, що входить до інтерфейсу IEnumerable, і його основне призначення полягає в поверненні ітератора, тобто об’єкта типу IEnumerator.
IEnumerator enumerator = enumerable.GetEnumerator();
Тепер ми маємо змогу напряму використати функціонал інтерфейсу IEnumerator, а саме — метод MoveNext, який буде відповідати на питання, чи є ще елементи для ітерації у внутрішньому масиві колекції, а також до властивості Current, яка буде повертати поточний єлмент ітерації. І, звісно ж, цей метод та властивість будуть використовуватись разом з циклом while.
while (enumerator.MoveNext()) { Student current = (enumerator.Current) as Student; Console.WriteLine(current.Name); }
Внутрішня будова циклу foreach показує нам, що всі колекції, які підтримують ітерацію, повинні реалізовувати як мінімум інтерфейс IEnumerable. Враховуючи цей факт, можемо повернутись до нашого методу FilterStudents та зробити його більш універсальним: замість того, щоб обмежувати його роботу лише з колекціями типу List, дозволимо йому працювати з будь-якою колекцією, яка реалізує IEnumerable. Це дасть можливість застосовувати FilterStudents до ширшого набору колекцій, не вимагаючи специфічного типу List у сигнатурі методу.
public class Service { public static IEnumerable<TSource> Filter<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { List<TSource> result = new List<TSource>(); foreach (var element in source) { if (predicate.Invoke(element)) { result.Add(element); } } return result; } }
Тепер метод Filter став більш універсальним, оскільки може працювати з будь-якою колекцією, яка реалізує IEnumerable. Однак у багатьох, ймовірно, виникло питання про створення колекції типу List, куди ми додаємо відфільтровані елементи, та подальший неявний upcast до IEnumerable при поверненні результату. Такий підхід не завжди є оптимальним. У цьому випадку доцільно було б скористатися ключовим словом yield, щоб повертати елементи в міру їх обробки, не створюючи додаткової колекції.
Ключове слово yield
Традиційно спершу розглянемо трохи теорії. Фактично yield спрощує створення ітераторів, дозволяючи повертати елементи з методу по одному, без необхідності формувати всю колекцію заздалегідь. Це особливо корисно, коли дані обробляються поступово або коли працюєте з великими колекціями й прагнете зменшити використання пам’яті. При використанні yield return метод тимчасово «призупиняє» свою роботу, повертаючи значення, і продовжує виконання з тієї ж точки при наступному зверненні до ітератора. Ітерація завершується, коли всі елементи повернуто. Наприклад, якщо нам потрібно повернути кілька імен, це можна зробити так:
static IEnumerable GetNames() { List<string> names = new List<string>() { "Alex", "Vova", "Peta" }; return names; }
У цьому прикладі ми створюємо колекцію List, а потім повертаємо її як IEnumerable, фактично не використовуючи всі можливості цієї колекції й витрачаючи пам’ять на зайвий об’єкт List. Щоб уникнути цього, можемо спростити код, використовуючи ключове слово yield:
static IEnumerable GetNames() { yield return "Alex"; yield return "Vova"; yield return "Peta"; }
Під капотом середовище створює клас, який реалізує лише два інтерфейси — IEnumerable та IEnumerator, без зайвого функціоналу. Це легко перевірити, декомпілювавши код. У результаті декомпіляції ми бачимо клас, який середовище згенерувало з реалізацією наступних інтерфейсів:
private sealed class <GetNames>d__1 : IEnumerable<object>, IEnumerable, IEnumerator<object>, IEnumerator, IDisposable
Цікаво, що в методі MoveNext() використовується патерн «машинний стан». Щоб детальніше зрозуміти, як працює yield, рекомендую самостійно декомпілювати цей приклад і подивитися на створений клас. Тепер повернемося до нашого методу й замінимо створення List на використання yield.
public static IEnumerable<TSource> Filter<TSource>(IEnumerable<TSource> ource, Func<TSource, bool> predicate) { //List<TSource> result = new List<TSource>(); foreach (var element in source) { if (predicate.Invoke(element)) { yield return element; //result.Add(element); } } //return result; }
Використання ключового слова yield у методі Filter дозволило відмовитись від створення колекції List, додавання елементів, що відповідають умові, до цієї колекції та її повернення як результат, з подальшим upcast до базового інтерфейсного типу. Це спростило код, усунувши непотрібні кроки. Фінальна версія коду на цьому етапі виглядатиме так.
public class Service { public static IEnumerable<TSource> Filter<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach (var element in source) { if (predicate.Invoke(element)) { yield return element; } } } }
І можна було б вважати, що на даному етапі це вся реалізація, але залишився останній штрих — як викликати наш метод. В поточній реалізації виклик виглядатиме таким чином:
var filtered = Service.Filter(students, student => student.Age > 20);
Фактично ми викликаємо метод, як звичайний статичний метод на об’єкті. В цілому такий підхід є нормальним, але уявіть ситуацію, коли потрібно два рази підряд викликати метод Filter. Тоді код виглядатиме так.
var filtered = Service.Filter(Service.Filter(students, student => student.Age > 20), student => student.Name == "Bob");
І в такому вигляді запис уже не здається таким простим. Щоб зробити виклик більш елегантним, пропоную використати екстеншен-методи.
Екстеншен методи (Extension Methods)
Екстеншен методи — це спеціальні методи, які дозволяють додавати новий функціонал до вже існуючих типів (класів, структур тощо), не змінюючи їхнього вихідного коду. Це особливо корисно, коли потрібно розширити функціональність типу, до якого немає доступу, наприклад, якщо ви використовуєте сторонню бібліотеку або, як у нашому випадку, коли потрібно додати метод для фільтрації до кожної колекції.
З погляду реалізації екстеншен-методи створюються в окремих класах, які мають бути статичними. Сам метод також має бути статичним, а перший параметр методу вказує тип, до якого цей метод додається, і перед ним ставиться ключове слово this. Зазвичай такі методи створюють, коли нема потреби використовувати внутрішній стан класу. Тепер пропоную переробити наш метод фільтрації на екстеншен-метод. Для цього потрібно зробити клас Service статичним та додати ключове слово this перед аргументом розширення IEnumerable source у методі.
public static class Service { public static IEnumerable<TSource> Filter<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach (var element in source) { if (predicate.Invoke(element)) { yield return element; } } } }
Тепер ми зможемо викликати наш метод Filter, як ніби він є частиною самого типу колекції, на якій ми його викликаємо.
var filtered = students.Filter(student => student.Age > 20);
І саме приємне, коли нам потрібно буде кілька разів викликати метод фільтрації, ми можемо це зробити, використовуючи ланцюжок викликів методів.
var filtered = students.Filter(student => student.Age > 20).Filter(student => student.Name == "Bob");
Фінальна реалізація методу Where
Мабуть, всі вже здогадались, що метод Filter фактично є аналогом методу Where з простору імен System.Linq. На цьому етапі пропоную перейменувати метод у Where, а також змінити ім’я класу з Service на Enumerable, оскільки всі LINQ-методи зазвичай розміщуються саме в цьому статичному класі.
public static class Enumerable { public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach (var element in source) { if (predicate.Invoke(element)) { yield return element; } } } }
Таким чином ми разом реалізували метод Where. З одного боку, тут немає нічого складного, але з іншого — це вимагає фундаментальних знань та розуміння основних концепцій мови. Як закріплення, пропоную ще реалізувати метод Select.
Реалізація методу Select
Метод Select є одним з основних у LINQ. Він дозволяє трансформувати кожен елемент колекції в новий тип даних, включно з анонімними типами. Цей метод працює з усіма типами колекцій, що реалізують інтерфейс IEnumerable, і використовується для проєкції — перетворення одного елемента на інший із застосуванням певної лямбда-функції. Наприклад, якщо нам потрібно створити колекцію типу Person з наших студентів, де цей тип містить лише Id та Name.
class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
З викоританням Select це буде виглядати так.
var persons = students.Select(student => new Person() { Id = student.Id, Name = student.Name });
Така трансформація дозволяє легко створювати нові об’єкти з елементів колекції. Тепер спробуймо реалізувати метод Select за аналогією з методом Where. Почнемо з простого варіанту. Перш за все, подумаймо над сигнатурою методу. Він має приймати в якості аргументу колекцію List, трансформувати кожного студента в об’єкт типу Person і повертати колекцію List. Також метод має приймати лямбда-вираз для опису трансформації студента в людину.
public static List<Person> Select(this List<Student> sourse, Func<Student, Person> selector)
Цей метод є методом розширення для можливості виклику на об’єктах будь-яких колекцій. Другий параметр методу, selector, є делегатом типу Func<Student, Person>, який приймає студента та повертає людину. Це фактично лямбда-вираз, який визначає правила трансформації. Наприклад, можна використати такий лямбда-вираз: student => new Person() { Id = student.Id, Name = student.Name }, де ми перемалюємо об’єкт Student на об’єкт Person, зберігаючи лише необхідні поля. В другому наближенні ми можемо оптимізувати цю сігнатуру, замінивши використання колекції List на універсальний базовий інтерфейс IEnumerable.
public static IEnumerable<Person> Select(this IEnumerable<Student> sourse, Func<Student, Person> selector)
Замість хардкоду типів Person та Student ми можемо використати дженеріки, про які згадували при реалізації методу Where. У цьому випадку нам потрібно два дженерікові параметри: TSource — тип вхідної колекції, у нас це Student, і TResult — тип вихідної колекції, у нашому випадку це Person. Фінальна версія сигнатури з використанням дженериків виглядатиме так.
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> sourse, Func<TSource, TResult> selector)
Коли ми визначили сигнатуру нашого методу, можна перейти до реалізації його тіла. Ідея реалізації методу схожа на роботу методу Where, але є кілька відмінностей. Спочатку потрібно перебрати елементи вхідної колекції за допомогою циклу foreach. Однак, на відміну від методу Where, у нашому випадку будемо трансформувати кожен елемент вхідної колекції в тип вихідної колекції й повертати його за допомогою ключового слова yield. Фактично ми по черзі перебираємо всі елементи вхідної колекції, застосовуємо до кожного елемента функцію трансформації, яка описана через лямбда-вираз, і повертаємо кожен трансформований елемент.
public static IEnumerable<TResult> Select<TSource, TResult>(this Enumerable<TSource> sourse, Func<TSource, TResult> selector) { foreach (var item in sourse) { yield return selector.Invoke(item); } }
Фактично це і є реалізація методу Select. Так само як і для методу Where, цей метод необхідно помістити в статичний клас Enumerable. Сподіваюся, ви зрозуміли основну ідею реалізації з коротшим описом. Насправді для більшості методів LINQ використовуються схожі концептуальні підходи, тому з часом ви побачите багато спільних елементів у їх реалізації, як-от використання yield return, робота з делегатами та дженеріками. Цей підхід дозволяє розширювати стандартні можливості .NET без потреби в модифікації існуючих класів, що дає гнучкість та зручність при роботі LINQ з колекціями.
Висновки та поради
У цій статті я намагався показати внутрішню реалізацію лише двох методів LINQ. Однак концепції, які використовуються в цих методах, застосовуються й в інших. Сподіваюся, що зміг донести підходи, які лежать в основі реалізації цих методів. Підсумовуючи підходи, можна виділити такі ключові пункти, розуміння яких необхідне для глибшого розуміння роботи LINQ:
Методи розширення (Extension Methods)
Більшість методів LINQ реалізовані як методи розширення для типів, що реалізують інтерфейс IEnumerable. Що дає змогу використовувати їх без зміни вихідного коду колекцій.
Дженеріки
LINQ-методи зазвичай використовують дженеріки для забезпечення гнучкості й роботи з різними типами колекцій. Завдяки цьому методи не дублюються та можуть бути застосовані до колекцій, що мають будь-який тип.
Делегати
Багато методів LINQ використовують делегати в ролі аргументів. Ці делегати можуть приймати різні умови, як-от умови трансформації (для зміни елементів колекції) або умови відбору (для фільтрації елементів). Делегати дозволяють абстрагувати логіку, роблячи методи більш універсальними та гнучкими, оскільки замість жорстко зашитих алгоритмів можна передавати функції, що визначають поведінку методу в конкретних випадках.
Функціональний стиль програмування
Використання лямбда-виразів і функціональних концепцій для опису операцій над колекціями.
Ітератори та ключове слово yield
Також необхідно розуміти патерн ітератор та роботу з двома базовими інтерфейсними типами IEnumerable та IEnumerator. І звісно є робота з ключовим словом yield, яке дозволяє повертати елементи, не створюючи цілу колекцію явно, адже за нас це робить середовище виконання.
P.S. Раджу для глибшого розуміння спробувати самостійно реалізувати ці методи, а також кілька інших. Крім того, важливо розібратися з патерном «машинний стан», який допоможе краще зрозуміти роботу ключового слова yield, а також з патерном «ітератор», що дозволить краще освоїти внутрішню роботу колекцій і базових інтерфейсів. Буду вдячний за ваші відгуки та сподіваюся, що ця стаття була корисною.
25 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів