参照渡しと値渡しの違い

参照渡しと値渡しを簡単におさらいすると、関数の引数につけるキーワードのByRefとByValのことを指し、ByRefを付けるか省略した場合は参照渡しの引数として扱い、ByValを付けた場合は値渡しの引数になります。

で、本題の参照渡しと値渡しの違いですが、関数(プロシージャ、メソッド)の中で変更した引数の「値」を呼び出し元に引き継がれるかどうか、という点です。

参照渡し(ByRef)の場合は関数内部で変数に加えた変更が呼び出し元でも引き継がれますが、値渡し(ByVal)の場合は引き継がれず、関数を呼び出したときの値のままです。

ただ、引数がプリミティブ型とオブジェクト型でByValを付けたときの挙動が変わるため注意が必要です

このページでは特にその注意点について記述しています。


構文

プロシージャの構文は以下になります。

Sub プロシージャ名([ByRef(省略時) | ByVal] 変数名 [As データ型])
Function プロシージャ名([ByRef(省略時) | ByVal] 変数名 [As データ型]) [As 戻り値のデータ型]

ByRefが参照渡しで、ByValが値渡しになります。

SubプロシージャとFunctionプロシージャのどちらも、ByRefまたはByValを引数に指定します。省略した場合はByRef(参照渡し)になります。

引数が複数ある場合は、引数それぞれでByRefまたはByValの設定を行います。

以下がプロシージャのサンプルです。

Sub aaa(ByRef a, ByVal b, c, ByVal d)

このaaaプロシージャには引数a、b、c、dがありますが、それぞれにByRefまたはByValを指定します。変数cは省略されているためByRef扱いになります。


プリミティブ型とオブジェクト型ではデータ管理方法が異なる

VBAではプリミティブ型とオブジェクト型ではメモリ上のデータの保持の仕方が異なります。

プリミティブ型とはInteger型やLong型やString型などの変数に直接値を保持するデータ型のことでメソッドやプロパティを持ちません。オブジェクト型はメソッドやプロパティを持ち、主にプロパティを使って値を参照するデータ型のことです。

プリミティブ型は変数が指し示すメモリのアドレスに変数が保持する値が格納されています。例えばString型の変数strであれば、変数strが指し示すメモリの11110000番目に値の”aaa”が格納されています。

ところがオブジェクト型はプリミティブ型とは異なり、まず、オブジェクト型変数(Dimで定義した変数や関数の引数)自体が指し示すメモリのアドレスには、値ではなくオブジェクト(クラスのインスタンス)の実体が格納された領域のアドレスが設定されてます。そのオブジェクトの実体のアドレスから始まる連続するメモリ領域のどこかに各プロパティに該当する箇所があり、そこにオブジェクト変数の各プロパティの値が格納されています。

このように、オブジェクト型はオブジェクト変数自体のアドレスと、オブジェクトの実体のアドレスの2つで構成されています。

整理すると、「オブジェクト型変数→オブジェクト変数のアドレス(VarPtr関数で取得可能)→実体アドレス(ObjPtr関数で取得可能)→値」のように参照されます。


プリミティブ型(Integerなど)引数のサンプルコード

一般的な参照渡しと値渡しの説明でもよく使われるプリミティブ型を引数に持つ関数を呼び出すサンプルコードです。

3つの引数があり、いずれもInteger型ですが、引数の渡し方がByValあり、ByRefあり、なし、の3種類あります。それぞれどのように動作するかを出力します。

実行結果
1
2
2

ByValの変数aは関数内の編集が呼び出し元に影響していませんが、ByRefのbと指定なしのcは呼び出し元にも影響しています。


Range型引数のサンプルコード

Range型の引数の参照渡しと値渡しのサンプルコードです。

Debug.Printだらけですが、ObjPtr関数とVarPtr関数を使ってオブジェクトのアドレスと変数のアドレスを出力しています。

実行結果(同じ色は同じアドレス)
1:初期化前
a=ByVal ObjPtr:0 – VarPtr:46066352
b=ByRef ObjPtr:0 – VarPtr:46066348

2:初期化後
a=ByVal ObjPtr:650982712 – VarPtr:46066352
b=ByRef ObjPtr:650981992 – VarPtr:46066348

3:関数内:Offset実行前
a=ByVal ObjPtr:650982712 – VarPtr:46066080
b=ByRef ObjPtr:650981992 – VarPtr:46066348

4:関数内:Offset実行後
a=ByVal ObjPtr:650982352 – VarPtr:46066080
b=ByRef ObjPtr:650976952 – VarPtr:46066348

5:関数処理直後
a=ByVal ObjPtr:650982712 – VarPtr:46066352
b=ByRef ObjPtr:650976952 – VarPtr:46066348

A1
C2

この結果の最後の2行を見ると、値渡しをしたRange型変数aはセルがA1から移動しておらず、参照渡しをした変数bはセルがB1からC2に移動していることがわかります。

オブジェクトのアドレスと変数のアドレスを細かく見てみます。

関数呼び出し前(2.)と関数呼び出した後の関数内(3.)を見ると、ByValで変数が再確保された変数aのアドレスは46066352から46066080変わっていますが、ByRefの変数bのアドレスは46066348で呼び出し元と同じです。ところがObjPtrを見ると、変数aとbのいずれも650982712650981992でアドレスが変わっていません。

これはどういうことかというと、オブジェクト変数をByVal、ByRefした場合は変数自体のアドレスには影響ありますが、その実体のオブジェクトはByValとByRefのどちらの場合も呼び出し元のオブジェクト変数と同じ実体を見ているということです。

処理を進めて、4.の関数内のOffsetで参照するセル座標を変更すると、変数aとbのいずれもオブジェクトの実体が650982712から650982352と、650981992から650976952へ変わります。そして、ByValの変数aは関数が終了すると破棄され、5.での呼び出し元のオブジェクトの実体アドレスは元の650982712ですが、ByRefの変数bは呼び出し元でもオブジェクトの実体が650976952に変わります。

Range型の場合、参照するセルの位置が変わると、セルが保持するオブジェクトの実体が書き換わります。

Range型は親オブジェクトとしてワークシートがあります。Excel関係のオブジェクト型の場合は、ブックやシートやセルなど、それぞれの部品に特化したデータ型になっているため、値を変えると対象のブックやシートやセルに影響が出ます。

このようなExcel関係のデータ型の場合は、呼び出し元と呼び出し先とで関数をまたがっていても、それぞれが見ているものがブックやシートやセルであることは同じであるため、関数に処理を任せたとしても、関数の内部で引数で受け取ったRange型などのオブジェクトを全然違うブックやシートやセルとして扱うことはほとんどないでしょう。

結果として、Excel関係のデータ型をByValで値渡しすることは可能ですが、わざわざByValするような用途はあまりないと思われます。


Dictionary型のサンプルコード

上のRange型のをDictionary型に変えただけですが、引数を4つにして、関数内部でByVal or ByRef と データ追加(Add()) or New(オブジェクト再生成)の組み合わせを行っています。

実行結果
1:初期化前
a = ByVal Add() ObjPtr:276313112 – VarPtr:46066352
b = ByRef Add() ObjPtr:276311744 – VarPtr:46066348
c = ByVal New ObjPtr:276311816 – VarPtr:46066344
d = ByRef New ObjPtr:276312104 – VarPtr:46066340

2:初期化後
a = ByVal Add() ObjPtr:276313112 – VarPtr:46066352
b = ByRef Add() ObjPtr:276311744 – VarPtr:46066348
c = ByVal New ObjPtr:276311816 – VarPtr:46066344
d = ByRef New ObjPtr:276312104 – VarPtr:46066340

3:関数内:Offset実行前
a = ByVal Add() ObjPtr:276313112 – VarPtr:46066076
b = ByRef Add() ObjPtr:276311744 – VarPtr:46066348
c = ByVal New ObjPtr:276311816 – VarPtr:46066072
d = ByRef New ObjPtr:276312104 – VarPtr:46066340

4:関数内:Offset実行後
a = ByVal Add() ObjPtr:276313112 – VarPtr:46066076
b = ByRef Add() ObjPtr:276311744 – VarPtr:46066348
c = ByVal New ObjPtr:276311888 – VarPtr:46066072
d = ByRef New ObjPtr:276312536 – VarPtr:46066340

5:関数処理直後
a = ByVal Add() ObjPtr:276313112 – VarPtr:46066352
b = ByRef Add() ObjPtr:276311744 – VarPtr:46066348
c = ByVal New ObjPtr:276311816 – VarPtr:46066344
d = ByRef New ObjPtr:276312536 – VarPtr:46066340

2
2
1
0

考え方はRange型と同じです。

ただ、Dictionary型はRange型とは異なり親オブジェクトがなく、純粋にデータ保持に特化したクラスです。

そのため、関数内部でのみ使って破棄するような使い方をすることは用途として十分に考えられます。そのような場合は関数内部で再生成を行い、呼び出し元には影響を入れたくないという場面がありますので、その場合にはByValを使うことは用途としてありえます。

ここがRange型と違う点です。


ByRef、ByValは付けた方がいいのか?

ByRefとByValですが、付けた方がいいのか?と聞かれれば、付けた方がいいです。厳密にいえば、ByValであれば、です。

細かい理由を挙げればいろいろありますが、一番の理由は、渡した引数が書き換わるのかどうかがプロシージャの定義を見れば一目で分かるためです。

ByValと付けてあれば「値は変わらない」とわかります。

文字列型(String型)、数値型(Integer、Longなど)などは上のサンプルコードの通り、呼び出し元の値を壊したくないという意図がわかります。

ただし、親オブジェクトを持たないDictionary型やCollection型のようなデータ保持を目的としたデータ型の場合は、値の操作は呼び出し元にも影響します。ByValが有効なのはオブジェクトの再生成(Set dic = New Dictionary)の場合で、この場合は呼び出し元には影響しません。

Range型などのExcel関係のオブジェクトの場合もDictionary型などと同様です。ただ、ByValを付けるような用途(参照セルの変更を禁止する、など)があまりないと思われます。


まとめ

プリミティブ型はByValを付けると呼び立し元の変数の値が変わらず保持されます。

オブジェクト型はByValを付けると値の書き換えは呼び出し元にも影響ありますが、変数の再生成は呼び出し元には影響されません。


実際のコーディングではどうした方がいいか

私自身はほとんどByValもByRefも付けません。なのでほとんどが暗黙の参照渡しで書いています。このサイトで紹介しているコードのほとんどは付いていないと思います。

理由はいくつかありますが、付けなくても問題が起きないから、というのが一番の理由です。なので、ByValを付けるのは以下の条件をすべて満たすときぐらいでいいと思っています。

  • 作った関数を他人が使う場合。
  • 業務としてきちんとした関数を作らなければならない場合。
  • 「ByValを付けておかないと変数値を壊しそうで心配・・・」というような関数作成に慣れていない場合。

技術面で整理するならば以下のルールがおすすめです。

  • ByRefはわざわざ書かない。省略での暗黙参照渡しでOK。
  • プリミティブ型の場合で、呼び出された時点の値を壊したくない場合はByValを付けた方がいいです。
  • Excel関係のWorksheet型やRange型などの場合はByValを付けても用途がまずないので付ける必要はほとんどありません。
  • Dictionary型やCollection型などの場合は、関数内部で再生成を行って別のオブジェクトとして処理した上で呼び出し元には影響させたくない場合はByValを付けます。同じオブジェクト変数のまま値の追加や削除を行う場合は付けても付けなくても結果は同じなので付けなくていいです。