.First vs .FirstOrDefault в C#: Когда что выбирать?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Знакомый страх каждого C# разработчика

Представьте ситуацию: вы пишете запрос к базе данных или коллекции в памяти. Вам нужно получить первый элемент, который удовлетворяет условию. Вы уверены, что такой элемент обязательно есть. Или не уверены? Вот тут и начинается танец с бубном: .First() или .FirstOrDefault()?

Этот вопрос — настоящий хит на Stack Overflow с тысячами просмотров. И не зря. Выбор между этими двумя методами может означать разницу между чистым, предсказуемым кодом и внезапным InvalidOperationException в Production. Давайте раз и навсегда разберемся, когда какой метод использовать, и навсегда забудем о сомнениях.

Коротко о главном: В чем разница?

Оба метода делают почти одно и то же: возвращают первый элемент последовательности, удовлетворяющий условию (или просто первый элемент, если условие не задано). Но есть критическое отличие:

  • .First() — если элемент не найден, выбрасывает исключение System.InvalidOperationException.
  • .FirstOrDefault() — если элемент не найден, возвращает значение по умолчанию для типа (null для ссылочных типов, 0 для int, false для bool и т.д.).

Казалось бы, все просто. Но дьявол, как всегда, кроется в деталях.

Когда использовать .First(): Когда "должен быть, и точка"

Используйте .First(), когда ваша бизнес-логика строго гарантирует, что элемент существует. Если его нет — это исключительная ситуация, ошибка в данных или логике приложения.

Пример 1: Поиск по уникальному идентификатору

Если вы ищете пользователя по его Primary Key в базе данных (например, по Id), и уверены, что такой пользователь есть, используйте .First():

public class UserService
{
    private readonly AppDbContext _context;

    public UserService(AppDbContext context)
    {
        _context = context;
    }

    public User GetUserById(int userId)
    {
        // Мы ожидаем, что пользователь с таким ID существует.
        // Если нет — это серьезная проблема, пусть лучше упадет исключение.
        return _context.Users.First(u => u.Id == userId);
    }
}

Исключение в этом случае — это сигнал тревоги. Оно говорит: "Эй, данные повреждены, или кто-то передал неверный ID". Вы поймаете это исключение выше по стеку и обработаете как критическую ошибку.

Пример 2: Первый элемент после сортировки (гарантированно)

Если вы сортируете список и берете первый элемент, и в списке точно есть элементы:

var highestScore = players
    .OrderByDescending(p => p.Score)
    .First(); // Игроков 0 быть не может, мы проверили это ранее

Если вы предварительно проверили, что коллекция не пуста (например, if (players.Any())), то .First() — ваш выбор.

Когда использовать .FirstOrDefault(): Когда элемента может не быть

Золотое правило: Если отсутствие результата — это нормальная ситуация, а не ошибка, используйте .FirstOrDefault().

Пример 1: Опциональный поиск

Представьте, что мы ищем заказ по номеру, но заказ может быть еще не создан:

public Order FindOrderByNumber(string orderNumber)
{
    // Заказ может не существовать. Это нормально.
    return _context.Orders
        .FirstOrDefault(o => o.OrderNumber == orderNumber);
    // Вернет null, если заказ не найден.
}

После вызова этого метода мы обязаны проверить результат на null:

var order = FindOrderByNumber("ORD-12345");
if (order != null)
{
    // Работаем с заказом
    Console.WriteLine($"Заказ найден: {order.Id}");
}
else
{
    Console.WriteLine("Заказ не найден. Создаем новый.");
}

Пример 2: Поиск с условием, которое может не выполниться

Допустим, мы ищем первый товар со скидкой больше 50%:

var bigDiscountProduct = products
    .FirstOrDefault(p => p.DiscountPercent > 50);

if (bigDiscountProduct != null)
{
    Console.WriteLine($"Суперскидка на {bigDiscountProduct.Name}!");
}
else
{
    Console.WriteLine("Товаров с такой большой скидкой нет.");
}

Здесь отсутствие результата — абсолютно нормальная ситуация. Не нужно паниковать и кидать исключение.

Типичные ошибки (и как их избежать)

Ошибка 1: Слепое использование .First() везде

Это путь к InvalidOperationException в самый неподходящий момент. Помните: если вы пишете .First(), вы даете обещание компилятору и коллегам: "Элемент гарантированно есть". Не давайте ложных обещаний.

Ошибка 2: Игнорирование null после .FirstOrDefault()

// ПЛОХО: Забыли проверить на null
var user = users.FirstOrDefault(u => u.Email == email);
Console.WriteLine(user.Name); // NullReferenceException, если user == null

// ХОРОШО: Проверяем
var user = users.FirstOrDefault(u => u.Email == email);
if (user != null)
{
    Console.WriteLine(user.Name);
}
else
{
    // Обработка ситуации
}

Ошибка 3: Использование .FirstOrDefault() для значимых типов (Value Types)

Для int, decimal, DateTime и других структур значение по умолчанию — 0 или default. Это может быть валидным значением!

var firstEvenNumber = numbers.FirstOrDefault(n => n % 2 == 0);
// Если чисел нет, вернет 0. Но 0 — это четное число!
// Как отличить "нет результата" от "результат равен 0"?

// Решение для значимых типов: используйте nullable
int? firstEvenNumber = numbers
    .FirstOrDefault(n => n % 2 == 0); // Вернет null, если нет четных

if (firstEvenNumber.HasValue)
{
    Console.WriteLine($"Первое четное число: {firstEvenNumber.Value}");
}
else
{
    Console.WriteLine("Четных чисел нет.");
}

Продвинутые сценарии: Entity Framework и производительность

При работе с базами данных через Entity Framework Core выбор между .First() и .FirstOrDefault() также важен, но с точки зрения семантики, а не производительности. Оба метода генерируют SQL-запрос с SELECT TOP 1. Разница только в том, что .First() дополнительно проверяет, что результат не пустой, на стороне C#, и кидает исключение.

Совет для EF Core: Если вы используете .FirstOrDefault() и знаете, что запрос может вернуть null, всегда обрабатывайте этот случай. Если вы используете .First(), будьте готовы к исключению на уровне сервиса.

Альтернатива: Single() и SingleOrDefault()

Блоги

Рекомендаций по книгам