LINQ (Language Integrated Query) в C#: Полное руководство для разработчиков игр

LINQ (Language Integrated Query) в C#: Полное руководство для разработчиков игр

LINQ — это набор инструментов в C# для удобной работы с коллекциями (списками, массивами). Он позволяет фильтровать, сортировать, группировать и преобразовывать данные в одну строку кода.

Например в стратегии это незаменимый инструмент для работы с инвентарем, юнитами, заданиями и ресурсами.

Что такое LINQ и зачем он нужен?

❌ Без LINQ (Много кода)

C#
List<Unit> selectedUnits = new List<Unit>();
foreach (var unit in allUnits)
{
    if (unit.IsSelected && unit.Health > 0)
    {
        selectedUnits.Add(unit);
    }
}
selectedUnits.Sort((a, b) => a.Level.CompareTo(b.Level));

✅ С LINQ (Одна строка)

C#
var selectedUnits = allUnits
    .Where(u => u.IsSelected && u.Health > 0)
    .OrderBy(u => u.Level)
    .ToList();

Выгода: Код короче, читаемее, меньше ошибок.

Подключение LINQ

В начале файла добавьте:

C#
using System.Linq;

Важно для Unity: LINQ создает небольшой overhead (аллокации памяти). В Update() с тысячами вызовов лучше использовать обычные циклы. Для UI, инвентаря, меню — LINQ идеален.

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

Фильтрация

Метод
Описание
Пример
Where()
Фильтрует коллекцию
.Where(u => u.Health > 0)
First()
Первый элемент
.First(u => u.IsSelected)
FirstOrDefault()
Первый или null
.FirstOrDefault(u => u.IsSelected)
Last()
Последний элемент
.Last(u => u.IsSelected)
Single()
Единственный элемент
.Single(u => u.IsLeader)
C#
// Найти всех живых юнитов
var aliveUnits = allUnits.Where(u => u.Health > 0).ToList();

// Найти первого выбранного юнита
var selectedUnit = allUnits.FirstOrDefault(u => u.IsSelected);

// Найти юнита по ID
var unit = allUnits.Single(u => u.Id == targetId);

Преобразование

Метод
Описание
Пример
Select()
Преобразует каждый элемент
.Select(u => u.Name)
SelectMany()
Flatten вложенных коллекций
.SelectMany(u => u.Inventory)
Cast<T>()
Приводит тип
.Cast<Soldier>()
OfType<T>()
Фильтрует по типу
.OfType<Building>()
C#
// Получить список имен всех юнитов
var names = allUnits.Select(u => u.Name).ToList();

// Получить все предметы из инвентарей всех юнитов
var allItems = allUnits.SelectMany(u => u.Inventory).ToList();

// Получить только здания
var buildings = allObjects.OfType<Building>().ToList();

Агрегация

Метод
Описание
Пример
Count()
Количество элементов
.Count(u => u.IsSelected)
Sum()
Сумма
.Sum(u => u.GoldCost)
Average()
Среднее
.Average(u => u.Level)
Min() / Max()
Мин/Макс
.Max(u => u.Power)
Any()
Есть ли хотя бы один
.Any(u => u.IsEnemy)
All()
Все ли соответствуют
.All(u => u.IsReady)
C#
// Сколько живых юнитов?
int aliveCount = allUnits.Count(u => u.Health > 0);

// Общая стоимость всех построек
int totalCost = buildings.Sum(b => b.Cost);

// Есть ли враги в радиусе?
bool hasEnemies = nearbyUnits.Any(u => u.IsEnemy);

// Все ли юниты готовы к атаке?
bool allReady = squad.All(u => u.IsReady);

// Средний уровень игроков в клане
float avgLevel = clanMembers.Average(m => m.Level);

Сортировка

Метод
Описание
Пример
OrderBy()
Сортировка по возрастанию
.OrderBy(u => u.Level)
OrderByDescending()
По убыванию
.OrderByDescending(u => u.Power)
ThenBy()
Дополнительная сортировка
.OrderBy(u => u.Type).ThenBy(u => u.Level)
C#
// Сортировать юнитов по уровню (сильные первые)
var sortedUnits = allUnits.OrderByDescending(u => u.Level).ToList();

// Сортировать по типу, затем по уровню
var sorted = allUnits
    .OrderBy(u => u.UnitType)
    .ThenByDescending(u => u.Level)
    .ToList();

Группировка

Метод
Описание
Пример
GroupBy()
Группирует по ключу
.GroupBy(u => u.Type)
C#
// Сгруппировать юнитов по типу
var grouped = allUnits.GroupBy(u => u.Type);

foreach (var group in grouped)
{
    Debug.Log($"Тип: {group.Key}, Количество: {group.Count()}");
    // group.Key = тип юнита
    // group = коллекция юнитов этого типа
}

// Получить количество юнитов каждого типа
var unitCounts = allUnits
    .GroupBy(u => u.Type)
    .ToDictionary(g => g.Key, g => g.Count());

Объединение

Метод
Описание
Пример
Union()
Объединение без дубликатов
list1.Union(list2)
Concat()
Объединение с дубликатами
list1.Concat(list2)
Intersect()
Общие элементы
list1.Intersect(list2)
Except()
Элементы из первого, которых нет во втором
list1.Except(list2)
C#
// Объединить два списка юнитов
var allTroops = infantry.Union(cavalry);

// Найти общих друзей между двумя игроками
var mutualFriends = player1.Friends.Intersect(player2.Friends);

// Найти юнитов, которых нет в отборе
var available = allUnits.Except(selectedUnits);

Практические примеры для стратегии

Инвентарь игрока

C#
// Найти все предметы редкости "Legendary"
var legendaryItems = inventory
    .Where(item => item.Rarity == Rarity.Legendary)
    .ToList();

// Посчитать общее количество золота во всех предметах
int totalGold = inventory
    .Where(item => item.Type == ItemType.Gold)
    .Sum(item => item.Amount);

// Найти самый дорогой предмет
var mostExpensive = inventory
    .OrderByDescending(item => item.Price)
    .FirstOrDefault();

Постройки

C#
// Найти все готовые постройки
var readyBuildings = buildings
    .Where(b => b.BuildTime <= Time.time)
    .ToList();

// Найти постройку, которая скоро завершится (в течение 5 минут)
var almostReady = buildings
    .Where(b => b.BuildTime > Time.time && b.BuildTime <= Time.time + 300)
    .OrderBy(b => b.BuildTime)
    .FirstOrDefault();

// Посчитать общую мощь всех зданий
int totalPower = buildings.Sum(b => b.Power);

Боевая система

C#
// Найти всех врагов в радиусе атаки
var enemiesInRange = allUnits
    .Where(u => u.IsEnemy && Vector3.Distance(u.Position, myPosition) <= attackRange)
    .OrderBy(u => u.Health) // Сначала слабых
    .ToList();

// Найти ближайшего врага
var nearestEnemy = allUnits
    .Where(u => u.IsEnemy)
    .OrderBy(u => Vector3.Distance(u.Position, myPosition))
    .FirstOrDefault();

// Проверить, есть ли живые союзники рядом
bool hasAlliesNearby = allUnits
    .Any(u => u.IsAlly && u.Health > 0 && Vector3.Distance(u.Position, myPosition) <= 10f);

Квесты и достижения

C#
// Проверить выполнение квеста "Убить 10 врагов"
bool questComplete = completedQuests
    .Any(q => q.Type == QuestType.KillEnemies && q.Count >= 10);

// Получить все доступные квесты (не начатые и не завершенные)
var availableQuests = allQuests
    .Where(q => q.Status == QuestStatus.Available && q.LevelRequirement <= player.Level)
    .ToList();

// Найти квест с наибольшей наградой
var bestRewardQuest = availableQuests
    .OrderByDescending(q => q.RewardGold)
    .FirstOrDefault();

Друзья и клан

C#
// Найти друзей онлайн
var onlineFriends = friends
    .Where(f => f.IsOnline)
    .OrderByDescending(f => f.LastLogin)
    .ToList();

// Посчитать общую мощь клана
int clanPower = clanMembers.Sum(m => m.Power);

// Найти лидеров клана (топ-3 по мощи)
var leaders = clanMembers
    .OrderByDescending(m => m.Power)
    .Take(3)
    .ToList();

Отложенное выполнение (Deferred Execution)

Важно! LINQ запросы не выполняются сразу, а только при обращении к результату.

C#
// Запрос создан, но НЕ выполнен
var query = allUnits.Where(u => u.Health > 0);

// Изменили данные
allUnits[0].Health = 0;

// Запрос выполнится ЗДЕСЬ с уже обновленными данными
var result = query.ToList();

Чтобы выполнить сразу — используйте .ToList() или .ToArray():

C#
var result = allUnits.Where(u => u.Health > 0).ToList(); // Выполняется сразу

Производительность LINQ в играх

⚠️ Проблемы

  1. Аллокации памяти: Каждый .Where(), .Select(), .ToList() создает новые объекты.
  2. GC (Garbage Collector): Частые аллокации вызывают сборку мусора → фризы.
  3. Медленнее циклов: LINQ примерно в 2-3 раза медленнее обычных foreach.

✅ Когда использовать LINQ

Ситуация
LINQ
Цикл
UI, меню, инвентарь
✅ Да
❌ Избыточно
Сохранение/загрузка
✅ Да
❌ Избыточно
Запросы к серверу
✅ Да
❌ Избыточно
Update() каждый кадр
❌ Нет
✅ Да
Тысячи юнитов в бою
❌ Нет
✅ Да
Прототипирование
✅ Да
⚠️ Дольше

🚫 Избегайте в Update()

C#
// ❌ ПЛОХО: Вызывается 60 раз в секунду
void Update()
{
    var aliveUnits = allUnits.Where(u => u.Health > 0).ToList();
    // Аллокация памяти каждый кадр!
}

// ✅ ХОРОШО: Кэшируем результат
private List<Unit> _aliveUnits;
private float _cacheTime;

void Update()
{
    if (Time.time - _cacheTime > 1f) // Обновляем раз в секунду
    {
        _aliveUnits = allUnits.Where(u => u.Health > 0).ToList();
        _cacheTime = Time.time;
    }
}

// ✅ ЛУЧШЕ: Обычный цикл без аллокаций
void Update()
{
    foreach (var unit in allUnits)
    {
        if (unit.Health > 0)
        {
            unit.Process();
        }
    }
}

LINQ без аллокаций (Unity LINQ)

Для критичных к производительности мест используйте:

Вариант 1: Обычный цикл

C#
List<Unit> GetAliveUnits(List<Unit> units)
{
    var result = new List<Unit>();
    for (int i = 0; i < units.Count; i++)
    {
        if (units[i].Health > 0)
        {
            result.Add(units[i]);
        }
    }
    return result;
}

Вариант 2: Unity.Collections (ECS)

C#
using Unity.Collections;

NativeList<Unit> aliveUnits = new NativeList<Unit>(Allocator.Temp);
foreach (var unit in allUnits)
{
    if (unit.Health > 0)
    {
        aliveUnits.Add(unit);
    }
}

Вариант 3: Библиотеки без аллокаций

  • Unity.Linq — LINQ без аллокаций
  • LinqAF — LINQ без аллокаций
  • Collections-Pooled — Пулы для коллекций

Расширенные возможности LINQ

Цепочка методов (Method Chaining)

C#
var result = allUnits
    .Where(u => u.IsSelected)           // 1. Фильтруем
    .Where(u => u.Health > 0)           // 2. Еще фильтр
    .OrderByDescending(u => u.Power)    // 3. Сортируем
    .Take(5)                            // 4. Берем топ-5
    .Select(u => u.Id)                  // 5. Преобразуем
    .ToList();                          // 6. В список

LINQ Query Syntax (Альтернативный синтаксис)

C#
// Method Syntax (более популярный)
var result = allUnits.Where(u => u.Health > 0).OrderBy(u => u.Level).ToList();

// Query Syntax (похож на SQL)
var result = (from u in allUnits
              where u.Health > 0
              orderby u.Level
              select u).ToList();

Группировка с агрегацией

C#
// Посчитать количество и общую мощь юнитов каждого типа
var stats = allUnits
    .GroupBy(u => u.Type)
    .Select(g => new 
    {
        Type = g.Key,
        Count = g.Count(),
        TotalPower = g.Sum(u => u.Power),
        AvgLevel = g.Average(u => u.Level)
    })
    .ToList();

foreach (var stat in stats)
{
    Debug.Log($"{stat.Type}: {stat.Count} юнитов, мощь {stat.TotalPower}");
}

Join (Объединение двух коллекций)

C#
// Соединить юнитов с их улучшениями
var unitsWithUpgrades = allUnits
    .Join(
        allUpgrades,
        unit => unit.Id,
        upgrade => upgrade.UnitId,
        (unit, upgrade) => new { Unit = unit, Upgrade = upgrade }
    )
    .ToList();

Частые ошибки

Ошибка
Проблема
Решение
.First() без проверки
Краш если элементов нет
Используйте .FirstOrDefault()
.Single() при 0 или 2+ элементах
Краш
Используйте .SingleOrDefault()
LINQ в Update()
Фризы из-за аллокаций
Используйте кэш или циклы
Забывают .ToList()
Отложенное выполнение, повторные вычисления
Добавляйте .ToList() в конце
Много цепочек
Сложно отлаживать
Разбивайте на переменные
C#
// ❌ ПЛОХО: Может крашнуть
var unit = allUnits.First(u => u.IsSelected);

// ✅ ХОРОШО: Безопасно
var unit = allUnits.FirstOrDefault(u => u.IsSelected);
if (unit != null) { /* ... */ }

// ❌ ПЛОХО: Запрос выполняется каждый раз при обращении
var query = allUnits.Where(u => u.Health > 0);
var count = query.Count(); // Выполняется
var list = query.ToList(); // Выполняется снова!

// ✅ ХОРОШО: Выполняется один раз
var list = allUnits.Where(u => u.Health > 0).ToList();
var count = list.Count;

Чек-лист для вашей игры

  1. UI и меню — ✅ Используйте LINQ freely
  2. Инвентарь — ✅ LINQ идеален для фильтрации предметов
  3. Сохранение данных — ✅ LINQ для преобразования перед сохранением
  4. Боевая логика — ⚠️ Осторожно, кэшируйте результаты
  5. Update() цикл — ❌ Избегайте LINQ, используйте обычные циклы
  6. Поиск путей, AI — ❌ Избегайте LINQ для производительности
  7. Запросы к серверу — ✅ LINQ для обработки ответов

Итоговая таблица методов

Категория
Методы
Фильтрация
Where(), First(), FirstOrDefault(), Single(), Take(), Skip()
Преобразование
Select(), SelectMany(), Cast(), OfType()
Агрегация
Count(), Sum(), Average(), Min(), Max(), Any(), All()
Сортировка
OrderBy(), OrderByDescending(), ThenBy(), Reverse()
Группировка
GroupBy(), ToDictionary(), ToLookup()
Объединение
Union(), Concat(), Intersect(), Except(), Join()
Преобразование
ToList(), ToArray(), ToDictionary(), IEnumerable()

Пример: Полный класс с LINQ для стратегии

C#
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class ArmyManager : MonoBehaviour
{
    public List<Unit> allUnits;

    // Получить армию игрока
    public List<Unit> GetPlayerArmy(string playerId)
    {
        return allUnits
            .Where(u => u.OwnerId == playerId && u.Health > 0)
            .ToList();
    }

    // Получить топ юнитов по мощи
    public List<Unit> GetTopUnits(int count)
    {
        return allUnits
            .Where(u => u.Health > 0)
            .OrderByDescending(u => u.Power)
            .Take(count)
            .ToList();
    }

    // Посчитать общую стоимость содержания армии
    public int GetTotalUpkeep(string playerId)
    {
        return allUnits
            .Where(u => u.OwnerId == playerId)
            .Sum(u => u.UpkeepCost);
    }

    // Найти юнитов, которых можно улучшить
    public List<Unit> GetUpgradeableUnits(string playerId, int maxCost)
    {
        return allUnits
            .Where(u => u.OwnerId == playerId && 
                        u.CanUpgrade && 
                        u.UpgradeCost <= maxCost)
            .OrderBy(u => u.UpgradeCost)
            .ToList();
    }

    // Проверить, есть ли армия игрока
    public bool HasArmy(string playerId)
    {
        return allUnits.Any(u => u.OwnerId == playerId && u.Health > 0);
    }

    // Сгруппировать юнитов по типу для UI
    public Dictionary<UnitType, int> GetUnitCounts(string playerId)
    {
        return allUnits
            .Where(u => u.OwnerId == playerId && u.Health > 0)
            .GroupBy(u => u.Type)
            .ToDictionary(g => g.Key, g => g.Count());
    }
}

LINQ — это мощный инструмент, который делает код чище и выразительнее. Он идеально подойдет для работы с инвентарем, постройками, квестами и UI. Главное — помните о производительности и не используйте LINQ в горячих циклах (Update(), бой, AI).

Оцените статью
Unity Learn
Добавить комментарий