1.3 プロシージャを使ってみよう

一般的には関数と呼ばれますが、VBAのヘルプではプロシージャ(procedure:処理手順)と呼んでいます。第2章ではMacro2()を改修してプログラムを作りました。その処理を改めて見ると、繰り返しに関する部分とセルの色を塗る部分に分けられます。色を塗る処理は繰り返しとは関係なく単独で実行する場合もあるでしょうし、その前の段階で完成している処理です。このような処理はブラックボックスにしておくとプログラムが見やすくなって便利です。また、他の場所で同じ手順が必要になったとき再利用できるようにしておくと、動作が確定しているのでテストの手間が省けます。このようにまとまった処理手順を分けて記述する方法をVBAではプロシージャ、一般的には関数といいます。VBAにはSub プロシージャとFunction プロシージャがあります。


1.3.1 Sub プロシージャ

1.2.3 処理を繰り返してみようで」作ったMacro2()の流れ図をもう一度見てみましょう。繰り返しの条件が満たされている間、セルを塗るという処理を繰り返しています。セルを塗るという処理は、繰り返しを作る時点では完成していました。こういう完成した処理部分は、プロシージャという形でまとめておくとプログラムが分かりやすくなります。加えて、すでに完成した部分なので、更に繰り返し処理など関係のない処理を改修した場合、プロシージャの中には変更がないので、プロシージャ内部に入ってデバッグする必要がありません。開発の手間が省けそうです。

1.2.3で作ったMacro2()の流れ図を書き変えてみました。プロシージャを使わない場合と使った場合の違いを理解してください。
図1.2.3-3図1.2.3-3  図1.3.1-1図1.3.1-1

図1.3.1-1の流れ図をもとに、1.2.3で作ったMacro2()をプロシージャを利用して作り変えたのが、以下のプログラムです。流れ図と見比べて、プログラムの流れを把握してください。

呼び出し元プロシージャ:

Sub Proc3()
'
' Proc3
'
    Dim c_ix As Long    ' index for column
    Dim r_ix As Long    ' index for row
'
    Dim i_counter As Integer    ' ループカウンタ
    Const C_start = 1           ' ループカウンタの初期値(定数)
    Const C_end = 7             ' ループカウンタの終了値(定数)
'
    Const C_rno = 3              ' 塗りつぶす行番号
'
    For i_counter = C_start To C_end
        c_ix = i_counter    ' 列の指標にi_counterを代入
        r_ix = C_rno         ' 行の指標にr_no(定数)を代入
        
        
        Call Sub3(r_ix, c_ix)    ' セルを塗る
    Next
End Sub

呼び出されるプロシージャ:

Sub Sub3(r_ix As Long, c_ix As Long)
'
' paint_cell:指定されたセルを指定された色で塗りつぶす
'
    Dim iro             ' 塗りつぶしの色

    If c_ix Mod 2 = 0 Then
        iro = 65535     ' 黄色
    Else
        iro = 5296274   ' 緑色
    End If

    Cells(r_ix, c_ix).Select            ' 指定されたセルを選択
    Selection.Interior.Color = iro      ' 選択したセルを指定の色で塗りつぶす
End Sub

呼び出し元の繰り返し処理の中で「セルを塗る」プロシージャを呼び出しています。括弧の中の(i_counter, i_counter)が呼び出されたプロシージャに渡されます。

    Call paint_cell(i_counter, i_counter)   ' セルを塗る

呼び出されたプロシージャでは、渡された変数(i_counter, i_counter)を(r_ix As Long, c_ix As Long)という名前で受け取っています。「As Long」というのは「長整数型(Long)としてデータを受け取ります」ということです。だから、呼び出し元のi_counterもLong型で宣言してあります。呼び出し元から呼び出される側に渡されるデータの形式と数、順番は同じでなければなりません。
呼び出されたプロシージャでは、渡された値を自分で宣言した名前で参照できますから、呼び出し元で名前を変更しても呼び出されたプロシージャの方では名前を変更する必要がありません。ただし、呼び出されたプロシージャから呼び出し元の変数を書き換えることはできません(これを実現するには「参照渡し」という方法を使いますが、ここではふれません)。

    Sub paint_cell(r_ix As Long, c_ix As Long)

ちなみに、長整数型 (Long)というのは、4 バイトの整数のことです。10進数で -2,147,483,648 から 2,147,483,647の範囲を表せます。プロシージャpaint_cellの中でもiroという変数を宣言していますが、データ型が省略されています。Variant型というデータ型になります。

    Dim iro             ' 塗りつぶしの色

プログラムの流れが理解できたら実際に動かしてみてください。デバッグ機能を使って動きを確認すると理解が深まるでしょう。

1.3.2 Function プロシージャ

Sub プロシージャと似ていますが、処理した結果を呼び出し元に返せる Function プロシージャというのもあります。ヘルプをFunctionをキーワードとして検索すると以下のような関数の例が紹介されています。

呼び出す側:

Sub Main()
    Dim temp As Long
    
    temp = Application.InputBox(Prompt:= _
        "華氏で温度を入力してください。", Type:=1)
    MsgBox "温度は摂氏 " & Celsius(temp) & " 度です。"
End Sub

呼び出される側:

Function Celsius(fDegrees As Long) As Long
    Celsius = (fDegrees - 32) * 5 / 9
End Function

呼び出す側のプロシージャmain()で使ってある Application.InputBox によって、利用者が入力した数値が、変数tempに代入されます。

    temp = Application.InputBox(Prompt:= _
        "華氏で温度を入力してください。", Type:=1)

Application.InputBoxは、アプリケーション即ち Excel に用意されている利用者と対話するためのメソッドです。Promptで指定している「華氏で温度を入力してください。」というメッセージをダイアログボックスに表示して、データを入力します。「Type:=1」と指定しているのでデータは数値として扱われます(詳細はヘルプを参照してください)。
「Prompt:=」後の行末にある「_(アンダースコア)」は継続行の印です。1行が長くなって見にくいようなときに使います。

呼び出す側で使ってある MsgBoxによって、温度を表示します。引数文字列の中に埋め込まれた関数 Celsius(temp) で、Celsius プロシージャを呼び出しています。

    MsgBox "温度は摂氏 " & Celsius(temp) & " 度です。"

MsgBoxは、ダイアログボックスにメッセージ(引数文字列)を表示する関数です。 また、「&(アンパサンド)」は、文字列をつなぐ演算子です。この MsgBox 関数では、「温度は摂氏 」と Celsius(temp)の値、そして「 度です。」という3つの文字列をつないで1つの文字列として表示します。Celsius プロシージャは Long 型の数値を返してきますが、VBA での文字列連結演算では自動的に文字列型に変換されるようになっています。

MsgBox関数の引数文字列の中に組み込まれている Celsius プロシージャは、Mainから与えられたtemp変数の値をfDegreesというLong型の変数名で受取っています。摂氏に変換する計算をして、結果をプロシージャ名 Celsius に代入することで呼び出し元に値を返しています。プロシージャが返す値のデータ型は、Function ステートメントの最後で「Long」として宣言してあります。

Function Celsius(fDegrees As Long) As Long
    Celsius = (fDegrees - 32) * 5 / 9
End Function

論より証拠。実際に動かして確認してみましょう。

1.3.3 変数の適用範囲を試してみよう

適用範囲は、スコープとも呼ばれ、「プログラム内で、変数、プロシージャ、オブジェクトが有効な範囲」のことです。詳しいことは、ヘルプを「適用範囲」で検索してみてください。

プロシージャでは、変数の受渡しに引数と戻り値を使いました。それは、プロシージャ内部で定義したプライベート変数は、他のプロシージャから読んだり書き換えたりすること、すなわち参照ができないからです。
図1.3.3-1図1.3.3-1
この例では、Option Explicitが宣言されているので、プロシージャproc2()では、エラーになってしまいます。次のソースのようにOption Explicitを省略すると実行できますが、変数hensuはプロシージャproc1()とproc2()とでは別の領域を表します。proc2()の方は初期化されていませんから空白のメッセージボックスを表示することになります。

Sub main()
    
    proc1
    proc2
    
End Sub

Sub proc1()
    Dim hensu
    
    hensu = "変数"
    MsgBox hensu

End Sub

Sub proc2()
    
    MsgBox hensu

End Sub

また、プライベート変数は、プロシージャの処理が終わって呼び出し元に処理が帰ると、領域もろともなくなってしまい、次に呼び出したときには、改めて初期化しなければなりません。このことは、次のようなプログラムを作ってみると理解できるでしょうか。

Sub main()
    
    proc1 (5)
    proc1 (7)
    
End Sub

Sub proc1(hikisu)
    Dim hensu
    
    hensu = hikisu + hensu
    MsgBox hensu

End Sub
プロシージャproc1()が最初に呼ばれたとき、変数hensuは初期化されていないので引数で渡された値が表示され、呼び出し元に帰るときにはhensuの値は5の筈です。2回目に呼び出されたときに5が入ったままだと12が表示される筈ですが、実際には変数の領域は新しく作られるので、引数で渡された7が表示されます。

複雑なプログラムを作るようになってくると、引数や戻り値だけでのデータの受け渡しでは不便です。

実は、呼び出されたプロシージャから呼び出し元の変数を変更できるようにプログラミングすることもできます。

その一つが、プロシージャの外で変数を定義する方法です。これをモジュールレベルの変数と言い、モジュールの中にあるプロシージャのどこからでも参照できます。モジュールレベルの変数を使うと、プロシージャ間のデータの受け渡しもできてしまいます。
図1.3.3-2図1.3.3-2

便利なので、初心者は多用しがちです。しかし、

プロシージャをたくさん作らなければならないプロジェクトで、安易にモジュールレベルの変数を多用すると、ある変数を使用している箇所がモジュール全体に分散してしまいます。

図1.3.3-2を例にとると、プロシージャproc1()を改修して変数hensuに保存する値の単位を変更したとします。例えば、g(グラム)単位で保存していたけれど都合でKg(キログラム)単位に変えたというような場合、proc2()の処理も変更しなければなりません。

このように、プログラムを一か所改修すると、改修したプロシージャから参照しているモジュールレベルの変数を洗い出し、更にそれらの変数を参照しているプロシージャを洗い出して、テストしなければなりません。デバッグや改修後のテストの範囲が広がり、時間もかかり、漏れも発生し安くなってしまいます。

モジュールレベルの変数は便利ですが、多用するとこのような面倒を引き起こしてしまうことになります。プログラミングには、プログラマの節度が要求されます。

こういった不具合を避けるために、できるだけ変数の受け渡しをしないで済むように設計しましょう。モジュールレベルの変数は必要最小限にしましょうということになっているのです。また、プロシージャの分け方も、できるだけ引数を必要としないような分け方を考えます。

なかなか難しい問題です。ちょっとしたプログラミングでしたら気軽に作ってよいと思います。数人で開発するような場合や、誰かに補修を引き継いでもらうようなプログラムを作るようになったときには思い出してください。