﻿# Что нового в \.NET 11?

Совсем недавно вышел \.NET 11, и это значит, что пора обновлять свои проекты\. Но сначала посмотрим, стоят ли нововведения в C\# 15, Runtime Async, JIT\-оптимизации и многое другое того, чтобы спешить с переходом на новую версию\.

![1425_dotnet11_ru/image1.png](https://import.viva64.com/docx/blog/1425_dotnet11_ru/image1.png)

\.NET 11 фокусируется на улучшении производительности и Runtime\. Сам же релиз является standard\-term support \(STS\), а поддерживаться будет два года\. Напомню, что, начиная с \.NET 9, срок поддержки STS\-релизов увеличили с 18 до 24 месяцев\.

В статье рассмотрим наиболее интересные изменения и улучшения Runtime, библиотек и SDK\. К сожалению, не получится изучить все изменения \.NET 11 из\-за их огромного числа, поэтому остановимся на самом интересном, а полный список можно посмотреть в [официальной документации](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview)\.

## C\# 15

Все нововведения C\# 15 были разобраны в отдельной [статье](https://pvs-studio.ru/ru/blog/posts/csharp/1421/)\. К примеру, в язык были добавлены `union`, закрытые иерархии, индексаторы расширений, был переработан подход к безопасности памяти\. Изменения в этом году получились довольно интересными и крупными\. А что бы вы сказали о новой версии языка?

## Производительность

Уже по традиции каждый год Стивен Тауб выпускает статью с обзором улучшений производительности в новом \.NET, где подробно разбирает и описывает все изменения\.

В этом году были затронуты JIT, Runtime Async, девиртуализация, проверки границ и ещё множество вещей\.

Всем заинтересованным рекомендуется посмотреть [статью](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/) Стивена Тауба, чтобы увидеть полноценный разбор именно этой части \.NET 11\.

## Библиотеки

### Сжатие

Теперь пространство имён [`System.IO.Compression`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression?view=net-11.0) включает в себя сжатие через Zstandard \([`ZstandardStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.zstandardstream?view=net-11.0), [`ZstandardEncoder`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.zstandardencoder?view=net-11.0) и другие\)\. При чтении ZIP\-архивов теперь проверяется контрольная сумма \(CRC32\), поэтому повреждённый архив выдаст исключение `InvalidDataException`\. Также у записей ZIP\-архива появилась поддержка паролей и открытие с указанием `FileAccess`\. Для энкодеров и декодеров Deflate, ZLib, GZip был добавлен метод `Reset()`, который позволяет сбросить состояние экземпляра и переиспользовать его, а не создавать новый объект\.

### LINQ

Новая версия приносит улучшения для существующих методов `Join`, а также добавляет метод [`FullJoin`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.fulljoin?view=net-11.0), который возвращает все элементы из обеих последовательностей, объединяя по ключу совпадающие элементы и сохраняя все комбинации, включая отсутствие пары как с левой, так и с правой сторон\. Посмотрим на `FullJoin` в деле:

```cpp
var products = new List<(string Name, string? Category)>
{
  ("Laptop", "Electronics"),
  ("Table", null)
};
var categories = new List<(string Name, string Description)>
{
  ("Electronics", "Electronic devices"),
  ("Furniture", "Office furniture")
};

var joined = products.FullJoin(categories, p => p.Category, c => c.Name);
foreach (var (product, category) in joined)
    Console.WriteLine($"{product.Name ?? "(none)"} 
    : {category.Description ?? "(none)"}");
```

Получаем такой вывод:

```cpp
Laptop: Electronic devices
Table: (none)
(none): Office furniture
```

В дополнение к этому LINQ позволил использовать `Join` и `GroupJoin` без указания `resultSelector`, чтобы сразу возвращать кортежи, как это делают `LeftJoin`, `RightJoin` и `FullJoin`\.

### JSON

Расширились возможности [`System.Text.Json`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-11.0)\. Например, для новой структуры `union`, добавленной в C\# 15, были адаптированы методы сериализации и десериализации: значение текущего типа объединения записывается напрямую, без обёртки и дискриминатора\. Также появилась запись в формате JSON Lines \(JSONL\), где каждое значение находится на отдельной строке\. Для этого у нового метода `JsonSerializer.SerializeAsyncEnumerable` есть параметр `topLevelValues`\.

У [`JsonPolymorphicAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute?view=net-11.0) появилось новое свойство [`InferClosedTypePolymorphism`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute.inferclosedtypepolymorphism?view=net-11.0), которое позволяет включать автоматический вывод производных типов для одной закрытой иерархии, не меняя при этом глобальные настройки сериализатора\. Ранее каждый производный тип нужно было явно регистрировать атрибутом `JsonDerivedType` на базовом типе\. При желании эту настройку можно задать и глобально: через `JsonSerializerOptions` или опции генератора исходного кода\. В \.NET 11 это выглядит так:

```cpp
[JsonPolymorphic(InferClosedTypePolymorphism = true)]
closed record Shape;
record Circle(double Radius) : Shape;
record Square(double Length) : Shape;
```

Добавлены generic\-методы `GetTypeInfo<T>()` и `TryGetTypeInfo<T>()`, возвращающие `JsonTypeInfo<T>`\. Также появились встроенная политика именования [`JsonNamingPolicy.PascalCase`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonnamingpolicy.pascalcase?view=net-11.0), атрибут `[JsonNamingPolicy]` для типов и их членов, возможность указать `[JsonIgnore]` сразу для всего типа и встроенные конвертеры для `BFloat16`, `Decimal32`, `Decimal64` и `Decimal128`\.

### Асинхронная валидация

Стала доступна асинхронная валидация с помощью [`AsyncValidationAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.asyncvalidationattribute?view=net-11.0) и [`IAsyncValidatableObject`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject?view=net-11.0), а также новых методов [`Validator.ValidateObjectAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator.validateobjectasync?view=net-11.0), [`Validator.TryValidateObjectAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator.tryvalidateobjectasync?view=net-11.0) и других\. Это пригодится для случаев, когда для проверки нужно обратиться к базе данных или внешнему сервису, не блокируя поток\. В `Microsoft.Extensions.Options` асинхронно можно валидировать и опции, в том числе при старте приложения через новый интерфейс [`IAsyncStartupValidator`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.options.iasyncstartupvalidator?view=net-11.0)\.

### Новые типы потоков

Добавлены четыре новых типа `Stream`: [`ReadOnlyMemoryStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream?view=net-11.0), [`WritableMemoryStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.writablememorystream?view=net-11.0), [`ReadOnlySequenceStream`](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequencestream?view=net-11.0) и [`StringStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream?view=net-11.0)\. Они позволяют без лишнего копирования передать уже имеющийся буфер или строку в API, принимающее `Stream`\. Соответственно, уменьшится число аллокаций и нагрузка на GC\.

### Числовые типы и парсинг

В [`System.Numerics`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics?view=net-11.0) появились типы [`Decimal32`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal32?view=net-11.0), [`Decimal64`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal64?view=net-11.0) и [`Decimal128`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal128?view=net-11.0)\. Это десятичные числа с плавающей точкой по стандарту IEEE 754\-2019 с точностью до 7, 16 и 34 значащих цифр соответственно\. Как и [`System.Decimal`](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-11.0), они хранят число в десятичной системе, поэтому, например, `0.1` в них представляется точно\. Но в отличие от `System.Decimal`, у которого собственный формат без NaN и бесконечностей, новые типы следуют стандарту, то есть поддерживают NaN, бесконечности и generic math\. Пригодятся они там, где нужно именно поведение IEEE 754, например для совместимости с другими языками и СУБД, использующими этот формат\. Заодно появился и обобщённый [`Complex<T>`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.complex-1?view=net-11.0), который работает с `float`, `Half`, `BFloat16` и новыми десятичными типами\.

Был добавлен новый метод [`INumberBase<TSelf>.TryParsePartial`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumberbase-1.tryparsepartial?view=net-11.0), который даёт возможность распарсить число до первого символа, не являющегося частью числа\. То есть если раньше нам бы попалось число вроде `123;789`, то `TryParse` вернул бы `false`, а в результат попал бы 0\. Посмотрим на примере, как это будет выглядеть теперь:

```cpp
public static void ParseTest()
{
  double.TryParsePartial("123;789", 
                          System.Globalization.NumberStyles.Number, 
                          null, out var result, out var consumed);
  Console.WriteLine($"Parsed value (TryParsePartial): {result}");
  Console.WriteLine($"Consumed: {consumed}");

  double.TryParse("456;032", null, out result);
  Console.WriteLine($"Parsed value (TryParse): {result}");
}
```

Вывод:

```cpp
Parsed value (TryParsePartial): 123
Consumed: 3
Parsed value (TryParse): 0
```

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

### Процессы

У класса [`Process`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=net-11.0) появился целый набор новых API\. Раньше, чтобы просто запустить утилиту и забрать её вывод, приходилось подписываться на `OutputDataReceived` и `ErrorDataReceived` или вручную вычитывать потоки\. Теперь это делается одним [вызовом](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0):

```cpp
ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", 
                           ["status", "--porcelain"]);

Console.WriteLine(result.StandardOutput);
Console.WriteLine($"Exit code: {result.ExitStatus.ExitCode}");
```

Также были добавлены следующие методы:

* [`Process.StartAndForget`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0) для запуска процесса, завершения которого ждать не нужно\.
* [`ProcessStartInfo.StartSuspended`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.startsuspended?view=net-11.0) для запуска процесса в приостановленном состоянии\.
* [`SafeProcessHandle.Resume()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safeprocesshandle.resume?view=net-11.0) для продолжения процесса\.

## Runtime

### Runtime Async

Крупное изменение Runtime в этом году\. Стоит вспомнить, что для каждого `async`\-метода компилятор генерирует конечный автомат \(state machine\), а [Runtime Async](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#runtime-async) перекладывает эту работу на сам Runtime\. В \.NET 10 это был эксперимент, а в \.NET 11 функция по\-прежнему помечена как preview, но для проектов под \.NET 11 больше не требует `EnablePreviewFeatures`\. Более того, сами библиотеки \.NET уже скомпилированы с Runtime Async и не содержат сгенерированных автоматов\. Для своего проекта функцию нужно включить явно:

```cpp
<PropertyGroup>
  <Features>runtime-async=on</Features>
</PropertyGroup>
```

Что это даёт? Во\-первых, меньше накладных расходов, так как JIT научился сворачивать `Task.FromResult`, `Task.CompletedTask` и `ValueTask.FromResult` в более быстрые пути, переиспользовать объекты продолжений и применять многоуровневую компиляцию к горячим асинхронным методам\. Во\-вторых, стек вызовов, получаемый через `new StackTrace()`, стал более читаемым, так как теперь туда выводится только реальная цепочка вызовов, исключая служебные\. Отладка тоже стала проще, в том числе благодаря тому, что шаг через `await` больше не уводит в инфраструктурный код\.

### Новые требования к процессору

Это [изменение](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#updated-minimum-hardware-requirements) может сломать запуск приложения на старом железе\. На x86/x64 минимальным уровнем для JIT и NativeAOT стал `x86-64-v2` вместо `x86-64-v1`\. На процессоре, который не дотягивает до этого уровня, приложение просто не запустится и сообщит о нехватке базовых наборов инструкций\.

Кроме того, образы ReadyToRun под Windows и Linux на x64 теперь ориентированы на `x86-64-v3` \(`AVX2`, `BMI2`, `FMA` и другие\)\. На процессорах без этих инструкций приложение запустится, но предкомпилированный код придётся перекомпилировать через JIT, что замедлит старт\. Такие процессоры сегодня ещё нужно поискать, но лучше проверить заранее, на каком оборудовании работает ваш проект\.

### Улучшения JIT

Помимо поддержки Runtime Async, [JIT](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#jit-improvements) научился делать ряд полезных вещей\. Теперь он умеет убирать больше проверок границ, в том числе для обращений вида `values[^1]` и `i + const < length`, а также для `span[0]` после проверки `!span.IsEmpty`\. Может вычислять на этапе компиляции `string.Equals` и `SequenceEqual`, когда оба значения известны\. Также он удаляет избыточные условия и ветвления и генерирует инструкцию `MUL` для `Math.BigMul` на x64\.

### NativeAOT и ReadyToRun

В [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#jit-improvements) вызовы методов интерфейсов теперь идут через общий вспомогательный механизм диспетчеризации, который затем перенаправляется на нужную реализацию\. Это уменьшает размер кода в местах вызова и ускоряет код, активно работающий с интерфейсами\.

`Comparer<T>.Default` и `EqualityComparer<T>.Default` при предкомпиляции теперь специализируются в [ReadyToRun](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#readytorun-improvements), поэтому им больше не нужен JIT\.

## SDK

### CLI стал быстрее

[CLI](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/sdk#cli-architecture) `dotnet` теперь по умолчанию работает как NativeAOT\-приложение\. Команды вроде `dotnet --version`, `dotnet --info` и `dotnet --help` выполняются полностью нативно, а внешние инструменты, например `dotnet ef`, запускаются в обход старта управляемой части CLI\.

Установщики SDK для Linux и macOS стали меньше: одинаковые сборки теперь заменяются символическими ссылками на одну копию\.

### Приложения из одного файла

Продолжает развиваться появившийся в \.NET 10 запуск \.cs\-файлов без проекта\. Директива [`#:include`](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps#include) позволяет разнести код по нескольким файлам и даже подключить готовую DLL\.

Кроме того, такие приложения теперь поддерживают `dotnet format`, а повторный запуск неизменённого приложения с NativeAOT переиспользует уже собранный результат\.

### dotnet run и dotnet watch

`dotnet run` научился передавать приложению переменные окружения прямо из командной строки с помощью параметра `-e`\.

`dotnet watch` получил интеграцию с хостом Aspire, автоматический перезапуск после падения и выбор устройства для MAUI\-проектов\.

### Тестирование

Много новых опций получил [`dotnet test`](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/sdk#dotnet-test-improvements) в Microsoft\.Testing\.Platform\. Например, теперь можно ограничить время прогона или остановиться после определённого числа упавших тестов\.

Также появилась опция `--no-dependencies`, позволяющая не собирать зависимые проекты\. Добавлен экспериментальный режим запуска только тех тестов, которые затронуты изменениями\.

## Заключение

\.NET 11 получился релизом, в первую очередь, про Runtime: Runtime Async уже используется в самих библиотеках \.NET, JIT стал ещё умнее, а CLI — быстрее\. В повседневной же работе заметнее всего будут нововведения C\# 15 и небольшие, но приятные методы вроде [`Process.RunAndCaptureTextAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0), [`FullJoin`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.fulljoin?view=net-11.0) и [`TryParsePartial`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumberbase-1.tryparsepartial?view=net-11.0)\.

Если планируете переход, то не забудьте проверить новые требования к процессорам\.

Полный список изменений можно найти в [документации Microsoft](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview)\. А какие нововведения понравились вам? Пишите в комментариях\.

Если вам понравилась статья, то в нашем [блоге](https://pvs-studio.ru/ru/blog/posts/) вы найдёте ещё больше интересного\. А подписаться на новые публикации можно [здесь](https://pvs-studio.ru/ru/subscribe/)\.