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はオーバーライドしなければ参照が等しいかどうかの値を返します。 つまり、内容の等しさでEqualsTrueを返すようにオーバーライドする必要があります。 また、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

タプル

こんなクッソ面倒なコードをいちいち書いていたら終わるものも終わりません。 それなら一度汎用的なものを実装して使い回すのが妥当です。 まぁ、すでに用意されているのですが。

それが今回紹介するタプルです。 HaskellErlangPython などを使用している人にとってはとても馴染みが深い概念だと思います。

Tuple クラス (System)

.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

連想配列のキーだけでなく、クラスを一つ用意するまでもないけどいくつかの情報を束ねてコレクションにブチ込みたい時にも便利です。

この場合、Item1Item2などのプロパティを介して格納した値にアクセス可能です。

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自体は項目として含む値のEqualsGetHashCodeを利用しているので、その辺をしっかりしていないクラスを要素として含んでしまうと等値判定などがうまくいかなくなります。

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はすごく便利です。 用法・用量を守って正しく使えば分かりにくいというデメリットもあまり感じません。

あと、なんか言ってなかったぽいですがタプルはイミュータブルなのでその辺はいい感じにお願いします。

おわり

*1:ジェネリックもありますが、まず使いません

*2:と言うよりも、如何なる場合でも Equals を実装したら GetHashCode も合わせて実装すべきです