Ради последующего вопроса предположим гипотетически, что я хочу преобразовать строку обычного текста в шестнадцатеричную строку; например, «Привет, мир!» в «48656c6c6f2c20576f726c6421».
Для этого требуется два шага:
Например:
string plainText = "Hello, World!";
byte[] bytes = Encoding.Default.GetBytes(plainText);
string hexadecimalString = Convert.ToHexString(bytes);
Далее я хочу обернуть эту функциональность в служебную функцию. До версии .NET Core 2.1 (и я удивлен, что ReadOnlySpan<T> настолько стар) наиболее гибким подходом было бы наличие перегрузок для string и char[]; например:
public static string ToHexString(string value, Encoding? encoding = null) =>
ToHexString(value.ToCharArray(), encoding);
public static string ToHexString(char[] value, Encoding? encoding = null) =>
Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));
Хотя этот подход относительно тривиален, он не особенно удобен с точки зрения распределения, поскольку требуется три распределения:
ToCharArray() выделит нового char[].GetBytes выделю нового byte[]ToHexString выделит нового string.Введение ReadOnlySpan<T> потенциально может улучшить ситуацию за счет сокращения количества выделений (хотя и только на 1), но также с точки зрения удобства сопровождения оно требует меньше перегрузок методов, поскольку string и char[] неявно конвертируются в ReadOnlySpan<char>; например (в идеальном мире):
public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null) =>
Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));
По иронии судьбы, хотя ReadOnlySpan<T> теоретически должен уменьшить количество выделений, проблема здесь в том, что GetBytes не имеет перегрузки, которая требует ReadOnlySpan<char>, поэтому единственный способ реализовать это — повторно ввести выделение; например:
public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null) =>
Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value.ToArray()));
В настоящее время единственный способ сократить выделение ресурсов — это предоставить перегрузки методов и реализовать их все отдельно; например:
public static string ToHexString(string value, Encoding? encoding = null) =>
Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));
public static string ToHexString(char[] value, Encoding? encoding = null) =>
Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));
Теперь у нас есть методы, которые приведут к меньшему количеству выделений, поскольку нет преобразований из string в char[] или ReadOnlySpan<char> в char[], но компромисс заключается в том, что это приводит к более высоким затратам на сопровождение, поскольку теперь мне приходится поддерживать два метода и меньшую гибкость. API для вызывающего абонента.
Итак, мой вопрос: когда правильно использовать ReadOnlySpan<T> в качестве параметра метода, а не использовать более явные/перегруженные типы? Должен ли компромисс быть смещен в сторону удобства сопровождения и надежного API, опирающегося на неявное преобразование типов, или в сторону производительности? Есть ли какие-либо рекомендации по этому поводу?
Перегрузка ReadOnlySpan недоступна до версии Net Core 2.1.
@EtiennedeMartel, я не хочу сказать, что у GetBytes есть перегрузка для string. Дело в том, что у него нет перегрузки для ReadOnlySpan<char>, тогда как у Convert.ToHexString она есть.
@beautifulcoder Я знаю. Вот почему в посте конкретно упоминается Core 2.1 и что мне придется реализовать перегрузки для string и char[], а не реализовать ReadOnlySpan<char>.
Примечание. Не используйте Encoding.Default. Его значение меняется в зависимости от того, используете ли вы .NET Framework или .NET Core/.NET 5+. В .NET Framework его значение зависит от кодовой страницы, настроенной на компьютере, на котором выполняется код.
@canton7 Спасибо, я этого не знал. Будет ли UTF8 лучшим вариантом по умолчанию в этом случае?
Конечно. UTF-8 стал стандартом де-факто во многих местах.
@MatthewLayton Но... у GetBytes есть перегрузка для ReadOnlySpan<char>. Или, может быть, я неправильно понимаю, о чем вы говорите?
@EtiennedeMartel Методы GetBytes, которые принимают string или char[], возвращают byte[]. Напротив, метод GetBytes, который принимает ReadOnlySpan<char>, требует второго аргумента Span<byte> и возвращает int (количество записанных байтов), поэтому его не так просто использовать. Ответ Александра Петрова демонстрирует этот метод.
Ну да, потому что весь смысл использования промежутков состоит в том, чтобы избежать выделения памяти. Зачем избегать только половины из них?
@EtiennedeMartel Вам все равно придется где-то выделить несколько байтов, независимо от того, выделены ли они в стеке или в куче. Очевидно, что первый вариант более эффективен, при условии, что вы не столкнетесь с переполнением стека. Тот факт, что вы пишете в Span<byte>, а не возвращаете byte[], кажется неважным.
Ну нет, в том-то и дело: память стека уже выделена. Вот как работают стеки. Вам не придется больше беспокоить сборщика мусора.





Рассмотрим следующий код
public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null)
{
encoding ??= Encoding.Default;
int count = encoding.GetByteCount(value);
Span<byte> bytes = stackalloc byte[count];
encoding.GetBytes(value, bytes);
return Convert.ToHexString(bytes);
}
Вы также можете переоценить размер массива и преобразовать его с помощью ToHexString(Byte[], Int32, Int32), если не хотите дважды сканировать исходный буфер.
Не выделяйте в стеке количество байтов, определенное пользователем-контроллером — это простой способ вызвать переполнение стека.
Если нам нужна производительность, мы можем проверить, является ли count маленьким (тогда мы используем stackalloc) или большим (и мы используем ArrayPool)
Итак, мой вопрос: когда правильно использовать ReadOnlySpan в качестве параметра метода, а не использовать более явные/перегруженные типы? Должен ли компромисс быть смещен в сторону удобства сопровождения и надежного API, опирающегося на неявное преобразование типов, или в сторону производительности? Есть ли какие-либо рекомендации по этому поводу?
Хотя это может быть в некоторой степени основано на мнениях, существует распространенная поговорка: Преждевременная оптимизация — корень всех зол.
В этом контексте это означало бы уклон в сторону удобства сопровождения по умолчанию, поскольку большая часть кода не чувствительна к производительности.
Но вам следует профилировать код, чтобы найти части, чувствительные к производительности, и оптимизировать эти части. Это может включать или не включать вашу функцию ToHex. Хотя профилировать распределение может быть сложнее, чем производительность, профилировщик должен сообщить вам, по крайней мере, такие вещи, как скорость выделения и время, проведенное в сборщике мусора. Вы также можете добавить ручные инструменты, если вам нужны более подробные данные.
Я бы также не стал помещать такую вспомогательную функцию ни в один общедоступный API, поэтому вам придется беспокоиться только о своем собственном коде, что значительно упрощает обслуживание. Для общедоступных API это немного сложнее, и вам, вероятно, придется принимать решение в каждом конкретном случае, хотите ли вы предоставлять несколько перегрузок или нет, исходя из ожидаемых шаблонов использования.
GetBytesтакая перегрузка.