今回はVBAのFor Eachステートメントが仕様上、出力順を保証しない理由についてC#で作った自作のコレクションを使って説明してみようと思う。
さて、普段ならコードから紹介して後で説明に入るスタイルを取るが、今回はちょっとややこしいのでいきなり本題の説明から入ろうと思う。
前提知識
Excel VBAは、VBA言語によってExcelオブジェクトを操作することをいう。初心者のうちはVBAの命令もExcelオブジェクトのメソッドも一緒くたに考えてしまうけれど、VBAはVBA、ExcelはExcelなので、この2つが頭の中できちんと分離できていることが前提となる。
説明
VBAのFor Eachはその仕様上、出力順を保証していない。
とはいっても、たとえば以下のような選択範囲に対し、
次のコードを実行してみると、
Sub hoge() For Each x In Selection Debug.Print x Next End Sub
常に、1,2,3,4,5,6,7,8,9の順に出力される。
にも関わらず、For Eachが出力順を保証しないとはどういうことか。
For Each「が」保証していないというところがミソ。
VBAのFor Each文はオブジェクトに対して、「次のアイテムちょうだい」っていう命令を出す。
このとき、何を次のアイテムとして差し出すかはオブジェクトの実装によるということだ。
つまり順序よく綺麗に出力されるのはVBAがやってるんじゃなくて、ExcelのRangeオブジェクトがそういう風にアイテムを返しているからだ。
じゃあExcel側が保証してるかというと、それはそれで特にドキュメントが無いのでやはり将来その仕様が絶対に変更されないとは言えない。
さて、説明はここまで。
ここからはこの説明を裏付ける証拠として、登録したのとは逆順にFor Eachで出力されるコレクションを作ってみようと思う。
実演
※注意:今回は手探りの実験になったので、読者の皆さんの環境において動作を保証するものではありません。もし真似される場合はくれぐれも自己責任でお願いします。
Visual Studioの操作
今回はVisual Studio Community 2017を使用した。
起動するとスタートページが開くので新しいプロジェクトの作成をクリック。
Visual C#のクラスライブラリ(.NET Standard)を選択して、名前はそのままでOK。
ソースコード「Class1.cs」の編集画面になるので全部テキストを削除する。
以下のコードを張り付け。
using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; namespace ThomSample { [ComVisible(true)] [Guid("A4496B44-8A89-4ABB-A6F0-91B81D56A1C9")] [InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface IContraryCollection : IEnumerable { new IEnumerator GetEnumerator(); void Add(string str); } [ComVisible(true)] [Guid("338178CD-EAFB-4AD6-B8AF-27AB8E50CB24")] [ProgId("ThomSample.ContraryCollection")] [ClassInterface(ClassInterfaceType.None)] public class ContraryCollection : IContraryCollection { public List<String> list; public ContraryCollection() { list = new List<string>(); } public IEnumerator GetEnumerator() { for (int i = list.Count-1; i >= 0; i--) { yield return list[i]; } } public void Add(string str) { list.Add(str); } } }
ファイルメニューからすべて保存。
ビルドメニューからClassLibrary1のビルドを実行。
左下のステータスバーにビルド正常終了とでたらOK。
ソリューションエクスプローラーの余白で右クリックしてメニューからエクスプローラーで開くを選択。
\bin\Debug\netstandard2.0の順に開くとClassLibrary1.dllが入っているので、デスクトップ等の分かりやすい場所に配置する。
dllのレジストリ登録
作成したdllはCOMとして登録する必要があるが、これにはVisual Studioについてくるregasmというコマンドツールを使う。
このコマンドツールを使うには開発者コマンドプロンプトを使用する必要があり、さらに管理者モードで起動する必要がある。
まず、Windows10の場合は以下のパスに開発者コマンドプロンプトへのリンクがあるのでパスを開く。
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2017\Visual Studio Tools
右クリックして管理者として実行する。
cdコマンドでClassLibrary1.dllを置いた場所に移動する。
コマンドで regasm /codebase ClassLibrary1.dll を実行する。
うだうだ文句を言われるが、ちゃんと管理者で実行できてれば一応成功する。
これでレジストリ登録もOK。
ちなみにregasm /u ClassLibrary1.dll で登録解除できる。
VBAからの利用
ここからようやくVBA。
適当にブックを作成して標準モジュールに次のコードを張り付けて実行する。
Sub hoge() Set c = CreateObject("ThomSample.ContraryCollection") c.Add "a" c.Add "b" c.Add "c" c.Add "d" c.Add "e" For Each x In c Debug.Print x Next End Sub
すると、For Each文で、登録したのとは逆順に、e, d, c, b, aと出力される。
ちょっとC#の知識がないとややこしいのでうまく説明できないけれど、逆順に返している部分はこちら。
public IEnumerator GetEnumerator() { for (int i = list.Count-1; i >= 0; i--) { yield return list[i]; } }
yield returnはEnumeratorによってアイテムが要求される度に値を返す性質があるようだ。
for文とyield returnによってリターン用に値がストックされてるイメージかな。
それでFor Eachが実行されると値が要求するたびに出力される。このとき逆順なのは、for文を回す際にiをマイナスしてるから。
私自身、あんましC#に詳しくないので上手く説明できないのが残念だけど雰囲気だけでも伝わればと思う。
検証が終わったらregasm /u ClassLibrary1.dll で登録解除も忘れずに。
まとめ
これで出力順をコントロールしてるのはFor Eachではないことが明白になった。
繰り返しになるが、For Eachで次に何のアイテムが出力されるかは、オブジェクト次第ということだ。
参考
C#でGetEnumeratorを実装したクラスをCOMとして公開する方法について参考にさせていいただいた記事。
Exposing an Enumerator from Managed Code to COM. - limbioliong
上記ではList型が持つEnumeratorを返している。
以下はC#でGetEnumeratorを自分で実装する方法
ledsun.hatenablog.com
既存型のEnumeratorではなくて自分で実装したかったのでこちらを参考にさせていただいた。
以下はGUID(UUID)の生成に使ったツール
GUID生成ツール
GUID(UUID)とは何ぞやというのは以下
e-words.jp