На момент написания этих строк вышло уже семь превью-версий .NET 6. Дальше — только релиз-кандидаты. Все основные фичи уже добавлены во фреймворк, идёт отладка, тестирование и оптимизация. Ожидать чего-то кардинально нового в RC-версиях, пожалуй, уже не стоит. Пришла пора рассмотреть .NET 6 поближе.


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


Поговорим об этом.


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


Разработчики .NET всегда делали упор на производительность. С одной стороны, в язык и фреймворк постоянно добавляются новый функционал — ref struct, stackalloc, System.Span<T> и всё такое прочее. С другой стороны, с каждой новой версией .NET добавляются новые оптимизации — многопроходная (tiered) компиляция, компиляция в нативный код и, разумеется, огромное количество оптимизаций, которые делает JIT-компилятор. Грамотное использование этих средств даёт свой эффект, который хорошо видно в реальных боевых условиях на графиках производительности.


В NET 6 представлены три инструмента, которые дают ещё большие возможности для повышения эффективности. Причём, не только для самих приложений, работающих в продакшне, но и для разработчиков. Речь идёт о прокачаной предварительной компиляции (через утилиту Crossgen2), оптимизации на основе профилирования (PGO) и горячей перезагрузке приложений во время отладки.


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

Предварительная компиляция


Как известно, преимущества JIT-компиляции имеют свою цену. В частности, повышенное время «прогрева» приложения во время старта, поскольку JIT-компилятору требуется перемолоть разом слишком много IL-кода. Эту проблему уже пытались решить компиляцией приложений сразу в нативный код, такая технология уже есть и называется Ready To Run. Но в новой версии фреймворка её значительно переработали.


Справка: предварительной компиляцией в этой статье называется аббревиатура AOT (Ahead Of Time), используемая в англоязычных источниках.

Старая технология предварительной компиляции была слишком примитивна и позволяла только генерировать нативный код для той платформы, на которой была запущена старая утилита crossgen. Разработчики полностью переписали её с нуля на управляемом коде и назвали Crossgen2. Теперь она предоставляет новые возможности: авторы делают упор на оптимизации, а также использование различных стратегий компиляции для разных платформ (Windows/Linux/macOS/x64/Arm). Всё это достигается новой архитектурой утилиты.


Вкратце это работает так: Crossgen2 разбирает IL-код, составляя некий граф приложения. Затем он запускет внутри себя JIT-компилятор для необходимой платформы, а этот компилятор, анализируя составленный граф, уже создаёт нативный код, применяя при необходимости различные оптимизации. Другими словами, утилита Crossgen2 может быть запущена на платформе x64, но она сгенерирует нативный и даже оптимизтированный код для Arm64. И, разумеется, наоборот это тоже работает.


В настоящий момент код .NET SDK скомпилирован уже с помощью Crossgen2, а старая утилита crossgen отправлена на пенсию.


Оптимизация на основе профилирования


Ещё одна новая старая фишка в .NET 6 — это Profile-Guided Optimization (PGO). Ни для кого не секрет, что обычно в приложении никогда не исполняется вообще весь написанный код. Какой-то код работает чаще других, какой-то вызывается в крайне редких случаях, а какой-то вообще никогда. Но компилятор обычно ничего об этом не знает, а лучше бы знал. Чтобы научить этому компилятор используется PGO-оптимизация. Её смысл заключается в том, что приложение просто прогоняется на разных стандартных кейсах, а заодно профилируется. Итоги профилирования анализируются компилятором, и он начинает распознавать самые часто используемые места кода, уделяя им особое внимание и оптимизируя их более тщательно.


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


Один из подходов — разделение на часто и редко используемый код (hot-cold splitting). Те части кода, которые используются наиболее часто (hot code), группируются и помещаются рядом в итоговом бинарнике. Если сильно повезёт, то такой сгруппированный код полностью поместится в кеш процессора, и вызовы различных часто используемых методов будут практически бесплатными и очень быстрыми. Напротив, некий крайне редко используемый код (very cold code) может вообще не быть скомпилирован в нативный. Например, else-ветки, в которых просто выбрасывается исключение. Такой код остаётся в виде IL-кода и будет скомпилирован в нативный уже после запуска приложения и только в том случае, если это будет необходимо. Такое разделение позволяет не только добиться более высокой производительности при старте, но и генерировать бинарники меньшего размера.


Другой подход — динамическая PGO. То есть, все этапы предварительного обучения JIT-компилятора пропускаются, а вместо этого он внимательно смотрит на то, как приложение работает в реальной среде и при необходимости заново компилирует какой-либо участок кода в более оптимальный. Если вы помните, то подобная технология уже существует — это многопроходная (tiered) компиляция (упоминается в начале статьи). Но разработчики JIT-компилятора просто серьёзно её прокачали.


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


Техника PGO работает в тесной связке с утилитой Crossgen2 и позволяет генерировать оптимизированный нативный код, а также экономить на размере итоговых бинарников. Но нужно отдавать себе отчёт в том, что статическая PGO — это довольно сложно для обычного разработчика. Ведь ему придётся заниматься многократным профилированием своего кода, результаты которого (а это очень много информации) нужно будет специальным образом подавать на вход при компиляции через Crossgen2. И хорошо, если результаты профилирования в тестовой среде будут пригодны и для продуктивной среды — тогда итоговый профит получить можно. Скажем, приложение будет гораздо быстрее запускаться и прогреваться. Это важный фактор, но надо помнить, что цена такой оптимизации — ресурсы, затраченные на предварительное профилирование, которое должно быть проведено очень аккуратно. Если при прогоне приложения на тестовой среде вы сделаете упор на редкие кейсы (например, тестировщики будут прогонять только негативные сценарии, пытаясь всё сломать), то данные профилирования у вас будут сильно отличаться от боевых. А значит, в итоговом бинарнике у вас предкомпилированным и оптимизированным может оказаться вообще не тот код.


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


В общем, выбор у вас есть. Делайте его по ситуации.


Горячая перезагрузка приложений


Эта новая возможность действительно впечатляет. Любому разработчику хочется при отладке быстро пофиксить какой-то мелкий кусок кода без последующей перезагрузки приложения и прохождения заново всего пути к месту отладки. Такая возможность была и раньше, но в очень сильно упрощённом варианте и только в мощной IDE, вроде Visual Studio. Теперь же её прокачали настолько, что она реально позволит сэкономить уйму времени, избавившись от постоянных действий остановка-правка-ребилд-деплой-запуск-достижение точки отладки, причём, в любой IDE, даже в VS Code.


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



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


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

В случаях посерьёзнее (например, при отладке приложений ASP.NET) вам придётся добавить настройку в launchSettings.json, разрешающую горячую перезагрузку, что вряд ли станет большой проблемой.


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


Ах, да: в F# горячая перезагрузка не поддерживается в принципе. Может, когда-нибудь позже. Просто попросите разработчиков об этом.


Более подробно о горячей перезагрузке написано в переводе на Хабре.


Прочие производительные плюшки


Кроме упомянутых трёх очень важных нововведений в обычном цикле разработки удалось найти массу других мест для оптимизации, ускорив тем самым процесс билда и запуска приложений: ликвидировали причины оверхедов, оптимизировали MSBuild, перевели Razor-компилятор на Roslyn source generator и даже позаботились о том, чтобы пореже трогать файлы и зря беспокоить антивирусное ПО.


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



Поддержка ОC и платформ


.NET 6 будет поддерживать ещё больше операционок и платформ. Полный список доступен по этой ссылке. Большое внимание уделяется платформе Arm64 в целом: улучшена поддержка Windows Arm64 и добавлена поддержка Arm64-чипов Apple. Что касается последних, то, как известно, эти чипы умеют работать как в нативном режиме, так и в режиме эмуляции x64. .NET 6 будет поддерживать оба режима и будет уметь создавать как обычный x64, так и нативный для Arm64 код. Для разработки под macOS теперь будут два типа проектов: net6.0-macos для x64 и net6.0-maccatalyst для Arm64 архитектур.


Полный список новых Target Framework'ов теперь выглядит так:


  • net6.0
  • net6.0-android
  • net6.0-ios
  • net6.0-maccatalyst
  • net6.0-macos
  • net6.0-tvos
  • net6.0-windows

Однако, с программированием для Apple-устройств есть один нюанс: существует требование, которые предъявляется к приложениям, публикуемым в App Store. Если разработчик приложения хочет, чтобы приложение запускалось как на x64, так и на Arm64 архитектурах, то оно должно быть скомпилировано как Universal Binaries. Вот с этим требованием пока всё плохо: оно просто не поддерживается в .NET 6. В следующей версии .NET 7 разработчики посмотрят, что можно сделать. Впрочем, это не самое критичное требование, пока можно прожить и без него.


В общем, теперь можно брать новые Макбуки.


Также .NET 6 теперь существует для нескольких новых Linux-дистрибутивов: Alpine 3.13, Debian 11 и Ubuntu 20.04 — соответствующие docker-образы создаются с первого превью .NET 6.


Унификация, поглощение Xamarin, "optional workloads" и MAUI


Ещё пару лет назад разработчики .NET объявили, что собираются объединить в одном .NET-флаконе разработку для всего сразу. Ну, то есть, ничего не будет, а будет одно сплошное телевидение один фреймворк для всего, что только есть на свете — и для мобильной, и для серверной, и для веб-разработки, и для IoT, и для… не знаю, что там ещё появится в будущем. И они назвали это .NET 5, перескочив, во-первых, через версию, чтобы не было путаницы с классическим .NET Framework 4.x, а во-вторых, объединив классический фреймворк с Core, к чему стремились с самого начала, просто осторожно шли окольными путями.


В качестве профита от такого объединения упоминались две ключевые фишки:


  • вы пишете на одном языке с использованием одного API;
  • вы не используете то, что вам не надо: новый фреймворк достаточно раздробленный, и вам не нужно устанавливать кучу ненужных библиотек.

Люди, знающие .NET, когда он ещё пешком под стол ходил, в этом месте начинали припоминать, что примерно такие же обещания раздавались налево и направо двадцать лет назад (а потом повторялись с появлением Silverlight и UWP). Классический фреймворк, вроде как, преследовал эти же самые цели, но только был неделимым, как атом, монолитом, заточенным под одну ОС. Однако, мир менялся быстрее и не в ту сторону. Но в MS вовремя опомнились и умудрились запрыгнуть в уходящий поезд, выпустив первую версию Core, да ещё и выведя разработку в Open Source.

Сейчас .NET далеко не в последнем вагоне этого поезда, и, похоже, тотальная унификация действительно не за горами.

Так вот. Пятую версию .NET выпустили, но унификация продолжается: добрались до Xamarin и поглотили его подружили его с .NET 6. Речь идёт, конечно же, о разработке под Android, iOS и macOS. Вообще, вы теперь и без Xamarin имеете возможность набрать команду dotnet new android и начать разрабатывать под Андроид. А запускать разработанное вы будете командой dotnet run. Но я попробовал — это не работает. Такого шаблона проекта даже нет в последней превью-версии .NET 6. Это потому, что соответствующие библиотеки для разработки под Андроид (а также iOS и macOS) — ну, то есть, то, что раньше было частью Xamarin — не являются частью стандартного .NET SDK. Их нужно скачивать отдельно. В первую очередь, это объясняется тем, что не хочется снова создать огромный монолит. В общем, всё постороннее, что пришло вместе с Xamarin, вынесено в "Optional SDK Workloads" — некие дополнительные части фреймворка, не входящие в стандартный SDK. Иначе размер SDK станет неприличным, а сам он начнёт противоречить одной из заявленных целей: не устанавливать кучу ненужного.


Вот этот вот новый "Optional SDK Workloads" теперь является частью .NET 6 и будет продолжать развиваться в .NET 7. Таким образом происходит слияние Xamarin с .NET. Но Xamarin в данном случае не только что-то отдаёт, но и получает взамен: разработка теперь будет вестись с использованием единой BCL, в едином стиле и с едиными подходами, а также можно будет использовать единую систему всех .NET-утилит, начиная с уже упомянутой dotnet new android. Разумеется, делается акцент и на сокращённом времени билда, уменьшении размеров итогового приложения, а таже улучшенной производительности.


Это ещё не всё, что происходит с Xamarin. Анонсировали новый .NET Multiplatform App UI (MAUI) — «эволюция» Xamarin Forms. С этого момента, думаю, про название "Xamarin Forms" можно уже начать забывать. Отныне вся кроссплатформенная UI-разработка будет называться MAUI. Разумеется, по своей сути MAUI — это мультиплатформенная абстракция над различными UI, родными для каждой конкретной платформы. На MAUI можно разработать интерфейсы, которые будут работать и на Blazor, и на мобильных платформах и даже в десктопных приложениях.


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

А пока разработчики на Xamarin получают возможность полноценно использовать родной .NET 6.0 SDK для кроссплатформенной мобильной разработки.

Как же теперь с этим всем работать, если не получается выполнить команду dotnet new android? Ну, утилиту dotnet, вообще-то, доработали: для работы с "optional SDK workloads" теперь есть команда dotnet workload. Интересно, что она пока не выводится как доступная при вызове dotnet --help, но пользоваться уже можно:


>dotnet workload search android

Workload ID                            Description
----------------------------------------------------------------
microsoft-android-sdk-full             Android SDK
maui-android                           .NET MAUI SDK for Android
microsoft-net-runtime-android          Android Mono Runtime
microsoft-net-runtime-android-aot      Android Mono AOT Workload

Никто не мешает вам уже сейчас загрузить нужный дополнительный SDK и попробовать написать небольшой "Hello World" для вашей мобилки. И даже, наверное, без установки Xamarin. Самое приятное: обещают, что можно будет работать с этим в VS Code, не надо будет ставить могучую и неповоротливую полноценную Студию. Желающие могут это сделать прямо сейчас, скачав готовые примеры из репозитория.



Ждём в .NET-разработку притока мобильщиков?


Опытные разработчики под iOS с интересом ждут выхода релиза .NET 6 и хотят посмотреть как будет выглядеть .NET-разработка под iOS без Apple-устройств и Xcode. Обещается, что с машин на Windows можно будет подключаться к устройствам Apple для отладки приложения в симуляторах. Посмотрим.

Blazor на десктопе


Оказывается, Blazor стал достаточно популярным (по заверениям разработчиков .NET), причём, настолько, что было решено сделать десктоп-версию Blazor-приложений. Модель разработки это позволяет.


В общем, теперь вы можете написать Blazor-приложение, которое запустится не только в браузере как WebAssembly, но и на Windows и macOS как нативное десктопное.


Улучшения в System.Text.Json


Вот и добрались до изменений в SDK. А их достаточно много, очень сложно пройти мимо. Начнём с System.Text.Json — эту библиотеку очень сильно прокачали.


Все примеры далее взяты из официальных пресс-релизов команды разработки .NET.

Игнор цикличных ссылок


В сериализатор добавили опцию игнорирования цикличных ссылок.


class Node
{
    public string Description { get; set; }
    public object Next { get; set; }
}

void Test()
{
    var node = new Node { Description = "Node 1" };
    node.Next = node;

    var opts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };

    string json = JsonSerializer.Serialize(node, opts);
    Console.WriteLine(json); // Prints {"Description":"Node 1","Next":null}
}

Обратите внимание на то, что сериализатор заменяет ссылку на null, а не игнорирует свойство полностью.


Если честно, то сложно представить себе ситуацию, продемонстрированную в примере. Но будем иметь в виду.

Поддержка IAsyncEnumerable<T>


Сериализатор System.Text.Json теперь поддерживает IAsyncEnumerable<T>-объекты. При сериализации он их превращает в массивы:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

static async IAsyncEnumerable<int> PrintNumbers(int n)
{
    for (int i = 0; i < n; i++) yield return i;
}

using Stream stream = Console.OpenStandardOutput();
var data = new { Data = PrintNumbers(3) };
await JsonSerializer.SerializeAsync(stream, data); // prints {"Data":[0,1,2]}

Для десериализации JSON-документов, которые представляют собой просто массив на корневом уровне, добавили новый удобный метод JsonSerializer.DeserializeAsyncEnumerable:


using System;
using System.IO;
using System.Text;
using System.Text.Json;

var stream = new MemoryStream(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
{
    Console.WriteLine(item);
}

JSON DOM


Самое интересное нововведение — это возможность работать с JSON-документом как с DOM. Эта особенность довольно полезна, поскольку часто просто не хочется плодить POCO-объекты для простых операций. Вот пример того, как это теперь работает:


// Parse a JSON object
JsonNode jNode = JsonNode.Parse("{"MyProperty":42}");
int value = (int)jNode["MyProperty"];
Debug.Assert(value == 42);
// or
value = jNode["MyProperty"].GetValue<int>();
Debug.Assert(value == 42);

// Parse a JSON array
jNode = JsonNode.Parse("[10,11,12]");
value = (int)jNode[1];
Debug.Assert(value == 11);
// or
value = jNode[1].GetValue<int>();
Debug.Assert(value == 11);

// Create a new JsonObject using object initializers and array params
var jObject = new JsonObject
{
    ["MyChildObject"] = new JsonObject
    {
        ["MyProperty"] = "Hello",
        ["MyArray"] = new JsonArray(10, 11, 12)
    }
};

// Obtain the JSON from the new JsonObject
string json = jObject.ToJsonString();
Console.WriteLine(json); // {"MyChildObject":{"MyProperty":"Hello","MyArray":[10,11,12]}}

// Indexers for property names and array elements are supported and can be chained
Debug.Assert(jObject["MyChildObject"]["MyArray"][1].GetValue<int>() == 11);

До сих пор в подобных случаях нужно было пользоваться классами Utf8JsonWriter/Utf8JsonReader, но DOM-подход тоже неплох.


Надо отдавать себе отчёт в том, что DOM-подход к работе с JSON неизбежно ведёт к падению производительности и перерасходу ресурсов. Разработчики утверждают, что это не так, и что Writable DOM Feature на самом деле высокопроизводительна, но нас легко рассудят бенчмарки, которые обязательно кем-нибудь будут сделаны в ближайшем будущем.

Поддержка source generators для сериализации


В плане перерасхода ресурсов от DOM-модели не сильно отстаёт обычная сериализация и десериализация. Она основана на рефлексии, а это заведомо медленно. Поэтому там, где реально нужна производительность, всегда лучше было работать с ...Writer и ...Reader классами (это правило касается не только работы с JSON, но также и с XML). Такая работа занимает больше времени, но окупается максимальной производительностью на продакшне.


Однако разработчики .NET 6 и тут придумали обходной манёвр для облегчения жизни разработчиков: source generators. Эту новую технологию завезли в System.Text.Json, и она решает все основные проблемы, связанные с низкой производительностью обычных сериализаторов: уменьшает время старта приложения и количество используемой памяти, увеличивает скорость работы, не использует рефлексию. Что же тогда используется взамен, если не рефлексия? Именно тот самый класс Utf8JsonWriter, через который и происходит работа с JSON.


Выглядит такая техника точно так же, как и при любой другой работе с source generators. Сначала вы создаёте тип для сериализации/десериализации:


namespace Test
{
    internal class JsonMessage
    {
        public string Message { get; set; }
    }
}

Как видите, он слишком простой, но для иллюстрации работы этого достаточно. Затем вы создаёте partial-класс и сопровождаете его соответствующим атрибутом:


using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(JsonMessage)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }
}

После этого на этапе компиляции ваш частичный класс будет расширен несколькими методами и свойствами:


internal partial class JsonContext : JsonSerializerContext
{
    public static JsonContext Default { get; }

    public JsonTypeInfo<JsonMessage> JsonMessage { get; }

    public JsonContext(JsonSerializerOptions options) { }

    public override JsonTypeInfo GetTypeInfo(Type type) => ...;
}

Через одно из этих свойств — JsonMessage вы получите доступ к сгенерированному сериализатору, работа с которым будет выглядеть как-то так:


using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);

JsonContext.Default.JsonMessage.Serialize(writer, new JsonMessage { "Hello, world!" });
writer.Flush();

// Writer contains:
// {"Message":"Hello, world!"}

Стандартный сериализатор также прокачан и может принимать на вход сгенерированный с помощью source generator код:


// Способ 1
JsonSerializer.Serialize(jsonMessage, JsonContext.Default.JsonMessage);

// Способ 2
JsonSerializer.Serialize(jsonMessage, typeof(JsonMessage), JsonContext.Default);

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


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


К сожалению, десериализация через source generators пока не поддерживается. Единственное, что разработчики добавили, — это поддержку в стандартном десериализаторе сгенерированных типов:


// Способ 1
JsonSerializer.Deserialize(json, JsonContext.Default.JsonMessage);

// Способ 2
JsonSerializer.Deserialize(json, typeof(JsonMessage), JsonContext.Default);

Но даже в этом случае никаких Utf8JsonReader не будет. Только рефлексия, только хардкор.


Поддержка нотификаций при (де)сериализации


В специальный неймспейс System.Text.Json.Serialization добавили четыре интерфейса: IJsonOnDeserialized, IJsonOnDeserializing, IJsonOnSerialized и IJsonOnSerializing. Они нужны для вызова методов в процессе (де)сериализации. Как правило, в целях валидации:


public class Person : IJsonOnDeserialized, IJsonOnSerializing
{
    public string FirstName{ get; set; }

    void IJsonOnDeserialized.OnDeserialized() => Validate(); // Call after deserialization
    void IJsonOnSerializing.OnSerializing() => Validate(); // Call before serialization

    private void Validate()
    {
        if (FirstName is null)
        {
            throw new InvalidOperationException("The 'FirstName' property cannot be 'null'.");
        }
    }
}

Но вы можете придумать и какое-нибудь своё применение.


Порядок следования полей при сериализации


С помощью специального атрибута JsonPropertyOrder теперь можно управлять порядком, в котором сериализованные поля будут помещаться в итоговый JSON:


public class Person
{
    public string City { get; set; } // No order defined (has the default ordering value of 0)

    [JsonPropertyOrder(1)] // Serialize after other properties that have default ordering
    public string FirstName { get; set; }

    [JsonPropertyOrder(2)] // Serialize after FirstName
    public string LastName { get; set; }

    [JsonPropertyOrder(-1)] // Serialize before other properties that have default ordering
    public int Id { get; set; }
}

Ранее порядок, в котором поля попадали в итоговый JSON, был, скажем так, не совсем предсказуем.


Utf8JsonWriter: возможность вывести напрямую JSON-текст


В класс System.Text.Json.Utf8JsonWriter добавили метод WtiteRawValue, и теперь в JSON можно писать raw-текст:


JsonWriterOptions writerOptions = new() { WriteIndented = true, };

using MemoryStream ms = new();
using UtfJsonWriter writer = new(ms, writerOptions);

writer.WriteStartObject();
writer.WriteString("dataType", "CalculationResults");

writer.WriteStartArray("data");

foreach (CalculationResult result in results)
{
    writer.WriteStartObject();
    writer.WriteString("measurement", result.Measurement);

    writer.WritePropertyName("value");
    // Write raw JSON numeric value using FormatNumberValue (not defined in the example)
    byte[] formattedValue = FormatNumberValue(result.Value);
    writer.WriteRawValue(formattedValue, skipValidation: true);

    writer.WriteEndObject();
}

writer.WriteEndArray();
writer.WriteEndObject();

В данном примере решается одна известная проблема сериализатора: он не пишет дробную часть вещественного числа, если она равна нулю. То есть, число 1.1 будет выведено с вещественной частью, а число 1.0 будет выведено как целое. В ряде случаев такое поведение неприемлемо, и теперь вы можете управлять им с помощью нового метода.


На момент написания этих строк метод WriteRawValue готов, но ещё не добавлен в основную ветку кода и болтается в одном из пулл-реквестов. Но сама фича заппрувлена в итоговый релиз. Просто ещё не дошёл ход до неё.

Десериализация из Stream


Оказывается, раньше не было возможности десериализовать поток. Теперь есть:


using MemoryStream ms = GetMyStream();
MyPoco poco = JsonSerializer.Deserialize<MyPoco>(ms);

И эта фича тоже пока болтается в пулл-реквесте.

Новая коллекция PriorityQueue


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


// creates a priority queue of strings with integer priorities
var pq = new PriorityQueue<string, int>();

// enqueue elements with associated priorities
pq.Enqueue("A", 3);
pq.Enqueue("B", 1);
pq.Enqueue("C", 2);
pq.Enqueue("D", 3);

pq.Dequeue(); // returns "B"
pq.Dequeue(); // returns "C"
pq.Dequeue(); // either "A" or "D", stability is not guaranteed.

Как видно из примера, в случае равенства приоритетов порядок извлечения элементов не гарантирован.


Очень интересная коллекция, вполне подойдёт для некоторых случаев.


Source Generator для ILogger


Новая фича .NET 5 — Source Generators — добралась до логгера. Теперь можно писать меньше кода для логгинга, потому что недостающий код будет создан автоматически. Вам достаточно лишь пометить специальные методы специальным атрибутом LoggerMessageAttribute, и весь недостающий код будет скомпилирован за вас, причём, он будет более оптимальным и производительным.


public static partial class Log
{
    [LoggerMessage(EventId = 0, Level = LogLevel.Critical, Message = "Could not open socket to `{hostName}`")]
    public static partial void CouldNotOpenSocket(ILogger logger, string hostName);
}

Только не забудьте пометить и метод, и содержащий его класс ключевым словом partial.


Детали уже можно почитать в документации.


Улучшения в System.Linq


В LinqExtensions добавили массу полезных методов и фич. Например, поддержку диапазонов и индексов. Теперь можно попросить вернуть второй с конца элемент коллекции:


Enumerable.Range(1, 10).ElementAt(^2); // returns 9

А в метод Take() добавили классную перегрузку:


source.Take(..3);       // instead of source.Take(3)
source.Take(3..);       // instead of source.Skip(3)
source.Take(2..7);      // instead of source.Take(7).Skip(2)
source.Take(^3..);      // instead of source.TakeLast(3)
source.Take(..^3);      // instead of source.SkipLast(3)
source.Take(^7..^3);    // instead of source.TakeLast(7).SkipLast(3)

Новый метод TryGetNonEnumeratedCount() сильно помогает в случаях, когда надо узнать количество элементов коллекции без её перебора:


List<T> buffer = source.TryGetNonEnumeratedCount(out int count) ? new List<T>(capacity: count) : new List<T>();
foreach (T item in source)
{
    buffer.Add(item);
}

Если source — это просто переменная типа IEnumerable, то попытка получить количество элементов коллекции может вызывать полный перебор коллекции раньше времени. А с помощью TryGetNonEnumeratedCount() можно и рыбку съесть узнать количество элементов для аллокации соответствующего массива, и полный перебор отложить на более подходящее время.


Четыре новых метода DistinctBy/UnionBy/IntersectBy/ExceptBy теперь позволяют явно указывать поле-селектор:


Enumerable.Range(1, 20).DistinctBy(x => x % 3); // {1, 2, 3}

var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) };
first.UnionBy(second, person => person.Age); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40), ("Drew", 33) }

А в дополнение к ним завезли ещё два аналогичных метода: MaxBy/MinBy.


var people = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
people.MaxBy(person => person.Age); // ("Ashley", 40)

А этого иногда сильно не хватало.

Странно, что до этого не додумались раньше, но теперь это есть. Методы FirstOrDefault/LastOrDefault/SingleOrDefault позволяют указывать дефолтное значение, как это делается в методе nullable-типов GetValueOrDefault:


Enumerable.Empty<int>().SingleOrDefault(-1); // returns -1

Ну и напоследок. Метод Zip теперь имеет перегрузку для итерации по трём коллекциям:


var xs = Enumerable.Range(1, 10);
var ys = xs.Select(x => x.ToString());
var zs = xs.Select(x => x % 2 == 0);

foreach ((int x, string y, bool z) in Enumerable.Zip(xs,ys,zs))
{
}

Кто-то вообще в курсе, что так можно было?

Дата и время


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


Разработчики .NET решили немного то ли облегчить, то ли усугубить страдания и добавили пару новых структур: DateOnly и TimeOnly, а также немного подшаманили с поддержкой временных зон и ещё по мелочи. Достаточно подробный обзор этих нововведений уже есть в этой переводной статье. И он обязателен к прочтению и глубокому осмыслению.


Preview Features и сразу Generic Math


Выпуск новых версий .NET уже давно встал на поток: в год — по LTS-версии. Это значительно быстрее, чем было раньше с классическим фреймворком, и это хорошо с одной стороны: можно оперативнее реагировать на запросы пользователей, быстрее выкатывать полезные фичи и вообще — не тормозить.


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


И они придумали механизм Preview Features. Теперь LTS-версию .NET можно будет поставлять с недоделанными превью-фичами. То, что раньше было доступно только в превью- и RC-версиях фреймворка, отныне может совершенно легально попасть в библиотеки, компиляторы и продакшн. В целях безопасности это всё обвешано атрибутами и настройками, чтобы по умолчанию быть выключенным. То есть, вы не сможете это использовать, специально не заморочившись. А вот захотите вы заморачиваться или нет — дело ваше.


Возможно, вам понравится первая превью-фича, для которой разработали весь этот механизм: статические абстрактные методы интерфейсов. Эта фича как раз из тех, что довольно сложно внедрить быстро. Её не успели обкатать в превью-версиях .NET 6 и решили выпустить в LTS-версии в том виде, в каком успеют реализовать к релизу. Поскольку это превью-фича, то нет никаких гарантий, что она не изменится даже в ближайших двух RC-выпусках .NET 6. Более того: нет никаких гарантий, что она не изменится в апдейтах .NET 6 после релиза. На этой новой фиче построен механизм арифметики в обобщениях. Детально об этом можно почитать в статье, и звучит это неплохо.


Итого, с помощью нового механизма превью-фич и разработчики .NET, и разработчики на .NET получают лишнее время на обкатку интересных идей. Вопрос: а не выльется ли это в конце концов в то же самое, во что превратились HTML и CSS? Ну, то есть, когда нумерация версий уже не имеет значения, а регулярно добавляемые фичи сначала какое-то время находятся в превью-стадии, а затем, после тестирования и доработок, просто переходят в спецификацию?


Больше анализаторов богу анализа!


С компилятором Roslyn наступила эра Roslyn-анализаторов, которые, вообще-то, здорово помогают в разработке. В .NET 5 в компилятор уже было встроено порядка 250 различных анализаторов, и ещё больше можно было скачать в виде nuget-пакетов. С какого-то момента команда dotnet build выводит дикое количество уведомлений от анализаторов о том, что разработчик говнокодит пишет что-то не то. С одной стороны, эти предупреждения, генерируемые анализаторами, помогают заметить косяки и сделать код чище. С другой стороны, всё равно есть много ложных срабатываний, в которых теряются действительно важные замечания. Это всё можно настроить, но на это нужно время.


В .NET 6 решили не останавливаться на достигнутом, и теперь встроенных анализаторов ещё больше (правда, среди них есть и те, что ранее поставлялись отдельно). Окинув беглым взглядом список новых анализаторов, нельзя не признать, что среди них, безусловно, много полезных.


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


API для выделения памяти


Лёгким движением руки C# можно превратить в C. И это почти не шутка: в .NET 6 завезли нативное выделение памяти. За это дело отвечают специальные методы в новом классе System.Runtime.InteropServices.NativeMemory.


namespace System.Runtime.InteropServices
{
    public static class NativeMemory
    {
        public static unsafe void* Alloc(nuint byteCount);
        public static unsafe void* Alloc(nuint elementCount, nuint elementSize);

        public static unsafe void* AllocZeroed(nuint byteCount);
        public static unsafe void* AllocZeroed(nuint elementCount, nuint elementSize);

        public static unsafe void* Realloc(void* ptr, nuint byteCount);

        public static unsafe void Free(void* ptr);

        public static unsafe void* AlignedAlloc(nuint byteCount, nuint alignment);
        public static unsafe void AlignedFree(void* ptr);
        public static unsafe void* AlignedRealloc(void* ptr, nuint byteCount, nuint alignment);
    }
}

К чёрту управляемые ресурсы, к чёрту сборщик мусора. Да здравствуют alloc и free методы! Разумеется, всячески подчёркивается, что это для низкоуровневого кода и алгоритмов. Но мы как-то упустили упустили момент, когда C# стал позиционироваться как язык для таких вещей.


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


Что будет дальше? Добавление возможности писать прямые ассемблерные инструкции в коде? Или движение в сторону C++? Подождём .NET 7.


И так далее


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


  • пул потоков полностью переписан с нативного на управляемый код;
  • оптимизация работы со структурами: они теперь могут целиком передаваться как параметры через регистры процессора;
  • ускорено приведение и проверка интерфейсных типов (будет быстрее работать Pattern Matching);
  • с помощью прекрасной новой команды dotnet sdk check вы можете проверить актуальность ваших SDK;
  • вебсокеты поддерживают компрессию;
  • BigInteger теперь парсит строки почти на 90% быстрее;
  • Vector<T> теперь поддерживает примитивы nint и nuint;
  • добавлена поддержка OpenTelemetry.

Но даже это не окончательный список.


А самое главное — за кадром остались нововведения в языке C# 10. Об этом — в другой раз.

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


  1. anonymous
    00.00.0000 00:00


    1. SanSYS
      23.08.2021 14:24

      Сильно раньше, кмк, вот статейка от мая 2018-го https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch

      Просто выглядит так, что теперь этот режим поддерживается не только из консоли, но и в IDE


  1. anonymous
    00.00.0000 00:00


  1. Krey
    19.08.2021 05:31

    Что будет дальше? Добавление возможности писать прямые ассемблерные инструкции в коде? 

    Шутка? Давно писали жеж.


    1. mrbaranovskyi
      19.08.2021 09:03

      Ну да, есть интристики.. по сути уже можно начиная с Core 3.0.


      1. DistortNeo
        19.08.2021 11:27

        Я недавно сравнивал производительность C# и C++ в вычислительных задачах на примерах базовых алгоритмов обработки изображений с интринсиками. Получил результат, что C++ быстрее C# где-то процентов на 20. Так что да, на C# уже смело можно писать высокопроизводительный код.


        1. tangro
          19.08.2021 11:42

          Так а писать высокопроизводительный код на шарпе никогда особо не было проблемой. Если стоит задача на десктопе, например, обработать картинку и показать юзеру, то случится это за 0.5 с или 0.6 с - не важно. На сервере для обработки картинок пачками - тоже не важно, ведь всегда можно добавить ядер, ОЗУ или больше инстансов и обрабатывать столько картинок в секунду, сколько нужно.

          Но всё становится совершенно иначе, когда картинку нам нужно обрабатывать для автопилота, для отображения на спидометре или для экране медицинского прибора. Тут важно не "вцелом почти так же быстро", а точное значение потребляемой памяти, точные моменты выделения и освобождения памяти, точное количество инструкций, тактов и милисекунд на обработку. И тут по-прежнему как не было места управляемому коду, так и не будет.


          1. Kanut
            19.08.2021 11:50
            +4

            Но всё становится совершенно иначе, когда картинку нам нужно обрабатывать для автопилота, для отображения на спидометре или для экране медицинского прибора. Тут важно не "вцелом почти так же быстро", а точное значение потребляемой памяти, точные моменты выделения и освобождения памяти, точное количество инструкций, тактов и милисекунд на обработку. И тут по-прежнему как не было места управляемому коду, так и не будет.

            Вот только тот же Сименс для своих CT/MRT вполне себе пишет код на C#. В том числе и для обработки изображений. To есть про это я точно знаю. И наверняка для каких-то других медицинских приборов это тоже так.


            1. apro
              22.08.2021 00:18
              +2

              Был как-то на докладе про Java для "High-frequency trading". Только я бы не смог назвать это Java. Garbage collector не использовался, память выделялась и освобождалась вручную, почти все функции статические, а классы final. В общем-то у многих после доклада был вопрос, а зачем Java если из специфичных для нее возможностей ничего не использовалось.

              И не совсем понятно с МРТ, одно дело управлять и собирать данные во время работы, другой вопрос обработка уже собранных данных постфактум для отображения на экране монитора. Для второго никакой работы в "реальном времени" не нужно и можно эту часть реализовать на чем угодно. А вот первое писать на языке с "garbage collection" довольно странно.


            1. GrigoryPerepechko
              23.11.2021 14:03

              Пруф что шарп на приборе исполняется будет или сразу начнёте шарманку что имели в виду не прибор, а бэкенд сервера и десктоп приложения на компьютере доктора?


              1. Kanut
                23.11.2021 14:35

                Ну вот банально у самого Сименса находится открытая вакансия: SPECT & PET Acquisition system for next generation MI scanners(C#, WPF).


                А вот ещё одна(C#/.Net Core on Linux) под Point of Care Diagnostics.


                Это на приборе исполняется?


    1. KvanTTT
      19.08.2021 11:39
      +6

      Ну обычный ассемблер — это слишком, а вот вставки IL кода были бы очень кстати для высокопроизводительного или специфичного кода (который не описать на C#). Есть дискуссия на гитхабе: Proposal: Inline Languages.


    1. Krey
      19.08.2021 12:26

      Отвечая на коменты к моему посту про асм...

      Да коммон ребята, были вставки на асме в шарпе, возможно они закончились на х86 перед переходом на х64, или какие-то ограничения наложились, дальше просто не следил. Но они были и хорошо работали. Основная проблема с ними была что специфичные команды, для которых асм вроде и нужен, мсовский компилятор не понимал, так что все равно брался в руки С++, интеловские компайлер и подключался как всегда через нативную dll. А на простых командах встроенный асм норм работал.

      Да и залезте вы внутрь fw2, там внутрях куча примеров использования асма.


      1. mayorovp
        19.08.2021 12:47

        Без пруфов не верю. Приведите хоть одну ссылку. Кстати, что такое "fw2"?


  1. mrbaranovskyi
    19.08.2021 09:14
    +5

    Но людей с определённым профессиональным опытом настораживает попытка создать универсальную платформу для всего сразу.

    Ну, люди с "определённым профессиональным опытом" должны понимать (я уверен что понимают), что ситуации бывают разные и иногда нужно выжимать из кода всё, что возможно. Пусть только в определённых точках кодовой базы. Убеждения, что для этого нужно писать код на Си, они хреновенько выглядят на практике ибо их трудно поддерживать, появляются гетерогенные (или как это назвать) проджекты с обертками для нативных вызовов. Тяжело билдить. Тяжело поставлять. Неприятно (иногда) дебажить.

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


    1. Hydro
      19.08.2021 09:43
      +2

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

      Кстати, привет, собедесующим из @htc-cs


      1. mrbaranovskyi
        19.08.2021 12:51
        +1

        Оо да... В добавок, уже на многие вопросы, которые были актуальны в 2010, перестали таковыми быть сейчас.
        К примеру, вопросы об аллокации классов уже не совсем не однозначные ибо в core 3.0 сделали escape analysis и теперь при должных условиях класс улетает в стек. Или вот сегодня утром читал свежевышедшую статью (на хабре есть её укороченная версия) Performance improvents in .NET 6.0 . Насколько понимаю, что .NET 6.0 при определенных условиях сможет сапрессить баудс чек массивов. (Я её еще не дочила до конца ибо она огромная, но подозреваю, что сюрпризов там достаточно).


        1. KvanTTT
          19.08.2021 14:01

          Насколько понимаю, что .NET 6.0 при определенных условиях сможет сапрессить баудс чек массивов. (Я её еще не дочила до конца ибо она огромная, но подозреваю, что сюрпризов там достаточно).

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


          public void M(int[] array) {
             for (int i = 0; i < array.Length; i++) {
                Console.Write(array[i]);
             }
          }

          Но наверное в статье идет речь о чем-то более продвинутом.


        1. Szer
          19.08.2021 14:24

          в core 3.0 сделали escape analysis и теперь при должных условиях класс улетает в стек

          Не сделали в итоге. Что добавляет конфузий, да.


          1. mrbaranovskyi
            19.08.2021 15:05

            Я на прошлой неделе, кажется, еще писал тест на пятой коре.
            В GC появился метод который возвращает аллоцированные байты. Выделяет класс. Смотрите разницу. Класс в стеке. выделено ноль. Коммит, вроде бы тоже как в мастере.

            https://github.com/dotnet/coreclr/pull/20814

            Они описывают как "limited support". Но всё же должен быть.

            Тестил я на очень простом кейсе. Собственно, и сам тест я взял из юнит тестов к коммиту. Было интересно поиграться.


            1. Szer
              19.08.2021 15:08

              Эта репа давно неактуальна.

              Нужная - dotnet/runtime

              Покажи полный код, вполне возможно что просто вырезано джитом.


              1. mrbaranovskyi
                19.08.2021 15:30

                аа.. вот балда.. значит наврал.
                Там по ссылке, в коммите есть юнит тесты.
                Gc
                Я был уверен, что сделали. Даже в .NET Blog упоминали, что добавили (limited).

                long allocatedBytesBefore = GC.GetAllocatedBytesForCurrentThread();

                int testResult = test();

                long allocatedBytesAfter = GC.GetAllocatedBytesForCurrentThread();

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


                1. Szer
                  19.08.2021 15:33

                  Действительно делали, во внутренних тестах не нашли импакта и не стали мержить в мастер.

                  По итогу в неткоре нет полноценного ескейп анализа, но многие ад хок кейсы джит и так оптимизирует. Новый инлпйнер в нет6 топчик в этом плане.


                  1. mrbaranovskyi
                    19.08.2021 15:40

                    Всё я нашел

                    В комментариях указанно, что выключено по-умочанию https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/.
                    Дальйшую судьбу узнал вот только от тебя.

                    Спасибо, за уточнение!! Дейсвительно запутали еще больше.

                    Инлайнер - да. Нужно будет вечером засеть и разобрать внимательно статью. Капец она здоровая.


    1. AikoKirino
      19.08.2021 10:07
      +2

      К слову: писать интероп со временем тоже стало удобнее. Обертки к либам для разных рантаймов с NativeLibrary теперь делать одно удовольствие.
      А так действительно фич и сахарка(в том же С#) очень много, и достаточно не написать Main, использовать new() и коллеги начнут спрашивать "Это точно сишарп?".


      1. mrbaranovskyi
        19.08.2021 12:43

        Верно! Не уверен, но я видел протитим delegate*, который не аллоцирует делегат.


    1. sepulkary
      19.08.2021 10:08

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

      Я учу .NET прямо сейчас, пытаясь из электронщика/системного программиста перестроиться в полосу C#-разработки. Не сказать, чтобы прям полыхает, но подгорает знатно.

      Существенный объем информации плюс отсутствие поблизости более опытных товарищей делают продвижение к каким-то осязаемым вершинам довольно медленным.


      1. Hydro
        19.08.2021 11:15
        +3

        Поздравляю, я шел по такому же пути в 2012-ом))) тогда учить C# было заметно проще))


      1. Krey
        19.08.2021 12:39

        А я вот обратно хочу, поближе к железу. И тупизна C и C++ меня вымораживает.

        Это кстати ответ автору зачем. Когда нибудь можно будет ОС и драйверы на шарпе писать, но я наверное не доживу :)

        Ps да я в курсе(смотрел) про singularity OS, cosmos итп...


        1. mayorovp
          19.08.2021 12:54

          Пожалуйста, не надо драйверов на шарпе. Только сборщика мусора в ядре и не хватало...


          Что же до близости к железу — учтите, что даже со всеми нововведениями "нативная" часть С# ещё тупее языка Си. Написать таким образом кусок критического к производительности кода или прослойку для взаимодействия с библиотекой ещё можно, но если писать так всю программу — зачем вообще выбирать C#?


          К слову о близких к железу языках, Rust смотрели?


          1. mrbaranovskyi
            19.08.2021 13:36
            +1

            Писать можно и без сборки мусора. Пулинг никто не отменял. Зачем? Если сравнивать си, то это безопаснее. В этом критичном к производительности куске после, находят парочку дыр.
            Но, да, есть Rust. Я его плохо знаю, пока не в курсе насколько он справляется с типичными сишными проблемами.


            1. mayorovp
              19.08.2021 14:13

              За счёт чего низкоуровневые возможности C# безопаснее Си?


              1. DistortNeo
                19.08.2021 14:25
                +1

                Ну, например, за счёт контроля выхода за границы области памяти при использовании низкоуровневого Span<T>.


                1. mayorovp
                  19.08.2021 14:38

                  Ну да, от переполнения буфера спаны защищают, тут соглашусь.


                  Но посмотрите на NativeMemory, про который написано в обсуждаемом посте. Это что, безопасное API?


                  Или возьмём идею "пулить" всё подряд. Да, сборку мусора оно "починит" — вот только в обмен будут все те же проблемы Си с ручным управлением временем жизни.


                  1. andreyverbin
                    10.09.2021 22:12

                    Или возьмём идею "пулить" всё подряд. Да, сборку мусора оно "починит" — вот только в обмен будут все те же проблемы Си с ручным управлением временем жизни.

                    Не починит, а только усугубит, скорее всего. Непонятно откуда берется идея, что выделение большого числа объектов это плохо. На скорость сборки мусора влияет количество живых объектов, а не выделенных. Если для обработки запроса выделить 100 000 объектов и через 100ms все они уже не нужны, то все ок, GC их даже не увидит. То есть, внимание, стоимость создания и удаления 100 000 объектов в этом случае равна почти 0. А если сделать пул из 1 000 000 объектов и каждую секунду каждый из них каким-то запросом используется и изменяется, то GC будет вынужден все 1 000 000 объектов проверить и это будет долго.

                    Есть еще кейс, когда объектов выделяется так много, что тупо память заканчивается за те самые 100ms и тогда наступает п..ц и все встает колом.


                    1. KvanTTT
                      23.11.2021 14:32
                      +1

                      Непонятно откуда берется идея, что выделение большого числа объектов это плохо.

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


                      1. andreyverbin
                        25.11.2021 14:13

                        Выделять объекты дёшево или даже бесплатно, а вот копировать данные из нативного буфера в поля этих объектов нет. Тут пригождается Span.


                      1. KvanTTT
                        25.11.2021 14:21

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


                      1. andreyverbin
                        25.11.2021 16:01
                        -1

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

                        Про лишние байты верно, расточительно создавать объекты, в которых 1 байт полезной нагрузки.


            1. AnthonyMikh
              19.08.2021 15:44

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

              Замечательно, после перехода на Rust волосы станут мягкими и шелковистыми вы забудете про гонки данных, доступ к неинициализированным значениям, use after free и buffer overwrite. А, ну и, конечно, типобезопасные обобщённые функции, вывод типов и нормальная модульность без деления на заголовочные файлы и файлы реализации и без include guard.


              1. mrbaranovskyi
                19.08.2021 15:48

                Надеюсь, что так и есть. Я начал смотреть, после того как прочитал, что Линус "одобрил". Я даже где-то в комментах писал, что если даже этот "Му**к" (в хороше смысле) одобрил, то видимо стоит взглянуть :D

                От мягких и шелковистых волос, я бы тоже не отказался ))


                1. 0xd34df00d
                  19.08.2021 20:41
                  +2

                  Глядеть на это всё стоит ИМХО хотя бы только ради того, чтобы знакомиться с новыми концепциями.


        1. mrbaranovskyi
          19.08.2021 12:55

          Уже была написана, как тестовый проект.Да Singularity называется. Если верить Торвалдсу, то ООЯ не очень хорошо подходят для написания операционных систем.

          (не совсем понимаю почему)

          но может ему виднее. На том же Шарпе, вполне можно писать достаточно низкоуровнево.

          Хз, может когда-то будет.


      1. mrbaranovskyi
        19.08.2021 12:54

        Ну, я начал учить его с 4.0 кажется. Но до этого уже немного знал Си. Это было сто процентов проще нежели сейчас. Могу только пожелать терпения. .net очень обширный.


      1. mayorovp
        19.08.2021 13:01
        +5

        А перед вами точно стоит задача "изучить весь C# от корки до корки"?


        Так-то учить весь язык целиком и сразу для написания программ не обязательно...


        1. Kanut
          19.08.2021 13:09
          +3

          Я бы сказал что даже если не стоит цель "от корки до корки", то порог вхождения как таковой сейчас однозначно выше чем он был 10-15-20 лет назад.


          То есть условный "Hello World" сейчас так же просто написать как и раньше. Или в принципе что-то совсем простое. Но даже туториалы и "прикладные" примеры содержать больше информации чем раньше.


          То есть на мой взгляд те же LINQ или async/await сейчас вполне уже можно ожидать от человека, который хочет зарабатывать деньги шарпом.


          1. DistortNeo
            19.08.2021 14:05
            -2

            На самом деле, LINQ и async/await — это не уникальные возможности шарпа.
            В JavaScript, например, принцип работы с асинхронным кодом абсолютно аналогичен.


            1. Kanut
              19.08.2021 14:15

              Во первых речь не о том что это что-то уникальное. Речь о том что 10-15 лет назад этого в шарпе не было. То есть если вы тогда хотели зарабатывать деньги шарпом, то вам не нужно было это знать. И следовательно порог вхождения был ниже.


              А во вторых насколько я знаю работа с асинхронным кодом шарпе и джаваскрипте только выглядит похоже. Принципы там как раз таки разные.


              П.С. Ну и как бы я отношусь к тем "счастливчикам", которые познакомились с шарпом на самом старте. И "расти" вместе с ним было относительно комфортно. Особенно где-то версии с третьей. Но я вижу какие проблемы возникают у людей, которые сейчас приходят в шарп и на которых зараз вываливают всё что с тех пор накопилось в языке.


              1. mayorovp
                19.08.2021 14:23

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


                1. Kanut
                  19.08.2021 14:27

                  Вы пришли когда LINQ уже был или когда LINQ уже этаблировался? То есть я например тоже не начал его использовать сразу когда он появился.


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


              1. DistortNeo
                19.08.2021 14:28
                +1

                А во вторых насколько я знаю работа с асинхронным кодом шарпе и джаваскрипте только выглядит похоже. Принципы там как раз таки разные.

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


                1. Kanut
                  19.08.2021 14:34
                  -1

                  "Синтаксический сахар для колбэков" это была "предыдущая версия" асинхронности в шарпе. Она же Event-based Asynchronous Pattern (EAP) . А сейчас у нас Task-based asynchronous pattern(TAP)


                  Плюс если я не ошибаюсь асинхронность в джаваскрипте у нас всё ещё однопоточная. В отличие от шарпа. Что добавляет приколов вроде контекста выполнения. Хотя тут я уже не уверен. Потому что не особо слежу за развитием джаваскрипта.


                  1. mayorovp
                    19.08.2021 14:49
                    +1

                    Ну нет, EAP это другое, "колбеками" неформально называют CPS, который в некотором смысле эквивалентен TAP.


                    Рассмотрим типичный асинхронный метод в стиле передачи продолжений. Его сигнатура будет примерно такой:


                        (TParam1, …, (TResult → void)) → void

                    Немного каррируем этот метод:


                        (TParam1, …) → (TResult → void) → void

                    Теперь упакуем всё, что после первой стрелки, в новый тип данных — Future/Promise/Task, что больше нравится:


                        (TParam1, …) → Task TResult

                    Оп! У нас получился TAP.


                    Что же до многопоточности, контекстов выполнения и прочего — это всего лишь детали реализации.


                    1. Kanut
                      19.08.2021 15:05

                      Ну нет, EAP это другое, "колбеками" неформально называют CPS, который в некотором смысле эквивалентен TAP.

                      Ок, у меня может быть проблема с терминологией. Но TAP позволяет разделить момент создания таска и момент его запуска/выполнения(блин как же мне уже сложно писать технические вещи на русском...). EAP этого не позволяло. Насколько я помню в джаваскрипте это тоже нельзя разделить. Для меня в своё время это было одним из основных изменений.


                      Что же до многопоточности, контекстов выполнения и прочего — это всего лишь детали реализации.

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


                      1. mayorovp
                        19.08.2021 15:21
                        +1

                        TAP позволяет разделить момент создания таска и момент его запуска/выполнения

                        Что требуется примерно никогда. Кроме того, async-методы в том же C# никогда не возвращают подобных задач, так что особенностью именно в async/await-подхода в C# это точно не является.


                        EAP этого не позволяло.

                        Ну да, за отсутствием самого понятия задачи. А если вынести понятие задачи за скобки — то как раз EAP только в таком режиме и работает:


                            foo.BarComplete += …; // создание "задачи"
                            foo.BarAsync(); // запуск "задачи"

                        Асинхронность в шарпе "намертво привязана" к многопоточности.

                        Не понимаю в каком смысле она "намертво привязана".


                      1. Kanut
                        19.08.2021 15:38

                        Что требуется примерно никогда.

                        Что требуется время от времени. Я могу насоздавать тасков и передать их кому-то. Он вообще не будет знать что это за таски но при этом выполнять их или нет в зависисмости от своей логики.


                        Не понимаю в каком смысле она "намертво привязана".

                        Например тем что это всё дело базируется на managed threading и без него нежизнеспособно?


                      1. mayorovp
                        19.08.2021 15:57
                        +1

                        Э-э-э, а почему без managed threading оно нежизнеспособно?


                      1. Kanut
                        19.08.2021 16:02
                        -2

                        В смысле "почему"? Потому что оно его использует. Ну то есть в теории можно написать какой-то новый async/await, который будет базироваться на чём-то другом и обойдётся без managed threading. Но нынешний вариант сделан именно так.


                      1. mayorovp
                        19.08.2021 17:38
                        +1

                        Хорошо, что вы понимаете под managed threading и чем оно отличается от простого threading?

                        И второй вопрос - в каком виде оно используется реализацией async/await?


                      1. Kanut
                        19.08.2021 18:52

                        mayorovp, DistortNeo прежде чем я начну тут расписывать моё понимание работы async/await, может вы мне просто объясните в чём их смысл в абсолютно однопоточном мире? И в чём преимущество перед синхронным исполнением? А то может это я чего-то не понимаю.


                      1. mayorovp
                        19.08.2021 19:05
                        +1

                        Ответ будет из двух частей.


                        Смысл непосредственно async/await — в упрощении асинхронности. Эта конструкция позволяет делать асинхронные вызовы последовательно.


                        Смысл же асинхронности — в возможности делать ввод-вывод параллельно.


                        В сумме и получается преимущество async/await перед синхронным исполнением — оно позволяет произвольным образом комбинировать последовательные и параллельные участки кода, чего синхронное исполнение в одном потоке делать не может.


                      1. Kanut
                        19.08.2021 19:09

                        Я может опять чего-то не понимаю, но что такое параллельное выполнение в абсолютно однопоточном мире?


                      1. DistortNeo
                        19.08.2021 19:28
                        +2

                        Не параллельное, а асинхронное. Сам код у вас параллельно не выполняется. Зато в фоне выполняются операции с таймерами и сокетами.

                        Например, вы пишете код, который не делает вычислений, зато много работает с сокетами и таймерами.

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

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

                        Поэтому вы вспоминаете сначала про select, затем про epoll, IOCP и переписываете код в event-based стиле. И получается очень эффективно. Например, nginx — однопоточный*. Он может держать тысячи соединений, и вся обработка идёт в одном потоке.

                        Но писать код в event-based стиле неудобно, очень неудобно. И тут нам приходят в помощь async-await. Получаемый код выглядит как синхронный и многопоточный, но под капотом это эффективный event-based код.

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


                      1. Kanut
                        19.08.2021 19:45
                        -2

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


                      1. mayorovp
                        19.08.2021 20:00

                        Вот как раз когда у вас поток всего один — асинхронность обязательна.


                        Как вы будете без неё делать два одновременных, допустим, HTTP запроса?


                      1. mayorovp
                        19.08.2021 20:10

                        Я может опять чего-то не понимаю, но что такое параллельное выполнение в абсолютно однопоточном мире?

                        Ну вот смотрите. Отправил я в сокет #1 первый запрос, потом отправил в сокет #2 второй запрос, теперь жду ответов. Где-то там сервера эти запросы параллельно обрабатывают, и им плевать сколько у меня потоков.


                      1. Kanut
                        19.08.2021 20:30

                        Ок, это я уже стормозил...


                      1. Kanut
                        20.08.2021 09:33
                        +1

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


                        А если написал неприятное но как минимум легитимное мнение по не техническому вопросу, то минусов прилететь может на порядок больше. Неладно что-то в датском королевстве...


                      1. Refridgerator
                        20.08.2021 11:46
                        -1

                        А ещё обиднее получать минусы не за мнение, а за вполне правильный (и аргументированный!) комментарий. Лично я стараюсь относиться к этому философски, и воспринимать минус без аргумента как признание в неправоте оппонента, раз ему больше нечем ответить. И вам того же рекомендую.


                      1. DistortNeo
                        19.08.2021 18:22

                        Например тем что это всё дело базируется на managed threading и без него нежизнеспособно?

                        А вы вообще в реализацию планировщика залезали? Лично я в своё время писал собственный планировщик: Net Core ещё ходил под стол, а поддержка асинхронщины в Mono под Linux была сделана через жопу, поэтому пришлось изобретать велосипед.


                        Короче, нет там никакой привязки к потокам. Единственная привязка — это thread static переменная SynchronizationContext.Current с методами типа Send/Post и аргументом типа Action. А где этот делегат будет вызываться, решает сам планировщик.


                      1. DistortNeo
                        19.08.2021 18:13

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

                        Нет, не привязана. Например, если вы в обработчике UI-события захотите сделать асинхронный вызов, то все задачи будут выполняться в Event Loop в единственном потоке. Так что всё зависит от того, какой SynchronizationContext используется.


                  1. DistortNeo
                    19.08.2021 14:55
                    +1

                    Плюс если я не ошибаюсь асинхронность в джаваскрипте у нас всё ещё однопоточная. В отличие от шарпа

                    Всё зависит от планировщика. В своих проектах C# я, например, использую однопоточный планировщик.


            1. mayorovp
              19.08.2021 14:20

              Вот LINQ как раз уникальная возможность. Ну, в теории это вроде то самое квазицитирование, которое со времён LISP известно — но вот такого чтобы оно активно использовалось в экосистеме мне за пределами экосистемы C# найти не удалось.


  1. glebasterBajo
    19.08.2021 09:22
    +4

    С каждым новом релизом .NET'а порог вхождения улетает всё выше и выше в небеса. Таким темпом плюсы догонят.
    +)


    1. hatman
      19.08.2021 13:25
      +2

      При этом зарплаты идут ниже, чем на java/python(в хайлоад проектах) и на Go.


    1. ZhilkinSerg
      20.08.2021 10:24
      +2

      Но это же неправда. При появлении в языке дополнительных возможностей порог вхождения не меняется


      1. AnthonyMikh
        20.08.2021 11:45
        +2

        C++ такой: что?


        1. ZhilkinSerg
          20.08.2021 17:05

          Что?


  1. Refridgerator
    19.08.2021 10:35
    -1

    Как видно из примера, в случае равенства приоритетов порядок извлечения элементов не гарантирован.
    А вот это противоречит самой идее очереди как структуры данных. Назвали бы её тогда уж как PrioritySet или типа того.


    1. mayorovp
      19.08.2021 12:10
      +2

      "Приоритетная очередь" или "очередь с приоритетами" — это устоявшееся название, поздно его менять уже.


      1. Refridgerator
        19.08.2021 12:15

        Я наивно думал, что последовательность элементов с одинаковым приоритетом сохраняется. Вопрос: разве это невозможно сделать в принципе?


        1. mayorovp
          19.08.2021 12:23
          +1

          Можно, но надо добавлять последовательный счётчик как часть приоритета, и в большинстве случаев это будет замедление на пустом месте. А если строгий порядок и правда нужен — подобное легко добавить и самому, благо есть кортежи с лексикографическим порядком.


          1. Refridgerator
            19.08.2021 12:41

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


            1. mayorovp
              19.08.2021 13:03
              +1

              StablePriorityQueue, по аналогии со стабильной сортировкой


  1. anonymous
    00.00.0000 00:00


  1. DeniSix
    19.08.2021 10:41
    +3

    Об оптимизации .net 6.0 недавно выкатили обобщённый пост с разбором около 400 PR в .NET Blog. Объём работы даже для компиляции информации поста поражает.


  1. Ilirium
    19.08.2021 10:56

    У меня вопрос к знающим людям про Xamarin Forms / MAUI. Можно ли уже сейчас сделать нативное приложение из одной кодовой базы под все платформы? В первую очередь интересует macOS и iOS, во вторую Windows и Android.

    В смысле, чтобы можно было как во Flutter (я не фронтендер и не мобильщик, просто хочется иногда для части своих датасайнс задач сделать не вебную, а нативную морду), сейчас Flutter немножко тыкаю, но вот интересно, есть ли еще какая альтернатива? (Compose от JetBrains еще в зачаточном состоянии, как я понял).


    1. Kanut
      19.08.2021 11:02
      +4

      Я бы сказал что если вы хотите использовать .NET, то на данный момент лучший кандидат для кроссплатформенного десктопа это AvaloniaUI. Но и они слегка сыроваты.


      1. Ilirium
        19.08.2021 11:12

        Получается, что рекламируемая мультиплатформенность в Xamarin Forms / MAUI пока не очень (или не про то)? Я пробовал гуглить, но как-то очень все туманно расписано про этот момент.


        1. Kanut
          19.08.2021 11:17
          +2

          Просто на данный момент мультиплатформенность в Xamarin Forms/MAUI рекламируется, но в реальности не особо существует. Это всё ещё только в стадии превью/пререлиза. И то, что лично мне удалось немного пощупать, тоже было довольно сыровато.


          То есть в перспективе да, выглядит интересно. Что из этого получится в итоге это отдельный большой вопрос. Silverlight тоже в своё время интересно выглядел.


          1. Ilirium
            19.08.2021 11:23

            Понятно, спасибо большое за ответ! А то время от времени гуглил на эту тему, и не мог понять, если есть заявление про мультиплатформенность, то почему нет внятных туториалов и примеров.


        1. sepulkary
          19.08.2021 12:19

          Посмотрите еще на Uno.


      1. DistortNeo
        19.08.2021 11:30

        Для совсем простых приложений (типа одно окно с графиком) мне фреймворк Eto.Forms понравился.


  1. KvanTTT
    19.08.2021 11:46
    +6

    Мне нравится в какую сторону развивается платфмора .NET и язык C#. Пусть они будут высокопроизводительными, несмотря на потенциальную сложность. Для тех, кто не хочет въезжать, существуют Java, Go и другие альтернативы.


    1. Krey
      19.08.2021 12:44

      .Net это вообще единственное что для меня осталось от МС. Если бы не visual studio вообще бы винду не ставил.


      1. KvanTTT
        19.08.2021 13:18
        +2

        А чем Rider не устраивает? Уже давно на него перешел и не жалею.


        1. DistortNeo
          19.08.2021 14:08
          +5

          1. Ценовая политика (VS бесплатен даже для коммерческой разработки).
          2. Есть моменты, связанные с отладкой, которые в Rider работают хуже, чем в VS.


  1. JacobL
    19.08.2021 14:38

    AOT для Blazor приложений дает ощутимый прирост скорости работы. Иногда в несколько раз.


  1. TargetSan
    19.08.2021 20:11

    stackalloc

    А появилась ли возможность вставлять массивы фиксированного размера в структуры и классы как поля by value? Что-то вроде


        struct Vec3 {
            public float[3] coords;
        }


    1. mayorovp
      19.08.2021 20:16

      Такая возможность давно была (но только для примитивных типов и только в небезопасном коде).


      1. TargetSan
        19.08.2021 23:14
        +3

        Интересно, откуда такие ограничения. По сути, пример выше эквивалентен чему-то вроде


        struct Vec3 {
            public float coord0;
            public float coord1;
            public float coord2;
        }

        но отдельными полями можно, а массивом нельзя.


        1. kasthack_phoenix
          20.08.2021 03:40

          но отдельными полями можно, а массивом нельзя.

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


          1. TargetSan
            20.08.2021 07:17

            Тут вы правы. Но всё равно интересно, почему только примитивные типы и только в unsafe контексте. У того же stackalloc ограничения не такие жёсткие.


  1. morgfrimen
    20.08.2021 10:24

    Добрый день. Я не очень в теме, но чисто формально. Есть nanoframework, есть старый MicroFramework. По сути обёртки над низкоуровневым кодом. Но добавили nativememory... Гипотетически теперь на c# можно писать наивный код под микроконтроллеры?

    Или я немного не понимаю схемы работы....

    Это же не только для оптимизации можно же использовать


  1. MaxLevs
    05.12.2021 11:16

    Если честно, то сложно представить себе ситуацию, продемонстрированную в примере. Но будем иметь в виду.

    Да не так, чтобы очень сложно. Если пытаться сделать сериализацию моделей того же Entity Framework, которые частенько содержат циклические ссылки на связанные модели, то в .Net 5 получим exception.

    Хотя сама сериализация чистых моделей - не "true way".