2006 年 5 月 8 日 19 時 13 分

ArrayList #3: インデクサ


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


[写真]


さて、これで通常のメソッドは全て実装した。
最後に ArrayList で最も重要な機能を実装する。

ArrayList が配列同様に振舞うためには、
配列アクセス(インデックス)表記が必要となる。
そのような機能を持つメソッドをインデクサと呼ぶ。

VBA にインデクサそのものは存在しないが、
COM 由来のプロシージャ ID と言う属性があり、
その中に「既定」という属性がある。

プロシージャの属性を「既定」に設定すれば、
プロシージャ名を省略して呼び出すことができる。
これを使えば同じような機能を実装することはできる。
通常、戻り値を持つ関数かプロパティを使う。

方針を固めよう。

これを ArrayList に実装するには、
まずはインデクサの機能を持つプロパティが必要となる
これは、Item という名前のプロパティにしよう。

引数として、整数値を一つ取るようにすれば、
以下のように呼び出せるはずだ。

    Dim a As ArrayList
    Dim v As Variant
    Set a = New ArrayList
    Call a.Add(12345)
    v = a.Item(0)
    a.Item(0) = 54321

Item プロパティの属性を既定の設定すると、
最後の 2 行は以下のように書くことができるようになる。

    v = a(0)
    a(0) = 54321

うむ。配列っぽい。

よし、それでは Item プロパティを作ろう。
最初は、プロパティ取得メソッド(getter)からだ。

    Public Property Get Item(ByVal Index As Long) As Variant
        If Index < 0 Or Index >= m_lCount Then
            Call Err.Raise(9)
        End If
        If IsObject(m_vValues(Index)) Then
            Set Item = m_vValues(Index)
        Else
            Item = m_vValues(Index)
        End If
    End Property

昨日やった、値型と参照型の問題と、
インデックス範囲の問題だけで OK なので簡単だ。

続いて、プロパティ設定メソッドだ。
プロパティの設定は、代入式によって行なわれるので、
昨日出てきた値型と参照型の問題がここでも表面化する。

何でも格納できる Variant を代入する場合、
値型と参照型で異なるプロシージャ必要なのだ。
VBA には、setter と letter があり、
setter は参照型用、letter が値型用となる。

では、letter を作る。

    Public Property Let Item(ByVal Index As Long, ByVal Value As Variant)
        If Index < 0 Or Index >= m_lCount Then
            Call Err.Raise(9)' 範囲外
        End If
        m_vValues(Index) = Value
    End Property

プロパティの値型代入プロシージャは、
Property Let という名前になる。
引数は、Property Get と同じものが並び、
一番最後に代入される値が追加される。

上では ByVal Value As Variant としているのがそれだ。
ByRef で受け取ることもできるが、
ここでは ByVal を使って受け取ることにしている。

Property Let は、値型の代入のみが処理されるため、
引数 Value を IsObject で調べる必要はない。

続いて setter だ。

    Public Property Set Item(ByVal Index As Long, ByVal Value As Variant)
        If Index < 0 Or Index >= m_lCount Then
            Call Err.Raise(9)' 範囲外
        End If
        Set m_vValues(Index) = Value
    End Property

殆ど letter と変わりない。
こちらは必ず参照型代入構文でのみ使用されるので、
m_vValues(Index) への代入は、Set を使う。

これでコードの実装は完了だ。
後は、Item プロパティを既定属性にするだけだ。

実は、Visual Basic 6 など、単体の製品においては、
メニューに、ツール⇒プロシージャ属性という項目がある。
しかし、Excel や Word などの VBA にはその項目がない。

VBA で既定属性を設定するには、
裏技的な方法を使う必要がある。
その方法は以下の通りだ。

まずは、ArrayList を右クリックし、
ファイルのエクスポートを選ぶ。
場所を聞かれるので、適当な所に保存する。
VB のクラスモジュールファイルとして保存される。

次に、今エクスポートした ArrayList.cls を、
メモ帳などのテキストエディタで開く。
そして、Public Property XXX Item ~ の行を探す。

Get/Let/Set の 3 つのプロシージャがあるはずなので、
一番上にあるものを探そう。
ここでは、Get が一番上にあったとする。

    Public Property Get Item(ByVal Index As Long) As Variant

の行を探し出し、その直下に以下の行を手入力して保存する。

    Attribute Item.VB_UserMemId = 0

修正後は以下のようになるはずだ。

    Public Property Get Item(ByVal Index As Long) As Variant
    Attribute Item.VB_UserMemId = 0
        If Index < 0 Or Index >= m_lCount Then
            Call Err.Raise(9)
        End If
        If IsObject(m_vValues(Index)) Then
            Set Item = m_vValues(Index)
        Else
            Item = m_vValues(Index)
        End If
    End Property

VBA に戻り、一度 ArrayList を削除する。
削除は、右クリック⇒ArrayList の解放だ。
エクスポートしますかと聞いてくるのでいいえで OK。

そして改めてプロジェクトを右クリックし、
ファイルのインポートを選ぶ。
そして、先ほど改ざんした ArrayList.cls を読み込む。

見た目には何の変更もないが、
これで既定プロシージャの指定が行なわれている。

確認するためには、オブジェクトブラウザを利用する。
表示⇒オブジェクトブラウザ(もしくは F2)を選び、
クラス一覧から、ArrayList を探して選択する。
右側にメソッド一覧が表示される。

Item プロパティに小さい水色の丸がついていれば成功だ。
Item プロパティを選択すると、下の説明表示部分にも、
Nonogram.ArrayList の「既定メンバ」と
表示されているはずだ。

これで ArrayList は実装できた。
この状態では For Each などによる列挙はできないが、
可変長配列をオブジェクト的に扱えるようになった。

========== クラスモジュール: ArrayList ==========

Option Explicit

'##################################################
'# 可変配列のラッパクラス(列挙はできない)
'##################################################

Private Const DEFAULT_CAPACITY As Long = 16

Private m_vValues() As Variant
Private m_lCount    As Long

' 初期化
Private Sub Class_Initialize()
    m_lCount = 0
    ReDim m_vValues(0 To DEFAULT_CAPACITY - 1) As Variant
End Sub

' 要素を取得(既定プロパティ)
'  Index 要素番号
Public Property Get Item(ByVal Index As Long) As Variant
    If Index < 0 Or Index >= m_lCount Then
        Call Err.Raise(9)
    End If
    If IsObject(m_vValues(Index)) Then
        Set Item = m_vValues(Index)
    Else
        Item = m_vValues(Index)
    End If
End Property

' 要素を設定(既定プロパティ)
'  Index 要素番号
'  Value 追加する値(代入式の右辺)
Public Property Let Item(ByVal Index As Long, ByVal Value As Variant)
    If Index < 0 Or Index >= m_lCount Then
        Call Err.Raise(9)
    End If
    m_vValues(Index) = Value
End Property
Public Property Set Item(ByVal Index As Long, ByVal Value As Variant)
    If Index < 0 Or Index >= m_lCount Then
        Call Err.Raise(9)
    End If
    Set m_vValues(Index) = Value
End Property

' 要素数を取得
Public Property Get Count() As Long
    Count = m_lCount
End Property

' 配列の再割り当てを行なわずに格納できる要素数を取得
Public Property Get Capacity() As Long
    Capacity = UBound(m_vValues) + 1
End Property

' 配列の再割り当てを行なわずに指定した要素数を格納できるように配列を拡大
'  MinimumCapacity 必要な最低容量
Public Sub EnsureCapacity(ByVal MinimumCapacity As Long)
    Dim lNew As Long
    If Capacity >= MinimumCapacity Then Exit Sub
    lNew = Me.Capacity * 2
    If lNew = 0 Then lNew = DEFAULT_CAPACITY
    If lNew < MinimumCapacity Then lNew = MinimumCapacity
    ReDim Preserve m_vValues(0 To lNew - 1) As Variant
End Sub

' 要素を最後に追加し、そのインデックスを返す
'  Value 追加する値
Public Function Add(ByVal Value As Variant) As Long
    Call EnsureCapacity(m_lCount + 1)
    If IsObject(Value) Then
        Set m_vValues(m_lCount) = Value
    Else
        m_vValues(m_lCount) = Value
    End If
    Add = m_lCount
    m_lCount = m_lCount + 1
End Function


' 要素を任意の位置に追加
'  Index 要素番号
'  Value 追加する値
Public Sub Insert(ByVal Index As Long, ByVal Value As Variant)
    Dim i As Long
    If Index < 0 Or Index > m_lCount Then
        Call Err.Raise(9)
    End If
    Call EnsureCapacity(m_lCount + 1)
    If Index < m_lCount Then
        Call ShiftElements(m_lCount - 1, Index, 1)
    End If
    If IsObject(Value) Then
        Set m_vValues(Index) = Value
    Else
        m_vValues(Index) = Value
    End If
    m_lCount = m_lCount + 1
End Sub

' 特定位置の要素を削除
'  Index 要素番号
Public Sub RemoveAt(ByVal Index As Long)
    Dim i As Long
    If Index < 0 Or Index >= m_lCount Then
        Call Err.Raise(9)
    End If
    If Index < m_lCount - 1 Then
        Call ShiftElements(Index + 1, m_lCount - 1, -1)
    End If
    m_lCount = m_lCount - 1
    m_vValues(m_lCount) = Empty
End Sub

' 全ての要素を削除
Public Sub Clear()
    Dim i As Long
    For i = 0 To m_lCount - 1
        m_vValues(i) = Empty
    Next
    m_lCount = 0
End Sub

'==================================================
' Private メンバ
'==================================================

' 特定範囲の要素を 1 つずらす
'  First 最初の要素番号
'  Last 最後の要素番号
'  Direction 要素をずらす方向。1 か -1
Private Sub ShiftElements(ByVal First As Long, ByVal Last As Long, ByVal Direction As Long)
    Dim i As Long
    For i = First To Last Step -Direction
        If IsObject(m_vValues(i)) Then
            Set m_vValues(i + Direction) = m_vValues(i)
        Else
            m_vValues(i + Direction) = m_vValues(i)
        End If
    Next
End Sub

========== end of ArrayList ==========



Copyright (c) 1994-2007 Project Loafer. All rights reserved.