VB.NETでも楽に複数の要素を連想配列のキーに使いたい
はじめに
連想配列ってあるじゃないですか。
.NETだとSystem.Collections.Generic.Dictionary(Of TKey, TValue)
ですね。*1
Dictionary
はキーに複数のインスタンスを指定できないので、複数のキーを指定したいときはそれらを含みさらに適切に実装したクラスを実装しないといけません。
たとえば、2つのInteger
をキーとするために以下のクラスを実装したとします。
Class MyKey Public Property Key1 As Integer Public Property Key2 As Integer Public Sub New(key1 As Integer, key2 As Integer) Me.Key1 = key1 Me.Key2 = key2 End Sub End Class
こんなコードを書いても、絶対の格納した値を取り出すことはできません。
Sub Main() Dim dic = New Dictionary(Of MyKey, String) dic.Add(New MyKey(1, 1), "Hoge") Console.WriteLine(dic(New MyKey(1, 1))) End Sub
当たり前です。Object.Equals
をオーバーライドしていないため、以下のコードがFalse
を返すためです。
Dim k1 = New MyKey(1, 1) Dim k2 = New MyKey(1, 1) Console.WriteLine(k1.Equals(k2))
Dictionary
はキーの等値にObject.Equals
を利用しており、Object.Equals
はオーバーライドしなければ参照が等しいかどうかの値を返します。
つまり、内容の等しさでEquals
がTrue
を返すようにオーバーライドする必要があります。
また、Dictionary
のようなハッシュを用いるクラスで使うためにはObject.GetHashCode
も実装する必要があります。*2
さらにさらに注意が必要な点として、ハッシュを計算するために用いる値は変更して(出来て)はいけません。 現実的に考えるとイミュータブルなクラスとして設計する必要があります。
あとはSystem.IEquatable(Of T)
を実装したりと、もはや自分が何をしたいのかを忘れてしまいそうです。
とまぁ、以上の事柄を実装するとこんな感じに以下略
Class MyKey2 Implements System.IEquatable(Of MyKey2) Private _key1 As Integer Private _key2 As Integer Public ReadOnly Property Key1 As Integer Get Return _key1 End Get End Property Public ReadOnly Property Key2 As Integer Get Return _key2 End Get End Property Public Sub New(ByVal key1 As Integer, ByVal key2 As Integer) _key1 = key1 _key2 = key2 End Sub Public Overrides Function Equals(obj As Object) As Boolean Dim other = TryCast(obj, MyKey2) If other Is Nothing Then Return False Else Return DirectCast(Me, IEquatable(Of MyKey2)).Equals(other) End If End Function Public Overrides Function GetHashCode() As Integer Return _key1 Xor _key2 End Function Public Function Equals1(other As MyKey2) As Boolean Implements IEquatable(Of MyKey2).Equals Return Me._key1 = other._key1 AndAlso Me._key2 = other._key2 End Function End Class
Sub Main() Dim dic = New Dictionary(Of MyKey2, String) dic(New MyKey2(1, 1)) = "Hoge" Console.WriteLine(dic(New MyKey2(1, 1))) End Sub
タプル
こんなクッソ面倒なコードをいちいち書いていたら終わるものも終わりません。 それなら一度汎用的なものを実装して使い回すのが妥当です。 まぁ、すでに用意されているのですが。
それが今回紹介するタプルです。 Haskell や Erlang、Python などを使用している人にとってはとても馴染みが深い概念だと思います。
.NET Framework 4 以上でしか使えませんが、そもそも今時3.5で開発するんか? と言った感じです。
使い方はとっても簡単。 以下のコードでokです。
Sub Main() Dim dic = New Dictionary(Of Tuple(Of Integer, Integer), String) dic(Tuple.Create(1, 1)) = "Hoge" Console.WriteLine(dic(Tuple.Create(1, 1))) End Sub
連想配列のキーだけでなく、クラスを一つ用意するまでもないけどいくつかの情報を束ねてコレクションにブチ込みたい時にも便利です。
この場合、Item1
、Item2
などのプロパティを介して格納した値にアクセス可能です。
Sub Main() Dim addressbook = New List(Of Tuple(Of String, String)) addressbook.Add(Tuple.Create("Alice", "alice@jyuch.com")) addressbook.Add(Tuple.Create("Bob", "bob@jyuch.com")) For Each it In addressbook Console.WriteLine("{0} {1}", it.Item1, it.Item2) Next End Sub
型推論により厳密に型指定ができ、異なる要素からなるタプルとはコンパイラレベルで区別できます。
そのため、ジェネリックにより型指定されたコレクションクラスなどにおいては異なる要素からなるタプルが混入するといったことは無くなります。
まぁ、一部のVBプログラマが大好きなOption Strict Off
と非ジェネリックコレクションの前には無力ですが。
とまぁ、詠唱破棄してクラスっぽいものを使えるTuple
は確かに便利ですが、欠点がないわけではありません。
デメリット:わかりずらい
クラス内の限られた範囲ならまだセーフだと思いますが、外部に公開すべきではありません。
たとえば、こんなメソッドがあったとして返り値の各項目が何を表すかは定義だけでは全く分かりません。 タプルが区別するのはあくまでも要素の型で、内容までは知ったこっちゃないです。
Public Function SelectByID(ByVal id As Integer) As Tuple(Of Integer, Tuple(Of String, String), String)
XMLコメントドキュメントで説明するという方法もありますが、その前にコードで表現可能な場合はコードで説明すべきです。
Function SelectByID(ByVal id As Integer) As MyAddress Return Nothing End Function
Class MyAddress Public Property ID As Integer Public Property FirstName As String Public Property LastName As String Public Property MailAddress As String End Class
仮に説明されていたとしても、Item1
が何を表しているかなんてパッと見ても分かりません。
個人的にはタプルはできればメソッド内、許されるギリギリがクラス内での利用ですね。 クラスの外に出てしまうと何を表しているのかが分かりにくいというデメリットが予想以上に効いてきます。
アセンブリの外に出してしまうのは本当にやめた方がいいです。
注意点:使える型に条件がある
Tuple
は等値判定やハッシュをいい感じにアレしてくれますが、Tuple
自体は項目として含む値のEquals
やGetHashCode
を利用しているので、その辺をしっかりしていないクラスを要素として含んでしまうと等値判定などがうまくいかなくなります。
Sub Main() Dim t1 = Tuple.Create(New MyKey(1, 1), 1) Dim t2 = Tuple.Create(New MyKey(1, 1), 1) Dim t3 = Tuple.Create(New MyKey2(1, 1), 1) Dim t4 = Tuple.Create(New MyKey2(1, 1), 1) Console.WriteLine(t1.Equals(t2)) ' False Console.WriteLine(t3.Equals(t4)) ' True End Sub
おわりに
とまぁ、メリットよりもデメリットを強調してしまった気がするのですが、Tuple
はすごく便利です。
用法・用量を守って正しく使えば分かりにくいというデメリットもあまり感じません。
あと、なんか言ってなかったぽいですがタプルはイミュータブルなのでその辺はいい感じにお願いします。
おわり