C#でバッファの内容を表示する処理を実装してみた
データ型に従って動的に書式を設定する

2021年2月22日

どもです。

最近、C#でバッファ/配列の値をコンソールに表示したい、という場面がよく発生します。
その場面は、基本的に一過性なので、テキトーに配列の内容を表示させる関数を実装してしのいでいました。
しかし、その場面が増えきて、別のツールを作っている場合だったり、さらにデータ型が違っているために、過去に実装した関数が流用できなかったりと、中々うまくいかないことが多くなりました。
そこで「バッファ/配列の値をコンソールに表示」するための処理を、汎用性を持たせて実装してみました。
今回のエントリは、この実装の中でぶつかった問題とその解決方法について書きます。

1.実装した処理の内容

今回実装した処理は、ざっくりですが、以下の内容になります。

  • 表示は16進数で、接頭辞「0x」をつける
  • 1行に表示するデータ数は、16コ
  • 列の先頭に、何行目なのかを表示する
  • 列ヘッダをつける

2.ぶつかった問題

C#で「16進数で値を表示する」といった場合、私はよく以下のように実装します。

int data = 0xAAAA
Console.WriteLine("0x{0:X4}", data);    //パターン1:基本
Console.WriteLine($"0x{data:X4}");      //パターン2:文字列補完書式
            

このように実装してる場合に問題になるのは、表示するデータの長さです。
上記の例では、「X4」の「4」がそれに当たります。
即ち、データ型がbyteの場合には「X2」、ushort型の場合には「X4」、ulong型の場合には「X8」としたいです。
各データ型ごとに関数をオーバーロードで実装する、という方法もあります。
具体的には、下記のような関数になります。

ShowBuff(byte[] buffer, int typeLen);
void ShowBuff(short[] buffer, int typeLen);
void ShowBuff(long[] buffer, int typeLen);

しかし、これではバグがあった場合の修正が大変など、保守の面で問題があります。
そのため、データ型に従って(「2」や「4」といった)「データの長さ」を引数で渡すのは、「イケていない」です。

3.ぶつかった問題の解決

では、この問題をどのように解決するか?
それは、「templateを使用する」です。
…まぁ、当たり前といえば当たり前の解決方法ですね。
バッファのデータ型をtemplateの型で指定して、あとは関数内部でデータ型のサイズ(バイト単位)を取得し、それに従って表示する書式を作成する、という方法です。

3.1.データ型のサイズの取得

C#では、データ型に対するサイズは「sizeof演算子」で取得できます。
ただし、この演算子を使用する際には、いくつか制限/上限があります。

3.1.1.制限事項1:unsafe

1つ目の制限事項は、「unsafe」です。
sizeof演算子を使用する際には、unsafeコンテキストが必要です。
unsafeコンテキストの詳細については、Microsoftの公式HPを参照してください。
プリミティブなデータ型、簡単に言うとコンパイル時にデータ型が判定、定数に評価される型であれば、unsafeコンテキストは必要ありません。
しかし、templateを使用する際には、データ型が判定できません。
そのため、unsafeコンテキストが必要になります。
なお「unsafe」は、関数全体とsizeof演算子を使用する範囲のみ、いずれにも設定が可能です。
即ち、以下の2通りの実装が可能です。

unsafe void ShowBuff<T>(T data)
{
    (...何か処理)
    int dataSize = sizeof(data);
    (...dataSizeを使用した処理)
}
void ShowBuff<T>(T data)
{
    (...何か処理)
    int dataSize = 0;
    unsafe
    {
        dataSize = sizeof(data);
    }
    (...dataSizeを使用した処理)
}

3.1.2.制限事項2:unmanaged

2つ目の制限事項は、「unmanaged」です。
sizeof演算子の引数は、アンマネージド型、またはアンマネージド型に制限される型パラメータである必要があります。
そのため、templateのデータ型をunmanagedに制限しなければなりません。
template型をunmanaged型に制限するためには、関数を以下のように実装します。

void ShowBuff<T>(T data) where T : unmanaged
{
    (...省略...)
}

ここでの「where」は「ジェネリック型制約」と呼ばれます。
whereの詳細は、Microsoftの公式HPを参照してください。

3.2.データ型のサイズの書式への反映

データ型のサイズが取得できたところで、この「サイズ」を書式にどのように反映するか、です。
この解決方法は、実は簡単です、
このエントリでも、すでに登場しています。
それは、「文字列補完書式」です。
即ち、「文字列補完書式を使用して、文字列の書式を作成」します。
説明よりも、実装を示した方が分かりやすいかと思います。

pulic void ShowBuff<T>>(T[] buffer) where T : unmanaged
{
    int charNumOfT = 0;
    unsafe
    {
        charNumOfT = sizeof(T) * 2;
    }
    string format = $"{{0:X{charNumOfT}}}";
    string bufferString = string.format(format, buffer);
    (...省略...)
}

「文字列補完書式を使用して、文字列の書式を作成」しているのが、以下のコード。

string format = $"{{0:X{charNumOfT}}}";

文字列補完書式では、「{」は2回重ねることでエスケープされ、1つの「{」となります。
そのため、仮に変数charNumOfTが「2」だとしたら、上記コードの変数formatには、「{0:X2}」が格納されます。
後は、stringのformatメソッドを使用することで、bufferの値をコンソールに表示ができます。

4.実装

それでは、実装です。

public class BufferViewer
{
    public void ShowBuff<T>(T[] buffer) where T : unmanaged
    {
        //Setup format.
        int charNumOfT = 0;
        unsafe
        {
            charNumOfT = sizeof(T) * 2;
        }
        string format = $"0x{{0:X{charNumOfT}}}";

        int rowHeaderLen = (buffer.Length / 16) + 1;
        int charNumOfRowHeader = rowHeaderLen.ToString().Length + 2;
        string rowHeaderFormat = $"{{0,{charNumOfRowHeader}}}";

        //Set column header.
        for (int index = 0; index < charNumOfRowHeader; index++)
        {
            Console.Write(" ");
        }
        Console.Write(" "); //Set the white space into table top and column index.
        int charNumOfColHeader = charNumOfT + 2;
        string colHeaderFormat = $"{{0, {charNumOfColHeader}}}";
        for (int colNum = 0; colNum < 16; colNum++)
        {
            Console.Write(colHeaderFormat, colNum);
            Console.Write(" ");
        }
        Console.WriteLine();

        //Start writing buffer data.
        int colIndex = 0;
        int rowIndex = 0;
        foreach (T item in buffer)
        {
            if (0 == (colIndex) % 16)
            {
                if (0 != colIndex)
                {
                    Console.WriteLine();

                    colIndex = 0;
                    rowIndex++;
                }
                Console.Write(rowHeaderFormat, rowIndex);
                Console.Write(" ");
            }

            var itemString = string.Format(format, item);
            Console.Write(itemString);
            Console.Write(" ");

            colIndex++;
        }
        Console.WriteLine();
    }
}

コレが、今回実装したコードの全てです。
この関数は、以下のようにして使用します。

class BufferViewerSample
{
    static void Main(string[] args)
    {
        int buffSize = 200;
        var buffer = new ushort[buffSize];
        for (int index = 0; index < buffSize; index++)
        {
            buffer[index] = (byte)index;
        }
        var viewr = new BufferViewer();
        viewr.ShowBuff(buffer);
    }
}

実行結果は、下図のようになります。
cs_buffer_viewer_001
次に、変数bufferのデータ型をushortからbyteに変更して実行すると、結果は下図になります。
cs_buffer_viewer_002
このように、データ型の異なるバッファの内容を、コードを変更することなく表示できていることが確認できました。

5.まとめ

今回は、templateと文字列補完を使用して、データ型に従って動的に書式を設定する実装について書きました。
また、この方法の適用して、型の異なるバッファ/配列の内容を、コードを変更することなく表示する方法を書きました。

配列の値を表示するのは、デバッグの時くらいしかないと思います。
しかし、デバッグの度にブレークポイントでプログラムを一時停止して、別途メモリウィンドウを表示してデータを確認する、ということを毎回行うのは効率が悪いです。
そのため、常にバッファの内容を表示できるようにすることでデータを簡単に確認できるようにして、デバッグの効率を向上・改善できれるます。

今回のエントリが、同じような問題を抱えているヒトの助けになれば、幸いです。

ではっ!