Вышел .NET 7, а это значит, что можно вдоволь насладиться различными нововведениями и фишками. Расскажем про самые интересные улучшения: C# 11, контейнеры, производительность, GC и прочее.

C# 11

Мы уже выпустили статью, посвящённую разбору нововведений в C# 11. В ней мы прошлись по новым особенностям: обобщённой математике, исходным строкам, модификатору required, параметрам типа в атрибутах и прочему.

Кстати, мы уже работаем над поддержкой .NET 7 и C# 11 — она будет добавлена в PVS-Studio 7.22. Релиз запланирован на начало декабря, а загрузить свежую версию анализатора можно будет здесь. Если хотите попробовать бету уже сейчас, напишите нам :).

Native AOT

AOT (ahead-of-time) – компиляция приложения не в промежуточный, а сразу в машинный код. Native AOT использует ahead of time компилятор для компиляции IL в машинный код во время публикации self-contained приложения. Native AOT был переведён из статуса экспериментального. Основными преимуществами native AOT приложений являются:

  • время запуска;

  • использование памяти;

  • работа на платформах с ограничениями (JIT недоступен).

Native AOT приложения имеют ряд ограничений:

  • нет динамической загрузки (например, Assembly.LoadFile);

  • нет генерации кода во время выполнения (например, System.Reflection.Emit);

  • нет C++/CLI;

  • и т. д.

On Stack Replacement (OSR)

В .NET существует такое понятие, как многоуровневая компиляция (Tiered Compilation). Если говорить простым языком, то многоуровневая компиляция снижает время запуска приложения. Как? Изначально JIT будет генерировать плохо оптимизированный машинный код (tier-0) для методов, т. к. это просто требует меньше времени. Если же количество вызовов метода переступает за определённый порог, то JIT cгенерирует для этого метода более оптимизированный код (tier-1). Данный подход не работает, например, с циклами, т. к. это может приводить к ухудшению производительности. К слову, на данный момент существует всего два уровня.

OSR позволяет заменять машинный код, который выполняется в данный момент, на новый — более оптимизированный. Ранее такая возможность была только между вызовами метода. Подобный подход позволяет применять многоуровневую компиляцию ко всем методам. Благодаря этому можно добиться более быстрой компиляции и при этом стабильной производительности. Судя по тестам Microsoft, внедрение данной технологии помогло на 25 % ускорить запуск нагруженных приложений. OSR в .NET 7 включена по умолчанию для x64 и Arm64.

Централизованное управление пакетами (CPM)

Управление зависимостями для многопроектных решений может оказаться сложной задачкой. Теперь же в ситуациях, когда требуется управление общими зависимостями для нескольких проектов, вы можете использовать централизованное управление NuGet пакетами. Для централизованного управления зависимостями потребуется добавить файл Directory.Packages.props в корень решения. Благодаря CPM версия пакета указывается только в Directory.Packages.props, а в проектах требуется лишь сослаться на пакет.

GC Regions

Регионы GC – это функция, которая разрабатывается уже несколько лет. Если раньше приходилось иметь несколько больших сегментов памяти (например, 1 ГБ), то теперь GC поддерживает множество маленьких областей (например, 4 МБ). Это позволяет GC быть более гибким в вопросах перепрофилирования областей памяти из одного поколения в другое.

В .NET 7 регионы используются по умолчанию для 64-битных приложений. Подробности по данному вопросу можно получить из статьи архитектора .NET GC.

Rate Limiting

Rate limiting – это механизм ограничения объёма доступа к ресурсу. Таким образом можно задать определённый лимит доступа, например, к базе данных.

Для написания ограничителя в .NET 7 был добавлен NuGet пакет System.Threading.RateLimiting. В основном работа будет происходить с абстрактным классом RateLimiter. Один из примеров работы от Microsoft:

RateLimiter limiter = GetLimiter();
using RateLimitLease lease = limiter.Acquire(permitCount: 1);
if (lease.IsAcquired)
{
  // Do action that is protected by limiter
}
else
{
  // Error handling or add retry logic
}

В данном случае мы пытаемся получить 1 разрешение с помощью метода Acquire. Далее идёт проверка – было ли получено разрешение:

  • если разрешение было получено, то можем использовать ресурс;

  • если разрешение не было получено, то это можно залогировать или обработать как ошибку.

Встроенная поддержка контейнеров

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

Например:

dotnet add package Microsoft.NET.Build.Containers
dotnet publish --os linux --arch x64 -c Release 
-p:PublishProfile=DefaultContainer

В данном случае мы добавляем временную ссылку на пакет для создания контейнера и публикуем проект для linux x64. Результатом выполнения команд является образ, который будет добавлен в Docker. После этого вы можете запустить приложение, используя контейнер:

docker run -it --rm -p 5010:80 my-awesome-container-app:1.0.0

Подробнее про это можно почитать здесь.

Улучшение производительности

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

Если интересны подробности улучшения производительности в .NET 7, можете почитать о них здесь.

Рефлексия

Существенно сокращены накладные расходы на использование рефлексии, когда вызов выполняется несколько раз для одного и того же элемента (будь то метод, конструктор или свойство). Производительность увеличена в 3-4 раза.

Подробнее про улучшение рефлексии можно прочитать здесь.

LINQ

В .NET 7 повышена производительность LINQ. Например, была существенно улучшена эффективность методов Min и Max при работе с массивами типа int и long. Это достигается за счёт векторизации обработки, а именно – использования Vector<T>. В итоге получаются следующие результаты:

Подробнее про улучшения производительности LINQ можно прочитать в статье.

Кстати, ещё очень здорово, что в System.Linq добавили новые методы Order и OrderDescending. Ранее при использовании OrderBy/OrderByDescending было необходимо ссылаться на собственное значение:

var data = new[] { 2, 1, 3 };
var sorted = data.OrderBy(x => x);
var sortedDesc = data.OrderByDescending(x => x);

Теперь же это не нужно:

var data = new[] { 2, 1, 3 };
var sorted = data.Order();
var sortedDesc = data.OrderDescending();

Регулярные выражения

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

Заключение

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

Лично для себя могу выделить как наиболее интересные и полезные:

  • естественно, C# 11;

  • on stack replacement (OSR);

  • централизованное управление пакетами (CPM);

  • GC Regions.

Естественно, в статье были перечислены не все новшества, а только самые интересные (по нашему мнению). Со всеми улучшениями вы можете ознакомиться здесь.

Используете ли вы уже что-то из фишек нового .NET? Пишите в комментариях.

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

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


  1. VanKrock
    16.11.2022 16:04
    +1

    Ну назвать основную фишку Generic Math не интересной - это такое


    1. rip_m Автор
      16.11.2022 16:52

      Я не называл Generic Math не интересной фишкой :). Если вы про то, что в статье не рассказывается про неё, то хочу заметить, что я дал ссылку на подробную статью по разбору нововведений C# 11.


    1. VBDUnit
      17.11.2022 17:07

      Имхо это лучше обобщить через статические методы в интерфейсах + утиную типизацию. Статические методы в интерфейсы, наконец-то, завезли, а вот утиную типизацию в широком смысле ещё нет.

      Тогда это распадается на синтаксический сахар конкретного языка.


  1. LabEG
    16.11.2022 16:43
    +1

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


    1. rip_m Автор
      16.11.2022 17:26

      С Go не знаком, но не могу не согласиться :). Native AOT выглядит очень круто.


    1. Nurked
      16.11.2022 19:40
      +2

      А вот я, например, уже после девятой версии самого C# и пятой версии .NET совсем перешёл на golang.

      Всё конечно круто и хорошо, и все эти финтифлюшечки и дополнения, но вот сам язык стал просто ужасным. Его превратили в какое-то подобие javascript. Весь этот синтаксический сахар стал просто неперевариваемым. Одни только switch expressions вызывают тихий ужас.

      static Point Transform(Point point) => point switch
      {
          { X: 0, Y: 0 }                    => new Point(0, 0),
          { X: var x, Y: var y } when x < y => new Point(x + y, y),
          { X: var x, Y: var y } when x > y => new Point(x - y, y),
          { X: var x, Y: var y }            => new Point(2 * x, 2 * y),
      };

      Зачем? Почему мне нужен turing-full-switch-operator?

      ХЗ. Слишком перегруженно и verbose.

      Хотя я на дотнетах писал с 2002 года...


      1. ijsgaus
        16.11.2022 19:44
        +3

        Продемонстрируйте простыню golang для этого же :-).


        1. Nurked
          16.11.2022 21:13

          Ну так там-то хоть понять можно, что там делается. Мне уж проще прочитать пару страниц if-statements, вместо того, чтобы пытаться вспомнить, что делает where в switch.


        1. alexdesyatnik
          16.11.2022 21:51
          +1

          func Transform(p Point) Point {
          	switch {
          	case p.X == 0 && p.Y == 0:
          		return Point{0, 0}
          	case p.X < p.Y:
          		return Point{p.X + p.Y, p.Y}
          	case p.X > p.Y:
          		return Point{p.X - p.Y, p.Y}
          	default:
          		return Point{2 * p.X, 2 * p.Y}
          	}
          }

          Не такая уж и простыня. Го начал осваивать недавно; после того, как преодолел первоначальное отторжение, начал понимать, что в его минимализме многое хорошо. За счёт большего в среднем количества строк по сравнению с Питоном или тем же C# он первоначально производит впечатление более многословного языка, но на самом деле он более вертикальный, чем горизонтальный, если можно так выразиться. Строк больше, но каждая строка при этом краткая и быстро читаемая.


          1. LordDarklight
            18.11.2022 10:11

            Во-первых Ваш код не особо лучше. ту и последовательность if-ов сгодилась бы

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


          1. LordDarklight
            18.11.2022 13:41

            В C# switch-выражение не ограничивает применение аналогичного паттерна (приведённого Вами, который и в вашем напсании , в общме-то, тоже остаётся доступным в C# в рамках классической инструкции switch case).

            Может, на первый взгляд, выгляди непривычно (я бы даже сказал немного кривовато), но вполне работоспособно, не громоздко и при быстром привыкании к такому шаблону вполне понятно и легко читаемо

            static Point Transform4(Point p) => 0 switch
            	{
            			_ when p.X==0 && p.Y==0	=> new Point(0, 0),
            			_ when p.X < p.Y 	=> new Point(p.X + p.Y, p.Y),
            			_ when p.X > p.Y 	=> new Point(p.X - p.Y, p.Y),
            			_            		=> new Point(2 * p.Y, 2 * p.Y),
            	};

            или так (если больше нравится)

            static Point Transform4(Point p) => p switch
            	{
            			var n when n.X==0 && n.Y==0	=> new Point(0, 0),
            			var n when n.X < n.Y 	=> new Point(n.X + n.Y, n.Y),
            			var n when n.X > n.Y 	=> new Point(n.X - n.Y, n.Y),
            			var n             		=> new Point(2 * n.Y, 2 * n.Y),
            	};

            Ниже в комментариях я привёл ещё пару вариантов - более привычных для C# - но требующих перехода от структуры к кортежу (ввожу вспомогательные функции). Вариант выше этого не требует - это просто прямой пример такой же реализации как и код выше - но в рамках switch-выражения. Показывающий неявные возможности switch-выражений.

            Надо только не забывать, что switch-выражения тоже имеют свои ограничения: нельзя объединять в одном блоке соответствия несколько паттернов-соответствия; нельзя в ответной части соответствия писать блок инструкций - там может быть только выражение.

            Зато, switch-выражение имеет куда больше возможностей проверки образца, чем switch case инструкция


      1. LordDarklight
        18.11.2022 09:59
        +1

        Новое switch выражение очень крутое, много чего может - тут ноги растут их функциональных языков и оператора сравнения с образцом + идея того, что каждая операция - это выражение (внедрять функциональные фишки в императивные ЯП нынче очень модно). Привидённый же Вами пример хоть и синтаксически верный, но на практике это очень редкий случай - всё-таки чаще будут применяться куда более простые инструкции. А сложные придут только на замену монструозным if операторам - причём последние без боя долго сдаваться не будут - программисты будут вибирать то, что написать проще - а указанная Вами конструкция не проще последовательности if-ов

        А указаный пример на switch-операторе на мой взгляд лаконичнее выглядет через деконструкцию на кортежи

        static class PointExt
        {
        	static public void Deconstruct(this Point p, out int X, out int Y)
        	{
        		X = p.X;
        		Y = p.Y;
        	}
        }
        
        public class Program
        {
        	
        	static Point Transform2(Point point) => point switch
        	{
        			(0,0)                    	=> new Point(0, 0),
        			(var x, var y) when x < y 	=> new Point(x + y, y),
        			var p when p.X > p.Y 		=> new Point(p.X - p.Y, p.Y),
        			(var x, var y)            	=> new Point(2 * x, 2 * y),
        	};
        }


        1. LordDarklight
          18.11.2022 10:33

          Или вариант 2

          static class PointExt
          {
          	
          	static public (int X, int Y) Decons(this Point p, out int X, out int Y)
          	{
          		X = p.X;
          		Y = p.Y;
          		return (p.X, p.Y);
          	}
          	
          }
          
          public class Program
          {
          	ыtatic Point Transform(Point point) => point.Decons(out var x, out var y) switch
          	{
          			(0,0)      		=> new Point(0, 0),
          			_ when x < y 	=> new Point(x + y, y),
          			_ when x > y 	=> new Point(x - y, y),
          			_            	=> new Point(2 * x, 2 * y),
          	};
          }
          

          если подумать - наверняка найдётся ещё какой-нибудь лаконичный вариант применения switch-выражения. В общем - кортежи и функции расширения рулят

          Только не говорите, что я написал куда больше кода - так как вводил функции расширения! Просто switch-операторы выражения больше затачивались под одинарные типы и под кортежи, чем под развёртывание кишок структур и классов, но поддерживают и операции над ними - коли надо будет!

          И для switch-выражений найдётся куча других примеров (других) ситуаций - где они будут куда более лаконичными!


        1. Nurked
          18.11.2022 10:41

          Это пример с docs.microsoft.com

          Я просто к тому же. Я вообще не понимаю, нафига надо было мешать строгий и лаконичный C# с Яваскриптом?

          Не поймите меня неправильно. Но меня напрягает то, что мне надо раз в 4-5 лет переписывать программы чтобы они компилировались. Это страшно. Не бывает так, что "написал и забыл". Разве что вы хотите переписывать проект с нуля каждые пять лет.

          А вот в реальности все эти примочки только раздражают. Когда ты не можешь вспомнить язык. Это после 20ти лет написания ПО появляется такая точка зрения.


          1. LordDarklight
            18.11.2022 10:44
            +1

            Это пример с docs.microsoft.com

            Тем более. В доках порой приводят далеко не лаконичные примеры - просто для демонстрации всех возможностей (да и сделать красивый практичный и легко понятный обучающий пример - это вообще большое искусство - не многим и не часто это удаётся). А на практике уже программисты сами выбирают - что им удобно!

            Я вообще не понимаю, нафига надо было мешать строгий и лаконичный C# с Яваскриптом?

            Как уже написал - тут смешивание с функциональным подходом, а не с JavaScript идёт. Это сейчас очень модно - скрещивать императивную и функциональную парадигму. И обычно это идёт только на пользу. А первым на подобные конструкции был не то Scala не то Nemerle

            раз в 4-5 лет переписывать программы чтобы они компилировались. 

            Зачем - C# достаточно стабилен в плане ломающих фич. switch-выражения ничего не ломают. И Вы можете использовать классические switch case инструкции.

            Язык C# развивается - это многих привлекает. Но как и любого развивающегося языка со временем он обрастает большим количеством синтаксического сахара и вынужден тянуть устаревшие конструкции. Поэтому со временем приходят новые языки и потихоньку начинают вытеснять старые! И это нормально. Вот, например, сейчас такой процесс идё с Java и Kotlin. В своё время такой процесс был между Java и C#. Так же такой процесс идёт между Objective-C и Swift. Когда то было между C и C++, а сейчас ожидается что будет между C++ и Rust (но не факт).

            В будущем продолжится. Я делаю прогноз - что придут более декларативные абстрагированные ЯП с глубокой интеграцией с AI-компилятором и AI-IDE - которые вытеснят все актуальные ЯП прикладного уровня


    1. JeanneD
      16.11.2022 19:43
      +1

      Ещё и grpc подтянули


  1. tonykk
    16.11.2022 19:43

    А зачем для сортировки теперь два разных метода? Можно было сделать один Order, который без аргументов ссылается на себя, а с аргументом аналогично текущей реализации OrderBy. Это какая-то дань обратной совместимости или под капотом разные реализации?


    1. PashaPash
      16.11.2022 22:26
      +2

      Это LINQ, там под капотом всех методов одна и та же реализация - вернуть новое дерево выражений с довешенным сверху method call. Реализацией и разбором что делать с каждым вызовом занимается конкретный LINQ Provider. Существующие провайдеры при виде method call с тем с именем метода OrderBy, но без аргумента будут падать.

      Они, в принципе, и при виде просто нового Order будут падать, но, по крайней мере, с конкретной ошибкой что метод не поддерживается, а не с NRE. Так что это все ради обратной совместимости.


      1. granit1986
        17.11.2022 08:13
        +4

        Нет не одна. Дерево только для провайдеров IQueryable. Для IEnumerable там обычные функции


        1. LordDarklight
          18.11.2022 10:53

          Но синтаксис вызова то должен для обоих случае быть единым!


          1. granit1986
            18.11.2022 11:06

            Если честно - не понял, что это значит.
            Я мне видится, что эти Order добавили по аналогии со старыми Min/Max, когда, например, коллекция примитивов и нет смысла писать OrderBy(x=>x).
            Ну или внутри класса есть описание для сортировки


            1. LordDarklight
              18.11.2022 13:12

              Я имею в виду не хорошо делать разный API для Entity framwork и для объектной базы. Поэтому приходится вести всё под одну гребёнку - невзирая на разную реализацию для обслуживания различных интерфейсов


  1. moroz69off
    18.11.2022 00:00

    Для меня самая жирная фича -- Support polymorphic deserialization


  1. ksbes
    18.11.2022 10:24

    Такая плотная интеграция с докером — сомнительная вещь.
    Всё-таки докер вещь самостоятельная (Микрософт их не купили? Я не в курсе) — кто знает что и как там сложится. И такая фича может повиснуть очередным «мёртвым легаси».


    1. Firensis
      18.11.2022 14:45

      Кто знает) Я тоже не до конца понял суть этого нововведения, но мб кто сможет пояснить за эту тему...


      1. Meloman19
        18.11.2022 23:27

        А про какую плотную интеграцию речь-то? Судя по примеру, просто добавили профиль публикации (PublishProfile, см. pubxml). Это по сути кастомный набор правил и команд для сборки и dotnet publish знать не знает, что этот DefaultContainer делает.