Методы массивов и коллекций в C# (.NET): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — библиотекарь. У вас есть огромный стеллаж с книгами. Каждая книга — это отдельный элемент данных (число, строка, объект). Сам стеллаж — это массив или коллекция.
Раньше, чтобы найти книгу по названию, вам приходилось вручную перебирать каждую полку, брать каждую книгу, читать её название и класть обратно. Это долго, скучно и легко ошибиться. А если нужно отсортировать все книги по году издания? Или найти самую толстую? Или удалить все книги с рваными страницами? Без специальных инструментов это превращается в настоящий кошмар.
Методы массивов и коллекций — это как набор волшебных кнопок на вашем библиотечном пульте. Вы нажимаете одну кнопку — и все книги автоматически сортируются. Нажимаете другую — и находятся все книги определённого автора. Третья кнопка — и вы получаете список только названий, без лишней информации.
В программировании эти методы позволяют нам не писать циклы вручную для каждой простой операции. Вместо того чтобы писать 10 строк кода для поиска элемента, мы пишем одну строку с методом Find. Вместо того чтобы создавать новый массив и вручную копировать в него отфильтрованные элементы, мы используем Where. Это делает код короче, понятнее и безопаснее.
Важно: Методы массивов и коллекций — это не просто «синтаксический сахар». Они оптимизированы профессионалами Microsoft, работают быстрее ручных циклов во многих сценариях и содержат встроенную обработку ошибок.
Теория: как это работает изнутри
Чтобы понять, как работают методы, нужно разобраться с двумя ключевыми понятиями: делегаты и лямбда-выражения. Не пугайтесь страшных названий — на деле всё просто.
Делегаты — это «контейнеры для функций»
Представьте, что вы хотите, чтобы ваш робот-библиотекарь мог выполнять разные команды: «найди книгу», «отсортируй», «удали». Вместо того чтобы вшивать каждую команду в робота, вы даёте ему слот, в который можно вставить любую команду. Этот слот и есть делегат.
В C# делегат — это тип, который может хранить ссылку на метод. Например, делегат Func<int, bool> может хранить любой метод, который принимает число и возвращает true или false.
Лямбда-выражения — это «функции на лету»
Вместо того чтобы писать отдельный метод для каждой простой операции, мы можем создать функцию «прямо на месте», не давая ей имени. Это и есть лямбда-выражение. Выглядит оно так:
// Обычный метод
bool IsEven(int number) {
return number % 2 == 0;
}
// То же самое, но лямбда-выражением
(int number) => number % 2 == 0;
Стрелочка => читается как «переходит в» или «возвращает». Всё, что слева — входные параметры, справа — выражение, которое будет вычислено и возвращено.
Как методы обрабатывают коллекции
Большинство методов массивов и коллекций работают по одному принципу:
- Берут каждый элемент коллекции по очереди.
- Применяют к нему вашу функцию (лямбда-выражение).
- В зависимости от метода, либо возвращают результат (например,
true/falseдляAny), либо формируют новую коллекцию (например, дляWhere).
Важно понимать, что многие методы (особенно из LINQ) используют отложенное выполнение. Это значит, что они не выполняются сразу, а только когда вы начинаете перебирать результат. Это экономит память и время, но может привести к неожиданностям, если вы не знаете об этой особенности.
Запомните: Массивы в C# имеют фиксированный размер. Коллекции (List, Dictionary, HashSet) могут расти и сжиматься. Методы для них во многом похожи, но есть важные отличия.
Синтаксис в C# (.NET): все формы и варианты
Рассмотрим основные методы, которые вам понадобятся. Я разделил их на группы по назначению.
1. Методы поиска и проверки
| Метод | Что делает | Пример | Результат |
|---|---|---|---|
Contains |
Проверяет, есть ли элемент в коллекции | list.Contains(5) |
true или false |
Any |
Проверяет, удовлетворяет ли хотя бы один элемент условию | list.Any(x => x > 10) |
true или false |
All |
Проверяет, удовлетворяют ли ВСЕ элементы условию | list.All(x => x > 0) |
true или false |
Find |
Находит ПЕРВЫЙ элемент, удовлетворяющий условию | list.Find(x => x.Name == "Вася") |
Элемент или null |
FindAll |
Находит ВСЕ элементы, удовлетворяющие условию | list.FindAll(x => x.Age > 18) |
Новый список |
IndexOf |
Находит индекс первого вхождения элемента | list.IndexOf("Привет") |
Число (индекс) или -1 |
2. Методы преобразования
| Метод | Что делает | Пример | Результат |
|---|---|---|---|
Select |
Преобразует каждый элемент по заданному правилу | list.Select(x => x * 2) |
Новая коллекция с удвоенными числами |
Where |
Фильтрует элементы по условию | list.Where(x => x.Length > 5) |
Новая коллекция только с длинными строками |
OrderBy |
Сортирует по возрастанию | list.OrderBy(x => x.Price) |
Новая отсортированная коллекция |
OrderByDescending |
Сортирует по убыванию | list.OrderByDescending(x => x.Date) |
Новая отсортированная коллекция |
ToList |
Преобразует результат в список | query.ToList() |
List<T> |
ToArray |
Преобразует результат в массив | query.ToArray() |
T[] |
3. Методы агрегации (подсчёта)
| Метод | Что делает | Пример | Результат |
|---|---|---|---|
Count |
Подсчитывает количество элементов | list.Count(x => x.IsActive) |
Число |
Sum |
Суммирует числовые значения | list.Sum(x => x.Salary) |
Число |
Average |
Вычисляет среднее арифметическое | list.Average(x => x.Grade) |
Число с плавающей точкой |
Min |
Находит минимальное значение | list.Min(x => x.Age) |
Число |
Max |
Находит максимальное значение | list.Max(x => x.Height) |
Число |
Aggregate |
Универсальный метод накопления | list.Aggregate((a, b) => a + b) |
Одно значение (сумма всех) |
4. Методы для работы со строками в коллекциях
| Метод | Что делает | Пример | Результат |
|---|---|---|---|
string.Join |
Соединяет элементы в одну строку через разделитель | string.Join(", ", names) |
"Аня, Вася, Петя" |
string.Concat |
Соединяет элементы без разделителя | string.Concat(numbers) |
"12345" |
5. Методы для изменения коллекций (только для List)
| Метод | Что делает | Пример |
|---|---|---|
Add |
Добавляет элемент в конец | list.Add(42) |
AddRange |
Добавляет несколько элементов | list.AddRange(new[] {1, 2, 3}) |
Insert |
Вставляет элемент по индексу | list.Insert(0, "Первый") |
Remove |
Удаляет первое вхождение элемента | list.Remove("Вася") |
RemoveAt |
Удаляет элемент по индексу | list.RemoveAt(3) |
RemoveAll |
Удаляет все элементы, удовлетворяющие условию | list.RemoveAll(x => x < 0) |
Sort |
Сортирует список на месте | list.Sort() |
Reverse |
Переворачивает список | list.Reverse() |
Clear |
Очищает список | list.Clear() |
Важное замечание: МетодыSort,Reverse,RemoveAllизменяют исходный список. МетодыOrderBy,Where,Selectсоздают НОВУЮ коллекцию, не трогая исходную. Это критически важно помнить, чтобы не получить ошибки в программе.
Примеры кода — от простого к сложному
Пример 1: Базовые методы поиска
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Создаём список чисел
List<int> numbers = new List<int> { 5, 12, 3, 8, 15, 7, 10 };
// 1. Contains — проверяем, есть ли число 8
bool hasEight = numbers.Contains(8);
Console.WriteLine($"Есть ли 8? {hasEight}"); // Вывод: Есть ли 8? True
// 2. Any — есть ли хотя бы одно число больше 10?
bool anyAboveTen = numbers.Any(x => x > 10);
Console.WriteLine($"Есть числа > 10? {anyAboveTen}"); // Вывод: True
// 3. All — все ли числа положительные?
bool allPositive = numbers.All(x => x > 0);
Console.WriteLine($"Все числа > 0? {allPositive}"); // Вывод: True
// 4. Find — найти первое чётное число
int firstEven = numbers.Find(x => x % 2 == 0);
Console.WriteLine($"Первое чётное: {firstEven}"); // Вывод: 12
// 5. FindAll — найти все числа больше 5
List<int> bigNumbers = numbers.FindAll(x => x > 5);
Console.WriteLine($"Числа > 5: {string.Join(", ", bigNumbers)}"); // Вывод: 12, 8, 15, 7, 10
}
}
Объяснение: В этом примере мы используем самые простые методы. Обратите внимание, что Find возвращает только первый подходящий элемент, а FindAll — все. Any и All работают как логические вопросы: «есть ли хоть один?» и «все ли такие?».
Пример 2: Преобразование данных (Select и Where)
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Создаём список имён
List<string> names = new List<string> { "аня", "вася", "петя", "маша", "иван" };
// 1. Select — преобразуем все имена в заглавные
var upperNames = names.Select(name => name.ToUpper());
Console.WriteLine($"Заглавные: {string.Join(", ", upperNames)}");
// Вывод: АНЯ, ВАСЯ, ПЕТЯ, МАША, ИВАН
// 2. Where — оставляем только имена длиннее 3 букв
var longNames = names.Where(name => name.Length > 3);
Console.WriteLine($"Длинные имена: {string.Join(", ", longNames)}");
// Вывод: вася, петя, маша, иван
// 3. Комбинируем: сначала фильтруем, потом преобразуем
var result = names
.Where(name => name.Length > 3) // Сначала фильтр
.Select(name => name.ToUpper()); // Потом преобразование
Console.WriteLine($"Результат: {string.Join(", ", result)}");
// Вывод: ВАСЯ, ПЕТЯ, МАША, ИВАН
}
}
Объяснение: Select — это как конвейер: на входе один элемент, на выходе другой. Where — это как сито: одни элементы проходят, другие нет. Их можно объединять в цепочки, и это одна из самых мощных возможностей LINQ.
Пример 3: Сортировка и работа с объектами
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
// Создаём простой класс для примера
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public double Grade { get; set; }
public override string ToString() => $"{Name} ({Age} лет, оценка: {Grade})";
}
static void Main()
{
// Создаём список студентов
List<Student> students = new List<Student>
{
new Student { Name = "Аня", Age = 20, Grade = 4.5 },
new Student { Name = "Вася", Age = 19, Grade = 3.8 },
new Student { Name = "Петя", Age = 21, Grade = 4.2 },
new Student { Name = "Маша", Age = 20, Grade = 5.0 }
};
// 1. Сортировка по возрасту (по возрастанию)
var byAge = students.OrderBy(s => s.Age);
Console.WriteLine("По возрасту:");
foreach (var s in byAge)
Console.WriteLine($" {s}");
// 2. Сортировка по оценке (по убыванию)
var byGradeDesc = students.OrderByDescending(s => s.Grade);
Console.WriteLine("
По оценке (от лучших к худшим):");
foreach (var s in byGradeDesc)
Console.WriteLine($" {s}");
// 3. Сортировка по нескольким полям: сначала по возрасту, потом по имени
var multiSort = students
.OrderBy(s => s.Age)
.ThenBy(s => s.Name);
Console.WriteLine("
По возрасту, затем по имени:");
foreach (var s in multiSort)
Console.WriteLine($" {s}");
}
}
Объяснение: OrderBy и OrderByDescending сортируют по одному полю. Если нужно сортировать по нескольким полям, используйте ThenBy и ThenByDescending. Обратите внимание: эти методы не изменяют исходный список, а возвращают новый.
Пример 4: Агрегатные функции (подсчёт статистики)
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Данные о продажах за неделю
List<double> sales = new List<double> { 150.50, 230.00, 89.99, 450.00, 120.00, 300.00, 175.50 };
// 1. Count — сколько всего продаж?
int totalSales = sales.Count;
Console.WriteLine($"Всего продаж: {totalSales}"); // 7
// 2. Count с условием — сколько продаж больше 200?
int bigSales = sales.Count(s => s > 200);
Console.WriteLine($"Продаж > 200: {bigSales}"); // 3
// 3. Sum — общая выручка
double totalRevenue = sales.Sum();
Console.WriteLine($"Общая выручка: {totalRevenue:C}"); // 1515.99
// 4. Average — средняя продажа
double averageSale = sales.Average();
Console.WriteLine($"Средняя продажа: {averageSale:C}"); // 216.57
// 5. Min и Max — минимальная и максимальная продажа
double minSale = sales.Min();
double maxSale = sales.Max();
Console.WriteLine($"Мин: {minSale:C}, Макс: {maxSale:C}"); // 89.99, 450.00
// 6. Aggregate — свой собственный подсчёт (например, произведение всех чисел)
double product = sales.Aggregate((acc, x) => acc * x);
Console.WriteLine($"Произведение всех продаж: {product:E}"); // Научная нотация
}
}
Объяснение: Count, Sum, Average, Min, Max — это стандартные статистические функции. Aggregate — это универсальный инструмент: он берёт первый элемент как начальное значение, а потом применяет вашу функцию к каждому следующему элементу, накапливая результат.
Пример 5: Работа со словарём (Dictionary)
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Создаём словарь: ключ — имя студента, значение — его оценка
Dictionary<string, double> grades = new Dictionary<string, double>
{
{ "Аня", 4.5 },
{ "Вася", 3.8 },
{ "Петя", 4.2 },
{ "Маша", 5.0 }
};
// 1. ContainsKey — проверяем, есть ли студент
bool hasVasya = grades.ContainsKey("Вася");
Console.WriteLine($"Есть Вася? {hasVasya}"); // True
// 2. TryGetValue — безопасно получаем значение
if (grades.TryGetValue("Петя", out double peteGrade))
{
Console.WriteLine($"Оценка Пети: {peteGrade}"); // 4.2
}
// 3. Keys и Values — получаем все ключи или все значения
var allNames = grades.Keys.ToList();
var allGrades = grades.Values.ToList();
Console.WriteLine($"Студенты: {string.Join(", ", allNames)}");
Console.WriteLine($"Оценки: {string.Join(", ", allGrades)}");
// 4. Where для словаря — фильтруем по значению
var excellentStudents = grades.Where(kvp => kvp.Value >= 4.5);
Console.WriteLine("Отличники:");
foreach (var student in excellentStudents)
Console.WriteLine($" {student.Key}: {student.Value}");
// 5. Добавление и удаление
grades.Add("Иван", 3.5); // Добавляем нового студента
grades.Remove("Вася"); // Удаляем Васю
Console.WriteLine($"Всего студентов после изменений: {grades.Count}"); // 4
}
}
Объяснение: Словарь — это коллекция пар «ключ-значение». Он идеален для быстрого поиска по ключу. Методы ContainsKey и TryGetValue работают очень быстро (O(1)), в отличие от поиска в списке (O(n)).
Пример 6: Продвинутая работа со строками
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Список предложений
List<string> sentences = new List<string>
{
"C# — отличный язык программирования.",
"LINQ делает работу с данными простой.",
"Массивы и коллекции — основа любого приложения."
};
// 1. Разбиваем каждое предложение на слова
var allWords = sentences.SelectMany(s => s.Split(' '));
Console.WriteLine("Все слова:");
Console.WriteLine(string.Join(", ", allWords));
// 2. Группировка слов по первой букве
var groupedByFirstLetter = allWords
.GroupBy(word => word[0])
.Select(group => $"Буква '{group.Key}': {group.Count()} слов");
Console.WriteLine("
Группировка по первой букве:");
foreach (var group in groupedByFirstLetter)
Console.WriteLine(group);
// 3. Соединяем слова в одно предложение
string combined = string.Join(" ", allWords);
Console.WriteLine($"
Объединённая строка: {combined}");
// 4. Удаляем знаки препинания и считаем длину каждого слова
var cleanWords = allWords
.Select(w => w.Trim('.', ',', '!', '?')) // Убираем знаки препинания
.Where(w => w.Length > 0); // Убираем пустые строки
Console.WriteLine("
Очищенные слова:");
foreach (var word in cleanWords)
Console.WriteLine($" {word} (длина: {word.Length})");
}
}
Объяснение: SelectMany — это мощный метод, который «разворачивает» вложенные коллекции. Если у вас есть коллекция коллекций, SelectMany превращает её в одну плоскую коллекцию. GroupBy группирует элементы по ключу, возвращая коллекцию групп, где каждая группа имеет ключ и список элементов.
Пример 7: Работа с HashSet и уникальностью
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// HashSet — коллекция уникальных элементов
HashSet<int> numbers1 = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> numbers2 = new HashSet<int> { 4, 5, 6, 7, 8 };
// 1. UnionWith — объединение множеств
HashSet<int> union = new HashSet<int>(numbers1);
union.UnionWith(numbers2);
Console.WriteLine($"Объединение: {string.Join(", ", union)}");
// Вывод: 1, 2, 3, 4, 5, 6, 7, 8
// 2. IntersectWith — пересечение (общие элементы)
HashSet<int> intersect = new HashSet<int>(numbers1);
intersect.IntersectWith(numbers2);
Console.WriteLine($"Пересечение: {string.Join(", ", intersect)}");
// Вывод: 4, 5
// 3. ExceptWith — разность (что есть в первом, но нет во втором)
HashSet<int> except = new HashSet<int>(numbers1);
except.ExceptWith(numbers2);
Console.WriteLine($"Разность: {string.Join(", ", except)}");
// Вывод: 1, 2, 3
// 4. SymmetricExceptWith — симметрическая разность (элементы, которые есть только в одном из множеств)
HashSet<int> symmetric = new HashSet<int>(numbers1);
symmetric.SymmetricExceptWith(numbers2);
Console.WriteLine($"Симметрическая разность: {string.Join(", ", symmetric)}");
// Вывод: 1, 2, 3, 6, 7, 8
// 5. IsSubsetOf и IsSupersetOf — проверка на подмножество
HashSet<int> smallSet = new HashSet<int> { 1, 2 };
Console.WriteLine($"smallSet является подмножеством numbers1? {smallSet.IsSubsetOf(numbers1)}"); // True
Console.WriteLine($"numbers1 является надмножеством smallSet? {numbers1.IsSupersetOf(smallSet)}"); // True
}
}
Объяснение: HashSet — это коллекция, которая не допускает дубликатов. Она идеальна для операций над множествами: объединение, пересечение, разность. Все эти операции работают очень быстро, потому что HashSet использует хеширование.
Сравнение с Python: чем похоже и чем отличается
Если вы уже знакомы с Python, вам будет проще понять C#. Но есть важные отличия, которые нужно знать.
| Операция | C# | Python | Комментарий |
|---|---|---|---|
| Фильтрация | list.Where(x => x > 5) |
filter(lambda x: x > 5, list) или [x for x in list if x > 5] |
В C# используется лямбда-выражение, в Python — lambda или list comprehension |
| Преобразование | list.Select(x => x * 2) |
map(lambda x: x * 2, list) или [x * 2 for x in list] |
Аналогично фильтрации |
| Сортировка | list.OrderBy(x => x) |
sorted(list) |
В C# нужно явно указывать, по какому полю сортировать |
| Проверка наличия | list.Contains(5) |
5 in list |
В Python используется оператор in, в C# — метод |
| Объединение строк | string.Join(", ", list) |
", ".join(list) |
Почти одинаково, но |