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

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

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

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

Откроем же серию с общего обзора того, как платформа .NET предоставляет удобство разработчику.

Удобство — это спектр

Мне нравится использовать термины “удобство” и “контроль” для описания двух крайностей “спектра удобства”. Удобство — это про опыт написания кода, контроль — про возможность определять его поведение.

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

Самый низкоуровневый код добавляет большое количество гибкости, контроля и оптимизаций по производительности, но взамен требует более аккуратного использования. Я, например, про File.OpenHandle() и RandomAccess.Read(), которые напрямую обращаются к обработчикам операционной системы и не практически не мешают побайтово читать файл с максимальной производительностью.

Я покажу пару примеров API. Нормально, если они вам не знакомы. Они перечислены от самого низкоуровнего (с максимальной степенью контроля) к самому комплексному (и самому удобному). Слева указан вызываемый метод, справа связанный с ним метод компаньон или возвращаемый тип.

Спектр удобства для чтения файла

  • File.Handle / RandomAccess.Read
  • File.Open / FileStream.Read
  • File.OpenText / StreamReader.ReadLine
  • File.ReadLines / IEnumerable<string>
  • File.ReadAllText / string

Спектр удобства для чтения JSON строки

  • Utf8JsonReader / Pipelines, Stream
  • JsonDocument / Stream
  • JsonSerializer / Stream
  • JsonSerializer / string

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

Удобство начинается с выбора

Вам может стать интересно, зачем же нужно такое количество API, которые делают абсолютно одно и то же. Во-первых, каждый из этих вариантов нужен для решения своей задачи и он будет удобен для этой конкретной задачи. На самом деле, сообщество .NET разработчиков просит нас добавить новые API и мы только рады это сделать. Во-вторых, мы собрали высокоуровневые API из кусочков низкоуровневых API. Это выглядит как строительство башни Lego. В теории мы могли бы оставить открытыми только высокоуровневые API, сделав низкоуровневые закрытыми, но это было бы нежелательно и в общем случае непрактично.

Некоторые группы разработчиков в основном предоставляют только API высокого уровня, построенные на нативных библиотеках, но в них отсутствуют полезные API более низкого уровня. Часто низкоуровневые API в этих нативных библиотеках не раскрываются для управляемого языка, потому что написаны таким образом, что это просто непрактично. Такие решения весьма ограничивают возможности. В .NET мы придерживаемся философии, согласно которой создаваемая нами библиотечная функциональность должна быть написана на C#, что означает, что вам будут доступны как низкоуровневые, так и высокоуровневые API. Кроме того, это значит, что вы сможете прочитать на Github код любого API, который вы используете (например, класс File)

Конечно мы не во всех местах раскрываем все слои; каждый новый API, который мы раскрываем по сути останется с нами навсегда и это влечёт за собой необходимость тестирования, поддержки, написания документации, проверок совместимости и так далее. Поэтому мы выбираем, какие слои раскрывать, и постоянно переоцениваем наши возможности по поддержке разных слоёв. Например, упомянутый раньше File.ReadAllText существовал долгие годы, но RandomAccess.Read появился относительно недавно. Наша долгосрочная тенденция развития заключается в том, чтобы предоставлять низкоуровневые API там, где это действительно нужно.

Удобство упрощает сотрудничество

Библиотеки .NET предоставляют широкий набор функциональности конечным пользователям. Во многих случаях (как например с типом File), большая часть связанной функциональности доступна в одном месте и создана для работы в качестве части более крупной согласованной системы. Это значит, что в одной части кода вы можете использовать удобные API, а в другой более контролируемые и они будут хорошо работать вместе.

”Я собираюсь записать эти данные в Stream, с помощью API, которые дают мне контроль, необходимый в нашем сервисе. Ты можешь использовать StreamReader.ReadLineAsync, чтобы считать данные из этого потока. А если это не подойдёт, я раскрою IAsyncEnumerable<string> с отдельными строками и ты сможешь использовать await foreach в качестве потокового решения. Меня устроят оба способа. Мне нравится, насколько они просты. Очень просто соединить наш код вместе так, чтобы всё работало быстро и удобно” — .NET разработчик из ACME Solutions.

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

Я всё контролирую

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

Мы используем удобные API в некоторых местах в библиотеках .NET, даже когда они не предоставляют максимальной скорости. Они делают код меньше, проще для написания и понимания и это более ценно, чем получение максимальной скорости.

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

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

Нарушая спектр

Бывают случаи, когда одно API покрывает большую часть задач. Это происходит, когда API с простым контрактом является рабочей лошадкой и требуется во многих сценариях.

Хороший пример — API класса string. IndexOf и IndexOfAny мои любимые. Мы используем эти API повсеместно в платформе .NET и так же часто они используются .NET разработчиками. Можно проследить, как много пул реквестов нацелены на эти API.

Многие вызовы IndexOf{Any} сейчас происходят на спанах, вместо обычного вызова string.IndexOf{Any}. Хотя спаны часто проецируются в строки, эти API работают на срезах (после внутреннего вызова string.AsSpan)

Это семейство API было сильно улучшено и теперь использует многие техники для повышения производительности. Например, они используют векторные инструкции процессора для поиска по строке. В .NET 8 была добавлена поддержка AVX512. Это изменение ничего не изменит для большей части железа, но благодаря этому IndexOf будет лучше работать на новом железе, когда оно у вас появится.

Мы подробнее рассмотрим IndexOfAny в посте про System.IO. Это отличный API.

Заключение

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