· · 4 минут чтения

C# 15 получает типы-объединения — и это именно то, что мы просили

C# 15 вводит ключевое слово union — размеченные объединения с исчерпывающим сопоставлением с образцом, контролируемым компилятором. Вот как они выглядят, почему это важно и как попробовать их уже сегодня.

csharp dotnet union-types pattern-matching dotnet-11 language-features
Эта статья также доступна на:English, Español, Deutsch, Français, Português, Italiano, 日本語, 中文, 한국어

Этот пост был переведён автоматически. Оригинальную версию можно найти здесь.

Это та самая фича, которую я ждал. 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:

  1. Установите .NET 11 Preview SDK
  2. Укажите net11.0 в качестве целевой платформы проекта
  3. Установите <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# активно слушает. Ваши граничные случаи и отзывы о дизайне повлияют на финальный релиз.

Полную языковую справку смотрите в документации по типам-объединениям и спецификации фичи.

Поделиться:
Просмотреть исходный код этой статьи на GitHub ↗
← Aspire 13.2 получает CLI для документации — и ваш ИИ-агент тоже может им пользоваться
VS Code 1.115 — Уведомления фонового терминала, режим SSH-агента и другое →