Что нового в C# 15. Union, closed, коллекции и безопасность памяти

7550
Что нового в C# 15. Union, closed, коллекции и безопасность памяти

Метод возвращает либо результат операции, либо ошибку. Разработчик создаёт базовый класс, несколько наследников и пишет switch, чтобы обработать все варианты. Через полгода коллега добавляет ещё один тип результата, забывает обновить старый обработчик, и программа начинает вести себя непредсказуемо. Компилятор вполне мог бы заметить проблему заранее, но раньше ему не хватало информации о допустимых типах.

В C# 15 подобные задачи решаются непосредственно средствами языка. Объединения union позволяют перечислить допустимые типы значений, а модификатор closed ограничивает наследование и помогает проверять полноту switch. Появились индексаторы расширений, новые способы создавать коллекции и удобные переходы между вложенными циклами. Отдельное направление связано с пересмотром правил безопасности памяти.

Главная идея C# 15 заключается в том, чтобы перенести больше проверок из работающей программы в компилятор. Разработчик точнее описывает возможные состояния, а ошибки обнаруживаются ещё при сборке. Но у нового синтаксиса есть ограничения. Union не гарантирует отсутствие выделений памяти, closed не делает всю иерархию автоматически закрытой, а обновлённая модель unsafe пока остаётся экспериментальной.

На момент подготовки обзора доступна .NET 11 RC2, выпущенная 13 октября 2026 года. Начиная с RC1, C# 15 выбрана версией языка по умолчанию для проектов под .NET 11. Основные возможности уже стабилизированы на уровне языка, но финальный выпуск .NET 11 запланирован на 10 ноября. RC2 имеет статус go-live и официально допускается к производственному применению, хотя для новых проектов без необходимости в предварительной версии разумнее дождаться окончательного релиза.

Как установить C# 15 и проверить новую версию языка

Для экспериментов потребуется .NET 11 SDK RC2. Его можно загрузить на сайте .NET для Windows, Linux или macOS. Подойдут Visual Studio 2026 Insiders и Visual Studio Code с расширением C# Dev Kit. При работе из терминала отдельная среда разработки не обязательна.

Проверим установленный SDK.

dotnet --list-sdks

В списке должна появиться версия, начинающаяся с 11.0.100-rc.2. Если установлено несколько SDK, команда покажет каждый из них.

Создадим консольное приложение.

dotnet new console -n CSharp15Demo -f net11.0
 
 cd CSharp15Demo
 
 dotnet run

В файле проекта CSharp15Demo.csproj будет указана целевая платформа net11.0. Для использования основных новшеств дополнительно включать предварительную версию языка не требуется.

<PropertyGroup>
     <TargetFramework>net11.0</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
 </PropertyGroup>

При желании C# 15 можно явно закрепить через параметр <LangVersion>15.0</LangVersion>. В обычном проекте под .NET 11 компилятор и без него выберет нужную версию.

Чтобы узнать фактическую версию компилятора, временно добавьте в Program.cs директиву.

#error version

После команды dotnet build компилятор выдаст диагностическое сообщение CS8304 с версией C# и номером компилятора. Затем удалите директиву.

Использовать LangVersion=latest в командных проектах нежелательно. После установки нового SDK значение может измениться, и разные разработчики получат разные результаты сборки. Для воспроизводимости лучше фиксировать SDK через global.json и выбирать конкретную версию языка, когда такая настройка необходима.

Union и closed. Главные изменения C# 15

Union позволяет объединить несколько типов без наследования

Предположим, платёжная система возвращает один из двух результатов. Успешная операция содержит номер квитанции, неудачная сообщает причину отказа.

Раньше разработчик мог вернуть object, завести общий интерфейс, создать базовый класс или подключить библиотеку объединений типов. Вариант с object особенно неудобен. Компилятор разрешает передать практически любое значение, даже когда метод должен работать только с двумя конкретными типами.

C# 15 добавляет специальную конструкцию union.

ChargeResult result = new Paid("R-1024");
 
 string message = result switch
 {
     Paid(var receipt) => $"Оплачено: {receipt}",
     Declined(var reason) => $"Отказ: {reason}"
 };
 
 Console.WriteLine(message);
 
 public sealed record Paid(string Receipt);
 public sealed record Declined(string Reason);
 
 public union ChargeResult(Paid, Declined);

Строка public union ChargeResult(Paid, Declined) задаёт объединение двух вариантов. Компилятор автоматически создаёт преобразования из Paid и Declined в ChargeResult.

Теперь нельзя случайно присвоить переменной ChargeResult произвольное число или строку. Значение должно соответствовать одному из объявленных вариантов.

Важнее другое. Компилятор знает перечень допустимых типов и проверяет switch expression на полноту. Если добавить третий вариант, например Pending, все обработчики, пропустившие новый тип, получат предупреждение CS8509.

Без дополнительной настройки предупреждение не останавливает сборку. Для проекта, где каждый вариант результата обязан обрабатываться, диагностику можно превратить в ошибку компиляции.

<PropertyGroup>
     <WarningsAsErrors>$(WarningsAsErrors);CS8509</WarningsAsErrors>
 </PropertyGroup>

После такой настройки разработчик не сможет проигнорировать неполное switch expression и успешно собрать проект.

Как union устроен внутри. Объявление union не создаёт несколько независимых полей под каждый вариант. Компилятор генерирует структуру, которая реализует IUnion и хранит активное значение через свойство Value типа object?.

У такого решения есть цена. Если объединение содержит int, double или другую структуру, сохранение значения требует boxing, то есть упаковки значимого типа в объект. Поэтому стандартный union не всегда подходит для участков кода, где критичны количество выделений памяти и нагрузка на сборщик мусора.

Для производительных библиотек предусмотрена возможность создавать пользовательские объединения с атрибутом UnionAttribute. Собственная реализация может хранить значения без упаковки, предоставляя методы TryGetValue и свойство HasValue для эффективного сопоставления с образцом. Однако такой вариант потребует значительно больше кода, а компилятор будет полагаться на корректность написанной реализации.

Есть ещё одна особенность, которую легко пропустить. Сгенерированный union является структурой, поэтому у него существует значение по умолчанию.

ChargeResult empty = default;
 
 Console.WriteLine(empty is null); // True

Хотя ChargeResult является структурой, специальное поведение сопоставления с образцом проверяет внутреннее значение Value. У пустого объединения внутреннее значение равно null.

Поэтому фраза «компилятор проверяет все варианты» не означает, что программа в принципе не может получить пустое значение. При проектировании публичных методов, работе с массивами и десериализации нужно учитывать default и null. В зависимости от состояния значения компилятор может потребовать отдельную ветку.

Также объединение может содержать интерфейс или базовый класс. В таком случае фактический объект может принадлежать производному типу. Union ограничивает допустимые варианты с точки зрения системы типов, но не запрещает наследование внутри каждого варианта.

Практическая польза лучше всего проявляется в обработчиках команд, результатах операций, парсерах, обработке сообщений и клиентах API, когда набор альтернатив известен заранее.

Closed делает наследование контролируемым

Объединения типов удобны, когда существующие классы не связаны наследованием. Но иногда разработчику нужна обычная иерархия с общими свойствами и поведением.

Например, система обработки заданий может находиться в нескольких состояниях.

JobState state = new Processing(35);
 
 string description = state switch
 {
     Queued => "Ожидает выполнения",
     Processing(var percent) => $"Готово {percent}%",
     Completed => "Завершено"
 };
 
 Console.WriteLine(description);
 
 public closed record class JobState;
 
 public sealed record class Queued : JobState;
 public sealed record class Processing(int Percent) : JobState;
 public sealed record class Completed : JobState;

Ключевое слово closed ограничивает прямое наследование пределами сборки, в которой объявлен базовый класс. Сторонний проект не сможет создать собственного непосредственного наследника JobState.

Компилятор видит известные прямые подклассы и может проверить, обработал ли switch каждый вариант. Если разработчик добавит новое состояние в исходную сборку, после повторной компиляции обработчики получат соответствующую диагностику.

Closed отличается от привычного sealed. Последний полностью запрещает наследование, тогда как closed разрешает создавать непосредственных потомков внутри сборки. Сам закрытый класс неявно становится абстрактным.

Ограничение не передаётся по цепочке наследования автоматически. Если потомок JobState не объявлен как sealed или closed, сторонняя сборка потенциально сможет создать уже его наследника. Именно поэтому в примере конечные состояния помечены sealed.

Дополнительное ограничение связано с видимостью типов. Если часть прямых потомков закрытого класса недоступна вызывающему коду, например из-за internal, компилятор может не признать switch исчерпывающим за пределами сборки.

Для архитектуры приложений и библиотек closed полезен там, где разработчик контролирует все допустимые состояния, но хочет сохранить общие методы, свойства и полиморфизм.

Union и closed по-разному работают с JSON

Одна из самых полезных практических деталей C# 15 связана с сериализацией. В .NET 11 библиотека System.Text.Json умеет обрабатывать объединения типов, но выбор между union и closed влияет на формат данных.

Рассмотрим объединение числа и строки.

public union Limit(int, string);

Такой формат встречается в реальных API. Например, в Kubernetes параметр maxUnavailable может задаваться абсолютным числом или процентной строкой.

При сериализации активный вариант записывается непосредственно как JSON-значение. Число превращается в 2, строка в "25%". Дополнительная обёртка не требуется.

Но при чтении JSON возникают сложности. По умолчанию сериализатор выбирает вариант по типу JSON-токена. Если оба типа представлены JSON-объектами, определить нужный класс без дополнительных правил зачастую невозможно.

Для объектных вариантов предусмотрен JsonUnionTypeStructuralClassifier, который пытается различать классы по именам свойств. Такой способ работает лишь тогда, когда структура данных позволяет однозначно определить тип.

Есть менее очевидная ловушка. Настройки JsonSerializerOptions.Web разрешают читать числа из строк. Поэтому объединение Limit(int, string) становится неоднозначным при чтении строкового JSON-значения. Сериализатор может выбросить JsonException даже для строки "25%", поскольку оба варианта претендуют на один тип JSON-токена.

Для подобных контрактов понадобится пользовательский классификатор либо иная схема данных.

С закрытой иерархией можно выбрать другой подход. Если добавить к базовому классу атрибут JsonPolymorphic(InferClosedTypePolymorphism = true), сериализатор сможет автоматически определить производные типы и включить в JSON признак $type.

Различие особенно важно для серверных приложений. В ASP.NET Core объединения типов работают при обработке JSON-тел запросов и ответов, поскольку соответствующие механизмы используют System.Text.Json. Однако автоматическое связывание union из параметров маршрута, строки запроса, HTTP-заголовков и полей форм пока не поддерживается.

Если проект использует сторонний сериализатор, поддержку новых типов следует проверять отдельно. Наличие конструкции union в языке не гарантирует, что любая библиотека сериализации правильно распознает активный вариант.

Для нового API с контролируемым JSON-контрактом я бы предпочёл закрытую иерархию и явный признак типа. Для существующего API, где значение уже может быть числом, строкой или объектом без дополнительной обёртки, union обычно естественнее.

Свойство union closed
Нужно общее наследование Нет Да
Можно объединить int и string Да Через отдельные типы
Проверка полноты switch Да Да, при выполнении условий иерархии
Общее поведение вариантов Через дополнительные методы и свойства объединения Через базовый класс
Типичная реализация Структура с object? Обычные классы
Сериализация JSON Активное значение без обязательного дискриминатора Возможен $type при включённой полиморфной сериализации
Оптимальный сценарий Разнородные результаты и существующие контракты Состояния, события, предметные модели

Коллекции, индексаторы расширений и новые возможности циклов

Следующая группа нововведений C# 15 не меняет архитектуру приложения настолько заметно, как union и closed. Зато помогает убрать вспомогательные конструкции из повседневного кода.

Параметры конструктора прямо в выражении коллекции

Выражения коллекций с квадратными скобками появились ещё в C# 12. Например, массив или список можно было объявить так.

int[] numbers = [1, 2, 3, 4, 5];
 
 List<string> users = ["Admin", "Guest"];

Однако выражение не позволяло напрямую передать конструктору некоторые параметры, например начальную ёмкость списка или правило сравнения элементов множества.

C# 15 добавляет элемент with(...).

string[] source = ["Admin", "ADMIN", "Guest"];
 
 List<string> names =
     [with(capacity: source.Length * 2), .. source];
 
 HashSet<string> users =
     [with(StringComparer.OrdinalIgnoreCase), .. source];
 
 Console.WriteLine(names.Count); // 3
 Console.WriteLine(users.Count); // 2

В первом случае конструктор списка получает заранее рассчитанную ёмкость. Во втором множество сравнивает строки без учёта регистра и удаляет дубликат ADMIN.

Конструкция with(...) должна стоять первой в выражении коллекции. Переданные аргументы направляются соответствующему конструктору либо фабричному методу целевой коллекции. Доступные параметры зависят от типа.

Практический смысл особенно заметен при создании больших списков и множеств со специальными правилами сравнения. Но ускорение зависит от характера данных. Если коллекция небольшая, заранее заданная ёмкость практически ничего не изменит. Если расчётная ёмкость существенно превышает реальный размер, приложение может расходовать больше памяти.

Новый синтаксис также готовит основу для будущих выражений словарей, однако полноценные словарные выражения с ключами и значениями не входят в закреплённый набор новшеств C# 15.

Индексаторы расширений

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

Например, интерфейс IEnumerable<T> сам по себе не имеет индексатора. Разработчик может добавить обращение через квадратные скобки.

IEnumerable<int> numbers =
     Enumerable.Range(10, 5);
 
 Console.WriteLine(numbers[2]); // 12
 
 public static class SequenceIndexers
 {
     extension(IEnumerable<int> source)
     {
         public int this[int index]
             => source.ElementAt(index);
     }
 }

После объявления расширения последовательность можно использовать через привычную запись numbers[2].

Индексатор расширения выглядит как обычный член типа, но фактически не меняет исходный тип и не добавляет ему новых внутренних возможностей. Если собственный подходящий индексатор уже существует, он имеет приоритет перед расширением.

Главная опасность связана со сложностью операции. Разработчики привыкли, что array[index] или List[index] обычно выполняются за постоянное время. Но ElementAt(index) для произвольной последовательности IEnumerable может потребовать перебора элементов от начала.

Поэтому индексатор расширения способен замаскировать операцию с линейной сложностью за синтаксисом, который визуально напоминает быстрый произвольный доступ.

В библиотеке с контролируемыми структурами данных такой механизм удобен. Например, можно добавить индексатор диапазона к собственному контейнеру или адаптировать существующий тип для более естественной работы с элементами. Но если вычисление индекса требует сетевого запроса, обращения к базе данных или полного перебора, обычный метод с говорящим названием может быть честнее.

Break и continue научились работать с метками

Вложенные циклы долго оставались неудобным местом C#. Обычный break завершал только ближайший цикл или switch, а continue переходил к следующей итерации ближайшего цикла.

Чтобы выйти сразу из нескольких вложенных конструкций, приходилось использовать флаги, goto или дополнительные методы.

В C# 15 можно обозначить целевой цикл меткой.

int[][] matrix =
 [
     [1, 2, 3],
     [4, -1, 6],
     [7, 8, 42]
 ];
 
 rows: for (int row = 0; row < matrix.Length; row++)
 {
     for (int col = 0; col < matrix[row].Length; col++)
     {
         int value = matrix[row][col];
 
         if (value == -1)
             continue rows;
 
         if (value == 42)
         {
             Console.WriteLine("Значение найдено");
             break rows;
         }
     }
 }

Оператор continue rows пропускает оставшуюся часть текущей строки и переходит к следующей итерации внешнего цикла. Оператор break rows завершает внешний цикл полностью.

Метка должна указывать на разрешённую охватывающую конструкцию. Break может завершать помеченный внешний цикл или switch, а continue применяется к циклам.

Нововведение хорошо подходит для обхода матриц, вложенных коллекций, поиска в древовидных структурах и обработки пакетов данных. Дополнительно правило анализатора IDE0410 умеет обнаруживать некоторые старые конструкции с флагами и goto, которые можно заменить помеченными переходами.

При этом метки не должны становиться заменой нормальной структуре программы. Если метод содержит пять уровней вложенности и множество переходов, проблема, скорее всего, уже в устройстве метода. Иногда проще выделить операцию поиска в отдельную функцию с ранним return.

Среди менее заметных новшеств C# 15 есть изменение поддержки невиртуальных статических членов интерфейсов. Компилятор снимает часть ограничений для таких членов при работе с целевыми средами, не поддерживающими реализацию методов интерфейсов по умолчанию. Изменение в основном затрагивает авторов библиотек и инструменты компиляции. Нового синтаксиса не появляется, а официальная поддержка C# 15 по-прежнему привязана к .NET 11 и более новым платформам.

Безопасность памяти, совместимость и риски перехода

Unsafe теперь рассматривается как контракт безопасности

Обычный C# работает с управляемой памятью. Среда .NET контролирует размещение объектов, проверяет границы многих операций и использует сборщик мусора. Но системные библиотеки, низкоуровневые обработчики данных и средства взаимодействия с нативным кодом иногда требуют прямой работы с указателями.

В традиционной модели наличие указателей обычно требует unsafe-контекста. Разработчики языка решили разделить две ситуации. Первая связана с хранением или передачей указателя, вторая с фактическим чтением и записью памяти через указатель.

В новой модели объявление указателя, получение адреса переменной, некоторые операции fixed и stackalloc больше не требуют самостоятельного unsafe-контекста. Однако разыменование указателя, доступ через p->member, обращение p[index] и вызов указателя на функцию остаются небезопасными операциями.

Главная перемена касается модификатора unsafe у членов программы. При включённых новых правилах он не просто разрешает низкоуровневые операции внутри метода, а сообщает вызывающему коду о необходимости соблюдать контракт безопасности. Компилятор требует, чтобы вызов происходил в подходящем контексте.

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

<PropertyGroup>
     <TargetFramework>net11.0</TargetFramework>
     <LangVersion>preview</LangVersion>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <Features>$(Features);updated-memory-safety-rules</Features>
 </PropertyGroup>

Параметр LangVersion=preview открывает предварительный синтаксис, включая ослабленные ограничения на создание указателей и выражение unsafe(expression). Отдельный флаг updated-memory-safety-rules включает новые правила распространения требований безопасности между членами программы. AllowUnsafeBlocks разрешает соответствующие небезопасные операции.

Попробуем простой пример.

int number = 42;
 
 int* pointer = &number;
 
 int result = unsafe(*pointer);
 
 Console.WriteLine(result); // 42

В предварительном режиме создание указателя не требует отдельного блока unsafe, но чтение значения через указатель остаётся операцией, безопасность которой компилятор не может полностью доказать. Выражение unsafe(*pointer) ограничивает небезопасный контекст одной операцией.

Новый модификатор safe применяется к объявлениям членов в предусмотренных моделью случаях, включая некоторые внешние методы и поля структур с особой компоновкой памяти. Он обозначает, что вызывающая сторона не должна принимать на себя дополнительное unsafe-обязательство. Однако сам модификатор не доказывает корректность нативного кода, не проверяет время жизни указателей и не устраняет ошибки в ручном управлении памятью.

В проекте также изменяется смысл unsafe на уровне объявлений. Новые правила запрещают применять модификатор к некоторым конструкциям, где он больше не имеет полезного значения, включая объявления типов, статические конструкторы и финализаторы.

Различия между двумя моделями и порядок включения предварительного режима подробно закреплены в правилах работы с памятью.

Предварительная модель безопасности памяти предназначена для экспериментов и проверки низкоуровневого кода. Работа с указателями может приводить к чтению недопустимой памяти, повреждению данных и аварийному завершению программы. Новые пометки помогают обнаруживать опасные места, но не заменяют проверку границ буферов, времени жизни объектов и корректности взаимодействия с нативными библиотеками.

Microsoft рассматривает переработку unsafe как проект на несколько версий языка. Полностью закрепить новую модель предполагается в последующих выпусках C#. Синтаксис и некоторые правила ещё могут измениться.

Поэтому нет смысла включать предварительный режим во всём приложении ради эксперимента с указателями. Лучше создать отдельный проект, проверить нужные операции и сохранить существующую стабильную модель для производственного кода.

Что может сломаться после обновления компилятора

Большинство существующих программ не потребуется переписывать. Однако C# 15 и новый Roslyn содержат изменения диагностики и правил компиляции, которые могут затронуть старый исходный код.

Первая потенциальная проблема связана со словами union и closed. В новых контекстах объявления типов они приобрели специальное значение. Если проект использовал такие идентификаторы для собственных классов или ссылок на типы, после обновления возможны ошибки разбора исходного кода.

Исправление обычно простое. Идентификатор можно экранировать символом @.

class @union
 {
 }
 
 class @closed
 {
 }

Вторая проблема связана с Span<T> и ReadOnlySpan<T>. Новый компилятор точнее применяет правила времени жизни значений, созданных через выражения коллекций. Некоторые присваивания Span за пределы разрешённой области видимости, которые прежний компилятор принимал, теперь вызывают ошибки.

Такое поведение нельзя считать произвольным ужесточением. Компилятор исправляет прежние расхождения с правилами безопасности ссылок. Если подобная ошибка появилась после обновления, нужно пересмотреть время жизни данных, а не пытаться отключить защиту.

Есть и организационные последствия. Добавление нового варианта в публичный union или нового потомка закрытой иерархии может потребовать изменений в коде потребителей. С точки зрения компилятора такая эволюция намеренно создаёт новые предупреждения в обработчиках switch.

Для публичных библиотек подобное поведение одновременно полезно и неудобно. Разработчик библиотеки получает контроль над типами, но любое расширение набора вариантов затрагивает вызывающий код. Если контракт предполагает свободное добавление новых реализаций независимыми командами, обычный открытый интерфейс или базовый класс зачастую подходит лучше.

Отдельно проверьте генераторы исходного кода, анализаторы Roslyn, сериализаторы, инструменты анализа API и другие компоненты, которые работают с синтаксическим деревом или метаданными C#. Для новых объединений типов компилятор предоставляет сведения о вариантах через API Roslyn, но сторонним инструментам может понадобиться обновление.

Практическую проверку перехода я бы проводил в следующем порядке.

  1. Закрепить прежнюю версию SDK и сохранить успешную контрольную сборку.
  2. Создать отдельную ветку и обновить проект до net11.0.
  3. Выполнить dotnet restore, затем dotnet build без подавления предупреждений.
  4. Запустить автоматические тесты и проверить JSON-контракты, публичные API и генераторы кода.
  5. Для критичных по производительности компонентов отдельно измерить выделения памяти и время выполнения.
  6. После проверки решить, какие новые конструкции действительно улучшают проект, вместо массовой замены существующих типов.

Для измерения производительности лучше использовать BenchmarkDotNet, а для анализа выделений памяти и работы сборщика мусора подойдут dotnet-counters и специализированные профилировщики.

Главный критерий успешного перехода не количество использованных новых конструкций, а сохранение поведения приложения, совместимости контрактов и приемлемых показателей производительности.

Частые вопросы о C# 15

Можно ли использовать C# 15 с .NET 10?

Официально C# 15 поддерживается для .NET 11 и более новых платформ. Принудительный выбор новой версии языка при старой целевой платформе считается неподдерживаемой конфигурацией. Некоторые возможности требуют типов и поведения среды выполнения, которых нет в .NET 10. Для рабочего проекта на .NET 10 лучше использовать C# 14.

Заменят ли union обычные исключения?

Нет. Union удобен для ожидаемых результатов, например отказа в авторизации, ошибки проверки введённых данных или отсутствующего объекта. Исключения продолжают подходить для неожиданных сбоев и ситуаций, когда обычное выполнение операции невозможно. Использовать union вместо каждого исключения не требуется.

Можно ли смешивать проекты на C# 14 и C# 15 в одном решении?

Да. Версия языка определяется для каждого проекта отдельно. Одно решение может содержать проект под .NET 10 с C# 14 и проект под .NET 11 с C# 15. При этом зависимости между проектами должны соответствовать правилам совместимости целевых платформ. Например, библиотека, рассчитанная только на net11.0, не становится автоматически доступной приложению под net10.0.

Union в C# 15 полностью заменяет сторонние библиотеки OneOf?

Не во всех случаях. Встроенный union предоставляет привычное сопоставление с образцом и проверку вариантов компилятором. Но существующие библиотеки могут предлагать специализированные операции, дополнительные методы, другие стратегии хранения, поддержку старых версий .NET или готовую интеграцию с определёнными платформами. Перед заменой нужно сравнить реальные возможности и ограничения API.

Будут ли union и closed автоматически улучшать производительность?

Нет. Стандартный union может создавать дополнительные объекты при хранении значимых типов. Closed ограничивает наследование и помогает компилятору проверять допустимые варианты, но не обещает ускорения программы. Главный выигрыш связан с точностью модели данных, сопровождаемостью кода и более ранним обнаружением ошибок.

Поддерживает ли C# 15 закрытые перечисления closed enum?

Нет. Закрытые перечисления рассматриваются как отдельное направление развития языка, но не входят в закреплённый набор нововведений C# 15. Обычные enum продолжают работать по прежним правилам. Не следует переносить на них все гарантии проверки полноты, предусмотренные для union и closed-иерархий.

Подходит ли C# 15 для разработки игр на Unity?

Совместимость зависит от версии Unity, встроенного компилятора и используемой среды выполнения. Установка .NET 11 SDK в операционную систему сама по себе не добавляет поддержку новых конструкций в редактор Unity. Возможности языка следует проверять по техническим требованиям конкретной версии движка.

Обязательно ли переходить на новую модель unsafe при обновлении до C# 15?

Нет. Обычный режим C# 15 сохраняет прежние правила unsafe. Экспериментальная модель включается отдельно через LangVersion=preview и специальные параметры компилятора. Проекты, которым не требуется тестировать новые механизмы работы с памятью, могут продолжать использовать существующий синтаксис.

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

Остальные возможности решают более локальные задачи. Параметры выражений коллекций сокращают вспомогательный код, индексаторы расширений делают некоторые API удобнее, а помеченные break и continue упрощают управление вложенными циклами. Пересмотр модели unsafe выглядит перспективным для системного программирования, но пока требует осторожности из-за экспериментального статуса.

Начать знакомство с новой версией лучше с небольшого приложения под .NET 11 RC2. Попробуйте вернуть результат операции через union, добавьте третий вариант и убедитесь, что компилятор обнаруживает неполный switch. Затем проверьте сериализацию JSON и оцените расходы памяти. Такой эксперимент даст больше пользы, чем механическое переписывание существующего проекта.

Для производственных систем без потребности в новых возможностях .NET 10 LTS остаётся разумным выбором. .NET 11 относится к ветке STS, но разница в названиях не означает, что её поддержка завершится значительно раньше. Согласно действующему графику, сроки окончания поддержки .NET 10 и .NET 11 приходятся на ноябрь 2028 года.

Главное достоинство C# 15 заключается в усилении контроля со стороны компилятора. Чем точнее код описывает допустимые состояния и типы, тем меньше ошибок приходится обнаруживать уже после развёртывания приложения. Но новые гарантии работают только в пределах правил языка. Сериализацию, производительность, совместимость и безопасность низкоуровневого кода разработчику по-прежнему придётся проверять самостоятельно.

CSharp DotNET union closed программирование компилятор коллекции индексаторы память безопасность Roslyn разработка
Alt text
Обращаем внимание, что все материалы в этом блоге представляют личное мнение их авторов. Редакция SecurityLab.ru не несет ответственности за точность, полноту и достоверность опубликованных данных. Вся информация предоставлена «как есть» и может не соответствовать официальной позиции компании.

Юрий Кочетов

Здесь я делюсь своими не самыми полезными, но крайне забавными мыслями о том, как устроен этот мир. Если вы устали от скучных советов и правильных решений, то вам точно сюда.