VBA やってますか?
VBAってプログラマーに人気ないですよね、たぶん(´・ω・`)
でも私のような個人で小さなお仕事をこなして食いつないでいる、ワープア系プログラマにとってはマクロ作成依頼が大事な収入源だったりします。
言語の機能はとても貧弱だし、私の大好きな C# とは比べることすら失礼なほどに手の掛かる子ですけど、慣れるとかわいいものです( ´∀`)
いまどき配列使ってる男の人って・・・
ダメなところは色々あるけど、やっぱり配列だけは最悪・・・
固定配列は扱いにくいし、可変配列はもっと困ります。
基本的には素直に Collection クラスを使うべきなんですけど、すでにあるマクロの保守で配列を使わざるをえないケースもあると思うので問題点を考えてみます。
可変配列は ReDim でメモリーを確保します。
要素数がいくつあるのかは UBound で配列の添え字の最大値から間接的に調べることが出来ます。
そこで以下のように処理を書いてみると
' ここではまだ要素数は決らない
Dim arr() As String
Sub test
' いろいろ処理があって・・・
' 中身がなかったら要素を5個確保しようね
' ### 例外発生 ###
If UBound(arr) < 0 Then
ReDim arr(5) As String
End If
End Sub
宣言して1度も ReDim していない配列は要素数すら調べることができないのです。
宣言と同時に ReDim するか、または宣言時に要素数がまだ分からない場合は、ReDim 済みかそうでないかを管理するフラグ変数をセットで持つしかないのです。(ひどいぉ)
結論、配列はかなぐり捨てるものだっ!
それでも配列を使わなければならないなら
なんとかして ReDim していない配列でも UBound で例外を発生させずに値を返せるようにしよう(゚∀。)アヒャ
Public Enum SAFEARRAY_VT
VT_BYTE = 1
VT_BOOL = 2
VT_INTEGER = 2
VT_LONG = 4
VT_SINGLE = 4
VT_DOUBLE = 8
VT_CURRENCY = 8
VT_DECIMAL = 14
VT_DATE = 8
VT_OBJECT = 4
VT_STRING = 10
VT_VARIANT = 16
End Enum
Private Declare Sub CopyMemory Lib "KERNEL32.DLL" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
Private Declare Function SafeArrayAllocDescriptor Lib "oleaut32" Alias "SafeArrayAllocDescriptor" (ByVal cDims As Long, ByVal ppsaOut As Long) As Long
Public Sub CreateEmptyArray(ByRef vArray As Variant, _
ByVal eType As SAFEARRAY_VT)
Dim lpArray As Long
Call CopyMemory(lpArray, ByVal VarPtr(vArray) + 8, 4)
Call SafeArrayAllocDescriptor(1, lpArray)
Call CopyMemory(lpArray, ByVal lpArray, 4)
Call CopyMemory(ByVal lpArray + 4, eType, 4)
End Sub
Sub test()
Dim arr() As Integer
Call CreateEmptyArray(arr, VT_INTEGER)
Debug.Print UBound(arr) ' -1
End Sub
VARIANT 構造体のドキュメントを見ながら書いてみたけど、コードが合っているのかも正直よくわかりませんw
いちおう UBound で -1 が返ってくるようにはなりましたが、素直にフラグ管理しよう(´・ω・`)
やっぱりコレクションだよね
コレクションを使うと要素数は Count で取れるし、追加も削除もメソッドがあるのでサクサクできますよね。
でもコレクションはどんな型でも追加出来るから、使う側がミスしても実行時エラーが発生するまで分からない・・・。
そこで、Collection をラップして String 型専用のコレクションを作ってみます。
Private m_strings As Collection
Private Sub Class_Initialize()
Set m_strings = New Collection
End Sub
Public Function Add(ByVal node As String) As String
Call m_strings.Add(node)
Add = node
End Function
Public Sub Remove(ByVal index As Integer)
Call m_strings.Remove(index + 1)
End Sub
Public Property Get Count() As Long
Count = m_strings.Count
End Property
Public Property Get Item(ByVal index As Long) As String
Item = m_strings(index + 1)
End Property
このようにして作った自作のコレクションは組み込みの Collection と違って、要素にアクセスするのに Item プロパティを明示的に呼ばなくてはいけません。
VBA のプロパティ省略機能って罠のような気もするので、これはこれでいいかもしれないけど For Each で回せないのはすっごい微妙です。
Sub test()
Dim list As New StringCollection
Call list.Add("りんご")
Call list.Add("みかん")
Call list.Add("いちご")
Debug.Print list.Item(0) ' りんご
For Each item In list ' エラー
Debug.Print item
Next
End Sub
VBA のクラスにも属性が存在します
省略プロパティと For Each 対応は属性を使うことで解決できます。
でも属性は VBA のエディタ画面では設定できません。
一旦クラスモジュールをエクスポートして、*.cls ファイルをメモ帳などで開いて編集してください。
まずはクラスのデフォルトプロパティを Item プロパティにしてみます。
Public Property Get Item(ByVal index As Long) As String
Attribute Item.VB_UserMemId = 0
Item = m_strings(index + 1)
End Property
2行目の Attribute で始まる記述がデフォルトプロパティを指定する属性です。
意味は分からなくてもいいと思います。仕様ですw
次は For Each 構文に対応します。
C# のコレクションでおなじみの GetEnumerator() メソッドで取得できる IEnumerator と大体一緒で、要素を列挙するクラスを Collection が持っています。
COM の IEnumVARIANT というインターフェースで、このインターフェースをもったクラス自体を自作することも可能ですけど、LINQ の VBA 版でも自分で作ろうと思わない限り使い道はないと思います。
Public Function NewEnum() As stdole.IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = m_strings.[_NewEnum]
End Function
上の Attribute はこのメソッドが For Each 構文でイテレーターを返すメソッドだぞーっていう指定です(たぶん)。
下の Attribute はこのメソッドはインテリセンスに表示しないようにしてね、っていう C# の EditorBrowsable 属性と大体おなじです。
m_strings.[_NewEnum] という見慣れない記述ですが、VBA はプロパティ名を括弧で囲むことで構文違反になるような名前のメンバーでも呼び出せる仕組みがあるんですね。
オブジェクトブラウザで Collection クラスを見てみると非表示メンバの中に _NewEnum メソッドがあるのが分かります。
これで先ほどのサンプルコードを実行するとちゃんと For Each で列挙できるようになります。
上手く使えば読み取り専用のコレクションを作ったり、使いやすいライブラリも作れますので是非お試しください。