Этот пост был переведён автоматически. Оригинальную версию можно найти здесь.
Это та самая фича, которую я ждал. C# 15 вводит ключевое слово union — настоящие размеченные объединения с исчерпывающим сопоставлением с образцом, контролируемым компилятором. Если вы когда-нибудь завидовали размеченным объединениям F# или enum в Rust, вы точно знаете, почему это важно.
Билл Вагнер опубликовал подробный разбор в блоге .NET, и честно? Дизайн чистый, практичный и очень в духе C#. Давайте я покажу, что тут на самом деле есть и почему это более значимо, чем может показаться на первый взгляд.
Проблема, которую решают объединения
До C# 15 возврат «одного из нескольких возможных типов» из метода всегда был компромиссом:
object— никаких ограничений, никакой помощи от компилятора, защитное приведение типов повсюду- Маркерные интерфейсы — лучше, но любой может их реализовать. Компилятор никогда не может считать набор полным
- Абстрактные базовые классы — та же проблема, плюс типам нужен общий предок
Ничто из этого не даёт того, что действительно нужно: замкнутого набора типов, где компилятор гарантирует, что вы обработали каждый случай. Именно это делают типы-объединения.
Синтаксис красиво прост
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);
Одна строка. Pet может содержать Cat, Dog или Bird. Неявные преобразования генерируются автоматически:
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // Dog { Name = Rex }
А вот и магия — компилятор обеспечивает исчерпывающее сопоставление:
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Дискард _ не нужен. Компилятор знает, что этот switch покрывает все возможные случаи. Если позже вы добавите четвёртый тип в объединение, каждое выражение switch, которое его не обрабатывает, выдаст предупреждение. Пропущенные случаи обнаруживаются на этапе сборки, а не в рантайме.
Где это становится практичным
Пример с Pet милый, но вот где объединения действительно сияют в реальном коде.
API-ответы, возвращающие разные формы
public union ApiResult<T>(T, ApiError, ValidationFailure);
Теперь каждый потребитель вынужден обрабатывать успех, ошибку и ошибку валидации. Больше никаких багов «забыл проверить случай ошибки».
Одно значение или коллекция
Паттерн OneOrMore<T> показывает, как объединения могут иметь тело со вспомогательными методами:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}
Вызывающий код передаёт удобную форму:
OneOrMore<string> tags = "dotnet";
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
foreach (var tag in tags.AsEnumerable())
Console.Write($"[{tag}] ");
// [dotnet]
Композиция несвязанных типов
Это убойная фича по сравнению с традиционными иерархиями. Можно объединять типы, не имеющие ничего общего — string и Exception, int и IEnumerable<T>. Общий предок не нужен.
Пользовательские объединения для существующих библиотек
Вот умное решение в дизайне: любой класс или struct с атрибутом [Union] распознаётся как тип-объединение, если он следует базовому паттерну (публичные конструкторы для типов-случаев и свойство Value). Библиотеки вроде OneOf, которые уже предоставляют типы, похожие на объединения, могут подключить поддержку компилятора без переписывания внутренней реализации.
Для чувствительных к производительности сценариев с типами-значениями библиотеки могут реализовать паттерн доступа без boxing через методы HasValue и TryGetValue.
Общая картина
Типы-объединения — часть более широкой истории исчерпывающей проверки в C#:
- Типы-объединения — исчерпывающее сопоставление по замкнутому набору типов (доступно сейчас в preview)
- Закрытые иерархии — модификатор
closedпредотвращает создание производных классов вне определяющей сборки (предложено) - Закрытые enum — предотвращает создание значений, отличных от объявленных членов (предложено)
Вместе эти три фичи дадут C# одну из самых полных систем типобезопасного сопоставления с образцом среди всех мейнстримных языков.
Попробуйте сегодня
Типы-объединения доступны в .NET 11 Preview 2:
- Установите .NET 11 Preview SDK
- Укажите
net11.0в качестве целевой платформы проекта - Установите
<LangVersion>preview</LangVersion>
Одно замечание: в Preview 2 вам нужно объявить UnionAttribute и IUnion в проекте, так как их ещё нет в рантайме. Возьмите RuntimePolyfill.cs из репозитория документации, или добавьте это:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Подводя итог
Типы-объединения — одна из тех фич, которые заставляют задуматься, как мы обходились без них. Исчерпывающее сопоставление, контролируемое компилятором, чистый синтаксис, поддержка дженериков и интеграция с существующим pattern matching — это всё, что мы просили, реализованное в стиле C#.
Попробуйте их в .NET 11 Preview 2, ломайте вещи и делитесь обратной связью на GitHub. Это preview, и команда C# активно слушает. Ваши граничные случаи и отзывы о дизайне повлияют на финальный релиз.
Полную языковую справку смотрите в документации по типам-объединениям и спецификации фичи.
