t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBAの標準モジュールはオブジェクトなのか

結論として、私は最近標準モジュールを「オブジェクト」と見做すようになったのだが、そのあたりの経緯と悩みをうだうだと書こうかと。

MSの公式見解によると、標準モジュールはオブジェクト モジュールではない

Excel VBAでコードを書ける場所は、Microsoft Excel Objects、フォーム、標準モジュール、クラスモジュールの4種類がある。Microsoft Excel Objectsのなかには更にGraph、Sheet、ThisWorkbookの3種類が存在する。

このうちMicrosoft Excel Objectsはその名が示すとおり、オブジェクトである。フォームもプロパティやメソッドを備えたオブジェクトである。クラスモジュールはオブジェクトの雛形として振る舞うモジュールである。

さて、長らく私はこれらのうち標準モジュールだけが、オブジェクトではない純粋なコーディング用の白紙なのだと認識していた。現に、Excel 2010 開発者用リファレンス(F1キーで表示されるヘルプ)にはオブジェクト モジュールについて以下の記述がある。

オブジェクト固有のコードを含むモジュール。たとえば、クラス モジュール、フォーム モジュールおよびドキュメント モジュールがあります。オブジェクト モジュールには、オブジェクトに関連付けられているコードが含まれます。オブジェクト モジュールの規則は、標準モジュールの規則とは異なります。

ということは、Microsoftの公式見解としては、標準モジュールはオブジェクト モジュールではないという訳だ。

いやでもちょっと待てよ。。「オブジェクト モジュール」の範疇に含められなかったというだけで、「オブジェクトではない」とは一言も書いてない。

同じことなんじゃないの?と思われるかもしれないがそのあたりは後述。

標準モジュールの振る舞いはオブジェクトに似ている

最近ちょっとどうもコイツ(標準モジュール)は怪しいなと思うようになってきた。

まず、他のモジュール名に続けてドットを入力するとパブリック変数やプロシージャが入力候補として表示されるが、これはオブジェクトと同じ挙動だ。もちろんこの挙動は知っていたけれど、たまたまオブジェクトと同じようにモジュールに対してもドットで候補が出るのだと思っていた。

しかし実際のところ、標準モジュールの正体は自由にプロパティやメソッドを実装できる白紙のオブジェクトなんじゃないかと思うようになってきた。SheetやThisWorkbookやフォームに比べるとやや特殊な一面を持っているものの、モジュールレベル変数に値を保持できたりモジュール名に続けてドットを入力することでメンバーの一覧が表示されたりといった振る舞いは考えてみればオブジェクトそのものである。

つまり標準モジュールというのはモジュール型の空インスタンスで、そのインスタンスに対し機能拡張するようにメソッド、フィールドを追加しているのではないだろうか。ちょうどJavaScriptのオブジェクトのように、プロトタイプベースのオブジェクトであると考えると分かりやすい。

ということは、モジュールレベル変数、パブリック変数と呼んでいるものは、実は標準モジュールオブジェクトのプライベートフィールド、パブリックフィールドなんじゃないかと。

そして、我々がいわゆるマクロと呼んでいるSubプロシージャは、標準モジュールオブジェクトのメソッドなんじゃないかと。

なぜまたこんなややこしいことを考えているかというと、そのように考えた方がVBAの言語としてのルールがシンプルになると思うからである。

人に説明するには、一貫性のあるシンプルなルールが欲しい

プログラミング言語を理解しようとするとき、場当たり的なアプローチと、根本的なアプローチの2つが考えられる。

私の考える場当たり的なアプローチとは、実現したいことに合わせて、この場合はこう、この場合は例外的にこう、という風に個別の事象に対してそれぞれルールを覚えていく方法のことを指す。

英語の学習でいえば、小難しい文法は置いといて、そのまま使えるフレーズを沢山覚えるという行為に近い。

英語でも覚えたフレーズはそのまま使えるように、プログラミングでも覚えたパターンを組み合わせてすぐに使えるプログラムが作れる。これが前者アプローチにおけるメリットである。

しかしこのように枝葉で覚えるアプローチでは覚えたことはできてもそれ以外に応用が利かない。また個別の事象は無限に存在するので、その都度覚えていてはキリがない。

やはりある程度学習が進んだら、根本的なアプローチに切り替えるのが良い。
私が考える根本的なアプローチとは、そのプログラミング言語で貫かれている原則を学ぶことである。

枝葉末端が違っても根っこは同じであるというところを理解し、シンプルなルールから無限に応用が利くようになるのが根本的なアプローチの目指すところである。

ルールは一貫性がありシンプルで、極力例外が存在しないほうが望ましい。そのほうが人に説明しやすく、覚える方も楽だから。事象をどう解釈するかによってルールは複雑にもシンプルにもなりえる。もちろん個別の事象をみていくと綺麗に一本化できるようなものではないが、可能な限り汎用性の高い解釈を見つけたい。

それで今回、標準モジュールもまたオブジェクトなのだという視点を持ち出してきたわけだ。

オブジェクトモジュール ≠ オブジェクト

VBAは一般的にオブジェクト指向言語とはみなされない。オブジェクト指向の三大要素といわれる「カプセル化」「継承」「ポリモーフィズム」のうち、「継承」をサポートしていないのも一つの理由だろう。
しかしオブジェクト指向とは単に「考え方」であり、オブジェクト指向言語でないからオブジェクト指向ができない訳ではない。
つまり言語機能面でオブジェクト指向をよりサポートしているものがオブジェクト指向言語と呼ばれているに過ぎない。

さて、これと同様の考えを冒頭の「標準モジュールはオブジェクト モジュールではない」に当てはめてみよう。
オブジェクトモジュールとは、標準モジュールに比べ、オブジェクト指向をよりサポートしたものであると考えてみる。
つまり標準モジュールはオブジェクト指向のサポート度合が弱いため「オブジェクト モジュール」とまでは呼べないが、それでもオブジェクトであることには変わりないと考えるのである。

要は「オブジェクト モジュール」と呼ぶかどうかは程度問題。どの程度オブオブしてるかという話かと。

標準モジュールのオブジェクトらしさ

まずはじめに、モジュールのプロパティにオブジェクト名と堂々と書かれていることが挙げられる。
f:id:t-hom:20161203041303p:plain

次に、モジュールのオブジェクト名につづけてドットを入力するとメソッドやフィールドが入力候補として表示されること。
f:id:t-hom:20161203041659p:plain

そして次のコードを標準モジュールに書いて、ClearX→IncrementXを5回→ShowXと順にF5キーで実行していった際に、ちゃんとイミディエイトウインドウに5が出るあたりがオブジェクトくさい。

Private x As Long

Sub ClearX()
    x = 0
End Sub

Sub IncrementX()
    x = x + 1
End Sub

Sub ShowX()
    Debug.Print x
End Sub

このxは通常、モジュールレベル変数として説明されるが、マクロの実行が終わっても値が保持され、ShowXしたときに参照できるという特性は、ModuleオブジェクトのPrivateフィールドが値を保持していると考えるのが自然に思える。

次にちょっとよくわからない挙動がある。
挿入メニューなどで標準モジュールを追加した場合と、マクロから「Modules.Add」で追加した場合と、見た目もExportした場合のファイルも全く同じなのだが、挙動が異なるのだ。

まず標準モジュール1つ(Module1だけ)の状態で、次のコードを張り付けて実行する。

Sub AddModuleSample()
    Modules.Add
End Sub

するとModule2ができる。
それからModule1に以下のコードを張り付けて実行する。

Sub ModuleCount()
    Debug.Print Modules.Count
End Sub

実際には2つのモジュールがあるにも関わらず、結果は1とでる。
手動追加したモジュールはカウントされず、Modules.Addによって追加されたモジュールだけカウントされるようだ。保存して開きなおすと挙動が統一されるかと思ったが、変わらない。

次に以下のコードを実行してみる。

Sub ModuleAddress()
    Dim m As Object
    Set m = Modules.Add
    Debug.Print ObjPtr(m)
End Sub

するとObjPtrでモジュールオブジェクトのアドレスが取得できる。
しかし不思議なことに連続実行しても同じアドレスが出力された。新しいモジュールは新しい領域に確保されるのではないのか。

以下のようにFor Eachでまわすと、それぞれ別のアドレスが取得できる。

Sub ModuleAddress()
    For Each m In Modules
        Debug.Print ObjPtr(m)
    Next
End Sub

しかし、For Eachで回せるのはマクロで追加したモジュールだけである。
やはり手動挿入したモジュールとは別物である。

標準モジュールのオブジェクトらしくなさ

標準モジュールは、名前で参照するとVariant変数に格納できない
コードで追加したモジュールがひとつでもあれば、次のコードは成功する。

Sub SetTestA()
    Set m = Modules(1)
    Debug.Print m.Name
End Sub

しかし、これはコンパイルエラー。

Sub SetTestB()
    Set m = Module2
End Sub

どうもオブジェクト名でそのまま参照させると変数に格納できないようだ。
シートなどとは挙動が異なる。

またオブジェクトモジュールと違って、WithEventsキーワードが使えない。

まとめ

これまでの私の解釈では、標準モジュールはオブジェクトではないというものだった。つまり「オブジェクト」という一貫したルールから外れた例外として見ていたのだ。
しかし今回は、標準モジュールもオブジェクトなのではと考え、その細かい挙動を例外と見做すことにした。

必ずしも本家・作り手の解釈が絶対的な正解だとは思わない。
たとえば、お茶を飲む想定で作られた茶碗が、お米を食べる用途に使われているように、解釈を変えることで便利になるのであれば、本家の説明を絶対視することもないのかなと。

ただ正直なところ、人に説明するときにどちらの解釈を採用するかまだ決めかねている。

プログラミングを人に説明する目的は、その言語を使えるようになってもらうためであるし、ルールに一貫性を持たせることができるなら本家MSや従来の説明から外れた解釈もアリかなと思うし、とはいえやはり標準モジュールをオブジェクトと見做すのはあまりに異端である気もするし。

しばらくは色々と悩もうと思う。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。