C#でバッファの内容を表示する処理を実装してみたデータ型に従って動的に書式を設定する
どもです。
最近、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);
}
}
実行結果は、下図のようになります。
次に、変数bufferのデータ型をushortからbyteに変更して実行すると、結果は下図になります。
このように、データ型の異なるバッファの内容を、コードを変更することなく表示できていることが確認できました。
5.まとめ
今回は、templateと文字列補完を使用して、データ型に従って動的に書式を設定する実装について書きました。
また、この方法の適用して、型の異なるバッファ/配列の内容を、コードを変更することなく表示する方法を書きました。
配列の値を表示するのは、デバッグの時くらいしかないと思います。
しかし、デバッグの度にブレークポイントでプログラムを一時停止して、別途メモリウィンドウを表示してデータを確認する、ということを毎回行うのは効率が悪いです。
そのため、常にバッファの内容を表示できるようにすることでデータを簡単に確認できるようにして、デバッグの効率を向上・改善できれるます。
今回のエントリが、同じような問題を抱えているヒトの助けになれば、幸いです。
ではっ!
ディスカッション
コメント一覧
まだ、コメントがありません