Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример реализации возврата состояния в C#
В C# существует несколько основных паттернов и подходов для возврата состояния, каждый из которых применяется в разных контекстах. Рассмотрим наиболее распространенные варианты с примерами.
1. Возврат через кортежи (ValueTuple)
Кортежи, появившиеся в C# 7.0, позволяют легко возвращать несколько значений, включая состояние операции:
public (bool Success, string ErrorMessage, int? Result) DivideNumbers(int a, int b)
{
if (b == 0)
return (false, "Division by zero is not allowed", null);
return (true, null, a / b);
}
// Использование
var (success, error, result) = DivideNumbers(10, 2);
if (success)
Console.WriteLine($"Result: {result}");
else
Console.WriteLine($"Error: {error}");
Преимущества: простота, встроенная поддержка языка, деконструкция. Недостатки: отсутствие семантической ясности, сложность масштабирования.
2. Классы-обертки результата операции
Создание специализированных классов для возврата состояния - более объектно-ориентированный подход:
public class OperationResult<T>
{
public bool IsSuccess { get; }
public string ErrorMessage { get; }
public T Value { get; }
private OperationResult(bool isSuccess, T value, string errorMessage)
{
IsSuccess = isSuccess;
Value = value;
ErrorMessage = errorMessage;
}
public static OperationResult<T> Success(T value) =>
new OperationResult<T>(true, value, null);
public static OperationResult<T> Failure(string errorMessage) =>
new OperationResult<T>(false, default, errorMessage);
}
// Использование
public OperationResult<int> CalculateDiscount(int price, int discountPercent)
{
if (price <= 0)
return OperationResult<int>.Failure("Price must be positive");
if (discountPercent < 0 || discountPercent > 100)
return OperationResult<int>.Failure("Discount must be between 0 and 100");
int finalPrice = price - (price * discountPercent / 100);
return OperationResult<int>.Success(finalPrice);
}
var result = CalculateDiscount(1000, 20);
if (result.IsSuccess)
Console.WriteLine($"Final price: {result.Value}");
else
Console.WriteLine($"Error: {result.ErrorMessage}");
Преимущества: типобезопасность, расширяемость, семантическая ясность. Недостатки: больше кода, необходимость создания классов.
3. Паттерн Result из функционального программирования
Более продвинутый подход, вдохновленный функциональным программированием:
public abstract class Result<T>
{
public abstract bool IsSuccess { get; }
public abstract T Value { get; }
public abstract string Error { get; }
public static Result<T> Ok(T value) => new SuccessResult<T>(value);
public static Result<T> Fail(string error) => new FailureResult<T>(error);
}
public class SuccessResult<T> : Result<T>
{
private readonly T _value;
public SuccessResult(T value) => _value = value;
public override bool IsSuccess => true;
public override T Value => _value;
public override string Error => throw new InvalidOperationException("Success result has no error");
}
public class FailureResult<T> : Result<T>
{
private readonly string _error;
public FailureResult(string error) => _error = error;
public override bool IsSuccess => false;
public override T Value => throw new InvalidOperationException("Failure result has no value");
public override string Error => _error;
}
// Использование с методами расширения
public static class ResultExtensions
{
public static Result<TOut> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Result<TOut>> func)
{
return result.IsSuccess ? func(result.Value) : Result<TOut>.Fail(result.Error);
}
public static Result<TOut> Map<TIn, TOut>(
this Result<TIn> result,
Func<TIn, TOut> func)
{
return result.IsSuccess
? Result<TOut>.Ok(func(result.Value))
: Result<TOut>.Fail(result.Error);
}
}
public Result<int> ParseInt(string input)
{
return int.TryParse(input, out var number)
? Result<int>.Ok(number)
: Result<int>.Fail($"Cannot parse '{input}' as integer");
}
public Result<int> DoubleValue(int value) => Result<int>.Ok(value * 2);
// Композиция операций
var result = ParseInt("10")
.Bind(DoubleValue)
.Map(x => x + 5);
if (result.IsSuccess)
Console.WriteLine($"Result: {result.Value}");
Преимущества: поддержка функциональной композиции, монадические операции (Bind, Map), исключение исключений для ошибок. Недостатки: сложность для новичков, больше boilerplate кода.
4. Использование исключений для ошибок состояния
Традиционный подход для критических ошибок:
public class OrderProcessor
{
public decimal CalculateTotal(Order order)
{
if (order == null)
throw new ArgumentNullException(nameof(order));
if (order.Items.Count == 0)
throw new InvalidOperationException("Order must contain at least one item");
if (order.Customer.IsBlocked)
throw new CustomerBlockedException(order.Customer.Id);
return order.Items.Sum(item => item.Price * item.Quantity);
}
}
public class CustomerBlockedException : Exception
{
public int CustomerId { get; }
public CustomerBlockedException(int customerId)
: base($"Customer with ID {customerId} is blocked")
{
CustomerId = customerId;
}
}
// Обработка
try
{
var total = processor.CalculateTotal(order);
Console.WriteLine($"Total: {total}");
}
catch (CustomerBlockedException ex)
{
Console.WriteLine($"Customer blocked: {ex.Message}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Invalid operation: {ex.Message}");
}
Преимущества: стандартный механизм .NET, прерывание потока выполнения, stack trace для отладки. Недостатки: накладные расходы на performance, не для бизнес-логики (лучше для exceptional ситуаций).
5. Использование OneOf библиотек
Для сложных сценариев с множественными типами результатов:
// Пример с библиотекой OneOf
// Установка: Install-Package OneOf
public class ValidationService
{
public OneOf<SuccessResult, NotFoundError, ValidationError> ValidateUser(User user)
{
if (user == null)
return new NotFoundError("User not found");
if (string.IsNullOrEmpty(user.Email))
return new ValidationError("Email is required");
if (user.Age < 18)
return new ValidationError("User must be at least 18 years old");
return new SuccessResult(user.Id);
}
}
public record SuccessResult(int UserId);
public record NotFoundError(string Message);
public record ValidationError(string Message);
// Использование
var result = ValidateUser(user);
result.Switch(
success => Console.WriteLine($"User ID: {success.UserId}"),
notFound => Console.WriteLine($"Not found: {notFound.Message}"),
validation => Console.WriteLine($"Validation failed: {validation.Message}")
);
Рекомендации по выбору подхода:
- Для простых сценариев с 1-2 возвращаемыми значениями - используйте кортежи
- Для бизнес-логики с необходимостью обработки ошибок - классы-обертки или паттерн Result
- Для критических, исключительных ситуаций - исключения
- Для сложных сценариев с множественными типами результатов - OneOf или аналоги
- В ASP.NET Core Web API - используйте встроенный
IActionResultсOk(),BadRequest(),NotFound()и т.д.
Наиболее современным и гибким подходом считается паттерн Result, особенно в комбинации с CQRS и MediatR, где он позволяет создавать чистые, тестируемые обработчики команд и запросов без побочных эффектов в виде исключений для бизнес-ошибок.