Вещи, упрощающие нашу жизнь есть в любой сфере — от поездки в аэропорт до написания кода. Удобство должно быть доступно когда в нём возникает необходимость и при этом решать вашу проблему. Дизайнеры платформы .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, найдётся решение и для вас.