Вышел .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)
LabEG
16.11.2022 16:43+1Native AOT шикарная тема если сравнивать с Go. У Go было преимущество маленький размер приложений, что удобно в утилитах. В шарпе приходилось качать весь рантайм, в т.ч. в контейнер. Теперь в C# приложения такие же маленькие как и в Go, и контейнеры станут много легче. Go можно выкидывать.
rip_m Автор
16.11.2022 17:26С Go не знаком, но не могу не согласиться :). Native AOT выглядит очень круто.
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 года...
ijsgaus
16.11.2022 19:44+3Продемонстрируйте простыню golang для этого же :-).
Nurked
16.11.2022 21:13Ну так там-то хоть понять можно, что там делается. Мне уж проще прочитать пару страниц if-statements, вместо того, чтобы пытаться вспомнить, что делает where в switch.
alexdesyatnik
16.11.2022 21:51+1func 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# он первоначально производит впечатление более многословного языка, но на самом деле он более вертикальный, чем горизонтальный, если можно так выразиться. Строк больше, но каждая строка при этом краткая и быстро читаемая.
LordDarklight
18.11.2022 10:11Во-первых Ваш код не особо лучше. ту и последовательность if-ов сгодилась бы
Во-вторых, учтите, что switch-выражение чаще применяются там где нет прямого возрата результат через return.
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 инструкция
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), }; }
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-выражений найдётся куча других примеров (других) ситуаций - где они будут куда более лаконичными!
Nurked
18.11.2022 10:41Это пример с docs.microsoft.com
Я просто к тому же. Я вообще не понимаю, нафига надо было мешать строгий и лаконичный C# с Яваскриптом?
Не поймите меня неправильно. Но меня напрягает то, что мне надо раз в 4-5 лет переписывать программы чтобы они компилировались. Это страшно. Не бывает так, что "написал и забыл". Разве что вы хотите переписывать проект с нуля каждые пять лет.
А вот в реальности все эти примочки только раздражают. Когда ты не можешь вспомнить язык. Это после 20ти лет написания ПО появляется такая точка зрения.
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 - которые вытеснят все актуальные ЯП прикладного уровня
tonykk
16.11.2022 19:43А зачем для сортировки теперь два разных метода? Можно было сделать один Order, который без аргументов ссылается на себя, а с аргументом аналогично текущей реализации OrderBy. Это какая-то дань обратной совместимости или под капотом разные реализации?
PashaPash
16.11.2022 22:26+2Это LINQ, там под капотом всех методов одна и та же реализация - вернуть новое дерево выражений с довешенным сверху method call. Реализацией и разбором что делать с каждым вызовом занимается конкретный LINQ Provider. Существующие провайдеры при виде method call с тем с именем метода OrderBy, но без аргумента будут падать.
Они, в принципе, и при виде просто нового Order будут падать, но, по крайней мере, с конкретной ошибкой что метод не поддерживается, а не с NRE. Так что это все ради обратной совместимости.
granit1986
17.11.2022 08:13+4Нет не одна. Дерево только для провайдеров IQueryable. Для IEnumerable там обычные функции
LordDarklight
18.11.2022 10:53Но синтаксис вызова то должен для обоих случае быть единым!
granit1986
18.11.2022 11:06Если честно - не понял, что это значит.
Я мне видится, что эти Order добавили по аналогии со старыми Min/Max, когда, например, коллекция примитивов и нет смысла писать OrderBy(x=>x).
Ну или внутри класса есть описание для сортировкиLordDarklight
18.11.2022 13:12Я имею в виду не хорошо делать разный API для Entity framwork и для объектной базы. Поэтому приходится вести всё под одну гребёнку - невзирая на разную реализацию для обслуживания различных интерфейсов
ksbes
18.11.2022 10:24Такая плотная интеграция с докером — сомнительная вещь.
Всё-таки докер вещь самостоятельная (Микрософт их не купили? Я не в курсе) — кто знает что и как там сложится. И такая фича может повиснуть очередным «мёртвым легаси».Firensis
18.11.2022 14:45Кто знает) Я тоже не до конца понял суть этого нововведения, но мб кто сможет пояснить за эту тему...
Meloman19
18.11.2022 23:27А про какую плотную интеграцию речь-то? Судя по примеру, просто добавили профиль публикации (PublishProfile, см. pubxml). Это по сути кастомный набор правил и команд для сборки и dotnet publish знать не знает, что этот DefaultContainer делает.
VanKrock
Ну назвать основную фишку Generic Math не интересной - это такое
rip_m Автор
Я не называл Generic Math не интересной фишкой :). Если вы про то, что в статье не рассказывается про неё, то хочу заметить, что я дал ссылку на подробную статью по разбору нововведений C# 11.
VBDUnit
Имхо это лучше обобщить через статические методы в интерфейсах + утиную типизацию. Статические методы в интерфейсы, наконец-то, завезли, а вот утиную типизацию в широком смысле ещё нет.
Тогда это распадается на синтаксический сахар конкретного языка.