Result pattern и обработка ошибок без исключений

↑ 8.1 Принципы и паттерны проектирования в C♯ · ← Предыдущая · → Следующая

Обязательно~4 мин чтенияУровень: senior

Зачем это на собесе

Различие исключений и ожидаемых ошибок — признак зрелого дизайна.

Объяснение

Исключения для исключительных ситуаций (сбой БД, баг). Для ожидаемых исходов (не найдено, невалидные данные, конфликт) удобнее возвращать результат.

public readonly record struct Error(string Code, string Message);
 
public class Result<T>
{
    public T? Value { get; }
    public Error? Error { get; }
    public bool IsSuccess => Error is null;
    private Result(T? v, Error? e) { Value = v; Error = e; }
    public static Result<T> Ok(T value) => new(value, null);
    public static Result<T> Fail(Error error) => new(default, error);
    public TOut Match<TOut>(Func<T, TOut> ok, Func<Error, TOut> fail) => IsSuccess ? ok(Value!) : fail(Error!.Value);
}
 
public async Task<Result<Order>> CancelAsync(Guid id, CancellationToken ct)
{
    var order = await repo.GetAsync(id, ct);
    if (order is null) return Result<Order>.Fail(new("order.not_found", "Заказ не найден"));
    if (order.Status == Status.Shipped) return Result<Order>.Fail(new("order.shipped", "Заказ уже отправлен"));
    order.Cancel();
    return Result<Order>.Ok(order);
}
 
// endpoint
return (await svc.CancelAsync(id, ct)).Match(o => Results.Ok(o.ToDto()), e => e.Code switch
{
    "order.not_found" => Results.NotFound(),
    _ => Results.Conflict(new ProblemDetails { Title = e.Message }),
});
ПодходПлюсыМинусы
Исключенияменьше кода на «счастливом» пути, стекдорого, скрытый поток управления, забывают ловить
Resultошибки видны в сигнатуре, дешёвыемногословнее
Discriminated union (OneOf, паттерн-матчинг)выразительные вариантыбиблиотека
Кортежи/TryXxxдля простых случаев

Библиотеки: ErrorOr, FluentResults, OneOf, LanguageExt.

Нюансы и подводные камни

  • Не смешивайте стили: часть ошибок исключениями, часть Result — определите правило.
  • Result не заменяет исключения для инфраструктурных сбоев.
  • Игнорирование Result (_ = await svc...) молча теряет ошибку — используйте анализаторы.
  • Исключения дороги (стек-трейс): не используйте их в горячих путях для управления потоком.

Практика

  1. Перепишите сервис на Result и сопоставьте ошибки со статус-кодами.
  2. Замерьте стоимость исключений против Result в BenchmarkDotNet.
  3. Введите единый каталог кодов ошибок.

Вопросы с ответами

Связанные темы