VBAではFileSystemObjectを使用することで簡単にWindowsのフォルダーを扱うことができる。
※FileSystemObjectを使用するには、VBEのツールメニューから参照設定で、MicrosoftScriptingRuntimeを参照しておく必要がある。
まず手始めに、特定フォルダ直下のファイルを列挙するサンプルプログラムを作成してみた。
Sub 特定フォルダ直下のファイルを列挙する() Dim FSO As New FileSystemObject Dim f As File For Each f In FSO.GetFolder("C:\users\thom").Files Debug.Print f.Name Next End Sub
このようにFor Eachを使って簡単に列挙できる。
フォルダも同様で、変数fの型をFolderに変更し、For Eachに渡すプロパティをSubFoldersにしてやるだけだ。
Sub 特定フォルダ直下のフォルダを列挙する() Dim FSO As New FileSystemObject Dim f As Folder For Each f In FSO.GetFolder("C:\users\thom").SubFolders Debug.Print f.Name Next End Sub
もっと下の階層を掘り下げるには
直下のフォルダを列挙することができたら、そのまた下の階層も列挙してみよう。
まず思いつくのはループの階層を増やすことだ。
Sub 特定フォルダ配下のフォルダと、そのまた配下のフォルダを列挙する() Dim FSO As New FileSystemObject Dim Subf As Folder, f As Folder For Each Subf In FSO.GetFolder("C:\users\thom\").SubFolders Debug.Print Subf.Name For Each f In Subf.SubFolders Debug.Print " "; f.Name Next Next End Sub
上のコードでは、子フォルダを変数Subfに入れ、さらに孫フォルダをfに入れている。
Debug.Printは直下のフォルダはそのまま出力し、孫フォルダはスペース2つ分インデントさせる。
これを実行すると…
おっと。変なエラーがでた。
デバッグモードで確認すると、SubfにC:\Users\thom\Application Dataが格納されている個所でエラーが発生しているようで、実際にExplorerで開こうとしてもエラーになった。
エラー処理を作りこむのは面倒なので、とりあえずResume Nextで囲む。
Sub 特定フォルダ配下のフォルダ配下のフォルダを列挙する() Dim FSO As New FileSystemObject Dim Subf As Folder, f As Folder On Error Resume Next '追加 For Each Subf In FSO.GetFolder("C:\users\thom\").SubFolders Debug.Print Subf.Name For Each f In Subf.SubFolders Debug.Print " "; f.Name Next Next On Error GoTo 0 '追加 End Sub
これで一応、列挙できるようになった。
しかしこれはあまりうまいやり方ではない。フォルダの階層ごとにループの階層を増やしていてはキリがないからだ。
ループできちんとやろうと思うと、スタックを使っていろいろとややこしい処理をしなければならない。
再帰とは
このようなケースには再帰(さいき)が適している。
プロシージャが自分自身を呼び出すことを再帰という。
フォルダーを扱う前に、シンプルな再帰プログラムで頭を慣らしておこう。
再帰の例としてはよく数学の階乗関数が使われるが、それでも難しいと感じる方も居ると思うので、さらにシンプルなものを用意した。
予め断っておくと、次のプログラムは再帰がどういうものか知ってもらうためだけに作成したので全く実用性はない。
Sub スタート() Call 再帰テスト(1) End Sub Sub 再帰テスト(n) Debug.Print n If n < 3 Then 再帰テスト (n + 1) End If End Sub
まず「スタート」プロシージャは「再帰テスト」プロシージャに引数1を渡して呼び出す。ここまでは普通の呼び出しである。
再帰テストプロシージャは、nが3未満だった場合、引数n+1をつけて自分自身を呼び出す。
このプログラムを実行するとイミディエイトウインドウに1 2 3の順で出力して終了する。
自分を呼び出すというのがイメージしにくいかもしれないが、普通の関数呼び出しに置き換えるとこうなる。
Sub スタート() Call 再帰テストA(1) End Sub Sub 再帰テストA(n) Debug.Print n If n < 3 Then 再帰テストB (n + 1) End If End Sub Sub 再帰テストB(n) Debug.Print n If n < 3 Then 再帰テストC (n + 1) End If End Sub Sub 再帰テストC(n) Debug.Print n If n < 3 Then 'この時点でnは3なので、実行されない。 Debug.Print "←実行されない。" End If End Sub
スタートが再帰テストAを呼び、AがBを呼び、BがCを呼ぶ。
呼び出しのたびに、受け取ったnに1を加えた値を次のプロシージャの引数にしているので、Cが呼ばれた時点で引数は3になっている。結局プロシージャCのDebug.Printは実行されない。
再帰テストCが終了すると呼び出し元である再帰テストBに戻り、Bも終了してAに戻り、Aが終了してスタートに戻り、それからマクロ全体が終了する。VBAインタプリターから見たら他のプロシージャーを呼び出すのも自分のプロシージャを呼び出すのも同じことである。
再帰でフォルダー階層を辿る
さて、いよいよフォルダー階層を辿るコードである。
Sub ViewFolders() Dim FSO As New FileSystemObject RecExplorer FSO.GetFolder("C:\Users\thom\documents"), 0 End Sub Sub RecExplorer(f As Folder, n As Long) Debug.Print String(n * 2, " "); f.Name Dim Subf As Folder On Error GoTo ErrCheck For Each Subf In f.SubFolders '★ここで再帰呼び出し RecExplorer Subf, n + 1 Next On Error GoTo 0 Exit Sub ErrCheck: '実行時エラー「書き込みできません。」(70)は想定内なので無視。 'それ以外はエラーを発生させる。 If Err.Number <> 70 Then Err.Raise Err.Number End If End Sub
まずViewFoldersがRecExplorerを呼び出し、そこでサブフォルダーの一つずつを引数として再帰呼び出しが実施される。
RecExplorerに渡すフォルダ「f」のSubFoldersが一つもなければFor Eachの要素が0になるので、再帰呼び出しは実行されず、呼び出し元に戻り、次のフォルダーが処理される。
※先ほどの再帰コードではnが処理終了の条件を左右する重要な変数であったが、今回のnはただインデントの深さを決めるだけの変数なので終了条件とは関係ない。
この処理を図で描くと、以下のようになる。
赤が再帰呼び出し、青が呼び出し元に戻る処理である。
このようなツリー構造はWindowsのフォルダー以外にも、Outlook VBAのメールフォルダーやレジストリ、TreeViewコントロールなど、色々と使いどころがあるので再帰は是非マスターしたい。
再帰の欠点
再帰呼び出しはスマートであるが、欠点もある。
ループと違って呼び出しのたびにローカル変数がメモリに積み重なっていくため、再帰呼び出しの階層が深すぎる場合はメモリのスタックと呼ばれる領域が枯渇してエラーになる。
例ではローカル変数がnしか無い為、4994まで呼び出しができたが、ローカル変数が増えるとそれだけオーバーフローしやすくなる。
ただ、ツリー構造を扱うシンプルな処理では、この欠点はほとんど問題にならない。
先ほどの図で説明しよう。
一番深い階層の「フォルダ1」を扱っているとき、ルートフォルダ、フォルダA、フォルダIはそれぞれメモリに残っている。
しかし呼び出しを終えてフォルダAの処理まで戻ってくると、フォルダIとフォルダ1はメモリから解放できる。
従って、ツリー構造を扱う際に再帰は有用である。
サイトによっては、再帰はオーバーフローの危険があるので使うべきではないという意見もあるが、よほどフォルダがネストしていなければ問題ないと考えている。
再帰のスタックオーバーフロー実験
試しに以下のようにかなりネストしたフォルダ構造を作ってみたが、問題なく実行できた。
試験的に呼び出しの途中にスタックに重たいデータを乗せてみたところ、cフォルダのネストの途中でクラッシュしてしまった。
50000文字の固定長文字列が1000要素集まった配列である。
しかし実際そこまで重たいデータを扱うケースはあまりない。
よほど特殊なケースでない限りは問題ないと思う。
さらに、先ほどの無限再帰をもう一度実験してみた。
さきほどは4994回でスタック不足になったのに対し、今回は6442回。
これは現在のスタック空き容量によって変化するということ。
この不安定さも商用ソフトのプログラミングで再帰が避けられる要因である。
また、無限再帰内で100個の変数を宣言してみたところ、呼び出し可能数は545まで低下した。
ちなみにこれはVariant型の場合で、Integer型100個なら2000回以上の呼び出しが可能だった。
スタック領域の呼び出し可能数が不定で、変数を作りすぎると呼び出し回数が減るといっても、フォルダの階層なんてたかだか10階層程度のものだろう。
検証の結果、普通のツリー構造に対して再帰を使う分には全く問題ないことが分かった。