読者です 読者をやめる 読者になる 読者になる

脱・初心者のために(1)

私が脱・初心者を自覚した瞬間

……といっても、ある瞬間にスイッチが入ったように「今日を以て初心者を卒業します。私のことを嫌いになっても、初心者のことは嫌いにならないでください!」とか思ったわけではない。

何度も何度も、それはもう何度も何度も、Time After Time……ですよ。「ぼちぼち初心者の域はだっしたかなー」、「いやいや、こんなことも分かっていないようじゃあまだまだだな……」というのを何度も繰り返して今に至るわけです。

今回は、そのたくさんある瞬間のうちの一つ、ということでご理解ください。

「値渡し」と「参照渡し」

コレ、最初何のことだかサッパリ分からなかった。

たいていの本には、

「値渡し」は、変数のコピーを呼び出した側のプロシージャに渡します。
「参照渡し」はその変数への参照を呼び出した側のプロシージャに渡します。

とかいうようなことが書いてある。

これ、最初全然意味が分からなかった。「ByVal」と「ByRef」を使い分けると何がうれしいのか分からなかったんだな。

で、たいていの本には、「とりあえず引数を受け取る側ではByValつけときゃいい」みたいな投げやりなアドバイスが載っていたりする。まあ、それでたいてい問題はないし、「ByVal」で渡せない引数(オブジェクト系)だとコンパイルエラーが出て実行させてもらえないから、「とりあえずByVal」で行けてしまう。

でもねえ……。

プログラミングというのは論理的思考の権化みたいなもんなんだから、そこを「おまじない」みたいな理解でごまかすのは良くないと思う。

で、どうすんの?

使い分けることで何がうれしいのか、については私もよく分かっていないが、「値渡し」と「参照渡し」がどう違うのか、というのはちゃんと理解しておいた方が良いと思う。

そもそも、「引数」ってのはよく料理やなんかの「材料」にたとえられる。メソッドとかプロシージャが「調理」という処理で、「調理」に必要な「材料」が「引数」というわけだ。

「引数」というもののイメージをつかむにはこれで問題ないと思うんだが、その理解しかないと、「値渡し」だの「参照渡し」だのといったときにつまづくもとだと思う。

「値渡し」と「参照渡し」の違い

「処理の材料」という意味では、値渡しにしようが参照渡しにしようがどっちでもいい。ただ、渡し方というか、「渡す」ということの意味が違う。

変数hogeに、「ち~んw」という文字列が入っているとしよう。

VBAのコードだと、

Dim hoge As String
hoge = "ち~んw"

こういう状態だな。

で、この「ち~んw」という文字列を「値渡し」にする場合と「参照渡し」にする場合とで何が違うのか、ということだ。

結論から述べる。渡しているものが違う。見た目は同じでも。

へ??? どういうこと?

次のコードを実行したら、どうなるだろうか。

リスト1
Sub hogeCaller()
  Dim hoge As String
  hoge = "ち~んw"
  Call hogeCalledByVal(hoge)
  Call hogeCalledByRef(hoge)
End Sub

Sub hogeCalledByVal(ByVal str As String)
  MsgBox str
End Sub

Sub hogeCalledByRef(ByRef str As String)
  MsgBox str
End Sub
リスト1の実行結果

f:id:akashi_keirin:20170402215641j:plain

まずはこいつが表示され、[OK]をクリックしたら、

f:id:akashi_keirin:20170402215650j:plain

こいつが表示される。

1回目のメッセージボックスと、2回目のメッセージボックスは、全く同じものに見えるし、実際同じものだ。

しかし、1回目の「ち~んw」と2回目の「ち~んw」の意味合いはまるで違う。

だから、何が違うのさ?

まず、hogeCalledByValに渡された「ち~んw」。こいつは、

純粋な文字列としての「ち~んw」

だ。

一方、hogeCalledByRefに渡されたのは、

ただの文字列「ち~んw」ではない

ということだ。

じゃあ、何なのか。hogeCalledByRefに渡されたのは、

変数hogeの中身としての「ち~んw」

ということだ。

といっても、(゚Д゚)ハァ? だろう。もうちょっと説明する。

「値渡し」の場合、渡された時点で「ち~んw」という文字列には、もはや「変数hogeの中身」という意味合いはない。「純粋な文字列」と言ったのはそういうことだ。

それに対して、「参照渡し」の場合は、文字列を渡しているのではない。ざっくり言うと、

変数hogeが使っているメモリの番地を教えている

のだ。

たとえば、変数hogeがメモリの1丁目1番地に値を保持しているとしたら、この場合「ち~んw」という文字列がメモリの1丁目1番地に保存されていることになる。

変数hogeを「参照渡し」にするということは、

そっちの処理で材料がいるって言うからくれてやるぜ!
ほれ! 中身が知りたきゃメモリの1丁目1番地にあるから好きに使いな!

という感じだ。

リスト1の場合、変数hogeの中身は「ち~んw」だから、確かに「ち~んw」を渡しているように見えるし、その通りなんだが、「参照渡し」の場合は、どこまでも

変数hogeの中身としての「ち~んw」

ということだ。

だから、参照渡しにした場合、渡した先で引数を加工すると、当然変数hogeの中身そのものが加工されることになる。

で、何なの?

たとえば、リスト1を次のように書き換えてみる。

スト2
Sub hogeCaller()
  Dim hoge As String
  hoge = "ち~んw"    '……(1)
  Call hogeCalledByRef(hoge)    '……(2)
  MsgBox hoge    '……(6)
End Sub

Sub hogeCalledByRef(ByRef str As String)
  MsgBox str    '……(3)
  str = "(゚Д゚)ハァ?"    '……(4)
  MsgBox str    '……(5)
End Sub

こいつを実行するとどうなるか。

リスト2の実行結果

f:id:akashi_keirin:20170402215655j:plain

まずはこいつが出てくる。

f:id:akashi_keirin:20170402215702j:plain

次はこいつ。

f:id:akashi_keirin:20170402215709j:plain

んで、こうなる。

リスト2の説明

カラクリはこうだ。

まず、(1)の

hoge = "ち~んw"

で、変数hogeに「ち~んw」が代入される。

次に、(2)でhogeをhogeCalledByRefに渡して処理をさせるわけだが、参照渡しなので、

hogeCalledByRefにhogeが値を保持しているメモリ上の位置を教えている

ことになる。

ここで処理がhogeCalledByRefに移る。hogeCalledByRefでは、変数strで引数を受け取るわけだが、「参照渡し」で受け取っているので、

strの中にはhogeの値を保持しているメモリ上の位置情報が入っている

と思えば良い。

だから、(3)の

MsgBox str

で、メッセージボックスに表示するためにプロシージャがstrの中身を取得しようとするが、そこにあるのは変数hogeのメモリ番地情報なので、そこを見に行って文字列「ち~んw」を得る。

だから、1回目のメッセージボックスには「ち~んw」が表示される。

その後、(4)の

str = "(゚Д゚)ハァ?"

で、strに「(゚Д゚)ハァ?」を代入している。代入しているといっても、strの正体はhogeの参照先なので、当然、

変数hogeの値を保持するメモリ上の位置に文字列「(゚Д゚)ハァ?」が書き込まれる

ことになる。

だから、(5)の

MsgBox str

を実行すると、メッセージボックス(2回目)には「(゚Д゚)ハァ?」が表示される。

ここで、hogeCalledByRefプロシージャが終わるので、処理が元のhogeCallerに戻る。

んで、(6)の

MsgBox hoge

で、メッセージボックスに表示するためにプロシージャがhogeの中身を得ようとするのだが、hogeが参照しているメモリ上の位置には、既にhogeCalledByRefプロシージャ内の(4)で「(゚Д゚)ハァ?」が書き込まれているので、当然プロシージャはhogeの値として「(゚Д゚)ハァ?」を得て、メッセージボックス(3回目)に「(゚Д゚)ハァ?」を表示する。

ざっと、こんな理屈で処理が進んでいたわけだ。

まとめ

このような理屈なので、基本的に値だけしか持たない変数を参照渡しにする意味はまるでないと思う。

変数の中身をいじくりたいのなら、変数を宣言したプロシージャ・メソッド内でやるべきであり、わざわざスコープ外でやる意義が見いだせないからだ。

んじゃ、なんで「参照渡し」なんてものがあるのか?

現時点での素人考えだけれど、

値渡しのしようがないものがある

からだと思う。

簡単な例だと、Excelのとあるセルを引数にしたいとき、

セルを値渡しにする

なんて意味不明でしょ?

「オブジェクト」レベルのものになると「値渡し」なんてしようがない。やりたくてもできない。だから、

Sub hogeHoge(ByVal cell As Range)

とか書いても、コンパイルエラーになって実行すらさせてくれないのだろう。

逆に、「整数」とか「文字列」といったものなら、

純粋な単独データ

として存在しうる。

そもそも「整数」とか「文字列」といったプリミティブなデータについて「値渡し」とか「参照渡し」について議論すること自体が無意味なんじゃないのかなあ……?

「値渡し」なんてしようのないデータ型があるから、「参照渡し」という概念が存在して、プリミティブなデータ型についても「参照渡し」自体はできるから「値渡し」も「参照渡し」もできるようになっている、そういうふうに理解した方がいいんでないか。

改めて入門者向けの書籍の「値渡し」・「参照渡し」の箇所を読んでみてそう思ったのだった。

追記

thom (id:t-hom)さんからのご指摘で、

Sub hogeHoge(ByVal cell As Range)

というのも普通にできると分かった。前に何かで「ByValなんてできねーよ、ハゲ!」みたいなエラーが出たことがあって、ずっと勘違いしていたみたい。でも、分かったつもりになっていた「値渡し」・「参照渡し」がまたまたよく分からなくなってしまった。情けないけど、今後の宿題ということにしよう。

練習問題

次のコードを実行したらどうなるか、考えてみてください。

リスト3
Sub pCaller()
  Dim x As Integer
  x = 10
  Debug.Print "1:xの値は " & x & " ですわ。"
  Debug.Print "2:xを値渡しの引数にしてpCalledWithValプロシージャを呼びまんねん。"
  Call pCalledWithVal(x)
  Debug.Print "5:処理がpCallerに帰ってきたで。xの値は、 " & x & " でんがな。"
  Debug.Print "6:じゃ、今度はxを参照渡しの引数にしてpCalledWithRefプロシージャを呼ぶぜ。"
  x = 10
  Call pCalledWithRef(x)
  Debug.Print "9:処理がpCallerに帰ってきたのう。xの値は、 " & x & " になっとりますの。"
End Sub

Sub pCalledWithVal(ByVal x As Integer)
  Debug.Print "3:こちらpCalledWithVal。今から受け取った" & x & " を10倍しまぁす。" & _
              "STAP細胞はありまぁす。"
  x = x * 10
  Debug.Print "4:こちらpCalledWithVal、受け取った x を10倍したので、xは" & x & "ですわ。"
End Sub

Sub pCalledWithRef(ByRef x As Integer)
  Debug.Print "7:こちらpCalledWithRef。今から受け取った" & x & " ば10倍するばい。"
  x = x * 10
  Debug.Print "8:こちらpCalledWithRef、受け取った x ば10倍したけん、xは" & x & "ばい。" & _
              "まさに、10倍ばい!"
End Sub

実際に、試してみてください。

@akashi_keirin on Twitter