カスタムDictionaryクラスを作ろう(8)

カスタムDictionaryクラスを作ろう(8)

akashi-keirin.hatenablog.com

前回の続きです。

過去記事

現時点のソースコードです。

現時点のDictionaryモジュール
ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

Public Property Let Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Let)"
    ' オブジェクトが渡された
    If IsObject(Item) Then _
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="オブジェクトを渡すときは`Set`を使わなければいけない。")
    
    m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Set Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Set)"
    
    Set m_Dictionary.Item(Key:=Key) = Item
End Property

Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

Keyプロパティの実装

最初に、仕様の確認。

Property Key(Key)

Scripting.Dictionaryのメンバー

キーを別のキーに変更します。

このようになっています。

少し珍しいのは、このプロパティが〝Writeonly〟(書き込み専用)であること。〝Readonly〟(読み取り専用)というのはよく見かけますが、書き込み専用は珍しい気がします。

こんな使い方をするんですね。

WriteonlyのKeyプロパティを実装する

では、ここまでの情報をもとに実装してみましょう。

……と、ここで、手がぱたっと止まります。

第2引数の仮引数名ってどうしたらいいの……?

となってしまうのです。

実は、本家Dictionaryは、

第1引数の仮引数名はKeyです。

で、

なんと、第2引数の仮引数名が表示されないのです。(カッコの中のKeyが強調されている意味がわかりません。)

これは困った……。

しかたがないので、第2引数の仮引数名をNewKeyにします。

リスト1
Public Property Let Key( _
            ByVal Key As Variant, _
            ByVal NewKey As Variant)
    m_Dictionary.Key(Key:=Key) = NewKey
End Property

とりあえずこれで動くことは動きます。

リスト1-1
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    dic.Item("pachinko") = 123
    dic.Key(Key:="pachinko") = "slot"
    Debug.Print dic.Item(Key:="slot")
End Sub

ここで、「おい! Property Setはいらねえのかよ!」と思った方は実に鋭い。(逆に思わなかった方は実に鈍いので、反省してください。)

現在の実装で、次のコードを実行するとどうなるでしょうか。

リスト1-2
Private Sub DictionaryTest02()
    Dim dic As Dictionary
    Set dic = New Dictionary
    dic.Item(Key:="pachinko") = "123"
    Debug.Print dic.Item(Key:="pachinko")
    Dim sh As Worksheet
    Set sh = Application.ActiveSheet
    dic.Key(Key:="pachinko") = sh
    Debug.Print dic.Item(sh)
End Sub

一旦、

dic.Item(Key:="pachinko") = "123"

として、キーが"pachinko"である要素を作成しておき、

dic.Key(Key:="pachinko") = sh

で、アクティブシートを突っ込んだshをキーに指定しようとしています。

shはオブジェクトですが、Setを使っていません。

実はこれはフツーに通ります。

キーワード引数の:=と同じで、Setを使わなくても、VBAの側がよしなにやってくれているようです。

カスタム例外の実装

ただ、本家Dictionaryオブジェクトの場合、存在しないキーを変更しようとすると、

実行時エラー'32811':

'Key' メソッドは失敗しました: 'IDictionary' オブジェクト

このように、非常にわかりやすいエラーが出ます。(っていうか、おまえ、Propertyじゃなかったのかよ……。しかもIDictionaryって、おまえ、インターフェースだったんかよ……。)

ところが、私たちのカスタムDictionaryで同じことをすると、

これはわかりにくい……。

というわけで、親切な例外を吐くようにしましょう。

リスト1改
Public Property Let Key( _
            ByVal Key As Variant, _
            ByVal NewKey As Variant)
    Const ERR_SOURCE As String = "Key property(Let)"
    
    If Not m_Dictionary.Exists(Key:=Key) Then _
        Call RaiseError(ERR_SOURCE, "存在しないキーを指定してはいけない。")
    
    If m_Dictionary.Exists(Key:=NewKey) Then _
        Call RaiseError(ERR_SOURCE, "既存のキーに変更することはできない。")
        
    m_Dictionary.Key(Key:=Key) = NewKey
End Property

これで、

わかりやすく親切なエラーを吐くようになりました。

ちなみに、既存のキーに変更しようとしたときの

このエラーもわかりにくいので、ついでに対応しておきました。

おわりに

……というわけで、とりあえずKeyプロパティも実装できたことにしておきます。

これで主要なメソッド、プロパティが出揃ってきた感じになりましたね。

次回はRemove()メソッドですね。

今回終了時点でのDictionaryモジュールのソースコード
ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

Public Property Let Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Let)"
    ' オブジェクトが渡された
    If IsObject(Item) Then _
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="オブジェクトを渡すときは`Set`を使わなければいけない。")
    
    m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Set Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Set)"
    
    Set m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Let Key( _
            ByVal Key As Variant, _
            ByVal NewKey As Variant)
    Const ERR_SOURCE As String = "Key property(Let)"
    
    If Not m_Dictionary.Exists(Key:=Key) Then _
        Call RaiseError(ERR_SOURCE, "存在しないキーを指定してはいけない。")
    
    If m_Dictionary.Exists(Key:=NewKey) Then _
        Call RaiseError(ERR_SOURCE, "既存のキーに変更することはできない。")
        
    m_Dictionary.Key(Key:=Key) = NewKey
End Property

Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

カスタムDictionaryクラスを作ろう(7)

カスタムDictionaryクラスを作ろう(7)

akashi-keirin.hatenablog.com

前回の続きです。

過去記事

現時点のソースコードです。

現時点のDictionaryモジュール
ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

Itemプロパティの実装(つづき)

Write用のプロパティを実装する

今回は、Read/WriteのプロパティであるItemプロパティのWrite側の実装です。

Itemプロパティには、基本的に何でも格納できてしまうので、あのときと同じように、Object型に配慮する必要があります。

リスト1
Public Property Let Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Set Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Set m_Dictionary.Item(Key:=Key) = Item
End Property

ともに、第1引数のKeyを内部変数m_Dictionaryのキーとして用い、そのキーに対応するアイテムとして第2引数のItemを格納する、という形です。

リスト1-1
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    dic.Item("pachinko") = 123
    Dim sh As Worksheet
    Set sh = Application.ActiveSheet
    Set dic.Item(Key:="slot") = sh
    Debug.Print dic.Item(Key:="pachinko")
    Debug.Print dic.Item(Key:="slot").Name
End Sub

このようなコードで動作確認してみると、

このとおり、バッチリです。

ただ、たとえば次のようなコードだとどうでしょうか。

リスト1-2
Private Sub DictionaryTest02()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Dim sh As Worksheet
    Set sh = Application.ActiveSheet
    dic.Item(Key:="slot") = sh
    Debug.Print dic.Item(Key:="slot").Name
End Sub

Object型(Worksheet型)のアイテムをSetを使わずに格納しようとしているので、当然エラーになるわけですが、

やはりエラーメッセージがかなり不親切です。

これでは、ユーザは何を反省したら良いのかわかりません。

親切なエラーを吐くようにする

私たちのDictionaryクラスには、すでに親切な例外送出用のRaiseError()メソッドがあるので、一気に実装してしまいましょう。

リスト2
Public Property Let Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Let)"

    ' オブジェクトが渡された
    If IsObject(Item) Then _
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="オブジェクトを渡すときは`Set`を使わなければいけない。")
    
    m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Set Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)  
    Set m_Dictionary.Item(Key:=Key) = Item
End Property

このようにしておいて、再度リスト1-2を実行すると……。

これだと反省がはかどります。

おわりに

これでItemプロパティも実装できました。

次回はKeyプロパティを実装していきましょう。

ちょっと癖のあるプロパティなので注意しながら進めていきます。

今回終了時点でのDictionaryモジュール
ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

Public Property Let Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Let)"
    ' オブジェクトが渡された
    If IsObject(Item) Then _
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="オブジェクトを渡すときは`Set`を使わなければいけない。")
    
    m_Dictionary.Item(Key:=Key) = Item
End Property

Public Property Set Item( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Item property(Set)"
    
    Set m_Dictionary.Item(Key:=Key) = Item
End Property

Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

カスタムDictionaryクラスを作ろう(6)

カスタムDictionaryクラスを作ろう(6)

 

akashi-keirin.hatenablog.com

前回の続きです。

過去記事

ここまでで、すでにだいぶ長くなってきているので、現時点のソースコードを載せておきましょう。

現時点のDictionaryモジュール
ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

Itemプロパティの実装(つづき)

今回は、前回作成したItemプロパティをデフォルトプロパティにします。

それにしても、オブジェクト ブラウザーで〝既定のメンバー〟であることを示す

このアイコン、何なのでしょう……?

VBA歴10年を超える剛の者である私にも、何を図案化したものなのかサッパリわかりません。

では、気を取り直してItemプロパティをデフォルトプロパティにしてみましょう。

.clsファイルをエクスポートする

まず、クラス モジュールDictionaryをエクスポートします。

プロジェクト エクスプローラー内でDictionaryを右クリックすると、コンテキストメニューが展開するので、「エクスポート」をクリックしましょう。

ファイル保存ダイアログが出るので、テキトーな場所に保存します。

ファイル拡張子.clsであるファイルとして保存されることになります。

ちなみに、このときにファイル名をAho.clsみたいに全然違う名前にしても、インポート(後述)したらちゃんとDictionaryクラスになります。

まあ、そんなことをするメリットはまずないですが。

昔はバージョンを管理するのにDictionary20191016.clsみたいにしていましたが、Gitを覚えた今、そんなこともしなくなりました。

テキストエディタで.clsファイルを開く

次に、テキトーなテキストエディタで先ほど保存したDictionary.clsファイルを開きます。

画像はVisual Studio Codeで開いたところです。

Visual Basic Editorには表示されない記述もありますね。

このあたりは、モジュールファイルのメタデータを記録した領域のようで、正直よくわからない部分です。

一般ユーザがいじる可能性があるとしたら、

Attribute VB_PredeclaredId = False

ここでしょうかね。

こいつをTrueにしてやると、変数にインスタンスを格納することなく、<クラス名>.<メンバ>の形でプロパティ・メソッドを利用することができるようになります。

状態を持たない、ないしは一度設定したら状態を変化させることがないようなオブジェクトなら、ここをTrueにしておくと、手軽に機能が利用できるので便利です。

私は、Windows API関数の宣言を大量に封印したクラスモジュールを作って(文字列のハッシュ値を算出するメソッドなんかを実装してある。)、便利に使い回しています。

テキストエディタで.clsファイルを編集する

ここで、前回実装したItemプロパティの記述を次のようにします。

リスト1
Public Property Get Item(ByVal Key As Variant) As Variant
Attribute Item.VB_UserMemId = 0
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

2行目のところに、

Attribute Item.VB_UserMemId = 0

これを追記しました。

エディタの画面はこんな感じになります。

ItemメンバのVB_UserMemIdという属性に0をセットした、ぐらいの意味でしょうか。

UserMemIdというのは、「User Member ID」の略でしょうかね?

〝ユーザが設定するメンバID〟を「0」(=先頭の値)に設定することによって〝デフォルトのメンバ〟ということを表しているのでしょう。(たぶん。)

保存をしたら、テキストエディタでの作業は終了です。

.clsファイルをプロジェクトにインポートしなおす

改造した.clsファイルをプロジェクトにインポートするには、プロジェクト エクスプローラーで右クリック→「ファイルのインポート」で良いのですが、

慌ててインポートすると、

こういうまぬけな事態を招きます。

同じ名前のモジュールをインポートしたときに〝上書き〟されるわけではないので注意。

このように、元あったDictionaryを「解放」(ポア)してからインポートし直しましょう。

VBEで見る限り、何にも変わってはいませんが、

オブジェクト ブラウザーで見ると、アイコンがよくわからないあのアイコンに変わっています。

おわりに

長くなったので続きは次回。

いよいよItemプロパティの「Write」側の実装ですね。

akashi-keirin.hatenablog.com

カスタムDictionaryクラスを作ろう(5)

カスタムDictionaryクラスを作ろう(5)

akashi-keirin.hatenablog.com

前回の続きです。

過去記事

Itemプロパティの実装

さあ、ついにItemプロパティの出番です。

時は来た!

それだけです。

仕様を調べる

例によってオブジェクト ブラウザーで仕様を調べます。

Property Item(Key)

となっているので、引数がKeyのプロパティということですね。

さらに、

指定したキーに対する項目を設定します。値の取得も可能です。

とあるので、Read/Writeということです。

また、

Scripting.Dictionaryの既定メンバー

とも書いてあります。これは、このItemプロパティがDictionaryクラスのデフォルトプロパティであることを意味します。

たとえば、Dictionaryのインスタンスdicに、キーが"pachinko"、アイテムが123という要素があったとしたら、本来

dic.Item("pachinko")

とすべきところ、

dic("pachinko")

でもアイテムの123を取り出せる、ということです。

ここまで情報量が多かったので、ちょいと整理しましょう。

  • Keyという引数(Variant)が要る
  • 返り値はVariant
  • Read/Writeともに必要
  • デフォルトプロパティである

これだけの条件を踏まえて実装しないといけないわけです。

では、順に潰していきましょう。

実装

Property Getを作る

まずは、Property Getから。

リスト1
Public Function Item(ByVal Key As Variant) As Variant
    Item = m_Dictionary.Item(Key:=Key)
End Function

単純に考えると、こうなります。

ただし、これだとうまくいきません。

リスト1-1
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Call dic.Add(Key:="pachinko", Item:=123)
    Debug.Print dic.Item(Key:="pachinko")
End Sub

これだと

うまくいくのですが、

リスト1-2
Private Sub DictionaryTest02()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Dim sh As Worksheet
    Set sh = Application.ActiveSheet
    Call dic.Add(Key:="pachinko", Item:=sh)
    Debug.Print dic.Item(Key:="pachinko").Name
End Sub

このように、たとえばWorksheet(=Object型のアイテム)を闘魂注入し、そいつを取得しようとすると、

エラーになるのです。

ステップ実行してみると、

このように、クラス モジュールDictionary内の

Item = m_Dictionary.Item(Key:=Key)

この行でエラーになっています。

エラーメッセージは

オブジェクトは、このプロパティまたはメソッドをサポートしていません。

なので、素直に読むと(m_Dictionaryの実体である)Scripting.DictionaryItemプロパティをサポートしていないように誤認してしまいますが、そんなはずはありません。

察しの良い方はもうお気づきだと思いますが、察しの悪い方のために申しましょう。

Item = m_Dictionary.Item(Key:=Key)

この代入のしかたに問題があるのです。

そう。Object型のアイテムを変数等に代入するときは〝Setが必須〟なのでした。

それが証拠に、リスト1

Item = m_Dictionary.Item(Key:=Key)

Set Item = m_Dictionary.Item(Key:=Key)

にしてやると、

このようにちゃんと意図した結果が得られます。

そこで、実装を変更します。

リスト1改
Public Property Get Item(ByVal Key As Variant) As Variant
    If IsObject(m_Dictionary.Item(Key:=Key)) Then
        Set Item = m_Dictionary.Item(Key:=Key)
    Else
        Item = m_Dictionary.Item(Key:=Key)
    End If
End Property

IsObject()関数を用いて、アイテムの型を判定し、Object型だったらSetを付けるようにしています。

リスト1-3
Private Sub DictionaryTest03()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Call dic.Add(Key:="pachinko", Item:=123)
    Dim sh As Worksheet
    Set sh = Application.ActiveSheet
    Call dic.Add(Key:="slot", Item:=sh)
    Debug.Print dic.Item(Key:="pachinko")
    Debug.Print dic.Item(Key:="slot").Name
End Sub

これで動作確認すると、

このとおり、ちゃんと値型にもObject型にも対応できています。

おわりに

この時点で、結構長くなってしまったので、続きは次回!

このItemプロパティをデフォルトプロパティにしていきましょう。

akashi-keirin.hatenablog.com

カスタムDictionaryクラスを作ろう(4)

カスタムDictionaryクラスを作ろう(4)

akashi-keirin.hatenablog.com

前回の続きです。

過去記事

Exists()メソッドの実装

仕様を調べる

例によってオブジェクト ブラウザーで仕様を調べます。

Function Exists(Key) As Boolean

となっているので、引数がKeyで、Boolean型の値を返すFunction、ということですね。

指定したキーがディクショナリに含まれているかどうかを示します。

とのこと。

実は、このExists()メソッドがDictionaryオブジェクトの優秀さの根拠です。こいつの存在のおかげで、Dictionaryが異様に使いどころの多いオブジェクトになっているのです。

それはさておき、さっさと実装してしまいましょう。

実装

リスト1
Public Function Exists(ByVal Key As Variant) As Boolean
    Exists = m_Dictionary.Exists(Key)
End Function

以上。

めちゃくちゃ簡単ですね。

ちなみに、Scripting.Dictionaryオブジェクトのキーは何でもいいらしいので、引数のKeyVariant型を指定しています。

何なら〝ディクショナリをキーにしたディクショナリ〟みたいなこともできてしまいます〔参考〕。何がうれしいのかわかりませんが。

では、次。

Countプロパティの実装

仕様を調べる。

Property Count As Long

とあるので、Long型の値を返すプロパティ、ということです。

読み取り専用

とありますので、Property Getだけ実装すれば良い、ということです。

実装

リスト2
Public Property Get Count() As Long
    Count = m_Dictionary.Count
End Property

はい。これだけです。

楽勝ですね。

動作確認

今回は実装が簡単すぎて記事の中身がスッカスカなので、動作確認もしておくことにしましょう。

Exists()メソッド

リスト3-1
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Call dic.Add(Key:="pachinko", Item:=123)
    Debug.Print dic.Exists("pachinko")
    Debug.Print dic.Exists("slot")
End Sub

たとえば、これを実行すると、"pachinko"というキーの要素は存在するのでTrue"slot"というキーの要素は存在しないのでFalseが返るはずです。

このとおり。

Countプロパティ

リスト3-2
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Call dic.Add(Key:="pachinko", Item:=123)
    Call dic.Add(Key:="slot", Item:=123)
    Call dic.Add(Key:="nobuta", Item:="group")
    Call dic.Add(Key:="Go", Item:="Go Go!!")
    Debug.Print dic.Count
End Sub

たとえば、これを実行すると、全部で4つの要素が追加されているので、4が返るはずです。

このとおり。

おわりに

次回は、いよいよItemプロパティの実装です。

akashi-keirin.hatenablog.com

カスタムDictionaryクラスを作ろう(3)

カスタムDictionaryクラスを作ろう(3)

過去記事

akashi-keirin.hatenablog.com

前回の続きです。

Add()メソッドの実装

仕様を調べる

まずは、オブジェクト ブラウザーで仕様を調べます。

Sub Add(Key, Item)

となっているので、返り値なしのSubで、引数はKeyItemの2つ。

非常にシンプルです。

実装する

では、さっそく実装。

リスト1
Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

たったこれだけ。

クラス内部の変数m_Dictionaryに保持しているScripting.DictionaryインスタンスのAdd()メソッドに受け取った引数をそのまま渡して、実行しているだけです。

これが〝ラップする〟ということですね。

カスタム例外を吐かせる

さて、Dictionaryオブジェクトは、キーの重複が許されません。

このため、同じキーの要素を追加しようとすると、エラーになります。

たとえば、

Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    Call dic.Add(Key:="pachinko", Item:=123)
    Call dic.Add(Key:="pachinko", Item:="ち~ん(笑)")
End Sub

このようなコードを実行しようとすると、

Call dic.Add(Key:="pachinko", Item:="ち~ん(笑)")

この部分でエラーになります。

このとおり。

ただ、このエラーメッセージ、すなわち

このキーは既にこのコレクションの要素に割り当てられています。

はちょい不親切です。( ?)

これでは、エラーが出たとして、何を反省したら良いのかわかりません。( ?)

……というわけで、もっとこのDictionaryクラスを利用するプログラマに優しいメッセージを表示するため、カスタム例外を吐かせるようにします。

リスト2-1
Private Sub RaiseError( _
            ByVal a_Source As String, _
            ByVal a_Description As String)
        ' エラーオブジェクトに渡すパラメータを用意
    Dim errNum As Long, errDesc As String, errSrc As String
    errNum = vbObjectError + 1
    errSrc = "Dictionary class: " & a_Source
    errDesc = "( ´,_ゝ`) < プークスクスw " & a_Description & "(クソが。)"
    
    ' 例外スロー
    Call Err.Raise( _
        Number:=errNum, _
        Source:=errSrc, _
        Description:=errDesc)
End Sub

まず、今後も似たような処理を複数書くことになるので、例外スロー用のPrivateメソッドを用意しておきます。

エラー番号はテキトーです。

引数a_Sourceにどこで発生した例外なのかを渡し、a_Descriptionに何がいけなかったのかを渡すという超絶親切設計です。これでデバッグが異様にはかどるようになるはずです。

では、こんどはAdd()メソッドのコードにひと手間加えることとしましょう。

リスト2-2
Public Sub Add( _
            ByVal Key As Variant, _
            ByVal Item As Variant)
    Const ERR_SOURCE As String = "Add() method(Sub)"
    
    ' 既存のキーにアイテムを設定しようとした
    If m_Dictionary.Exists(Key) Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="既存のキーにはアイテムを設定できない。")
    End If
    
    Call m_Dictionary.Add( _
        Key:=Key, _
        Item:=Item)
End Sub

まず、

Const ERR_SOURCE As String = "Add() method(Sub)"

このようにして、エラーの出所を表す文字列を定数にしておきます。

実は、Add()メソッドには、他にもエラーになりそうな場面があるため、そこで使い回せるようにするためです。

そして、

If m_Dictionary.Exists(Key) Then
    Call RaiseError( _
        a_Source:=ERR_SOURCE, _
        a_Description:="既存のキーにはアイテムを設定できない。")
End If

ここ。

クラス内部の変数m_Dictionaryに格納したScripting.DictionaryインスタンスのExists()メソッド(これも後ほど実装します。)を使って、既存のキーかどうかを判定し、Exists()メソッドがTrue、すなわち既存のキーだった場合に、先ほど(リスト2-1)のRaiseError()を呼び出す、というしくみです。

すごくわかりやすいエラーメッセージに生まれ変わりました!

ついでにCompareModeプロパティもメンテナンスする

前回実装したCompareModeプロパティですが、

Dictionaryに要素を追加した後でCompareModeの値を変更しようとするとエラーになる

という非常にわかりにくい仕様となっています。

これでは何を反省したら良いのかさっぱりわかりませんね。

そこで、〝わからせる〟ために、こちらにもカスタム例外を仕込んでおくとしましょう。

リスト3
Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    Const ERR_SOURCE As String = "`CompareMode` property(Let)"
    ' 本家Dictionaryは、要素追加後にCompareModeを設定しようとするとエラーになる
    '   -> ちょっと親切なエラーを吐く
    If m_Dictionary.Count > 0 Then
        Call RaiseError( _
            a_Source:=ERR_SOURCE, _
            a_Description:="要素追加後にCompareModeの変更はできない。")
    End If
    m_Dictionary.CompareMode = CompareMode
End Property

Property Let CompareModeプロシージャにカスタム例外を投げる処理を追加しました。

既存の要素の有無を確認するのにScripting.DictionaryのCountプロパティ(これも後ほど実装します。)を利用しています。

いやあ! 実にわかりやすい!

おわりに

次は、今回出てきたExists()メソッド、Countプロパティを一気に実装してしまいましょう。

ではまた!

akashi-keirin.hatenablog.com

カスタムDictionaryクラスを作ろう (2)

カスタムDictionaryクラスを作ろう (2)

akashi-keirin.hatenablog.com

前回の続き。

CompareModeプロパティの実装

では、手始めにCompareModeというプロパティを実装してみましょう。

CompareModeプロパティは

文字列を比較するメソッドを設定します。値の取得も可能です。

とのこと。ちょいわかりにくいですが、「設定」ができるということは書き込み可能ということ、「値の取得」ができるということは読み取り可能ということなので、要するにProperty LetProperty Getの両方が必要、ということです。

また、

Property CompareMode As CompareMethod

とあるので、CompareModeプロパティはCompareMethod型であるようです。

とりあえず、ここまでの情報を元に実装してみます。

リスト1
Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    m_Dictionary.CompareMode = CompareMode
End Property

上段はCompareModeの値を取得するコードです。

たとえば、

Dim dic As Dictionary
Set dic = New Dictionary
Debug.Print dic.CompareMode

こんなコードを実行したとき、3行目のdic.CompareModeの部分が評価されるときにProperty Get CompareMode()プロシージャが実行され、その時点で変数m_Dictionaryに格納されているインスタンスのCompareModeプロパティの値が返される、というしくみです。

下段は、逆にCompareModeに値をセットするコードです。

たとえば、

Dim dic As Dictionary
Set dic = New Dictionary
dic.CompareMode = TextCompare

こんなコードを実行したとき、3行目のdic.CompareMode = TextCompareが評価されるときにProperty Let CompareMode()TextCompareという値が渡され、変数m_Dictionaryに格納されているインスタンスのCompareModeプロパティにTextCompareという値がセットされる、というしくみです。

CompareMethod列挙体の実装

ところが、リスト1のように実装した状態で「デバッグ」-> 「……のコンパイル」を実行すると、あえなくコンパイルエラーになります。

これは、CompareMethodという列挙体がMicrosoft Scripting Runtimeで定義された列挙体だからです。

参照設定しないと使えないのです。

では、どうするか。

ないものは作ればよろしい。

リスト2
Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

これを、クラス モジュールDictionaryの宣言セクションに追加するのです。

念のため、ここまでの全コードを載せておきましょう。

ソースコードを
Option Explicit

Public Enum CompareMethod
    BinaryCompare = 0
    TextCompare = 1
    DatabaseCompare = 2
End Enum

Private m_Dictionary As Object

Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
End Sub

Public Property Get CompareMode() As CompareMethod
    CompareMode = m_Dictionary.CompareMode
End Property

Public Property Let CompareMode(ByVal CompareMode As CompareMethod)
    m_Dictionary.CompareMode = CompareMode
End Property

使ってみる

では、さっそくこのカスタムDictionaryオブジェクトを使ってみましょう。

テキトーな標準モジュールにプロシージャを作って、次のリスト3のように入力します。

リスト3
Private Sub DictionaryTest01()
    Dim dic As Dictionary
    Set dic = New Dictionary
    
End Sub

まず、dim dic as dicとかset dic = new dicまで打った時点でDictionaryが候補に出てくることでしょう。

さらに、dic.と打てばCompareModeが入力候補に出てくるはずです。

このとおり。

あとは、この要領で必要なプロパティ・メソッドを生やしていくだけですが、その前にCompareModeプロパティの仕上げをしておきましょう。

CompareMethodプロパティのデフォルト値を明示しておく

別に何もしなくてもCompareMethodプロパティのデフォルト値はBinaryCompareなのですが、それをソースコード上で明示しておきましょう。

Class_Initialize()メソッドに次のように追記します。

リスト4
Private Sub Class_Initialize()
    Set m_Dictionary = CreateObject("Scripting.Dictionary")
    m_Dictionary.CompareMode = BinaryCompare
End Sub

m_Dictionaryには〝ホンモノ〟のScripting.Dictionaryインスタンスが格納されており、Scripting.DictionaryのCompareModeプロパティのデフォルト値はBinaryCompareなので、別に何もしなくても良いのですが、ソースコード上で明示しておくのは良い心掛けだと思うのでこうしておきました。

おわりに

次回はAdd()メソッドの実装です。

akashi-keirin.hatenablog.com