こんなことをやろうとする人がどの程度いるのか、いないから似たことやってる記事がヒットしないんだろう...
というわけで今回は「比較的」独自性の高い内容です。
前提条件
OS:windows10
IDE:VS2019, VB.Net framework v4.7.2
目的:任意のバラバラのデータを、任意の構造体へ割り当てるコードの作成。
どうでもいい背景
先人が書いた応用性も可読性も移植性も保守性もないクソコードを整理している最中、次のようなコードがでてきた。
Public Structure mySt
Public int1 As Integer
Public str1 As String
Public bool1 As Boolean
'......
End Structure
Public gSt As mySt
Public Class hoge
Public Sub SetStValue()
gSt.int1 = data1
gSt.str1 = data2
gSt.bool1 = data3
'......
End Sub
Public Sub GetStValue()
data1 = gSt.int1
data2 = gSt.str1
data3 = gSt.bool1
'......
End Sub
End Class
あのですね、こんなコードを書いていては日が暮れる。それも何回も
これ、構造体の定義を変える必要があったり、データ割り当て元が変わったり、別のクラス作ったりしたときに一々一々Set関数Get関数をベタ書きで、構造体のすべての要素に対し、すべての割り当て元を代入する文書く構造になってるんですよ。やってられるか
いや、効率の悪さは(残業でどうにかなるから)置いておいても、いつか起きうる「緊急で改造しなきゃいけないとなったとき」にこんなことやってる暇はないんですよ。あほらしい
どうにかして、任意の構造体に、データ群(任意のメンバ)から、自動でSet/Getできるようにならんかなー
最小限のコード改造で、構造体のメンバの変更ができるようにしたいなー
てか、しないといけない。
概要
そもそもどういう設計で行くか
ひとまず、構造体は構造体として置いておくとして、割り当てるデータ群は何らかの方法で指定できないといけない。ひとまず今回は実現させることを優先して、String配列に全部ぶちこんでしまおう(なかなかのリソースの無駄だが、パフォーマンスに余裕あれば問題ない)(Object配列の方がまだマシ?)
データ型が数値、文字列、Booleanといろいろあるのが一つのハードルである。ここはのちほど精査した方がいい(永久にしない可能性もある)
さて、どこにどれを割り当てるか、順番と名前が必要だ。となると、列挙体を定義するとぴったりではないか。
いや本当は列挙体って順番を意識させないことがポイントなのだけども。
列挙体の定義順と、For Each(など)で呼び出す順番が一致することを必ず確認しておこう。
まずければ、ほかの方式を考える。データ格納と別に名前を格納するString配列を定義してベタ書きでもよい。コード量そんなに変わらない。
つまり
構造体 ← 列挙体で順番に対応する名前を参照する ← データ配列から名前が一致する番号のデータを割り当てる
だ。
構造体メンバの名前の取得
調べてみると、System.Reflectionを使えば行けるようだ。すごいな、この機能...
Reflection.MemberInfoないしFiledInfoを使えばよさそう。
SetValue/GetValueもできるようだ。
列挙体の名前の取得
これはできなければString配列を使えばよい。
が、できることが判明した。
[Enum].GetNames(GetType(myEnm))で定義したKey名の配列が取得できる。あれ、結局文字列配列取得してる...
さて実装、あれ?
Public Sub SetValueFromArr(ByRef o As Object, DataArray As String(), myEnm As Type)
Dim m As Reflection.MemberInfo
For lc As Integer = 0 To DataArray.Count - 1
For Each m in Gettype(mySt).GetMembers
If m.Name = [Enum].GetNames(myEnm)(lc) Then
m.SetValue(o, DataArray(lc))
End If
Next m
Next lc
End Sub
いろいろ問題はあるし最適化の余地があるが、ひとまずこれで動くはず、が、値が割り当てされないではないか。例外発生でもないのに。
ボックス化
結論から言うと上記コードは似たことをC#でやればうまくいく。
VBだから失敗する。
助かります。
構造体は値型なので、SetValueに渡したインスタンスと元の構造体インスタンスは別物。
いくらByRefで取っても無駄。
これを解決するにはValueTypeを使う。
Public Sub SetValueFromArr(ByRef o As Object, DataArray As String(), myEnm As Type)
Dim m As Reflection.MemberInfo
Dim v As ValueType
If o.ValueType Then
v = o
End If
For lc As Integer = 0 To DataArray.Count - 1
For Each m in Gettype(mySt).GetMembers
If m.Name = [Enum].GetNames(myEnm)(lc) Then
If o.ValueType Then
m.SetValue(v, DataArray(lc))
Else
m.SetValue(o, DataArray(lc))
End If
End If
Next m
Next lc
If o.ValueType Then
o = v
End If
End Sub
きわめて曖昧で可読性が低く今後の動作保証性もない気がするがこれでボックス化/解除(つまり参照の受け渡し)ができてうまく元のインスタンスに値が渡せるようだ。この辺、VBの指針(C言語的な参照いじる部分はなるべく隠蔽)の限界を感じる。そもそものコード設計から見直すべきなのだろう。構造体使わず、ちゃんとしたクラスでGet/Set用意するとか。
ByRefを使っていることや、上記コードは洗練されていないという問題はあるが、ひとまずこれで構造体と配列で名前を参照して自動で値を割り当てる関数ができたので、目的達成とする。
これで構造体メンバに変更が入っても、構造体と列挙体の定義のみ書き換えるだけであとは勝手に良い感じになってくれる。
補足
Type.GetMembers
で列挙される順番は定義されておらず、実装によるため、列挙体やString配列(にリテラルや定義したファイルからデータを割り当て)を使って順番を定義しないとどのメンバに配列のどの値がセットされるかの保証がない。これには上記のほか、Attributeを使って構造体メンバに順番を定義する方法もある。定義したAttributeを参照すれば列挙をソートできる考えだ。だがAttributeの自動定義(書いてある順に割り振り)とかそういう方法は正規では見つからなかった。コードの自動出力するソフトを書いたりしない限り。
記事は以上。