Как преобразовать байтовый массив в шестнадцатеричную строку и наоборот?

Как преобразовать массив байтов в шестнадцатеричную строку и наоборот?

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

Wim Coenen 06.03.2009 19:41

Я думаю, что класс SoapHexBinary делает именно то, что вы хотите.

Mykroft 01.04.2010 00:44
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1 453
2
937 961
46
Перейти к ответу Данный вопрос помечен как решенный

Ответы 46

Ответ принят как подходящий

Либо:

public static string ByteArrayToString(byte[] ba)
{
  StringBuilder hex = new StringBuilder(ba.Length * 2);
  foreach (byte b in ba)
    hex.AppendFormat("{0:x2}", b);
  return hex.ToString();
}

или же:

public static string ByteArrayToString(byte[] ba)
{
  return BitConverter.ToString(ba).Replace("-","");
}

Вариантов еще больше, например здесь.

Обратное преобразование будет выглядеть так:

public static byte[] StringToByteArray(String hex)
{
  int NumberChars = hex.Length;
  byte[] bytes = new byte[NumberChars / 2];
  for (int i = 0; i < NumberChars; i += 2)
    bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
  return bytes;
}

Использование Substring - лучший вариант в сочетании с Convert.ToByte. См. этот ответ для получения дополнительной информации. Если вам нужна лучшая производительность, вы должны избегать Convert.ToByte, прежде чем вы сможете отказаться от SubString.

отсутствует точка с запятой в строке hex.AppendFormat ("{0: x2}", b)

el2iot2 23.12.2008 03:37

Вы используете SubString. Разве этот цикл не выделяет ужасное количество строковых объектов?

Wim Coenen 06.03.2009 19:36

Честно говоря, до тех пор, пока это не резко снизит производительность, я буду игнорировать это и доверять Runtime и GC, которые позаботятся об этом.

Tomalak 06.03.2009 20:11

FWIW Я мог бы получить 4-кратное ускорение на моей машине, исключив подстроки. Не могу опубликовать код, потому что я написал это для своего работодателя.

Wim Coenen 08.03.2009 21:26

Ваш StringToByteArray () не работает, если у вас нечетное количество шестнадцатеричных символов. Это легко исправить, если заполнить нечетные строки "0" впереди.

Carlos Rendon 23.11.2009 20:21

Поскольку байт - это два полубайта, любая шестнадцатеричная строка, которая корректно представляет массив байтов, должна иметь четное количество символов. 0 не следует нигде добавлять - его добавление означало бы сделать предположение о недопустимых данных, что потенциально опасно. Во всяком случае, метод StringToByteArray должен генерировать исключение FormatException, если шестнадцатеричная строка содержит нечетное количество символов.

David Boike 09.03.2010 22:01

Первый пример возвращает другое значение, чем второй. Кто-нибудь может объяснить почему?

iJK 20.04.2010 18:39

@DavidBoike Хотя я согласен, если вас интересует только математическое представление, можно добавить нули ко всей строке, но вы правы, это почти наверняка плохие данные, потому что вы запрашиваете байты взамен, и байты всегда будут соответствующим образом дополнены.

McKay 22.06.2012 23:22

См. stackoverflow.com/a/14332574/22656 для версии, которая не использует Substring.

Jon Skeet 15.01.2013 11:09

@DavidBoike - "F" недопустимая шестнадцатеричная строка? Разве это не то же самое, что "0F"? Таким образом, у вас может быть шестнадцатеричная строка с нечетным количеством символов.

00jt 28.01.2013 19:15

@ 00jt Вы должны сделать предположение, что F == 0F. Либо это то же самое, что и 0F, либо вход был обрезан, и F на самом деле является началом чего-то, чего вы не получили. Это зависит от вашего контекста, чтобы делать эти предположения, но я считаю, что функция общего назначения должна отклонять нечетные символы как недопустимые, а не делать это предположение для вызывающего кода.

David Boike 28.01.2013 19:35

@DavidBoike Вопрос не имел НИЧЕГО общего с «как обрабатывать возможно обрезанные значения потока». Он говорит о String. String myValue = 10.ToString («X»); myValue - это «A», а не «0A». Теперь прочтите эту строку обратно в байты, ой, вы ее сломали.

00jt 30.01.2013 23:25

В вопросе прямо указано, что они хотели бы, чтобы он действительно конвертировался обратно. F! = 0F, поэтому вы получите другой результат. Кроме того, делать ненужные предположения - это вообще плохая практика. Ваш пример глупый, вы бы использовали ToString ("X2"). Ваш пример идентичен тому, что если вы используете ToString ("X3"), он не проходит через функции шестнадцатеричного анализа. Конечно, не потому, что вы его нестандартно закодировали.

Rushyo 24.10.2013 14:18

В .Net Micro Framework нет ни Convert.ToByte, принимающего два аргумента, ни StringReader, было бы здорово увидеть в ответе StringToByteArray без этих аргументов.

dumbledad 20.07.2014 00:43

var str = System.Text.Encoding.Default.GetString (результат);

Hamed Zakery Miab 14.01.2015 09:12

@Link указывает на спам. thinksharp.org/hex-string-to-byte-array-converter/

kanchirk 18.03.2015 20:09

@DavidBoike Я никогда не знал, что такое клев. Ваш комментарий заставил меня задуматься. Спасибо.

RBT 21.11.2016 10:18
StringToByteArray - ужасное название для этой функции. Назовите его EncodeHex или DecodeHex или создайте класс Hex и вставьте методы Encode или Decode. Вы также можете декодировать base64, кодировать как UTF-8 или UTF-16 и называть его StringToByteArray. Где-то нужно вписать часть Hex в название.
Maarten Bodewes 11.06.2018 03:40

желаю, чтобы в нем были данные для примера.

Jake Gaston 14.08.2020 00:18

Методы расширения (отказ от ответственности: полностью непроверенный код, BTW ...):

public static class ByteExtensions
{
    public static string ToHexString(this byte[] ba)
    {
        StringBuilder hex = new StringBuilder(ba.Length * 2);

        foreach (byte b in ba)
        {
            hex.AppendFormat("{0:x2}", b);
        }
        return hex.ToString();
    }
}

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

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

jww 16.02.2017 22:08

Вы можете использовать метод BitConverter.ToString:

byte[] bytes = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}
Console.WriteLine( BitConverter.ToString(bytes));

Выход:

00-01-02-04-08-10-20-40-80-FF

Дополнительная информация: BitConverter.ToString - метод (Byte [])

Отвечает только на половину вопроса.

Sly Gryphon 28.06.2011 10:49

Где вторая часть ответа?

Sawan 25.12.2012 13:12

Если вы хотите большей гибкости, чем BitConverter, но не хотите этих неуклюжих явных циклов в стиле 1990-х годов, вы можете:

String.Join(String.Empty, Array.ConvertAll(bytes, x => x.ToString("X2")));

Или, если вы используете .NET 4.0:

String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")));

(Последнее из комментария к исходному сообщению.)

Еще короче: String.Concat (Array.ConvertAll (bytes, x => x.ToString ("X2"))

Nestor 25.11.2009 18:04

Просто обратите внимание, что хорошая техника maxc требует .net 4.0

Will Dean 26.11.2009 01:05

Еще короче: String.Concat (bytes.Select (b => b.ToString ("X2"))) [.NET4]

Allon Guralnek 16.06.2011 10:39

Отвечает только на половину вопроса.

Sly Gryphon 28.06.2011 10:50

Зачем второму .Net 4? String.Concat находится в .Net 2.0.

Polyfun 17.10.2014 15:42

эти циклы в стиле 90-х обычно быстрее, но на достаточно незначительную величину, так что в большинстве случаев это не имеет значения. Тем не менее, стоит упомянуть

Austin_Anderson 24.10.2017 22:47

Я столкнулся с той же проблемой сегодня и наткнулся на этот код:

private static string ByteArrayToHex(byte[] barray)
{
    char[] c = new char[barray.Length * 2];
    byte b;
    for (int i = 0; i < barray.Length; ++i)
    {
        b = ((byte)(barray[i] >> 4));
        c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
        b = ((byte)(barray[i] & 0xF));
        c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
    }
    return new string(c);
}

Источник: сообщение форума byte [] Массив в шестнадцатеричную строку (см. Сообщение PZahra). Я немного изменил код, убрав префикс 0x.

Я провел небольшое тестирование производительности кода, и это было почти в восемь раз быстрее, чем при использовании BitConverter.ToString () (самый быстрый согласно сообщению Патриджа).

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

Chochos 16.10.2009 21:36

Отвечает только на половину вопроса.

Sly Gryphon 28.06.2011 10:50

Это здорово, потому что работает практически с любой версией NET, включая NETMF. Победитель!

Jonesome Reinstate Monica 06.02.2012 08:26

Принятый ответ предоставляет 2 отличных метода HexToByteArray, которые представляют вторую половину вопроса. Решение Waleed отвечает на текущий вопрос о том, как это сделать, не создавая при этом огромное количество строк.

Brendten Eickstaedt 10.10.2012 20:08

Копирует и перераспределяет новую строку (c) или достаточно ли умен, чтобы знать, когда можно просто обернуть char []?

jjxtra 15.10.2013 21:24

@PsychoDad, он копирует. Строка должна быть неизменной, в то время как char [] может измениться после создания строки.

Brian Reichle 21.01.2014 10:51

@SlyGryphon На самом деле, если вы прокрутите вниз от упомянутого сообщения на форуме, я фактически предоставил другую сторону этого, которая с тех пор была обновлена ​​здесь: stackoverflow.com/a/22158486/278889

Patrick 27.09.2018 17:59

И для вставки в строку SQL (если вы не используете параметры команды):

public static String ByteArrayToSQLHexString(byte[] Source)
{
    return = "0x" + BitConverter.ToString(Source).Replace("-", "");
}

если Source == null или Source.Length == 0 у нас проблема сэр!

Andrei Krasutski 07.06.2019 20:37

Если вы хотите получить "4-кратное увеличение скорости", о котором сообщает wcoenen, то, если это не очевидно: замените hex.Substring(i, 2) на hex[i]+hex[i+1].

Вы также можете пойти дальше и избавиться от i+=2, используя i++ в обоих местах.

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

private byte[] HexStringToByteArray(string hexString)
{
    int hexStringLength = hexString.Length;
    byte[] b = new byte[hexStringLength / 2];
    for (int i = 0; i < hexStringLength; i += 2)
    {
        int topChar = (hexString[i] > 0x40 ? hexString[i] - 0x37 : hexString[i] - 0x30) << 4;
        int bottomChar = hexString[i + 1] > 0x40 ? hexString[i + 1] - 0x37 : hexString[i + 1] - 0x30;
        b[i / 2] = Convert.ToByte(topChar + bottomChar);
    }
    return b;
}

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

Marc Novakowski 26.01.2010 22:17

Это проницательное наблюдение Марк. Код был написан для отмены решения Валида. Вызов ToUpper несколько замедлит алгоритм, но позволит ему обрабатывать альфа-символы нижнего регистра.

Chris F 26.01.2010 23:27

Convert.ToByte (topChar + bottomChar) можно записать как (byte) (topChar + bottomChar)

Amir Rezaei 13.02.2011 00:17

Чтобы справиться с обоими случаями без значительного снижения производительности, hexString[i] &= ~0x20;

Ben Voigt 01.08.2014 02:31

Есть класс под названием Мыло, который делает именно то, что вы хотите.

using System.Runtime.Remoting.Metadata.W3cXsd2001;

public static byte[] GetStringToBytes(string value)
{
    SoapHexBinary shb = SoapHexBinary.Parse(value);
    return shb.Value;
}

public static string GetBytesToString(byte[] value)
{
    SoapHexBinary shb = new SoapHexBinary(value);
    return shb.ToString();
}

SoapHexBinary доступен из .NET 1.0 и находится в mscorlib. Несмотря на забавное пространство имен, оно делает именно то, что задан вопросом.

Sly Gryphon 28.06.2011 10:48

Отличная находка! Обратите внимание, что вам нужно будет дополнить нечетные строки начальным 0 для GetStringToBytes, как и в другом решении.

Carter Medlin 31.10.2011 21:10

Вы видели реализацию мысли? У принятого ответа есть лучший ИМХО.

mfloryan 26.01.2012 17:42

Вы имеете в виду реализацию SoapHexBinary? Если да, то что делает его хуже, чем реализация в принятом ответе?

Mykroft 26.01.2012 23:20

Интересно увидеть здесь реализацию Mono: github.com/mono/mono/blob/master/mcs/class/corlib/…

Jeremy 29.04.2012 08:40

В моих тестах (которые я собираюсь бросить в ответ) Mono's impl. примерно на 10% быстрее, чем SoapHexBinary, и в 16 раз медленнее моего ...

Ben Mosher 22.05.2012 20:39

SoapHexBinary не поддерживается в .NET Core / .NET Standard ...

juFo 11.03.2020 12:12

Я не получил код, который вы предложили для работы, Олипро. hex[i] + hex[i+1], по-видимому, вернул int.

Однако я добился некоторого успеха, взяв несколько подсказок из кода Waleeds и сколотив все это вместе. Это чертовски уродливо, но, похоже, работает и работает в 1/3 раза быстрее, чем другие, согласно моим тестам (с использованием механизма тестирования патчей). В зависимости от размера ввода. Переключение?: S для отделения 0-9 сначала, вероятно, даст немного более быстрый результат, поскольку здесь больше цифр, чем букв.

public static byte[] StringToByteArray2(string hex)
{
    byte[] bytes = new byte[hex.Length/2];
    int bl = bytes.Length;
    for (int i = 0; i < bl; ++i)
    {
        bytes[i] = (byte)((hex[2 * i] > 'F' ? hex[2 * i] - 0x57 : hex[2 * i] > '9' ? hex[2 * i] - 0x37 : hex[2 * i] - 0x30) << 4);
        bytes[i] |= (byte)(hex[2 * i + 1] > 'F' ? hex[2 * i + 1] - 0x57 : hex[2 * i + 1] > '9' ? hex[2 * i + 1] - 0x37 : hex[2 * i + 1] - 0x30);
    }
    return bytes;
}

С точки зрения скорости это кажется лучше, чем что-либо здесь:

  public static string ToHexString(byte[] data) {
    byte b;
    int i, j, k;
    int l = data.Length;
    char[] r = new char[l * 2];
    for (i = 0, j = 0; i < l; ++i) {
      b = data[i];
      k = b >> 4;
      r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
      k = b & 15;
      r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
    }
    return new string(r);
  }

От разработчиков Microsoft приятное и простое преобразование:

public static string ByteArrayToString(byte[] ba) 
{
    // Concatenate the bytes into one long string
    return ba.Aggregate(new StringBuilder(32),
                            (sb, b) => sb.Append(b.ToString("X2"))
                            ).ToString();
}

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

public static string ByteArrayToString(byte[] ba)   
{   
   StringBuilder hex = new StringBuilder(ba.Length * 2);   

   for(int i=0; i < ba.Length; i++)       // <-- Use for loop is faster than foreach   
       hex.Append(ba[i].ToString("X2"));   // <-- ToString is faster than AppendFormat   

   return hex.ToString();   
} 

Это самая быстрая из всех процедур, которые я когда-либо видел здесь. Не верьте мне на слово ... проверьте производительность каждой подпрограммы и самостоятельно проверьте ее код CIL.

Итератор - не главная проблема этого кода. Вы должны протестировать b.ToSting("X2").

dolmen 21.08.2013 03:49

Зачем усложнять? В Visual Studio 2008 это просто:

C#:

string hex = BitConverter.ToString(YourByteArray).Replace("-", "");

VB:

Dim hex As String = BitConverter.ToString(YourByteArray).Replace("-", "")

причина в производительности, когда вам нужно высокопроизводительное решение. :)

Ricky 04.08.2016 09:28

Если производительность имеет значение, вот оптимизированное решение:

    static readonly char[] _hexDigits = "0123456789abcdef".ToCharArray();
    public static string ToHexString(this byte[] bytes)
    {
        char[] digits = new char[bytes.Length * 2];
        for (int i = 0; i < bytes.Length; i++)
        {
            int d1, d2;
            d1 = Math.DivRem(bytes[i], 16, out d2);
            digits[2 * i] = _hexDigits[d1];
            digits[2 * i + 1] = _hexDigits[d2];
        }
        return new string(digits);
    }

Это примерно в 2,5 раза быстрее, чем BitConverter.ToString, и примерно в 7 раз быстрее, чем BitConverter.ToString + удаление символов «-».

Если бы производительность имела значение, вы бы не использовали Math.DivRem для разделения байта на два полубайта.

dolmen 21.08.2013 03:53

@dolmen, вы запускали тесты производительности с Math.DivRem и без? Я серьезно сомневаюсь, что это влияет на производительность Любые: реализация Math.DivRem - это именно то, что вы делали бы вручную, и метод очень прост, поэтому он всегда встроен JIT (на самом деле он предназначен для встроенного, как предполагает примененный атрибут TargetedPatchingOptOut к нему)

Thomas Levesque 21.08.2013 04:12

@ThomasLevesque Реализация DivRem выполняет модульную операцию и деление. Почему вы думаете, что эти операции выполняются вручную? Для меня естественной реализацией является github.com/patridge/PerformanceStubs/blob/master/…, которая выполняет битовый сдвиг и логическое и. Эти операции намного дешевле модуля и деления даже на современных процессорах.

Søren Boisen 03.08.2016 19:08

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

  • Таблица кодировщика 512 bytes или 1024 bytes (дважды размер, если и верхний, и нижний регистр нужно)
  • Таблица декодера 256 bytes или 64 KiB (поиск одного символа или двойной поиск символов)

В моем решении для таблицы кодирования используется 1024  байт, а для декодирования - 256  байт.

Расшифровка

private static readonly byte[] LookupTable = new byte[] {
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

private static byte Lookup(char c)
{
  var b = LookupTable[c];
  if (b == 255)
    throw new IOException("Expected a hex character, got " + c);
  return b;
}

public static byte ToByte(char[] chars, int offset)
{
  return (byte)(Lookup(chars[offset]) << 4 | Lookup(chars[offset + 1]));
}

Кодирование

private static readonly char[][] LookupTableUpper;
private static readonly char[][] LookupTableLower;

static Hex()
{
  LookupTableLower = new char[256][];
  LookupTableUpper = new char[256][];
  for (var i = 0; i < 256; i++)
  {
    LookupTableLower[i] = i.ToString("x2").ToCharArray();
    LookupTableUpper[i] = i.ToString("X2").ToCharArray();
  }
}

public static char[] ToCharLower(byte[] b, int bOffset)
{
  return LookupTableLower[b[bOffset]];
}

public static char[] ToCharUpper(byte[] b, int bOffset)
{
  return LookupTableUpper[b[bOffset]];
}

Сравнение

StringBuilderToStringFromBytes:   106148
BitConverterToStringFromBytes:     15783
ArrayConvertAllToStringFromBytes:  54290
ByteManipulationToCharArray:        8444
TableBasedToCharArray:              5651 *

* это решение

Примечание

Во время декодирования могут возникнуть исключения IOException и IndexOutOfRangeException (если у символа слишком высокое значение> 256). Должны быть реализованы методы де / кодирования потоков или массивов, это просто доказательство концепции.

Использование памяти в 256 байт незначительно при запуске кода в среде CLR.

dolmen 21.08.2013 04:05

Для производительности я бы выбрал решение drphrozens. Небольшой оптимизацией для декодера может быть использование таблицы для любого символа, чтобы избавиться от «<< 4».

Очевидно, что вызовы двух методов обходятся дорого. Если выполняется какая-либо проверка входных или выходных данных (может быть CRC, контрольная сумма или что-то еще), if (b == 255)... может быть пропущен и, следовательно, также и вызовы метода в целом.

Использование offset++ и offset вместо offset и offset + 1 может дать некоторые теоретические преимущества, но я подозреваю, что компилятор справится с этим лучше, чем я.

private static readonly byte[] LookupTableLow = new byte[] {
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

private static readonly byte[] LookupTableHigh = new byte[] {
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

private static byte LookupLow(char c)
{
  var b = LookupTableLow[c];
  if (b == 255)
    throw new IOException("Expected a hex character, got " + c);
  return b;
}

private static byte LookupHigh(char c)
{
  var b = LookupTableHigh[c];
  if (b == 255)
    throw new IOException("Expected a hex character, got " + c);
  return b;
}

public static byte ToByte(char[] chars, int offset)
{
  return (byte)(LookupHigh(chars[offset++]) | LookupLow(chars[offset]));
}

Это просто не в моей голове и не тестировалось и не тестировалось.

Еще один вариант разнообразия:

public static byte[] FromHexString(string src)
{
    if (String.IsNullOrEmpty(src))
        return null;

    int index = src.Length;
    int sz = index / 2;
    if (sz <= 0)
        return null;

    byte[] rc = new byte[sz];

    while (--sz >= 0)
    {
        char lo = src[--index];
        char hi = src[--index];

        rc[sz] = (byte)(
            (
                (hi >= '0' && hi <= '9') ? hi - '0' :
                (hi >= 'a' && hi <= 'f') ? hi - 'a' + 10 :
                (hi >= 'A' && hi <= 'F') ? hi - 'A' + 10 :
                0
            )
            << 4 | 
            (
                (lo >= '0' && lo <= '9') ? lo - '0' :
                (lo >= 'a' && lo <= 'f') ? lo - 'a' + 10 :
                (lo >= 'A' && lo <= 'F') ? lo - 'A' + 10 :
                0
            )
        );
    }

    return rc;          
}

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

Public Function BufToHex(ByVal buf() As Byte) As String
    Dim sB As New System.Text.StringBuilder
    For i As Integer = 0 To buf.Length - 1
        sB.Append(buf(i).ToString("x2"))
    Next i
    Return sB.ToString
End Function

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

Brian Reichle 02.12.2011 17:50

Простое преобразование байтов в английский язык :)

Behrooz 17.12.2012 01:44

Чтобы не наваливаться на многие ответы здесь, но я нашел довольно оптимальную (в ~ 4,5 раза лучше, чем принято), прямую реализацию парсера шестнадцатеричных строк. Во-первых, результат моих тестов (первая партия - моя реализация):

Give me that string:
04c63f7842740c77e545bb0b2ade90b384f119f6ab57b680b7aa575a2f40939f

Time to parse 100,000 times: 50.4192 ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F

Accepted answer: (StringToByteArray)
Time to parse 100000 times: 233.1264ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F

With Mono's implementation:
Time to parse 100000 times: 777.2544ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F

With SoapHexBinary:
Time to parse 100000 times: 845.1456ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F

Строки base64 и BitConverter'd предназначены для проверки правильности. Обратите внимание, что они равны.

Реализация:

public static byte[] ToByteArrayFromHex(string hexString)
{
  if (hexString.Length % 2 != 0) throw new ArgumentException("String must have an even length");
  var array = new byte[hexString.Length / 2];
  for (int i = 0; i < hexString.Length; i += 2)
  {
    array[i/2] = ByteFromTwoChars(hexString[i], hexString[i + 1]);
  }
  return array;
}

private static byte ByteFromTwoChars(char p, char p_2)
{
  byte ret;
  if (p <= '9' && p >= '0')
  {
    ret = (byte) ((p - '0') << 4);
  }
  else if (p <= 'f' && p >= 'a')
  {
    ret = (byte) ((p - 'a' + 10) << 4);
  }
  else if (p <= 'F' && p >= 'A')
  {
    ret = (byte) ((p - 'A' + 10) << 4);
  } else throw new ArgumentException("Char is not a hex digit: " + p,"p");

  if (p_2 <= '9' && p_2 >= '0')
  {
    ret |= (byte) ((p_2 - '0'));
  }
  else if (p_2 <= 'f' && p_2 >= 'a')
  {
    ret |= (byte) ((p_2 - 'a' + 10));
  }
  else if (p_2 <= 'F' && p_2 >= 'A')
  {
    ret |= (byte) ((p_2 - 'A' + 10));
  } else throw new ArgumentException("Char is not a hex digit: " + p_2, "p_2");

  return ret;
}

Я пробовал кое-что с unsafe и переместил (явно избыточную) последовательность if от символа к полубайту другим методом, но это был самый быстрый способ.

(Я признаю, что это отвечает на половину вопроса. Я чувствовал, что преобразование строка-> byte [] было недостаточно представлено, в то время как угол byte [] -> строка, кажется, хорошо покрыт. Таким образом, этот ответ.)

Для последователей Кнута: я сделал это, потому что мне нужно анализировать несколько тысяч шестнадцатеричных строк каждые несколько минут или около того, поэтому важно, чтобы это было как можно быстрее (во внутреннем цикле, как это было). Решение Tomalak не заметно медленнее, если не выполняется много таких синтаксических анализов.

Ben Mosher 22.05.2012 21:01

Это работает, чтобы перейти от строки к массиву байтов ...

public static byte[] StrToByteArray(string str)
    {
        Dictionary<string, byte> hexindex = new Dictionary<string, byte>();
        for (byte i = 0; i < 255; i++)
            hexindex.Add(i.ToString("X2"), i);

        List<byte> hexres = new List<byte>();
        for (int i = 0; i < str.Length; i += 2)
            hexres.Add(hexindex[str.Substring(i, 2)]);

        return hexres.ToArray();
    }

Думаю, его скорость стоит 16 дополнительных байтов.

    static char[] hexes = new char[]{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    public static string ToHexadecimal (this byte[] Bytes)
    {
        char[] Result = new char[Bytes.Length << 1];
        int Offset = 0;
        for (int i = 0; i != Bytes.Length; i++) {
            Result[Offset++] = hexes[Bytes[i] >> 4];
            Result[Offset++] = hexes[Bytes[i] & 0x0F];
        }
        return new string(Result);
    }

На самом деле это медленнее, чем другие подходы, основанные на поиске в таблице (по крайней мере, в моих тестах). Использование != вместо < нарушает некоторые шаблоны оптимизации JIT, а дополнительный счетчик для Offset также кажется дорогостоящим.

CodesInChaos 15.01.2013 13:32

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

К тому же это довольно быстро.

static string ByteToHexBitFiddle(byte[] bytes)
{
    char[] c = new char[bytes.Length * 2];
    int b;
    for (int i = 0; i < bytes.Length; i++) {
        b = bytes[i] >> 4;
        c[i * 2] = (char)(55 + b + (((b-10)>>31)&-7));
        b = bytes[i] & 0xF;
        c[i * 2 + 1] = (char)(55 + b + (((b-10)>>31)&-7));
    }
    return new string(c);
}

Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn


Abandon all hope, ye who enter here

Объяснение странной возни с битами:

  1. bytes[i] >> 4 извлекает старший полубайт байта
    bytes[i] & 0xF извлекает младший полубайт байта
  2. b - 10
    < 0 для значений b < 10, который станет десятичной цифрой
    >= 0 для значений b > 10, который станет буквой от A к F.
  3. Использование i >> 31 для 32-битного целого числа со знаком извлекает знак благодаря расширению знака. Это будет -1 для i < 0 и 0 для i >= 0.
  4. Объединение 2) и 3) показывает, что (b-10)>>31 будет 0 для букв и -1 для цифр.
  5. Глядя на регистр букв, последнее слагаемое становится 0, а b находится в диапазоне от 10 до 15. Мы хотим отобразить его на A (65) в F (70), что подразумевает добавление 55 ('A'-10).
  6. Рассматривая регистр цифр, мы хотим адаптировать последнее слагаемое, чтобы оно отображало b из диапазона от 0 до 9 в диапазон 0 (48) в 9 (57). Это означает, что он должен стать -7 ('0' - 55) .
    Теперь мы могли бы просто умножить на 7. Но поскольку -1 представлен всеми битами, равными 1, мы можем вместо этого использовать & -7, начиная с (0 & -7) == 0 и (-1 & -7) == -7.

Некоторые дополнительные соображения:

  • Я не использовал вторую переменную цикла для индексации в c, поскольку измерения показывают, что вычисление ее из i дешевле.
  • Использование именно i < bytes.Length в качестве верхней границы цикла позволяет JITter исключить проверки границ на bytes[i], поэтому я выбрал этот вариант.
  • Превращение b в int допускает ненужные преобразования из и в байты.

А hex string к byte[] array?

AaA 18.01.2013 11:56

+1 за правильное цитирование вашего источника после того, как вы применили эту черную магию. Приветствую Ктулху.

Edward 03.08.2013 00:41

Лучший ответ (для части вопроса с шестнадцатеричным кодированием)!

dolmen 21.08.2013 03:43

А как насчет строки в байт []?

Syaiful Nizam Yahya 06.11.2013 14:14

Я хотел сказать, что если у меня "0x1B", как мне преобразовать его в байт?

Syaiful Nizam Yahya 07.11.2013 07:59

Хороший! Для тех, кому нужен вывод в нижнем регистре, выражение, очевидно, меняется на 87 + b + (((b-10)>>31)&-39)

eXavier 06.01.2014 21:36

Теперь у меня есть его на Java и C# как для кодирования, так и для декодирования, чтобы продемонстрировать мою «темную магию» (одиночный цикл, без ветвей, кроме последней для шестнадцатеричных ошибок). Конечно, помимо вашего краткого кода, я люблю иногда ломать голову.

Maarten Bodewes 21.01.2014 03:17

@AaA вы хотите преобразовать его в массив байтовых массивов? ;)

CoolOppo 10.06.2015 05:46

@CoolOppo, я не уверен, что вы имеете в виду, но шестнадцатеричная строка находится в формате "123456789ABCDEF", что означает, что каждые два символа преобразуются в one byte

AaA 10.06.2015 05:59

@AaA Вы сказали "byte[] array", что буквально означает массив байтовых массивов или byte[][]. Я просто подшучивал.

CoolOppo 10.06.2015 06:09

Почему бы просто не использовать 'A'-0xA вместо 55? Пусть компилятор во всем разберется; магические числа, такие как 55, трудны для понимания людьми и подвержены ошибкам.

David R Tribble 16.07.2018 18:44

@DavidRTribble Потому что я не хочу брать на себя венок Великого Лорда Ктулху.

CodesInChaos 16.07.2018 20:43

@CodesInChaos - я знаю, вы имели в виду гнев. Но видите ли, это проблема с символом 101, который я, конечно, записал бы как 'e'. ;-)

David R Tribble 23.07.2018 18:47

@CodesInChaos Удивительно, ни разу, хотя этот минус не играет в такую ​​игру с оператором 'and', никогда не использовал его. Похоже на волшебство. Что такое фон? По моему мнению, минус в таком выражении должен означать, что установлен крайний левый бит, а обычное 'and' должно давать минус, если источник только минус, но он другой: 3 & 9 == 1, все в порядке, но 3 & -9 = = 3 и (-3) & (-9) == -11, какого хрена?

Oleg Skripnyak 25.05.2019 11:41

Можно ли здесь использовать (ReadOnly) Span?

juFo 11.03.2020 12:40

Два мэшапа, которые объединяют две операции полубайта в одну.

Наверное, довольно эффективная версия:

public static string ByteArrayToString2(byte[] ba)
{
    char[] c = new char[ba.Length * 2];
    for( int i = 0; i < ba.Length * 2; ++i)
    {
        byte b = (byte)((ba[i>>1] >> 4*((i&1)^1)) & 0xF);
        c[i] = (char)(55 + b + (((b-10)>>31)&-7));
    }
    return new string( c );
}

Декадентская версия linq-with-bit-hacking:

public static string ByteArrayToString(byte[] ba)
{
    return string.Concat( ba.SelectMany( b => new int[] { b >> 4, b & 0xF }).Select( b => (char)(55 + b + (((b-10)>>31)&-7))) );
}

И наоборот:

public static byte[] HexStringToByteArray( string s )
{
    byte[] ab = new byte[s.Length>>1];
    for( int i = 0; i < s.Length; i++ )
    {
        int b = s[i];
        b = (b - '0') + ((('9' - b)>>31)&-7);
        ab[i>>1] |= (byte)(b << 4*((i&1)^1));
    }
    return ab;
}

HexStringToByteArray ("09") возвращает 0x02, что плохо.

CoperNick 29.07.2013 14:26

Вот мой шанс. Я создал пару классов расширения для расширения строки и байта. В тесте с большими файлами производительность сопоставима с Byte Manipulation 2.

Приведенный ниже код для ToHexString - это оптимизированная реализация алгоритма поиска и сдвига. Он почти идентичен таковому от Behrooz, но оказалось, что для итерации используется foreach, а счетчик работает быстрее, чем явно индексирующий for.

На моей машине он занимает 2-е место после Byte Manipulation 2 и представляет собой очень читаемый код. Также представляют интерес следующие результаты тестов:

ToHexStringCharArrayWithCharArrayLookup: 41589,69 средних тиков (более 1000 прогонов), 1,5X ToHexStringCharArrayWithStringLookup: 50 764,06 средних тиков (более 1000 прогонов), 1,2X ToHexStringStringBuilderWithCharArrayLookup: 62812,87 средних тиков (более 1000 прогонов), 1,0X

На основании приведенных выше результатов можно с уверенностью заключить, что:

  1. Штрафы за индексацию строки для выполнения поиска по сравнению с Массив char важен в тесте с большим файлом.
  2. Штрафы за использование StringBuilder известной емкости по сравнению с char массив известного размера для создания строки еще более значим.

Вот код:

using System;

namespace ConversionExtensions
{
    public static class ByteArrayExtensions
    {
        private readonly static char[] digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

        public static string ToHexString(this byte[] bytes)
        {
            char[] hex = new char[bytes.Length * 2];
            int index = 0;

            foreach (byte b in bytes)
            {
                hex[index++] = digits[b >> 4];
                hex[index++] = digits[b & 0x0F];
            }

            return new string(hex);
        }
    }
}


using System;
using System.IO;

namespace ConversionExtensions
{
    public static class StringExtensions
    {
        public static byte[] ToBytes(this string hexString)
        {
            if (!string.IsNullOrEmpty(hexString) && hexString.Length % 2 != 0)
            {
                throw new FormatException("Hexadecimal string must not be empty and must contain an even number of digits to be valid.");
            }

            hexString = hexString.ToUpperInvariant();
            byte[] data = new byte[hexString.Length / 2];

            for (int index = 0; index < hexString.Length; index += 2)
            {
                int highDigitValue = hexString[index] <= '9' ? hexString[index] - '0' : hexString[index] - 'A' + 10;
                int lowDigitValue = hexString[index + 1] <= '9' ? hexString[index + 1] - '0' : hexString[index + 1] - 'A' + 10;

                if (highDigitValue < 0 || lowDigitValue < 0 || highDigitValue > 15 || lowDigitValue > 15)
                {
                    throw new FormatException("An invalid digit was encountered. Valid hexadecimal digits are 0-9 and A-F.");
                }
                else
                {
                    byte value = (byte)((highDigitValue << 4) | (lowDigitValue & 0x0F));
                    data[index / 2] = value;
                }
            }

            return data;
        }
    }
}

Ниже приведены результаты тестирования, которые я получил, когда поместил свой код в проект тестирования @patridge на своей машине. Я также добавил тест на преобразование в массив байтов из шестнадцатеричного. Тестовые прогоны, в которых использовался мой код, - это ByteArrayToHexViaOptimizedLookupAndShift и HexToByteArrayViaByteManipulation. HexToByteArrayViaConvertToByte был взят из XXXX. HexToByteArrayViaSoapHexBinary - это тот, который был из ответа @Mykroft.

Intel Pentium III Xeon processor

    Cores: 4 <br/>
    Current Clock Speed: 1576 <br/>
    Max Clock Speed: 3092 <br/>

Converting array of bytes into hexadecimal string representation


ByteArrayToHexViaByteManipulation2: 39,366.64 average ticks (over 1000 runs), 22.4X

ByteArrayToHexViaOptimizedLookupAndShift: 41,588.64 average ticks (over 1000 runs), 21.2X

ByteArrayToHexViaLookup: 55,509.56 average ticks (over 1000 runs), 15.9X

ByteArrayToHexViaByteManipulation: 65,349.12 average ticks (over 1000 runs), 13.5X

ByteArrayToHexViaLookupAndShift: 86,926.87 average ticks (over 1000 runs), 10.2X

ByteArrayToHexStringViaBitConverter: 139,353.73 average ticks (over 1000 runs),6.3X

ByteArrayToHexViaSoapHexBinary: 314,598.77 average ticks (over 1000 runs), 2.8X

ByteArrayToHexStringViaStringBuilderForEachByteToString: 344,264.63 average ticks (over 1000 runs), 2.6X

ByteArrayToHexStringViaStringBuilderAggregateByteToString: 382,623.44 average ticks (over 1000 runs), 2.3X

ByteArrayToHexStringViaStringBuilderForEachAppendFormat: 818,111.95 average ticks (over 1000 runs), 1.1X

ByteArrayToHexStringViaStringConcatArrayConvertAll: 839,244.84 average ticks (over 1000 runs), 1.1X

ByteArrayToHexStringViaStringBuilderAggregateAppendFormat: 867,303.98 average ticks (over 1000 runs), 1.0X

ByteArrayToHexStringViaStringJoinArrayConvertAll: 882,710.28 average ticks (over 1000 runs), 1.0X


Дополнение к ответу от @CodesInChaos (обратный метод)

public static byte[] HexToByteUsingByteManipulation(string s)
{
    byte[] bytes = new byte[s.Length / 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        int hi = s[i*2] - 65;
        hi = hi + 10 + ((hi >> 31) & 7);

        int lo = s[i*2 + 1] - 65;
        lo = lo + 10 + ((lo >> 31) & 7) & 0x0f;

        bytes[i] = (byte) (lo | hi << 4);
    }
    return bytes;
}

Объяснение:

& 0x0f поддерживает также строчные буквы

hi = hi + 10 + ((hi >> 31) & 7); совпадает с:

hi = ch-65 + 10 + (((ch-65) >> 31) & 7);

Для "0" ... "9" это то же самое, что и hi = ch - 65 + 10 + 7;, то есть hi = ch - 48 (это из-за 0xffffffff & 7).

Для "A" ... "F" это hi = ch - 65 + 10; (это из-за 0x00000000 & 7).

Для 'a' .. 'f' нам нужны большие числа, поэтому мы должны вычесть 32 из версии по умолчанию, сделав несколько битов 0 с помощью & 0x0f.

65 - это код для 'A'

48 - это код для '0'

7 - количество букв между '9' и 'A' в таблице ASCII (...456789:;<=>?@ABCD...).

Эта версия ByteArrayToHexViaByteManipulation могла бы быть быстрее.

Из моих отчетов:

  • ByteArrayToHexViaByteManipulation3: 1,68 средних тиков (более 1000 прогонов), 17,5X
  • ByteArrayToHexViaByteManipulation2: 1,73 средних тика (более 1000 прогонов), 16,9X
  • ByteArrayToHexViaByteManipulation: 2,90 средних тика (более 1000 прогонов), 10,1X
  • ByteArrayToHexViaLookupAndShift: 3,22 средних тика (более 1000 прогонов), 9,1X
  • ...

    static private readonly char[] hexAlphabet = new char[]
        {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    static string ByteArrayToHexViaByteManipulation3(byte[] bytes)
    {
        char[] c = new char[bytes.Length * 2];
        byte b;
        for (int i = 0; i < bytes.Length; i++)
        {
            b = ((byte)(bytes[i] >> 4));
            c[i * 2] = hexAlphabet[b];
            b = ((byte)(bytes[i] & 0xF));
            c[i * 2 + 1] = hexAlphabet[b];
        }
        return new string(c);
    }
    

И я думаю, что это оптимизация:

    static private readonly char[] hexAlphabet = new char[]
        {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    static string ByteArrayToHexViaByteManipulation4(byte[] bytes)
    {
        char[] c = new char[bytes.Length * 2];
        for (int i = 0, ptr = 0; i < bytes.Length; i++, ptr += 2)
        {
            byte b = bytes[i];
            c[ptr] = hexAlphabet[b >> 4];
            c[ptr + 1] = hexAlphabet[b & 0xF];
        }
        return new string(c);
    }

Не оптимизирован для скорости, но больше LINQy, чем большинство ответов (.NET 4.0):

<Extension()>
Public Function FromHexToByteArray(hex As String) As Byte()
    hex = If(hex, String.Empty)
    If hex.Length Mod 2 = 1 Then hex = "0" & hex
    Return Enumerable.Range(0, hex.Length \ 2).Select(Function(i) Convert.ToByte(hex.Substring(i * 2, 2), 16)).ToArray
End Function

<Extension()>
Public Function ToHexString(bytes As IEnumerable(Of Byte)) As String
    Return String.Concat(bytes.Select(Function(b) b.ToString("X2")))
End Function

Еще одна быстрая функция ...

private static readonly byte[] HexNibble = new byte[] {
    0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
    0x8, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
    0x0, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x0,
    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
    0x0, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF
};

public static byte[] HexStringToByteArray( string str )
{
    int byteCount = str.Length >> 1;
    byte[] result = new byte[byteCount + (str.Length & 1)];
    for( int i = 0; i < byteCount; i++ )
        result[i] = (byte) (HexNibble[str[i << 1] - 48] << 4 | HexNibble[str[(i << 1) + 1] - 48]);
    if ( (str.Length & 1) != 0 )
        result[byteCount] = (byte) HexNibble[str[str.Length - 1] - 48];
    return result;
}

Также есть XmlWriter.WriteBinHex (см. MSDN страница). Это очень полезно, если вам нужно поместить шестнадцатеричную строку в поток XML.

Вот автономный метод, чтобы увидеть, как это работает:

    public static string ToBinHex(byte[] bytes)
    {
        XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
        xmlWriterSettings.ConformanceLevel = ConformanceLevel.Fragment;
        xmlWriterSettings.CheckCharacters = false;
        xmlWriterSettings.Encoding = ASCIIEncoding.ASCII;
        MemoryStream memoryStream = new MemoryStream();
        using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))
        {
            xmlWriter.WriteBinHex(bytes, 0, bytes.Length);
        }
        return Encoding.ASCII.GetString(memoryStream.ToArray());
    }

Безопасные версии:

public static class HexHelper
{
    [System.Diagnostics.Contracts.Pure]
    public static string ToHex(this byte[] value)
    {
        if (value == null)
            throw new ArgumentNullException("value");

        const string hexAlphabet = @"0123456789ABCDEF";

        var chars = new char[checked(value.Length * 2)];
        unchecked
        {
            for (int i = 0; i < value.Length; i++)
            {
                chars[i * 2] = hexAlphabet[value[i] >> 4];
                chars[i * 2 + 1] = hexAlphabet[value[i] & 0xF];
            }
        }
        return new string(chars);
    }

    [System.Diagnostics.Contracts.Pure]
    public static byte[] FromHex(this string value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        if (value.Length % 2 != 0)
            throw new ArgumentException("Hexadecimal value length must be even.", "value");

        unchecked
        {
            byte[] result = new byte[value.Length / 2];
            for (int i = 0; i < result.Length; i++)
            {
                // 0(48) - 9(57) -> 0 - 9
                // A(65) - F(70) -> 10 - 15
                int b = value[i * 2]; // High 4 bits.
                int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
                b = value[i * 2 + 1]; // Low 4 bits.
                val += (b - '0') + ((('9' - b) >> 31) & -7);
                result[i] = checked((byte)val);
            }
            return result;
        }
    }
}

Небезопасные версии Для тех, кто предпочитает производительность и не боится ненадежности. Примерно на 35% быстрее ToHex и на 10% быстрее FromHex.

public static class HexUnsafeHelper
{
    [System.Diagnostics.Contracts.Pure]
    public static unsafe string ToHex(this byte[] value)
    {
        if (value == null)
            throw new ArgumentNullException("value");

        const string alphabet = @"0123456789ABCDEF";

        string result = new string(' ', checked(value.Length * 2));
        fixed (char* alphabetPtr = alphabet)
        fixed (char* resultPtr = result)
        {
            char* ptr = resultPtr;
            unchecked
            {
                for (int i = 0; i < value.Length; i++)
                {
                    *ptr++ = *(alphabetPtr + (value[i] >> 4));
                    *ptr++ = *(alphabetPtr + (value[i] & 0xF));
                }
            }
        }
        return result;
    }

    [System.Diagnostics.Contracts.Pure]
    public static unsafe byte[] FromHex(this string value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        if (value.Length % 2 != 0)
            throw new ArgumentException("Hexadecimal value length must be even.", "value");

        unchecked
        {
            byte[] result = new byte[value.Length / 2];
            fixed (char* valuePtr = value)
            {
                char* valPtr = valuePtr;
                for (int i = 0; i < result.Length; i++)
                {
                    // 0(48) - 9(57) -> 0 - 9
                    // A(65) - F(70) -> 10 - 15
                    int b = *valPtr++; // High 4 bits.
                    int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
                    b = *valPtr++; // Low 4 bits.
                    val += (b - '0') + ((('9' - b) >> 31) & -7);
                    result[i] = checked((byte)val);
                }
            }
            return result;
        }
    }
}

Кстати При тестировании производительности при инициализации алфавита каждый раз, когда вызывается неправильная функция convert, алфавит должен быть константным (для строки) или статическим только для чтения (для char []). Тогда преобразование байта [] в строку на основе алфавита становится таким же быстрым, как и версии с манипуляциями с байтами.

И, конечно же, тест должен быть скомпилирован в Release (с оптимизацией) и с отключенной опцией отладки «Подавить JIT-оптимизацию» (то же самое для «Включить только мой код», если код должен быть отлаживаемым).

Я приму участие в этом соревновании по игре с битами, так как у меня есть ответ, в котором также используется битовая игра в шестнадцатеричные числа расшифровать. Обратите внимание, что использование символьных массивов может быть даже быстрее, поскольку вызов методов StringBuilder также потребует времени.

public static String ToHex (byte[] data)
{
    int dataLength = data.Length;
    // pre-create the stringbuilder using the length of the data * 2, precisely enough
    StringBuilder sb = new StringBuilder (dataLength * 2);
    for (int i = 0; i < dataLength; i++) {
        int b = data [i];

        // check using calculation over bits to see if first tuple is a letter
        // isLetter is zero if it is a digit, 1 if it is a letter
        int isLetter = (b >> 7) & ((b >> 6) | (b >> 5)) & 1;

        // calculate the code using a multiplication to make up the difference between
        // a digit character and an alphanumerical character
        int code = '0' + ((b >> 4) & 0xF) + isLetter * ('A' - '9' - 1);
        // now append the result, after casting the code point to a character
        sb.Append ((Char)code);

        // do the same with the lower (less significant) tuple
        isLetter = (b >> 3) & ((b >> 2) | (b >> 1)) & 1;
        code = '0' + (b & 0xF) + isLetter * ('A' - '9' - 1);
        sb.Append ((Char)code);
    }
    return sb.ToString ();
}

public static byte[] FromHex (String hex)
{

    // pre-create the array
    int resultLength = hex.Length / 2;
    byte[] result = new byte[resultLength];
    // set validity = 0 (0 = valid, anything else is not valid)
    int validity = 0;
    int c, isLetter, value, validDigitStruct, validDigit, validLetterStruct, validLetter;
    for (int i = 0, hexOffset = 0; i < resultLength; i++, hexOffset += 2) {
        c = hex [hexOffset];

        // check using calculation over bits to see if first char is a letter
        // isLetter is zero if it is a digit, 1 if it is a letter (upper & lowercase)
        isLetter = (c >> 6) & 1;

        // calculate the tuple value using a multiplication to make up the difference between
        // a digit character and an alphanumerical character
        // minus 1 for the fact that the letters are not zero based
        value = ((c & 0xF) + isLetter * (-1 + 10)) << 4;

        // check validity of all the other bits
        validity |= c >> 7; // changed to >>, maybe not OK, use UInt?

        validDigitStruct = (c & 0x30) ^ 0x30;
        validDigit = ((c & 0x8) >> 3) * (c & 0x6);
        validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);

        validLetterStruct = c & 0x18;
        validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
        validity |= isLetter * (validLetterStruct | validLetter);

        // do the same with the lower (less significant) tuple
        c = hex [hexOffset + 1];
        isLetter = (c >> 6) & 1;
        value ^= (c & 0xF) + isLetter * (-1 + 10);
        result [i] = (byte)value;

        // check validity of all the other bits
        validity |= c >> 7; // changed to >>, maybe not OK, use UInt?

        validDigitStruct = (c & 0x30) ^ 0x30;
        validDigit = ((c & 0x8) >> 3) * (c & 0x6);
        validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);

        validLetterStruct = c & 0x18;
        validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
        validity |= isLetter * (validLetterStruct | validLetter);
    }

    if (validity != 0) {
        throw new ArgumentException ("Hexadecimal encoding incorrect for input " + hex);
    }

    return result;
}

Конвертировано из кода Java.

Хм, мне действительно нужно оптимизировать это для Char[] и использовать Char внутри вместо целых чисел ...

Maarten Bodewes 21.01.2014 03:46

Для C# инициализация переменных там, где они используются, а не вне цикла, вероятно, предпочтительнее, чтобы позволить компилятору оптимизировать. В любом случае я получаю одинаковую производительность

Peteter 12.06.2019 19:50

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

private static readonly uint[] _lookup32 = CreateLookup32();

private static uint[] CreateLookup32()
{
    var result = new uint[256];
    for (int i = 0; i < 256; i++)
    {
        string s=i.ToString("X2");
        result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
    }
    return result;
}

private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
    var lookup32 = _lookup32;
    var result = new char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        var val = lookup32[bytes[i]];
        result[2*i] = (char)val;
        result[2*i + 1] = (char) (val >> 16);
    }
    return new string(result);
}

Я также тестировал варианты этого, используя ushort, struct{char X1, X2}, struct{byte X1, X2} в таблице поиска.

В зависимости от цели компиляции (x86, X64) они либо имели примерно одинаковую производительность, либо были немного медленнее, чем этот вариант.


А для еще более высокой производительности его собрат unsafe:

private static readonly uint[] _lookup32Unsafe = CreateLookup32Unsafe();
private static readonly uint* _lookup32UnsafeP = (uint*)GCHandle.Alloc(_lookup32Unsafe,GCHandleType.Pinned).AddrOfPinnedObject();

private static uint[] CreateLookup32Unsafe()
{
    var result = new uint[256];
    for (int i = 0; i < 256; i++)
    {
        string s=i.ToString("X2");
        if (BitConverter.IsLittleEndian)
            result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
        else
            result[i] = ((uint)s[1]) + ((uint)s[0] << 16);
    }
    return result;
}

public static string ByteArrayToHexViaLookup32Unsafe(byte[] bytes)
{
    var lookupP = _lookup32UnsafeP;
    var result = new char[bytes.Length * 2];
    fixed(byte* bytesP = bytes)
    fixed (char* resultP = result)
    {
        uint* resultP2 = (uint*)resultP;
        for (int i = 0; i < bytes.Length; i++)
        {
            resultP2[i] = lookupP[bytesP[i]];
        }
    }
    return new string(result);
}

Или, если вы считаете приемлемым записать в строку напрямую:

public static string ByteArrayToHexViaLookup32UnsafeDirect(byte[] bytes)
{
    var lookupP = _lookup32UnsafeP;
    var result = new string((char)0, bytes.Length * 2);
    fixed (byte* bytesP = bytes)
    fixed (char* resultP = result)
    {
        uint* resultP2 = (uint*)resultP;
        for (int i = 0; i < bytes.Length; i++)
        {
            resultP2[i] = lookupP[bytesP[i]];
        }
    }
    return result;
}

Почему создание таблицы поиска в небезопасной версии меняет местами полубайты предварительно вычисленного байта? Я думал, что порядок байтов меняет только порядок объектов, которые были сформированы из нескольких байтов.

Raif Atef 05.11.2014 16:13

@RaifAtef Здесь важен не порядок кусочков. Но порядок 16-битных слов в 32-битном целом числе. Но я подумываю переписать его, чтобы тот же код мог работать независимо от порядка байтов.

CodesInChaos 07.11.2014 15:09

Перечитывая код, я думаю, что вы сделали это, потому что, когда вы позже преобразуете char * в uint * и назначите его (при генерации шестнадцатеричного символа), среда выполнения / ЦП перевернут байты (поскольку uint не обрабатывается так же, как 2 отдельных 16-битных символа), поэтому вы предварительно переворачиваете их для компенсации. Я прав ? Порядок байтов сбивает с толку :-).

Raif Atef 07.11.2014 16:26

Хорошо, я укушу - какое преимущество есть в том, чтобы закрепить _lookup32Unsafe на неопределенный срок вместо того, чтобы просто выполнять третий оператор fixed и позволять GC перемещать массив в его основное содержимое, когда этот метод не работает?

Joe Amenta 09.01.2016 15:24

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

CodesInChaos 09.01.2016 18:32

Это просто ответ на половину вопроса ... Как насчет того, чтобы от шестнадцатеричной строки к байтам?

Narvalex 08.03.2017 20:28

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

TamaMcGlinn 30.01.2018 13:33

@TamaMcGlinn В безопасной реализации для внутреннего представления используется прямой порядок байтов, но не делается никаких предположений относительно машинного порядка байтов. Так что он все равно должен работать на машинах с прямым порядком байтов (возможно, немного медленнее). В отличие от этого небезопасный вариант повторно интерпретирует два 16-битных слова как одно 32-битное слово, поэтому он должен быть осторожен с порядком байтов хоста.

CodesInChaos 30.01.2018 14:00

@CodesInChaos Интересно, можно ли теперь использовать Span вместо unsafe ??

Konrad 04.12.2019 16:12

@Konrad, наверное, мог бы; Интересно то, что в этот момент static ReadOnlySpan, инициализированный массивом литералов, становится статическими данными в DLL.

to11mtm 17.01.2021 01:37

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

    /// <summary>
    /// Converts the byte array to a hex string very fast. Excellent job
    /// with code lightly adapted from 'community wiki' here: https://stackoverflow.com/a/14333437/264031
    /// (the function was originally named: ByteToHexBitFiddle). Now allows a native lowerCase option
    /// to be input and allows null or empty inputs (null returns null, empty returns empty).
    /// </summary>
    public static string ToHexString(this byte[] bytes, bool lowerCase = false)
    {
        if (bytes == null)
            return null;
        else if (bytes.Length == 0)
            return "";

        char[] c = new char[bytes.Length * 2];

        int b;
        int xAddToAlpha = lowerCase ? 87 : 55;
        int xAddToDigit = lowerCase ? -39 : -7;

        for (int i = 0; i < bytes.Length; i++) {

            b = bytes[i] >> 4;
            c[i * 2] = (char)(xAddToAlpha + b + (((b - 10) >> 31) & xAddToDigit));

            b = bytes[i] & 0xF;
            c[i * 2 + 1] = (char)(xAddToAlpha + b + (((b - 10) >> 31) & xAddToDigit));
        }

        string val = new string(c);
        return val;
    }

    public static string ToHexString(this IEnumerable<byte> bytes, bool lowerCase = false)
    {
        if (bytes == null)
            return null;
        byte[] arr = bytes.ToArray();
        return arr.ToHexString(lowerCase);
    }

Обратная функция для кода Валида Эйссы (Hex String To Byte Array):

    public static byte[] HexToBytes(this string hexString)        
    {
        byte[] b = new byte[hexString.Length / 2];            
        char c;
        for (int i = 0; i < hexString.Length / 2; i++)
        {
            c = hexString[i * 2];
            b[i] = (byte)((c < 0x40 ? c - 0x30 : (c < 0x47 ? c - 0x37 : c - 0x57)) << 4);
            c = hexString[i * 2 + 1];
            b[i] += (byte)(c < 0x40 ? c - 0x30 : (c < 0x47 ? c - 0x37 : c - 0x57));
        }

        return b;
    }

Функция Waleed Eissa с поддержкой нижнего регистра:

    public static string BytesToHex(this byte[] barray, bool toLowerCase = true)
    {
        byte addByte = 0x37;
        if (toLowerCase) addByte = 0x57;
        char[] c = new char[barray.Length * 2];
        byte b;
        for (int i = 0; i < barray.Length; ++i)
        {
            b = ((byte)(barray[i] >> 4));
            c[i * 2] = (char)(b > 9 ? b + addByte : b + 0x30);
            b = ((byte)(barray[i] & 0xF));
            c[i * 2 + 1] = (char)(b > 9 ? b + addByte : b + 0x30);
        }

        return new string(c);
    }

Другой способ - использовать stackalloc для уменьшения нагрузки на память GC:

static string ByteToHexBitFiddle(byte[] bytes)
{
        var c = stackalloc char[bytes.Length * 2 + 1];
        int b; 
        for (int i = 0; i < bytes.Length; ++i)
        {
            b = bytes[i] >> 4;
            c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
            b = bytes[i] & 0xF;
            c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
        }
        c[bytes.Length * 2 ] = '\0';
        return new string(c);
}
static string ByteArrayToHexViaLookupPerByte2(byte[] bytes)
{                
        var result3 = new uint[bytes.Length];
        for (int i = 0; i < bytes.Length; i++)
                result3[i] = _Lookup32[bytes[i]];
        var handle = GCHandle.Alloc(result3, GCHandleType.Pinned);
        try
        {
                var result = Marshal.PtrToStringUni(handle.AddrOfPinnedObject(), bytes.Length * 2);
                return result;
        }
        finally
        {
                handle.Free();
        }
}

Эта функция в моих тестах всегда является второй записью после небезопасной реализации.

К сожалению, тестовый стенд не так надежен ... если вы запустите его несколько раз, список будет перетасован настолько, что кто знает, что после небезопасного действительно самое быстрое! Он не учитывает предварительное нагревание, время jit-компиляции и снижение производительности сборщика мусора. Я хотел бы переписать его, чтобы получить больше информации, но у меня не было на это времени.

Базовое решение с поддержкой расширений

public static class Utils
{
    public static byte[] ToBin(this string hex)
    {
        int NumberChars = hex.Length;
        byte[] bytes = new byte[NumberChars / 2];
        for (int i = 0; i < NumberChars; i += 2)
            bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
        return bytes;
    }
    public static string ToHex(this byte[] ba)
    {
        return  BitConverter.ToString(ba).Replace("-", "");
    }
}

И используйте этот класс, как показано ниже

    byte[] arr1 = new byte[] { 1, 2, 3 };
    string hex1 = arr1.ToHex();
    byte[] arr2 = hex1.ToBin();

Я придумал другой код, устойчивый к лишним символам (пробелы, тире ...). Он в основном основан на некоторых достаточно быстрых ответах здесь. Это позволяет разобрать следующий "файл"

00-aa-84-fb
12 32 FF CD
12 00
12_32_FF_CD
1200d5e68a
/// <summary>Reads a hex string into bytes</summary>
public static IEnumerable<byte> HexadecimalStringToBytes(string hex) {
    if (hex == null)
        throw new ArgumentNullException(nameof(hex));

    char c, c1 = default(char);
    bool hasc1 = false;
    unchecked   {
        for (int i = 0; i < hex.Length; i++) {
            c = hex[i];
            bool isValid = 'A' <= c && c <= 'f' || 'a' <= c && c <= 'f' || '0' <= c && c <= '9';
            if (!hasc1) {
                if (isValid) {
                    hasc1 = true;
                }
            } else {
                hasc1 = false;
                if (isValid) {
                    yield return (byte)((GetHexVal(c1) << 4) + GetHexVal(c));
                }
            }

            c1 = c;
        } 
    }
}

/// <summary>Reads a hex string into a byte array</summary>
public static byte[] HexadecimalStringToByteArray(string hex)
{
    if (hex == null)
        throw new ArgumentNullException(nameof(hex));

    var bytes = new List<byte>(hex.Length / 2);
    foreach (var item in HexadecimalStringToBytes(hex)) {
        bytes.Add(item);
    }

    return bytes.ToArray();
}

private static byte GetHexVal(char val)
{
    return (byte)(val - (val < 0x3A ? 0x30 : val < 0x5B ? 0x37 : 0x57));
    //                   ^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^   ^^^^
    //                       digits 0-9       upper char A-Z     a-z
}

При копировании обращайтесь к полный код. Включены юнит-тесты.

Некоторые могут сказать, что это слишком терпимо к лишним символам. Поэтому не полагайтесь на этот код для выполнения проверки (или ее изменения).

    // a safe version of the lookup solution:       

    public static string ByteArrayToHexViaLookup32Safe(byte[] bytes, bool withZeroX)
    {
        if (bytes.Length == 0)
        {
            return withZeroX ? "0x" : "";
        }

        int length = bytes.Length * 2 + (withZeroX ? 2 : 0);
        StateSmall stateToPass = new StateSmall(bytes, withZeroX);
        return string.Create(length, stateToPass, (chars, state) =>
        {
            int offset0x = 0;
            if (state.WithZeroX)
            {
                chars[0] = '0';
                chars[1] = 'x';
                offset0x += 2;
            }

            Span<uint> charsAsInts = MemoryMarshal.Cast<char, uint>(chars.Slice(offset0x));
            int targetLength = state.Bytes.Length;
            for (int i = 0; i < targetLength; i += 1)
            {
                uint val = Lookup32[state.Bytes[i]];
                charsAsInts[i] = val;
            }
        });
    }

    private struct StateSmall
    {
        public StateSmall(byte[] bytes, bool withZeroX)
        {
            Bytes = bytes;
            WithZeroX = withZeroX;
        }

        public byte[] Bytes;
        public bool WithZeroX;
    }

Кратчайший способ и поддерживаемое ядро ​​.net:

    public static string BytesToString(byte[] ba) =>
        ba.Aggregate(new StringBuilder(32), (sb, b) => sb.Append(b.ToString("X2"))).ToString();

Это прекрасно, если размер ba составляет 16 байт или меньше. Но он должен быть new StringBuilder(ba.Length * 2), чтобы эффективно обрабатывать массив байтов любой длины.

Andy 09.03.2021 05:50

Существует еще не упомянутое простое однострочное решение, которое преобразует шестнадцатеричные строки в байтовые массивы (здесь нас не волнует отрицательная интерпретация, поскольку это не имеет значения):

BigInteger.Parse(str, System.Globalization.NumberStyles.HexNumber).ToByteArray().Reverse().ToArray();

Это не сохраняет начальные байты 0. Например, строка "000080" приводит к однобайтовому массиву { 0x80 }, а не к ожидаемому трехбайтовому массиву { 0x00, 0x00, 0x80 }.

Roger Stewart 11.03.2020 15:52

Да, я полагаю, что что-то вроде Enumerable.Repeat <byte> (0, (len (str) / 2 - len (bigIntBytes)). Concat (bigIntBytes) .ToArray () необходимо в этом случае

Gregory Morse 12.03.2020 16:33

Вместо того, чтобы переворачивать массив, вы можете сделать массив в режиме прямого байта: .ToByteArray(isBigEndian: true)

Simon Bondo 17.04.2020 17:28

Спасибо, Симон, я не знал или хотя бы забыл об этом удобном параметре!

Gregory Morse 18.04.2020 19:43

С Java 8 мы можем использовать Byte.toUnsignedInt

public static String convertBytesToHex(byte[] bytes) {
    StringBuilder result = new StringBuilder();
    for (byte byt : bytes) {
        int decimal = Byte.toUnsignedInt(byt);
        String hex = Integer.toHexString(decimal);
        result.append(hex);
    }
    return result.toString();
}

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

Maarten Bodewes 02.05.2020 03:35

Для удобства копирования и вставки я объединил несколько ответов в класс:

/// <summary>
/// Extension methods to quickly convert byte array to string and back.
/// </summary>
public static class HexConverter
{
    /// <summary>
    /// Map values to hex digits
    /// </summary>
    private static readonly char[] HexDigits =
        {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };

    /// <summary>
    /// Map 56 characters between ['0', 'F'] to their hex equivalents, and set invalid characters
    /// such that they will overflow byte to fail conversion.
    /// </summary>
    private static readonly ushort[] HexValues =
        {
            0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
            0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
            0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x000A, 0x000B,
            0x000C, 0x000D, 0x000E, 0x000F
        };

    /// <summary>
    /// Empty byte array 
    /// </summary>
    private static readonly byte[] Empty = new byte[0];

    /// <summary>
    /// Convert a byte array to a hexadecimal string.
    /// </summary>
    /// <param name = "bytes">
    /// The input byte array.
    /// </param>
    /// <returns>
    /// A string of hexadecimal digits.
    /// </returns>
    public static string ToHexString(this byte[] bytes)
    {
        var c = new char[bytes.Length * 2];
        for (int i = 0, j = 0; i < bytes.Length; i++)
        {
            c[j++] = HexDigits[bytes[i] >> 4];
            c[j++] = HexDigits[bytes[i] & 0x0F];
        }

        return new string(c);
    }

    /// <summary>
    /// Parse a string of hexadecimal digits into a byte array.
    /// </summary>
    /// <param name = "hexadecimalString">
    /// The hexadecimal string.
    /// </param>
    /// <returns>
    /// The parsed <see cref = "byte[]"/> array.
    /// </returns>
    /// <exception cref = "ArgumentException">
    /// The input string either contained invalid characters, or was of an odd length.
    /// </exception>
    public static byte[] ToByteArray(string hexadecimalString)
    {
        if (!TryParse(hexadecimalString, out var value))
        {
            throw new ArgumentException("Invalid hexadecimal string", nameof(hexadecimalString));
        }

        return value;
    }

    /// <summary>
    /// Parse a hexadecimal string to bytes
    /// </summary>
    /// <param name = "hexadecimalString">
    /// The hexadecimal string, which must be an even number of characters.
    /// </param>
    /// <param name = "value">
    /// The parsed value if successful.
    /// </param>
    /// <returns>
    /// True if successful.
    /// </returns>
    public static bool TryParse(string hexadecimalString, out byte[] value)
    {
        if (hexadecimalString.Length == 0)
        {
            value = Empty;
            return true;
        }

        if (hexadecimalString.Length % 2 != 0)
        {
            value = Empty;
            return false;
        }

        try
        {

            value = new byte[hexadecimalString.Length / 2];
            for (int i = 0, j = 0; j < hexadecimalString.Length; i++)
            {
                value[i] = (byte)((HexValues[hexadecimalString[j++] - '0'] << 4)
                                  | HexValues[hexadecimalString[j++] - '0']);
            }

            return true;
        }
        catch (OverflowException)
        {
            value = Empty;
            return false;
        }
    }
}

Начиная с .NET 5 RC2 вы можете использовать:

  • Convert.ToHexString(byte[] inArray), который возвращает string и
  • Convert.FromHexString(string s), который возвращает byte[].

Доступны перегрузки, которые принимают параметры диапазона.

Самый быстрый способ для людей старой школы ... скучаю по указателям

    static public byte[] HexStrToByteArray(string str)
    {
        byte[] res = new byte[(str.Length % 2 != 0 ? 0 : str.Length / 2)]; //check and allocate memory
        for (int i = 0, j = 0; j < res.Length; i += 2, j++) //convert loop
            res[j] = (byte)((str[i] % 32 + 9) % 25 * 16 + (str[i + 1] % 32 + 9) % 25);
        return res;
    }

.NET 5 добавил метод Convert.ToHexString.

Для тех, кто использует старую версию .NET

internal static class ByteArrayExtensions
{
    
    public static string ToHexString(this byte[] bytes, Casing casing = Casing.Upper)
    {
        Span<char> result = stackalloc char[0];
        if (bytes.Length > 16)
        {
            var array = new char[bytes.Length * 2];
            result = array.AsSpan();
        }
        else
        {
            result = stackalloc char[bytes.Length * 2];
        }

        int pos = 0;
        foreach (byte b in bytes)
        {
            ToCharsBuffer(b, result, pos, casing);
            pos += 2;
        }

        return result.ToString();
    }

    private static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
    {
        uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
        uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;

        buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
        buffer[startingIndex] = (char)(packedResult >> 8);
    }
}

public enum Casing : uint
{
    // Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
    Upper = 0,

    // Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
    Lower = 0x2020U,
}

Адаптировано из репозитория .NET https://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/System.Private.CoreLib/src/System/Convert.cshttps://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/Common/src/System/HexConverter.cs

Другие вопросы по теме