C#を用いたプログラミングにおいて、16進数は非常に頻繁に登場する要素です。

バイナリデータの解析、通信プロトコルの実装、色情報の操作、あるいは低レイヤーのビット演算など、その用途は多岐にわたります。

初心者からベテランまで、C#で16進数を扱う機会は多いものの、「どのように記述するのが最も効率的か」「最新の.NET環境ではどのAPIを使うべきか」という点については、意外と見落とされがちです。

本記事では、C#における16進数の基本的な扱い方から、近年のアップデートで追加された高性能な変換手法、そして実務で役立つ実装パターンまでを体系的に解説します。

単なる相互変換の紹介に留まらず、メモリ効率を意識したモダンな書き方についても触れていくため、現在の開発現場ですぐに活用できる知識が身につくはずです。

C#における16進数リテラルの基本

C#のソースコード上で数値を16進数として直接記述する場合、プレフィックスとして 0x を使用します。

これはC言語やJavaなど多くの言語と共通の仕様です。

数値リテラルの記述方法

C# 7.0以降では、数値の可読性を高めるためにアンダースコア _ を桁区切り文字として使用できるようになりました。

C#
// 基本的な16進数リテラル
int hexValue = 0xFF; // 255

// 桁区切りを使用した記述(可読性が向上)
long largeHex = 0xFFFF_ABCD_1234_EF00;

Console.WriteLine(hexValue);
Console.WriteLine(largeHex);
実行結果
255
-14445347200256

このように、リテラルとして記述された16進数は、内部的には通常の整数型(intやlongなど)として扱われます。

したがって、「16進数型」という独立した型が存在するわけではなく、あくまで数値の表現方法の一つであるという点を理解しておくことが重要です。

数値から16進数文字列への変換

数値を人間が読みやすい16進数形式の文字列に変換する方法は、C#において最も一般的な操作の一つです。

ToStringメソッドによる変換

最も簡単な方法は、数値型の ToString メソッドに書式指定子を渡す方法です。

書式指定子説明例 (値: 255)
"X"大文字の16進数FF
"x"小文字の16進数ff
"X2"2桁でゼロ埋めFF
"X4"4桁でゼロ埋め00FF
C#
int value = 165;

// 大文字で変換
string hexUpper = value.ToString("X");
// 小文字で変換
string hexLower = value.ToString("x");
// 4桁ゼロ埋め
string hexPadded = value.ToString("X4");

Console.WriteLine($"Upper: {hexUpper}");
Console.WriteLine($"Lower: {hexLower}");
Console.WriteLine($"Padded: {hexPadded}");
実行結果
Upper: A5
Lower: a5
Padded: 00A5

文字列補間(String Interpolation)での利用

C# 6.0以降で導入された文字列補間を使用すると、より直感的に記述できます。

C#
int colorValue = 255;
// 文字列補間内での書式指定
string result = $"Color Code: #{colorValue:X2}";
Console.WriteLine(result);
実行結果
Color Code: #FF

16進数文字列から数値への変換

逆の操作である「16進数形式の文字列を数値に変換する」方法も多用されます。

特に外部ファイルやAPIからのレスポンスを処理する際に必須となります。

int.Parse と int.TryParse

特定の文字列が16進数であることを明示するために、System.Globalization.NumberStyles.HexNumber を指定します。

C#
string hexInput = "1A";

// 数値への変換
if (int.TryParse(hexInput, System.Globalization.NumberStyles.HexNumber, null, out int result))
{
    Console.WriteLine($"変換成功: {result}");
}
else
{
    Console.WriteLine("変換失敗");
}
実行結果
変換成功: 26

ここで注意すべき点は、0x というプレフィックスが含まれている文字列の場合、そのままでは HexNumber 指定でエラーになる可能性があることです。

その場合は、文字列の先頭を置換するか、AllowHexSpecifier を適切に扱う必要があります。

Convertクラスを利用する方法

Convert.ToInt32 を使用する場合、第2引数に「基数(Base)」を指定することで16進数として処理できます。

C#
string hexWithPrefix = "0xFF";
// 第2引数に16を指定すると、0x プレフィックスを自動的に解釈できる
int convertedValue = Convert.ToInt32(hexWithPrefix, 16);

Console.WriteLine($"Converted: {convertedValue}");
実行結果
Converted: 255

バイト配列(byte[])と16進数の相互変換

ネットワーク通信や暗号化処理では、単一の数値ではなく「バイト配列」を16進数文字列に変換したい場面が多くあります。

モダンな手法:Convert.ToHexString

.NET 5以降、バイト配列を16進数文字列に一括変換する最も効率的かつ簡潔な方法として Convert.ToHexString が追加されました。

C#
byte[] data = { 0x00, 0x1A, 0xFF, 0x7F };

// バイト配列を16進数文字列に変換
string hexString = Convert.ToHexString(data);

Console.WriteLine(hexString);
実行結果
001AFF7F

このメソッドは、従来のBitConverter.ToString().Replace(“-“, “”)といった手法よりも高速で、メモリ割り当て(アロケーション)が少ないという特徴があります。

同様に、文字列からバイト配列に戻す Convert.FromHexString も用意されています。

従来の BitConverter クラス

古いバージョンの.NET Frameworkなどでは、BitConverter がよく使われていました。

C#
byte[] data = { 10, 20, 30 };
string oldStyle = BitConverter.ToString(data);

Console.WriteLine(oldStyle);
実行結果
0A-14-1E

ただし、この方法は各バイトの間にハイフンが入るため、特定のフォーマットが要求される場合には不向きなこともあります。

モダンな開発環境では Convert.ToHexString を優先して使用しましょう。

Span を活用した高パフォーマンスな変換

大量のデータを扱うアプリケーションにおいて、文字列への変換はガベージコレクション(GC)の負荷を高める要因となります。

最新のC#では、Span<T>ReadOnlySpan<T> を活用することで、ヒープへのメモリ割り当てを最小限に抑えた変換が可能です。

TryWrite を使った効率的な書き込み

ISpanFormattable インターフェースを実装している数値型は、スタック上のバッファに直接16進数を書き込むことができます。

C#
using System;

int value = 4096;
// スタック上にバッファを確保
Span<char> buffer = stackalloc char[8];

// バッファに16進数形式で書き込み
if (value.TryFormat(buffer, out int charsWritten, "X"))
{
    ReadOnlySpan<char> result = buffer.Slice(0, charsWritten);
    Console.WriteLine($"Written to span: {result.ToString()}");
}
実行結果
Written to span: 1000

この手法は、「一度も新しい string インスタンスを生成せずに16進数表現を得る」ことができるため、ループ処理内で頻繁に変換を行う場合に極めて有効です。

ジェネリック数値(Generic Math)による実装

C# 11から導入された「Generic Math」により、任意の数値型に対して共通の16進数処理を記述できるようになりました。

これにより、int用、long用といった個別のメソッドを作成する必要がなくなります。

C#
using System.Numerics;

// 任意の整数型を受け取って16進数文字列を返すメソッド
public string GetHexRepresentation<T>(T value) where T : IBinaryInteger<T>
{
    return string.Format("{0:X}", value);
}

// 使用例
Console.WriteLine(GetHexRepresentation(255));      // int
Console.WriteLine(GetHexRepresentation(1024L));    // long
Console.WriteLine(GetHexRepresentation((byte)15)); // byte
実行結果
FF
400
F

IBinaryInteger<T> インターフェースを使用することで、整数として振る舞うあらゆる型に対して型安全かつ汎用的な16進数処理が可能になります。

16進数処理における注意点とベストプラクティス

16進数を扱う際、いくつかハマりやすいポイントがあります。

これらを意識することで、バグの少ない堅牢なコードを書くことができます。

エンディアン(Endianness)の考慮

BitConverter を使用してマルチバイトの数値(intやlong)をバイト配列に変換し、それを16進数にする場合、実行環境のエンディアンに依存します。

C#
int value = 0x12345678;
byte[] bytes = BitConverter.GetBytes(value);

// リトルエンディアンの環境では 78-56-34-12 となる
Console.WriteLine(BitConverter.ToString(bytes));

ネットワーク通信など、特定のエンディアン(通常はビッグエンディアン)が指定されている場合は、BinaryPrimitives クラスを使用して明示的に変換を行うのが安全です。

符号の扱い

負の整数を16進数に変換する場合、結果は「2の補数」表現になります。

C#
int negativeValue = -1;
Console.WriteLine(negativeValue.ToString("X"));
実行結果
FFFFFFFF

この動作を理解していないと、期待した数値に戻せなくなる可能性があるため注意が必要です。

実践的な応用例:カラーコードのパース

最後に、実務でよくある「カラーコード(#RRGGBB)」をC#で安全に扱う実装例を紹介します。

C#
public record RgbColor(byte R, byte G, byte B);

public static RgbColor? ParseHexColor(string hex)
{
    // 先頭の#を除去
    string cleanHex = hex.StartsWith("#") ? hex.Substring(1) : hex;

    if (cleanHex.Length != 6) return null;

    try
    {
        byte r = Convert.ToByte(cleanHex.Substring(0, 2), 16);
        byte g = Convert.ToByte(cleanHex.Substring(2, 2), 16);
        byte b = Convert.ToByte(cleanHex.Substring(4, 2), 16);
        return new RgbColor(r, g, b);
    }
    catch
    {
        return null;
    }
}

// 使用例
var color = ParseHexColor("#FF5733");
if (color != null)
{
    Console.WriteLine($"R:{color.R}, G:{color.G}, B:{color.B}");
}
実行結果
R:255, G:87, B:51

このように、16進数の知識を応用することで、外部からの文字列データをプログラム内で扱いやすい構造体に変換するロジックを簡潔に記述できます。

まとめ

C#における16進数の扱いは、言語の進化とともに非常に洗練されてきました。

単なる ToString("X") だけではなく、以下のような使い分けがモダンな開発では推奨されます。

  1. 基本変換ToString("X")int.Parse を使用。
  2. 一括変換: バイト配列には Convert.ToHexString を使用。
  3. 高パフォーマンスSpan<T>TryFormat でアロケーションを抑制。
  4. 汎用性: Generic Math を活用して型に依存しない処理を記述。

16進数は、コンピュータがデータを処理する際の根源的な表現形式です。

C#が提供する多彩なAPIを適切に選択することで、「読みやすく、かつ高速な」コードを実現できます。

この記事で紹介したパターンを、ぜひ日々の開発に取り入れてみてください。