Sub と Function の違い
VBA でコードを書いていくと、「処理をまとめて名前を付けて再利用したい」という場面が必ず出てきます。VBA では処理のまとまりをプロシージャと呼び、大きく分けて Sub プロシージャと Function プロシージャの2種類があります。
この2つは似ているようで明確な違いがあり、使い分けを理解することはVBAプログラミングの基礎として非常に重要です。この記事では、Sub と Function の違いから、引数の渡し方(ByRef / ByVal)、スコープの制御まで詳しく解説します。
Sub と Function を理解するメリット
プロシージャの基礎を正しく理解することで、以下のようなメリットがあります:
- コードの再利用: 同じ処理を何度も書かずに済む
- 可読性の向上: 処理に名前を付けることで、何をしているかが一目でわかる
- 保守性の向上: 修正が必要なとき、1箇所を直すだけで済む
- テストのしやすさ: 処理を小さな単位に分割することで、個別にテストできる
- ワークシート関数としての利用: Function はセルの数式から呼び出せる
Sub プロシージャ
基本構文
Sub プロシージャは、一連の処理を実行するための最も基本的なプロシージャです。戻り値を返しません。
' 構文
Sub プロシージャ名()
' 処理内容
End Sub
基本的な使用例
Sub SayHello()
MsgBox "こんにちは!"
End Sub
Sub FormatHeader()
With Range("A1")
.Font.Bold = True
.Font.Size = 14
.Interior.Color = RGB(68, 114, 196)
.Font.Color = RGB(255, 255, 255)
End With
End Sub
Sub の特徴
- マクロとして直接実行できる(Alt + F8、ボタンへの割り当てなど)
- 戻り値を返さない(処理を実行するだけ)
- ワークシートの数式から呼び出すことはできない
VBA で「マクロ」と呼ばれるものは、基本的に引数なしの Public Sub プロシージャのことです。Alt + F8 のマクロ一覧に表示されるのは、この条件を満たすプロシージャだけです。
Function プロシージャ
基本構文
Function プロシージャは、処理を実行して結果(戻り値)を返すプロシージャです。
' 構文
Function 関数名() As 戻り値の型
' 処理内容
関数名 = 戻り値
End Function
基本的な使用例
' 消費税込みの金額を計算する関数
Function CalcTax(price As Long) As Long
CalcTax = price * 1.1
End Function
' 使用例
Sub TestCalcTax()
Dim result As Long
result = CalcTax(1000)
Debug.Print result ' => 1100
End Sub
戻り値の返し方
VBA の Function では、関数名に値を代入することで戻り値を返します。他のプログラミング言語の return に相当します。
Function GetFullName(firstName As String, lastName As String) As String
' 関数名に値を代入 = 戻り値を返す
GetFullName = lastName & " " & firstName
End Function
Sub TestGetFullName()
Debug.Print GetFullName("太郎", "田中") ' => 田中 太郎
End Sub
Function 内で関数名に値を代入しないと、戻り値は既定値(数値型なら 0、文字列型なら ""(空文字)、Boolean なら False)になります。予期しない結果の原因となるため、必ず関数名に値を代入しましょう。
' × 戻り値を返し忘れている
Function BadAdd(a As Long, b As Long) As Long
Dim result As Long
result = a + b
' BadAdd = result を忘れている!→ 常に 0 が返る
End Function ワークシート関数としての利用
Function プロシージャは、Excel のセルの数式から**ユーザー定義関数(UDF)**として呼び出すことができます。
' 標準モジュールに記述
Function FullWidthToHalf(text As String) As String
Dim i As Long
Dim result As String
result = ""
For i = 1 To Len(text)
Dim char As String
char = Mid(text, i, 1)
' 全角数字を半角に変換
If char >= "0" And char <= "9" Then
result = result & Chr(Asc(char) - &HFF10 + Asc("0"))
Else
result = result & char
End If
Next i
FullWidthToHalf = result
End Function
上記の関数を定義すると、セルに =FullWidthToHalf(A1) と入力して使えます。
ワークシートから呼び出す Function では、セルの書式変更やメッセージボックスの表示など、ワークシートに副作用を与える操作はできません。値を計算して返すことに専念しましょう。
Sub と Function の違い
比較表
| 特徴 | Sub | Function |
|---|---|---|
| 戻り値 | × なし | ○ あり |
| マクロとして直接実行 | ○ 可能 | △ 可能だが通常はしない |
| ワークシートの数式から呼出 | × 不可 | ○ 可能(UDF) |
| 主な用途 | 処理の実行 | 値の計算・変換 |
| Call文での呼び出し | ○ | ○(戻り値は無視される) |
使い分けの基準
Sub が適しているケース:
- セルの書式を変更する
- メッセージボックスを表示する
- ファイルの読み書きを行う
- シートの追加・削除を行う
- 他のマクロを呼び出して一連の処理を実行する
Function が適しているケース:
- 値を計算して結果を返す(税込金額、文字列変換など)
- 条件に基づいて True/False を返す(バリデーション)
- ワークシート関数として使用する
- 他のプロシージャに計算結果を渡す
' ○ Sub が適切:セルの書式変更(副作用がある処理)
Sub HighlightCell(targetRange As Range)
targetRange.Interior.Color = RGB(255, 255, 0)
targetRange.Font.Bold = True
End Sub
' ○ Function が適切:値の計算(結果を返す処理)
Function IsAdult(age As Long) As Boolean
IsAdult = (age >= 18)
End Function
' 使用例
Sub Main()
If IsAdult(20) Then
Call HighlightCell(Range("A1"))
End If
End Sub
引数の基礎
引数とは
**引数(ひきすう)**とは、プロシージャに渡すデータのことです。引数を使うことで、同じプロシージャを異なるデータで再利用できます。
' 引数なし
Sub GreetDefault()
MsgBox "こんにちは!"
End Sub
' 引数あり(名前を受け取る)
Sub GreetByName(name As String)
MsgBox "こんにちは、" & name & " さん!"
End Sub
' 複数の引数
Sub CreateReport(sheetName As String, title As String, startRow As Long)
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets(sheetName)
ws.Cells(startRow, 1).Value = title
ws.Cells(startRow, 1).Font.Bold = True
Set ws = Nothing
End Sub
Optional 引数(省略可能な引数)
Optional キーワードを使うと、呼び出し時に省略できる引数を定義できます。
Function FormatPrice(price As Long, Optional taxRate As Double = 0.1) As String
Dim total As Long
total = price * (1 + taxRate)
FormatPrice = Format(total, "#,##0") & " 円"
End Function
Sub TestFormatPrice()
Debug.Print FormatPrice(1000) ' => 1,100 円(既定の税率 10%)
Debug.Print FormatPrice(1000, 0.08) ' => 1,080 円(税率 8%)
End Sub
Optional 引数は、引数リストの末尾にまとめて配置する必要があります。必須引数の後に Optional
引数を置きます。
IsMissing 関数
Variant 型の Optional 引数が省略されたかどうかを判定するには IsMissing 関数を使用します。
Sub LogMessage(message As String, Optional logLevel As Variant)
Dim prefix As String
If IsMissing(logLevel) Then
prefix = "[INFO]"
Else
prefix = "[" & logLevel & "]"
End If
Debug.Print prefix & " " & message
End Sub
Sub TestLogMessage()
Call LogMessage("処理を開始します") ' => [INFO] 処理を開始します
Call LogMessage("エラーが発生しました", "ERROR") ' => [ERROR] エラーが発生しました
End Sub
ByRef と ByVal - 引数の渡し方
VBA の引数の渡し方には ByRef(参照渡し) と ByVal(値渡し) の2種類があります。この違いはVBA初心者が最もつまずきやすいポイントの一つです。
ByRef(参照渡し)- 既定
ByRef は引数を参照で渡す方式で、VBA の既定の動作です。プロシージャ内で引数の値を変更すると、呼び出し元の変数も変更されます。
Sub DoubleValue(ByRef num As Long)
num = num * 2
End Sub
Sub TestByRef()
Dim value As Long
value = 10
Debug.Print "呼び出し前: " & value ' => 10
Call DoubleValue(value)
Debug.Print "呼び出し後: " & value ' => 20(変更された!)
End Sub
ByVal(値渡し)
ByVal は引数を値のコピーで渡す方式です。プロシージャ内で引数を変更しても、呼び出し元の変数は変更されません。
Sub DoubleValueByVal(ByVal num As Long)
num = num * 2
Debug.Print "プロシージャ内: " & num ' => 20
End Sub
Sub TestByVal()
Dim value As Long
value = 10
Debug.Print "呼び出し前: " & value ' => 10
Call DoubleValueByVal(value)
Debug.Print "呼び出し後: " & value ' => 10(変更されない)
End Sub
ByRef と ByVal の比較
| 特徴 | ByRef(参照渡し) | ByVal(値渡し) |
|---|---|---|
| 既定の動作 | ○ 省略時はこちら | × 明示的に指定が必要 |
| 呼び出し元への影響 | あり(値が変更される) | なし(コピーが渡される) |
| メモリ効率 | ○ コピーを作らない | △ コピーを作成する |
| 安全性 | △ 意図しない変更のリスク | ○ 安全 |
' ByRef: 呼び出し元の変数が変更される
Sub IncrementByRef(ByRef counter As Long)
counter = counter + 1
End Sub
' ByVal: 呼び出し元の変数は変更されない
Sub IncrementByVal(ByVal counter As Long)
counter = counter + 1 ' このプロシージャ内でのみ有効
End Sub
Sub TestComparison()
Dim count As Long
' ByRef のテスト
count = 0
Call IncrementByRef(count)
Debug.Print "ByRef 後: " & count ' => 1
' ByVal のテスト
count = 0
Call IncrementByVal(count)
Debug.Print "ByVal 後: " & count ' => 0
End Sub
意図しない副作用を防ぐため、特別な理由がなければ ByVal を使用するのがお勧めです。ByRef
は「呼び出し元の値を意図的に変更したい」場合にのみ使用しましょう。VBA では ByRef
が既定のため、うっかりミスで呼び出し元の変数が書き換わってしまうことがよくあります。
引数に ByRef も ByVal も付けない場合、VBA では自動的に ByRef になります。これは他のプログラミング言語と異なる挙動であり、VBA特有のバグの原因になりやすいポイントです。
' ByRef が暗黙的に適用される
Sub ModifyValue(num As Long) ' ← 実質的に ByRef num As Long
num = 999
End Sub プロシージャの呼び出し方
Sub の呼び出し
Sub プロシージャを呼び出すには、Call 文を使う方法と、Call を省略する方法があります。
Sub Greet(name As String)
MsgBox "こんにちは、" & name & " さん!"
End Sub
Sub TestCallSub()
' 方法1: Call 文を使う(引数は括弧で囲む)
Call Greet("太郎")
' 方法2: Call を省略する(引数は括弧で囲まない)
Greet "太郎"
End Sub
VBA では Call を使う場合は引数を括弧で囲み、Call を省略する場合は括弧を付けないというルールがあります。この違いは非常にわかりにくく、初心者がよく混乱するポイントです。
' ○ 正しい
Call Greet("太郎") ' Call あり → 括弧あり
Greet "太郎" ' Call なし → 括弧なし
' × 間違い(単一のByRef引数で問題が起きうる)
Greet("太郎") ' Call なし → 括弧あり(意図しない動作の可能性)統一性のために、Call 文を常に使う方針をお勧めします。
Function の呼び出し
Function プロシージャの戻り値を受け取るには、関数呼び出しの結果を変数に代入します。
Function AddNumbers(a As Long, b As Long) As Long
AddNumbers = a + b
End Function
Sub TestCallFunction()
' 戻り値を変数に代入
Dim result As Long
result = AddNumbers(10, 20)
Debug.Print result ' => 30
' 直接使用することもできる
Debug.Print "合計: " & AddNumbers(5, 3) ' => 合計: 8
' 条件式に使用
If AddNumbers(1, 2) > 0 Then
Debug.Print "正の値です"
End If
End Sub
スコープ(公開範囲)
Public と Private
プロシージャのスコープ(他のモジュールから呼び出せるかどうか)は、Public と Private で制御します。
' Public: 他のモジュールからも呼び出せる(既定)
Public Sub PublicProcedure()
Debug.Print "どこからでも呼び出せます"
End Sub
' Private: 同じモジュール内からのみ呼び出せる
Private Sub PrivateProcedure()
Debug.Print "同じモジュール内からのみ呼び出せます"
End Sub
' Public は省略可能(Sub は既定で Public)
Sub DefaultProcedure()
Debug.Print "これも Public として扱われます"
End Sub
| 修飾子 | 呼び出せる範囲 | マクロ一覧に表示 |
|---|---|---|
Public | すべてのモジュールから呼び出せる | ○(引数なしの場合) |
Private | 同じモジュール内からのみ | × |
| 省略時 | Public と同じ | ○(引数なしの場合) |
メインの処理から呼び出されるだけの補助的なプロシージャは Private
にしましょう。マクロ一覧に不要なプロシージャが表示されなくなり、ユーザーが間違って実行するリスクも減ります。
実践的な活用例
例1:処理を Sub に分割して構造化する
Option Explicit
' メインの処理
Public Sub GenerateReport()
Call InitializeSettings
Call ClearPreviousData
Call ImportData
Call FormatReport
Call RestoreSettings
MsgBox "レポートの生成が完了しました", vbInformation
End Sub
Private Sub InitializeSettings()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
End Sub
Private Sub ClearPreviousData()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("レポート")
ws.Cells.Clear
Set ws = Nothing
End Sub
Private Sub ImportData()
' データのインポート処理
Dim wsData As Worksheet
Dim wsReport As Worksheet
Set wsData = ThisWorkbook.Sheets("データ")
Set wsReport = ThisWorkbook.Sheets("レポート")
Dim lastRow As Long
lastRow = wsData.Cells(wsData.Rows.Count, 1).End(xlUp).Row
wsData.Range("A1:D" & lastRow).Copy Destination:=wsReport.Range("A1")
Set wsData = Nothing
Set wsReport = Nothing
End Sub
Private Sub FormatReport()
' レポートの書式設定処理
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("レポート")
With ws.Rows(1)
.Font.Bold = True
.Interior.Color = RGB(68, 114, 196)
.Font.Color = RGB(255, 255, 255)
End With
Set ws = Nothing
End Sub
Private Sub RestoreSettings()
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
例2:汎用的な Function を作成する
Option Explicit
' 指定した範囲内の数値セルの平均を計算
Function AverageNonEmpty(targetRange As Range) As Double
Dim cell As Range
Dim total As Double
Dim count As Long
total = 0
count = 0
For Each cell In targetRange
If IsNumeric(cell.Value) And cell.Value <> "" Then
total = total + CDbl(cell.Value)
count = count + 1
End If
Next cell
If count > 0 Then
AverageNonEmpty = total / count
Else
AverageNonEmpty = 0
End If
End Function
' 文字列が指定した文字で始まるかを判定
Function StartsWith(ByVal text As String, ByVal prefix As String) As Boolean
StartsWith = (Left(text, Len(prefix)) = prefix)
End Function
' 文字列が指定した文字で終わるかを判定
Function EndsWith(ByVal text As String, ByVal suffix As String) As Boolean
If Len(text) < Len(suffix) Then
EndsWith = False
Else
EndsWith = (Right(text, Len(suffix)) = suffix)
End If
End Function
' 使用例
Sub TestFunctions()
Debug.Print StartsWith("Excel VBA", "Excel") ' => True
Debug.Print EndsWith("report.xlsx", ".xlsx") ' => True
Debug.Print EndsWith("data.csv", ".xlsx") ' => False
End Sub
例3:ByRef を活用した複数の戻り値
Function の戻り値は1つだけですが、ByRef 引数を使うと実質的に複数の値を返すことができます。
Option Explicit
' 範囲内のデータの統計情報を取得
Sub GetStatistics(targetRange As Range, _
ByRef outMin As Double, _
ByRef outMax As Double, _
ByRef outAvg As Double, _
ByRef outCount As Long)
Dim cell As Range
Dim total As Double
outMin = 1E+308 ' 十分に大きな値で初期化
outMax = -1E+308
total = 0
outCount = 0
For Each cell In targetRange
If IsNumeric(cell.Value) And cell.Value <> "" Then
Dim val As Double
val = CDbl(cell.Value)
If val < outMin Then outMin = val
If val > outMax Then outMax = val
total = total + val
outCount = outCount + 1
End If
Next cell
If outCount > 0 Then
outAvg = total / outCount
Else
outMin = 0
outMax = 0
outAvg = 0
End If
End Sub
Sub TestGetStatistics()
Dim minVal As Double
Dim maxVal As Double
Dim avgVal As Double
Dim cnt As Long
Call GetStatistics(Range("A1:A100"), minVal, maxVal, avgVal, cnt)
Debug.Print "件数: " & cnt
Debug.Print "最小: " & minVal
Debug.Print "最大: " & maxVal
Debug.Print "平均: " & Format(avgVal, "#,##0.0")
End Sub
注意点とよくある間違い
1. Function の戻り値が返されない
' × 戻り値を代入し忘れている
Function CalcArea_Bad(width As Double, height As Double) As Double
Dim area As Double
area = width * height
' CalcArea_Bad = area を忘れている → 0 が返る
End Function
' ○ 正しい
Function CalcArea_Good(width As Double, height As Double) As Double
CalcArea_Good = width * height
End Function
2. Function 内で関数名を再帰呼び出ししてしまう
' × 意図しない再帰呼び出し
Function Factorial(n As Long) As Long
If n <= 1 Then
Factorial = 1
Else
Factorial = n * Factorial(n - 1) ' これは再帰呼び出し(意図的ならOK)
End If
End Function
VBA では 関数名 = 値 が戻り値の設定、関数名(引数)
が再帰呼び出しです。戻り値の設定なのか再帰呼び出しなのかを明確にコメントで示しましょう。
3. Call 文の括弧ルール
Sub ShowMessage(msg As String)
MsgBox msg
End Sub
Sub TestCallRules()
' ○ 正しい呼び出し方
Call ShowMessage("テスト") ' Call + 括弧
ShowMessage "テスト" ' Call なし + 括弧なし
' △ 動くが紛らわしい
ShowMessage ("テスト") ' 括弧が式の評価として扱われる
End Sub
練習問題
まとめ
- Sub プロシージャは処理を実行するためのもので、戻り値を返さない。マクロとして直接実行できる
- Function プロシージャは値を計算して戻り値を返す。ワークシートの数式からも呼び出せる
- 戻り値は関数名に値を代入することで返す(
関数名 = 値) - 引数の渡し方には ByRef(参照渡し) と ByVal(値渡し) の2種類がある
- VBA の既定は ByRef であり、意図しない副作用を防ぐには ByVal を明示的に指定する
- Optional キーワードで省略可能な引数を定義できる
- Public / Private でプロシージャの公開範囲を制御する
- 処理を小さな Sub / Function に分割すると、コードの可読性と保守性が向上する