Дискриминируемые Объединения в C#
Дискриминируемые объединения (discriminated unions) – один из давних запросов на включение в язык C#. В F# они используются давно, а C#-разработчикам придется подождать их ещё немного.
Дискриминируемые объединения позволяют сообщить компилятору (и другим инструментам, таким как IDE), что данные могут быть одним из ряда предопределённых типов. Например, у вас может быть метод RegisterUser(), который возвращает класс User, UserAlreadyExists или InvalidUsername. Эти классы не обязаны наследовать друг от друга. Вы просто хотите поддерживать 3 потенциальных типа возврата, сообщить об этом языку и получать ошибки компилятора, если возвращаете другой тип.
В минимальных API ASP.NET Core типы Results<> и TypedResults позволяют определить, какие типы объектов могут быть возвращены. Например, здесь может быть возвращён тип результат ApiItem или Unauthorized:
app.MapGet("/item/{id}",
async Task<Results<Ok<ApiItem>, Unauthorized>>(
[FromRoute]int id,
GroceryListDb db) => {
// …
return TypedResults.Ok(item);
});
Тип Results<> по сути представляет собой дискриминируемое объединение: возвращаемое значение будет одним из (в данном случае) двух типов, и механизм минимальных API может использовать эту информацию для возврата правильного типа.Упрощённо класс Results с поддержкой 2 типов выглядит так:
public class Results<TResult1, TResult2>Очевидно, что аналогично выглядят классы для 3, 4 и т.д. типов результата. Используя неявные операторы, класс Results может быть создан из любого типа, поддерживающего преобразования.
{
private Results(object result)
{
Result = result;
}
public object Result { get; }
public static implicit operator
Results<TResult1, TResult2>(TResult1 result)
=> new(result);
public static implicit operator
Results<TResult1, TResult2>(TResult2 result)
=> new(result);
}
Вы можете использовать аналогичный класс в вашем коде, например, для создания метода, который может возвращать int, bool или строку, но ничего больше:
Results<int, bool, string> GetData()Если тип результата будет другим, IDE (и компилятор) сообщат вам об ошибке. Даже сопоставление по шаблону доступно (для свойства Result):
=> "Hello, world!";
var data = GetData();Недостатком является то, что если вы измените метод GetData(), чтобы он возвращал один из 4 типов (вместо 3), вы не получите ошибку компиляции в приведенном выше выражении switch. Это должно быть одним из преимуществ дискриминируемых объединений (если их когда-либо реализуют в языке): возможность получить инструментальную поддержку для этих случаев, информирующую вас о том, что у вас нет исчерпывающего соответствия для всех типов. Для минимальных API класс Results<> работает отлично. Использование результата является частью механики фреймворка, и в идеале вам никогда не придётся проводить исчерпывающее сравнение самостоятельно.
var type = data.Result switch
{
int => "int",
bool => "bool",
string => "string",
_ => throw new NotImplementedException()
};
Console.WriteLine(type);
Вне минимальных API вы можете использовать пакет OneOf, который позволяет работать с дискриминируемыми объединениями, выдаёт ошибки компилятора, когда сравнения не являются исчерпывающими, и т. д.
Источник: https://blog.maartenballiauw.be/post/2023/09/18/discriminated-unions-in-csharp.html