Дождались! .NET 10 вот-вот выйдет, а значит, самое время запускать фейерверки и отмечать юбилей любимой платформы! Ну и, само собой, ознакомимся с улучшениями JIT, расширением стандартной библиотеки, новыми возможностями SDK и с другими нововведениями в этой статье.

.NET 10 сфокусирован на улучшении производительности, а также безопасности — в частности, криптография подверглась серьезной доработке. Это long-term support (LTS) релиз, а значит будет поддерживаться в течение трёх лет.

Эта статья расскажет лишь о некоторых важных и интересных улучшениях в библиотеках, Runtime и SDK. Поскольку одной публикации не хватит, чтобы рассказать о всех изменениях в новом .NET 10.

Также отмечу, что мы уже работаем над поддержкой новой версии .NET 10 и C# 14 — она появится в PVS-Studio 7.40. Релиз запланирован на начало декабря, и чтобы его не пропустить, приглашаю подписаться на рассылку пресс-релизов.

C# 14

Все новшества C# 14 были разобраны в отдельной статье. Среди них extension-блоки, ключевое слово field, развитие паттерн-матчинга и много другое. Также можно выделить возможность использования оператора ? при присваивании.

До С# 14 вам требовалось проверять переменную на null для безопасного присваивания её свойству значения:

if (user is not null)
{
    user.LastActive = DateTime.UtcNow;
}

Сейчас же эквивалентный код может выглядеть следующим образом:

user?.LastActive = DateTime.UtcNow;

С этим нововведением бойлерплейтного кода станет меньше, что не может не радовать.

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

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

В частности, изменения затронули LINQ, регулярные выражения, криптографические алгоритмы, JIT, AOT, I/O и много другое.

Чтобы быть в курсе всех изменений, рекомендую хотя бы тезисно ознакомиться со статьей.

Библиотеки

Криптография

В библиотеку System.Security.Cryptography были добавлены типы для поддержки трёх новых асимметричных постквантовых алгоритмов: ML-KEM (FIPS 203), ML-DSA (FIPS 204) и SLH-DSA (FIPS 205).

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

Коллекции

Были добавлены дополнительные перегрузки методов TryAdd и TryGetValue для OrderedDictionary<TKey, TValue>. Единственное отличие новых методов в том, что они вдобавок возвращают индекс в качестве out параметра.

Сериализация

Теперь можно настраивать обработку циклический ссылок при сериализации или десериализации. Сделать это можно, указав ReferenceHandler в JsonSourceGenerationOptionsAttribute.

Также представлен параметр JsonSerializerOptiops.AllowDuplicateProperties, который нужен для запрета повторения свойств JSON.

Строки

В .NET 10 появились новые API, которые расширяют нормализацию за пределы строковых типов. Существующие API работали только с типом строки, поэтому данные в других формах — таких как массивы символов или интервалы — приходилось приводить к строковому типу. Теперь в этом нет необходимости: новые API позволяют работать с диапазонами символов.

Также были добавлены методы для преобразований между последовательностью байтов UTF-8 и шестнадцатеричными представлениями без необходимости выделения промежуточных строк. Эти методы перегружают существующие реализации, которые работают только для string и ReadOnlySpan<char>, но обеспечивают более высокую производительность, так как напрямую работают с байтами в кодировке UTF-8.

Кроме того, была добавлена опция NumericOrdering, которая указывает, что сравнение строк должно производиться численно, а не лексикографически. В данном случае строка "2" будет эквивалента строке "02".

Runtime

Поддержка AVX10.2

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

Будем ждать.

Стековая аллокация для небольших массивов

.NET 10 добавляет стековую аллокацию для маленьких массивов значимых и ссылочных типов. В данном случае, массив размещается на стеке, если JIT уверен, что данные не переживут контекст, в котором были созданы. Это решение направлено на снижение числа объектов, которые нужно отслеживать сборщику мусора. Таким образом, это снижает нагрузку на GC и открывает возможности для будущих оптимизаций.

Пример:

static void Sum()
{
    int[] numbers = {1, 2, 3};
    int sum = 0;

    for (int i = 0; i < numbers.Length; i++)
    {
        sum += numbers[i];
    }

    Console.WriteLine(sum);
}

Массив из трех чисел numbers будет размещен на стеке, потому что компилятор знает, что эти данные не переживут вызов метода Sum. То же самое относится и к массивам ссылочных типов.

static void Print()
{
    string[] words = {"Hello", "World!"};
    foreach (var str in words)
    {
        Console.WriteLine(str);
    }
}

Как и в предыдущем примере, жизненный цикл массива words ограничивается лишь методом Print, поэтому компилятор разместит этот массив на стеке.

write-barrier GC

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

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

Тем не менее, для производительности компилятора лучший барьер записи — это тот, которого вообще нет.

Так вот, .NET 10 также включает правки, которые позволяют убрать некоторые барьеры записи GC, тем самым улучшая производительность.

Улучшение JIT

.NET 10 принёс значительные улучшения для JIT-компиляции, которые направлены на повышение производительности. Среди изменений: усовершенствование инлайнинга, улучшение создания кода для элементов структур, отмена абстракции перечисления массива и многое другое.

SDK

Очистка ссылок на пакеты

Теперь, начиная с версии .NET 10, функция аудита NuGet может удалять ссылки на пакеты, которые не используются. Это нововведение также направлено на оптимизацию: уменьшается количество пакетов, которые восстанавливаются и анализируются во время процесса сборки.

Эта функция по умолчанию будет включена для всех фреймворков проекта, который нацелен на .NET 10 и последующие версии.

MSBuild

MSBuild, работающий в Visual Studio (или через msbuild.exe), является приложением .NET Framework; а MSBuild, работающий в dotnet CLI — приложением .NET. Это означает, что любые задачи MSBuild, написанные для работы в .NET, нельзя использовать при сборке в Visual Studio или при использовании msbuild.exe из-за разницы в окружении.

Начиная с .NET 10, Visual Studio 2026 и msbuild.exe будут запускать MSBuild-задачи, которые собраны для .NET. Это означает, что теперь можно использовать те же задачи как при сборке в Visual Studio, так и с помощью msbuild.exe через dotnet CLI. Это изменение позволит избежать лишних переписываний и дальнейшего поддержания MSBuild-задач под другой фреймворк.

Новые команды

Была добавлена команда dotnet tool exec, которая позволяет выполнить средство .NET без его глобальной или локальной установки — это особенно полезно для CI/CD.

Кроме того, добавлен новый скрипт dnx, который предоставляет упрощённый способ выполнения инструментов. Он перенаправляет все аргументы в интерфейс командной строки dotnetдля обработки.

С выходом .NET 10 также появляется возможность инспекции интерфейса командной строки с помощью параметра --cli-schema. При использовании он выводит представление JSON-дерева команд CLI для вызываемой команды или подкоманды.

Запуск одиночных файлов C# без проектов

Теперь можно создавать приложения на основе одиночных файлов без проекта, что упрощает создание и запуск программ. Для этого нужно использовать команду dotnet run для одиночного *.cs файла. По задумке разработчиков, такой подход найдёт применение для создания небольших служебных программ командной строки, прототипов, а также различных экспериментов.

Все приложения на основе файлов по умолчанию нацелены на native AOT-компиляцию и поддерживают публикацию в собственных исполняемых файлах с помощью dotnet publish.

Заключение

Подводя итог, можно смело заявить, что .NET 10 — это крепкая версия, достойная быть как юбилейной, так и LTS-версией. Радует, что разработчики из года в год фокусируются на улучшении производительности и поддерживают актуальность стандартной библиотеки.

В статье были приведены лишь некоторые наиболее примечательные, по нашему мнению, нововведения. С полным списком улучшений вы можете ознакомиться здесь. Также пишите в комментариях, какие важные или любопытные изменения я упустил, а также ваше мнение о .NET 10 — будет интересно почитать.

Теперь все бежим отмечать юбилей :)

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Georgii Tormozov. What's new in .NET 10.

Комментарии (26)


  1. Ashita
    11.11.2025 10:26

    Поддержка запуска одиночных .cs файлов звучит интересно и перспективно


  1. Mizantrop777
    11.11.2025 10:26

    Ещё бы каким то образом рефлексию в aot завезли


    1. equmag
      11.11.2025 10:26

      В Native AOT есть рефлексия! Но только ее урезанная версия, например typeof и Enum.Parse<T> будут работать, эти данные сохраняются. Это просто статические метаданные, они нужны банально для работы BCL.

      Впрочем речь здесь явно не об этом, поэтому стоит упомянуть, что это невозможно, это логически невыполнимо. Native AoT прекомпилирует все методы и большинство метаданных удаляется, в этом весь ее смысл. Также как в C++ не может быть рефлексии, при ahead of time сборке в C# ее также не может быть. Рефлексия следствие JiT компилятора и везде где есть рефлексия есть JiT (ну или интерпритация)


      1. Spearton
        11.11.2025 10:26

        Почему вы так уверены, что рефлексия не может существовать в нативе?


        1. equmag
          11.11.2025 10:26

          Есть статическая compile-time рефлексия, она реализуется генераторами в том же C# без проблем. А вот runtime рефлексия требует переписывание кода в процессе работы приложения, что требует виртуальную машину. Это ведь крайне банальная логика


      1. IgnisNoir
        11.11.2025 10:26

        А в go у нас где jit? Есть рефлексия и там натив


        1. equmag
          11.11.2025 10:26

          В go compile time рефлексия, это просто метаданные. В C# runtime рефлексия (RTTI). C# поддерживает статическую рефлексию в native aot уже!

          В общем ваши слова не противоречат моим, а мои не противоречат вашим. Было бы проще, если бы немного внимательнее читали написанное


          1. IgnisNoir
            11.11.2025 10:26

            Что значит в go рефлексия compile time? Я может что-то путаю, но рефлексия это по определению та вещь которая происходит во время выполнения. То есть runtime. И в go именно такая. Го сохраняет метаданные и ты во время выполнения смотришь их и работаешь с ними. Рефлексия не обязывает иметь виртуальную машину как и переписывать свой код. Рефлексия по определению говорит о модификации приложения во время выполнения.

            Как пример в go вы просто с помощью пакета reflection можете посмотреть теги у структуры и модифицировать поля этой самой структуры, или же создать функцию, или узнать информцию о типе или полях структуры и так далее. Никакой compile time "магии" тут нет. Обычные метаданные


  1. equmag
    11.11.2025 10:26

    Мне кажется что люди во многом недооценивают важность этого релиза

    Выделение коллекций на стеке это game changer. Многие думают, "когда мне вообще нужно выделять такие буферы, это редкость? Да и есть ArrayPool для этого", но здесь речь не только про массивы, как можно подумать. RyuJiT научился аллоцировать на стек и сами объекты, те же делегаты(да, делегаты в некотором смысле коллекции) или Enumerator могут быть стек аллоцированными. И список будет только расширяться.

    И тут стоит обратить внимание, что это используется везде в BCL, от этих оптимизаций выигрывает огромная часть стандартной библиотеки, давая бесплатный прирост производительности почти везде.

    Фантастика! Я ждал этого ещё со времён анонса .NET9.

    Когда этот барьер наконец-то преодолен, это открывает множество возможностей для оптимизаций в будущем. CLR догоняет JVM с огромной скоростью. Реальный список оптимизаций просто километровый, советую почитать официальные статьи на тему.

    И мы уже знаем о планах runtime async и discriminated Unions в .NET11! Когда я начинаю думать "ну куда дальше", выходит Мэдс и предлагает новый game changer.

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


    1. Kano
      11.11.2025 10:26

      Я почему то думал что такая очевидная вещь уже была добавлена как часть оптимизации, но оказалось что прямо киллерфича какая то


    1. DieSlogan
      11.11.2025 10:26

      CLR догоняет JVM с огромной скоростью.

      Угу, догоняет, но уже по второму кругу


    1. Siemargl
      11.11.2025 10:26

      . CLR догоняет JVM с огромной скоростью

      Я джвадцать лет этого ждал =)


    1. pashagoroshko
      11.11.2025 10:26

      Только не понятно небольшие размеры массивов это какие? 3 элемента ок, а 4 уже нет? Где эта грань что то я пока не понимаю. Как и каких типов это касается, судя из статьи только примитивных


      1. equmag
        11.11.2025 10:26

        Это касается ссылочных типов тоже, но на пользовательских классах может сломаться escape-analysis и JiT решит, что выделять на стеке небезопасно. "Небольшие размеры" очень грубо это около 10000 элементов. В целом не сомневаюсь, что в .Net11 список сильно расширится.

        Аллоцировать массивы на стек можно было ещё в .Net9, но это касалось только знаковых примитивов и часто не срабатывало, поэтому и хайпа избежало. Сейчас это полноправная фишка

        Причина в ограничении размера в том, что стек ограничен в размере операционкой, около 8Мб. Я здесь уже спекулирую, но я думаю на кучу уходят объекты, которые должны попасть в Large object heap, а это 80 килобайт


        1. maxcat
          11.11.2025 10:26

          stackallock существует все таки сильно дольше, чем net9


          1. equmag
            11.11.2025 10:26

            stackalloc это явная ref struct, а здесь на стек отправляется стандартный int[], большая разница


    1. maxcat
      11.11.2025 10:26

      В bcl заменились переписывать на stackallock?

      А чего догонять в jvm? Там же все отстаёт от дотнета всегда, там до сих пор даже нельзя стек использовать для своих типов данных


      1. equmag
        11.11.2025 10:26

        BCL написан через Span и ArrayPool во многом, но делегаты, и перечислители так заменить было нельзя (и ещё ряд объектов, так сходу не вспомню). Больше всего счастливо от этих изменений LINQ, хотя у них были ещё свои оптимизации сверху, они добавили больше реализаций перечислителей для частных случаев в цепочках.

        И вы не плюйтесь на JVM, она в разработке намного дольше CLR. JiT научился грузить массивы на стек, но Java могла так уже очень давно и даже больше. Из-за отсутствия структур может показаться, что все выделяется на куче, но в реальности как раз JVM может выделять ваше классы на стеке, если считает что это безопасно.

        C# производительный за счёт языковых фич, за счёт очень низкоуровневой библиотеки и штук вроде async await и true generics. Java имеет ужасный дизайн языка, но сама JVM очень крутая технология. Даже с точки зрения самого банального — инлайнинга

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


        1. maxcat
          11.11.2025 10:26

          Но разве приложения на джаве не жрут уже со старта больше памяти, чем дотнетовые?

          И если jvm когда-нибудь выигрывала в производительности за счёт оптимизаций на уровне JIT, то почему на ней нет популярных игровых движков? И даже тот же майнкрафт переписали на плюсы


          1. equmag
            11.11.2025 10:26

            Так ведь и на C# нет популярных игровых движков, Unity компилирует C# в C++ код под капотом, он вообще не использует CLR даже в debug режиме, шарпы там не нативные. И использует его юнити потому что IL удобный и синтаксис простой, а не потому что производительность.

            В C# есть очень мощный контроль, можно писать через указатели если нужно и стандартная библиотека этим пользуется. Там все оптимизировано вусмерть, часть вообще написана на C. У джавы ситуация другая, она не даёт программистам инструментов, но JVM берет эту ответственность за себя и решает за программиста где и что она будет оптимизировать.

            Если очень просто, C# будет производительней и дешевле по памяти, если его реально оптимизировать, но обычный "просто рабочий" код высока вероятность будет лучше и дешевле по памяти на Java. Что мы собственно и видим со стандартным стеком.


            1. maxcat
              11.11.2025 10:26

              Il2cpp появился не сразу в юнити.

              И при il2cpp все равно используется CLR. Там даже рефлексия доступна (ограниченно, как и в любом AOT).

              Мне кажется, что unity использует dotnet не только из-за простоты IL (как будто джавовый байткод сильно сложнее), а именно из-за структур. Потому что можно просто взять и написать кучу своих типов на стеке, - быть уверенными что они точно будут на стеке: не бояться что GC захлебнется, а ещё гонять их в плюсы (и обратно) не боясь какого-нибудь смещения кучи. А не надеяться на чудоJIT.

              Кстати, что в BCL написано на C?


              1. equmag
                11.11.2025 10:26

                Например Array.Sort написан на C. Или по крайней мере был написан, так сразу с головы тяжело сказать.

                Да, в шарпе больше контроля, Шарп крутой. Но это не вина JVM и не заслуга CLR, это вина самого языка


  1. maxcat
    11.11.2025 10:26

    >Все приложения на основе файлов по умолчанию нацелены на native AOT-компиляцию и поддерживают публикацию в собственных исполняемых файлах с помощью dotnet publish

    Зачем тратить время и память на AOT? Это же фича в первую очередь для скриптов, как аналог PowerShell


  1. ichukayev
    11.11.2025 10:26

    "собери сейчас, расшифруй потом" - вот это подстава, конечно. даже не задумывался об этом((


    1. Kahelman
      11.11.2025 10:26

      А кому это будет надо когда можно будет расшифровать?


  1. 0Bannon
    11.11.2025 10:26

    А где конец этим новшествам? Каждый год проект переписывать на новой версии c#? Ещё на предыдущей не успели переписать.