C#にもRustの列挙型ほすい。
でも普通は無理です。C#には可変長構造体がないので。
ちなみにここで「可変長構造体」とは、データのすべてがスタック領域における(=unmanaged)なデータのことを指します。スタック/ヒープとかマネージド/アンマネージドがわかんない人は回れ右!(ゴメンっ)
結論
無理ぽ。でもunsafeで強引になんとかなるかもしれない。
可変長構造体の定義方法
C#でスタックから可変長にメモリを取れるのはstackalloc
だけです。なのでstackalloc
をこねくり回して扱う必要があります。
一般的な方法
スタック領域から取ってくるので、愚直に実装しようと思うとポインタかSpan<T>
を使う方法しかないでしょう。
たとえば数値(int
)と文字列(char*
)を有つ可変長構造体はこんな感じ。
ref struct Fuga
{
int _id;
ReadOnlySpan<char> _str;
public Fuga(int id, ReadOnlySpan<char> str)
{
_id = id;
_str = str;
}
}
無難です。
構造体を使う側はこんな感じ。
void CallFuga
{
var fuga = new Fuga(86, stackalloc char[]{ '藤', '原', 'と', 'う', 'ふ', '店' })
}
Rustの列挙型みたいなやつ
でも、私がやりたいのはそうじゃなくて、RustのEnumみたいに可変長だけど保持するデータが列状でないパターンです。これはポインタを使ってどうにかするしかありませんね。
例えばこんなのはどうでしょう?
// Ver. 1
unsafe struct Hoge
{
int _id;
void* _ptr;
public Hoge(int id, int num)
{
_id = id;
_ptr = #
}
public Hoge(int id, long num)
{
_id = id;
_ptr = #
}
public long ValueAsLong => *((long*)_ptr);
public int ValueAsInt => *((int*)_ptr);
}
当然アウトです。
&num
で取れるのはコンストラクタのブロック内にコピーされた値の参照なので、コンストラクタを出たら無効になります。
じゃぁ外部から参照貰えば?ってんでこんなのはどうでしょう?
// Ver. 2
unsafe ref struct Hoge
{
int _id;
void* _ptr;
public Hoge(ref int num)
{
_id = 0;
_ptr = Unsafe.AsPointer<int>(ref num);
}
public Hoge(ref long num)
{
_id = 1;
_ptr = Unsafe.AsPointer<long>(ref num);
}
public int? ValueAsInt => _id == 0 ? *((int*)_ptr) : null;
public long? ValueAsLong => _id == 1 ? *((long*)_ptr) : null;
}
外部から参照もらっているので構造体もref struct
の方がいいでしょう。これで参照は有効っぽいし、めでたしめでたし...
とはなりません。問題があります。呼び出し側がヒープから取ってきてたら悲惨なことになります。
例えばこんな場面
class Piyo { public int num; }
void CallHoge()
{
var piyo = new Piyo() { num = 114514 } ;
var huga = new Huga(ref piyo.num); // ヒープの参照を渡す。
}
Huga
はポインタで管理しているので、コンパクションが走り次第即終了。どうしよう。
要はrefだったらいいんでしょということで、以下のようにしてもだめです。
// Ver. 3
unsafe ref struct Hoge
{
int _id;
ref byte _ptr; // 適当な型のref
public Hoge(int num)
{
_id = 0;
_ptr = Unsafe.As<int, byte>(ref num); // ここで ぬるぽ
}
public Hoge(long num)
{
_id = 1;
_ptr = Unsafe.As<long, byte>(ref num); // ここでも ぬるぽ
}
public int? ValueAsInt => _id == 0 ? *((int*)_ptr) : null;
public long? ValueAsLong => _id == 1 ? *((long*)_ptr) : null;
}
なぜかNullReferenceException
が起きて実行できないのです。
stack overflowの記事によると
in fact, the addresses from 0 to 64K are invalid in all processes (in Windows) so as to catch programer mistakes.
とあるので、システム的な問題からできないようですが、正直原因不明。
まぁ、仮にできたとしても安全に動く保証はありませんが。
ちなみにfixedを挟んでもだめでした。
public void Hoge(ref int num)
{
fixed(void* p = &num)
{
_ptr = Unsafe.AsRef<byte>(p);
}
}
結論:どうにもなりません。
ヒープの参照が渡されることがない or コンパクションが走らないことを祈りつつ使うしかなさそうです。
stackallocの乱用
こうなったのも全部、構造体の裁量でスタック領域が確保できないのが悪いのです。本当はこんなコードが書きたい。
struct Piyo
{
void* _ptr;
public Piyo()
{
_ptr = stackalloc byte[364364]; // いけません!stackallocは1kiBまで。
}
}
じゃぁ書けるようにしようぜってことで、相変わらずunsafeですが、こんなのはどうでしょう。
struct Piyo
{
void* _ptr;
public Piyo()
{
_ptr = AllockedMemory.Get(364364);
}
}
unsafe static class AllockedMemory
{
static byte* _ptr;
static byte* _cur;
static byte* _end;
public static void Set(Span<byte> span)
{
fixed (var ptr = span)
{
_ptr = ptr;
_cur = _ptr;
_end = _cur + byteLength;
}
}
public static void Get(int length)
{
var r = _cur;
_cur += length;
if (_cur < _end) return r;
else throw new Exception("確保されたメモリが足りません。");
}
}
こう使います。
void CallPiyo()
{
AllockedMemory.Set(stackalloc byte[1024]); // 十分な数
var p = new Piyo();
}
相変わらずAllockedMemory.Set()
にヒープの参照が渡る可能性があるので問題は解決していませんが、その問題の呼出が一回しかないことと、可変長構造体の初期化が、あたかも普通の構造体に対してであるかのように書けることなどのメリットがあるので、何かのときに使えるかも知れません。
結論(再)
無理ぽ。
どうしても参照がスタック領域にあることを信じてコードを書く必要があるので、一般的な場面で運用することはできない。
どうしても最速コードが書きたいときは、internal
な範囲で使いましょう。