VBAではSubプロシージャの他にFunctionプロシージャを使用することができます。
SubとFunctionの違いは簡単に言うと戻り値の有無です。
関数の使い方
SubもFunctionも関数(Subは戻り値のない関数、Functionは戻り値のある関数)といって差し支えのないところであるかもしれませんが、VBAでは特にFunctionを指して関数と呼びます。このページでは以降SubもFunctionも関数と呼びます。
Subプロシージャは呼び出されると何らかの処理を行いますが、Functionプロシージャは何らかの値(戻り値)を呼び出し元に返すことを特徴とします。
簡単な例として、Cells(4,3)に「晴れ」、Cells(5,4)に「曇り」と入力されているシートで、これらの値を変数cellValueに格納してそれぞれ表示するコードを見てみましょう。SubプロシージャとFunctionプロシージャの違いに注意してください。
'行と列を指定し、セルの値を取得するプログラム
Option Explicit
Sub getCell()
Dim rowNum As Long '行番号
Dim columnNum As Long '列番号
Dim cellValue As Variant '取得したセルの値
rowNum = 4 '行を指定
columnNum = 3 '列を指定
Call getBySub(rowNum, columnNum, cellValue) 'getBySubを呼び出し
msgbox cellValue '①
cellValue = getByFunction(rowNum, columnNum) 'getByFunctionを呼び出し
msgbox cellValue '②
End Sub
Sub getBySub(ByRef R, ByRef C, ByRef V) 'セルの値を取得し、行列を加算
V = Cells(R, C) '参照型の変数VにCells(R, C)の値をセット
R = R + 1 'Rの値を変更
C = C + 1 'Cの値を変更
End Sub
Function getByFunction(ByVal R, ByVal C) 'セルの値を返す
getByFunction = Cells(R, C) '戻り値にCells(R, C)をセット
End Function
Subプロシージャを呼び出す場合は『Call Subの名前(引数)』や『Subの名前 引数』のように書きましたが、Functionプロシージャを呼び出す場合、『左辺=Functionの名前(引数)』の形で記述します。処理を実行するだけでなく、左辺にFunctionの実行結果を渡すという点が関数と呼ばれる所以です。数学でいうところの『y=f(x)』と同じです。
Function内の処理の結果、自分自身に対して値を設定することで、処理が終了した時にその値が呼び出し元に対して戻り値として渡されます。
上のコードを実行すると以下のようになります。
…①↓
…②最初のgetBySub呼び出しではcellValueを参照型の変数Vとして渡しており、呼び出し先でVの値を変更することにより、呼び出し元のcellValueの値も変更されています。また、同じく参照型のRとCにそれぞれ1を加えています。
ここでは引数を参照型(ByRef)で渡したために、関数内での変数の変更が呼び出し元の変数にも反映されていますが、引数を値型(ByVal)で渡した場合は関数内で変数を変更したとしても呼び出し元の変数には影響しません。
次のgetByFunction呼び出しではcellValue変数は引数として渡さずに、getByFunctionの結果として返された関数の戻り値を左辺のcellValueに代入しています。ここでは先に呼び出したgetBySub内で行番号と列番号が1ずつ加算されているため、セル(5,4)の値を取得することができます。
関数の戻り値の型
ところで、上の例でgetByFunction関数がどのような戻り値を返すかが一見して判断できません。
セルの値というのは文字列かもしれないし小数かもしれません。cellValue変数をvariant型で宣言することによりどんな戻り値でも受け取ることができますが、呼び出し元が仮に整数の戻り値を期待していた場合、プログラムがうまく動きません。
厳密に整数型の戻り値を返す関数を定義したい場合、以下のように関数の戻り値の型を指定することができます。というよりもむしろ、よほどのことがない限り関数の型は明示すべきです。
'行と列を指定し、セルの値(整数)を取得するプログラム
Option Explicit
Sub getCell2()
Dim rowNum As Long '行番号
Dim columnNum As Long '列番号
Dim cellValue As Long '取得したセルの値
rowNum = 4 '行を指定
columnNum = 3 '列を指定
cellValue = getByFunction2(rowNum, columnNum) 'getByFunction(整数)を呼び出し
msgbox cellValue '③
End Sub
Function getByFunction2(ByVal R, ByVal C) As Long 'セルの値を整数で返す
If IsNumeric(Cells(R, C)) Then'セルの値が数値の場合
getByFunction2 = Cells(R, C) '戻り値にCells(R, C)をセット
Else
msgbox "セルの値が整数ではありません。", vbExclamation
getByFunction2 = -1 'エラーを示す仮の値をセット
End If
End Function
getByFunction2は整数型の関数として定義したため、整数以外を返すことができません。ここではセルの値が整数ではなかった場合に一旦エラーメッセージを表示し、-1を返すことにしています。この時の-1のように、エラーが発生した場合に関数から返される仮の値をエラーコードと呼びます。ここでは-1自体が整数であり、正常に取得できた値と区別するために関数内で警告を表示させています。
このように関数の型を限定することを最初は窮屈に感じるかもしれませんが、予期せぬ動作を未然に防ぐことができるというメリットがあります。
VBAの説明で初心者に対してvariant型の多用を推奨しない場合が多いのはメモリや実行速度の面だけでなく、意図しない型でも格納できてしまうことにより明確なエラーを起こさず見つけづらいバグを組み込んでしまうリスクを抑えるという理由が大きいのです。
型に関する注意点
関数getByFunction2の戻り値を整数(LONG型)にしましたが、セルの値はIsNumeric関数でチェックしたのみです。ということは、セルの値が数値ではあっても小数である可能性があります。ですが、仮にセルの値が9.8であってもエラーは発生しません。
VBAでは整数型の変数に小数を格納しようとした場合、特にエラーを発生させずに格納しようとします。上の例で9.8を整数型に格納しようとすれば10が格納されます。整数型に文字列型を格納しようとすればエラーが発生しますが、小数→整数間のこの親切(?)機能には注意する必要があります。
上のコードでは数値チェックの後に小数か否かをチェックするようにした方がベターです。数値が小数であることは例えば『if a - fix(a) > 0』のような方法で調べることができます。
引数の渡し方による動作の違い
条件分岐の項で少し触れましたが、引数の渡し方によって意図しない実行結果が得られるケースを紹介します。
以下のコードは、変数aに100をセットした状態でtest関数に引数としてaを参照渡しして、test関数内でaの値を変更し、変更されたaの値を表示しようとしたプログラムです。①②③と3つの書き方ができますが、実行結果はどうなるでしょうか。
'記述の仕方による関数の挙動の違い
Sub reftest()
Dim a As Long
a = 100
test a '①
msgbox a
a = 100
Call test(a) '②
msgbox a
a = 100
test (a) '③
msgbox a
End Sub
Sub test(ByRef b)
b = 1
End Sub
Subプロシージャを呼び出す際、①関数名 引数、②Call 関数名(引数)、③関数名 (引数)のように書くことができます。
test関数は参照型でbを受け取るため、一見してどれも引数として渡されたaの値がtest関数内で変更されると考えられるかもしれません。
結論から言うと、③ではそのようになりません。③ではtest関数に対して値型の変数を渡したものとして処理が行われるようです。これは特殊なケースで、引数が複数になると③のように書くことはできません。
あえてこのように記述することが有効なケースが思いつきづらく、かえって余計なバグを混入させる原因になる恐れが高いと考えられるため、エラーにならず実行できるからといって③のように記述することは避けることをお勧めします。