Visual Basic 初級講座 |
Visual Basic 中学校 > 初級講座 >
第34回 値型と参照型
値型と参照型の違いを知っていないとプログラムが意図したとおりに動作せず思わぬバグの原因となります。今回はこの値型と参照型について必要十分な知識を取得してください。
この回の要約 ・クラスは参照型。 ・それ以外は値型。 ・参照型の変数はCloneメソッド以外の方法でコピーすることができない。
・値型の変数は = 演算子でコピーを作ることができる。
・値型の変数にNothingをセットすることはできない。
・値渡しの引数に対する変更は呼び出し元に影響せず、参照渡しの引数に対する変更は呼び出し元に影響する。ただし、参照型の変数を値渡しした場合の挙動には多少の注意が必要となる。 |
突然ですが、重大なバグがあるプログラムを紹介します。
このプログラムは次のように画面に表示することを意図して作られたものです。
■画像1:期待する結果
左と右で「ろ」の部分だけ異なり他の部分は同じです。 ところが実際に実行するとこの通りにはならないのです。どこに間違いがあるかわかりますか?
'このプログラムは意図したとおりに動作しません! Dim MyArray1 As New ArrayList Dim MyArray2 As ArrayList '▼いろはカルタ パターン1 '値をセット '▼いろはカルダ パターン2 '値をセット '▼表示 ListBox1.Items.AddRange(MyArray1.ToArray) |
■リスト1:意図どおりに動作しない
このプログラムをみてすぐに間違いが指摘できるようでしたら今回の説明を読む必要はほとんどありません。ただし、後半では「値渡しと参照渡し」に関する説明もあるので、場合のよってはそこだけ読んでいただければけっこうです。
このプログラムの間違いを指摘できない方はこれからの説明で理解していただけると嬉しいです。
このプログラムを実行すると実際には次のように表示されます。
■画像2:リスト1の実行結果
はじめの意図と異なる点は、ListBox1側では「ろ - 論より証拠」と表示されるはずなのに「ろ - 論語読みの論語知らず」と表示されている点です。
プログラムではMyArray2の方だけに「ろ - 論語読みの論語知らず」をセットしているので、MyArray1の内容まで変ってしまっているのは妙な気がしますが、実はこれで正常なのです。
原因はMyArray2 = MyArray1の行にあります。この指示を『MyArray1の内容をMyArray2に「コピー」する』指示と取り違えているのがこのプログラムの間違いです。
この場合のMyArray2 = MyArray1という命令は『MyArray1の見ているものをMyArray2も見なさい。』という意味の命令で、図式化すると次のような状態を作り出す命令です。
■図1:参照型の構造
そのため、後でMyArray2の内容を書き換えると、MyArray1の内容も変ってしまったかのように見えるのです。MyArray2とMyArray1は実体は同じものを共通で見ているのだけなので当然の結果です。
しかし、このことを知らないでプログラムしていると意図していない値が変更されてしまうというわけですからこのことは大変重要です。
この例で、意図したとおりにプログラムを動作させるにはMyArray2 = MyArray1の行をMyArray2 = MyArray1.Cloneと書き換えます。
Cloneメソッド(読み方:Clone = クローン)は「コピー」するメソッドなのでこの場合はMyArray1とMyArray2は別々のものを見ていることになります。つまり意図どおりです。
今の説明で「同じものを見ている」とか、「別々のものを見ている」など「見る」という言葉を使いましたが、この部分やより専門的には「参照している」と言います。
そこで、この例で出てきたように、1つの実体を複数の変数で参照する型のことを参照型(読み方:参照型 = さんしょうがた)と呼びます。 重要なのはすべてのクラスが参照型である点です。クラスを使用している以上必ず上述の問題が発生するのです。 配列や属性、例外などがすべてクラスである点にも注意してください。これらはすべて参照型ということになります。また、初級講座では扱いませんがデリゲートと呼ばれる クラスも参照型です。
イメージがわかない方もいらっしゃると思いますから、参照型である型(=クラス)の例をいくつかあげておきます。
以下の型はすべて参照型です。
逆に単純に変数1つにつき実体も1つである型のことを値型(読み方:値型 = あたいがた)と呼びます。たとえば数値型は値型なので次のプログラムを実行するとまず、「1」と表示され次に「2」と表示されます。もし数値型が参照型であったならば両方とも「2」と表示されてしまうところです。
Dim x
As
Integer Dim y As Integer
x = 1 MsgBox(x) |
■リスト2
クラス以外のものはすべて値型です。つまり、構造体が値型である点に留意してください。こちらも参考のために値型であるものの例をいくつか挙げておきます。
以下の型はすべて値型です。
このように参照型であるものと値型であるものとではプログラム上の動作が異なり、その差異は重要ですので要注意が必要です。
プログラム中でオブジェクトが値型であるか参照型であるか調べるにはIsReference関数(読み方:IsRefernece = イズリファレンス)を使用します。たとえば、次の例ではStringが参照型であるために「参照型です」と表示されます。
Dim
St As
String
If IsReference(St)
Then |
■リスト3
通常はIsReference関数の出番はほとんどありません。Object型の変数には値型の変数でも参照型の変数でも代入することができるので、Object型の変数を使っている場合はIsReference関数が活躍する場合もあるかもしれません。
値型と参照型について説明したところで、この2つの違いをまとめてみます。両者には主に3つの違いがあります。
A = B とした場合、 値型ではAはBのコピーとなるが、 参照型ではAとBは同じものを指している別名となる。 |
これは最初の例でも説明したとおりです。この代入の相違点こそが値型と参照型の区別で最も重要です。
なお、参照型のコピーをつくるにはたいていの場合Cloneメソッドを使用します。これも既に紹介済みですが、中にはCloneメソッドを持っていないクラスもあります。
Cloneメソッドを持っていないクラスの場合は簡単にコピーを作ることはできません。プロパティの値を1つずつ代入していくことになりますが、そのプロパティがまた参照型だったりしてなかなか大変です。
値型では値にNothingをセットできないが、参照型では値にNothingをセットできる。 |
Nothing(読み方:Nothing = ナッシング)とは何も参照していない状態を指しています。参照型の変数の既定値は必ずNothingです。値がNothingである状態を「Null参照」(読み方:Null = ヌル [ナルと発音する場合もあります])と呼びます。値がNothingである参照型の変数にアクセスするとNullReferenceExceptionが発生します。
たとえば、次のコードではNullReferenceExceptionが発生します。
Dim
St As
String Dim i As Integer i = St.Length 'ここでエラーになります。 MsgBox(i) |
■リスト4
変数StがNothingなのに、そのLengthプロパティを使用しようとしたからです。値がNothingである変数に許されるアクションは代入のみです。
通常のクラスはこのNull参照の問題はそれほど煩わしくないのですが、Stringはよく使うクラスですからNull参照の問題は少々面倒です。また、空文字とNull参照を混同してい方が時々いらっしゃいますが両者は別物です。空文字は文字数が0である文字列を明確に指している状態ですが、Null参照は何も指していない状態です。
一般的にStringのNull参照対策として次の2つのスタンスがあるようです。
1つ目は常に初期値を設定するスタンスです。次の例は正常に動作します。
Dim
St As String =
"" Dim i As Integer i = St.Length MsgBox(i) |
■リスト5
もう1つはできるだけStringクラスのメソッドやプロパティを使用しないスタンスです。次の例はNull参照であるにもかかわらず、変数Stのメンバにアクセスしていないので正常に動作します。
Dim
St As
String Dim i As Integer i = Len(St) MsgBox(i) |
■リスト6
ただし、VB2005はこの例に対して警告を表示します。
もちろん、特にNull参照対策をしないという第3のスタンスもあります。この場合はNull参照のままでメソッドやプロパティを使わないように常に注意する必要があります。
プログラム中で変数がNothing、つまりNull参照かどうか調べるにはIsNothing関数(読み方:IsNothing = イズナッシング)を使用します。
メモ - 構造体へのNothingの代入 値型の変数にNothingを代入すると変数の値が初期化されるだけで、値自体はNothingになりません。コードの可読性が低下しますからこのような記述は極力避けるべきでしょう。 しかし、このことを逆手に取ると構造体を簡単に初期化できることになります。特に自作の構造体を使用する場合はこのことが役に立つことでしょう。 |
値型の変数が適用範囲からはずれると内容が破棄されるが、参照型の変数は適用範囲からはずれても、他の変数が同じオブジェクトを参照している可能性があるためメモリ上に内容が残る場合がある。 |
プログラム中で使用する変数はすべてメモリ上に格納されるのですが、一度格納された値をいつ破棄するかが値型と参照型とで異なります。値型は変数が適用範囲から離れた時点ですぐにメモリが開放されますが、参照型の場合にはすぐに開放されるわけではありません。
というのは、参照型の変数の場合、その変数が適用範囲を離れても、別の変数が同じものを参照している可能性があるため「どの変数もそのオブジェクトを参照していない」という状態になるまで開放されないのです。
しかも、その状態を確認するタイミングは.NET Frameworkが適当に決めているため実際上すべての変数が適用範囲から外れても直ちにメモリが開放されるわけではありません。
このメモリの開放を管理しているのはガベージコレクションという仕組みです。ガベージコレクションについては今回は深入りしません。ガベージコレクションの謎に満ちた挙動については中級講座で詳しく取り上げるつもりです。
以上で値型と参照型の注意すべき点はすべて説明しました。これを読んでいる皆さんの中には文字列型の挙動について不思議に感じている方がいらっしゃるのではないでしょうか?
たとえば、次のプログラムを実行するとなんと表示されるでしょうか?正解を読む前に皆さんの考えを整理してみてください。
Dim St1 As
String Dim St2 As String St1 =
"リキニウス" MsgBox(St1) |
■リスト7
実行してみるとわかるのですが答えは「リキニウス」です。「セクスティウス」ではありません。
先ほどの参照型の説明からして St2 = St1 とした時点で St1とSt2は同じものを参照するようになるので、その後でSt2の値を変更するとSt1にも影響すると考えている方はいらっしゃいませんか?または、そのように考えていなくてもなぜこの場合そのようにならないのか説明できますか?
文字列型の場合は、ダブルクォーテーションでくくられた文字列自体が文字列型のインスタンスである点に注意してください。このように文字列型はプログラム中に直接インスタンスを記述できる珍しい型ですから他の型とは一見異なる動作のように思えるのです。
この例ではSt2 = St1とした時点で確かに、St1とSt2は同じオブジェクト(="リキニウス")を参照することになりますが、次のSt2 = "セクスティウス"とした時点で、今度はSt2と"セクスティウス"が同じオブジェクト(="セクスティウス")を参照することになるのです。
結果としてSt1は"リキニウス"を参照し、St2は"セクスティウス"を参照するということになります。
リキニウスとセクスティウスは共和制ローマ時代の護民官で、貴族対平民の階級闘争でリキニウス=セクスティウス法の制定を以って平民の勝利を勝ち取った功績で有名です。
文字列型のほかに配列も直接インスタンスを記述できますが、こちらはプログラムをみればすぐに挙動が推測できるでしょう。
Dim
Ar1() As
String Dim Ar2() As String Ar1 =
New String() {"Apple",
"Banana", "Cat"} MsgBox(Ar1(1)) |
■リスト8
この例では最後に「Banana」と表示されます。
しつこくなりますが重要ですからもう1つ例を挙げましょう。次の例では何が表示されるかわかりますか?
Dim
Ar1() As
String Dim Ar2() As String Ar1 =
New String() {"Apple",
"Banana", "Cat"} MsgBox(Ar1(1)) |
■リスト9
こちらは「バナナ」と表示されます。これらの例が当たり前と感じるようになれば値型と参照型の話は終わりです。これ以上発展的な事柄はありません。
「値型と参照型」と名前が似ているテーマに「値渡しと参照渡し」というものがあります。この2つのテーマを同じ解説記事の中で扱うといらない混乱を招きそうですが、ここは一挙に説明してしまいます。
値渡しと参照渡しというのはメソッドやプロパティを呼び出すときの引数の扱いの話です。簡単に説明するとByVal(読み方:ByVal = バイバル)が指定されているものが値渡しの引数で、ByRef(読み方:ByRef = バイレフ)が指定されているものが参照渡しの引数です。
値渡しと参照渡しの基本的な違いは引数に変数のコピーを渡すか、変数本体を渡すかの違いです。この違いによるメソッドやプロパティ内部で引数の値を変更したときの挙動が変ります。基本的には値渡しの場合はメソッドやプロパティ内部でいくら引数の値を変更しても、変数のコピーを変更していることになるので呼び出し元の変数には影響しませんが、参照渡しでは本体を渡しているのでメソッドやプロパティ内部での引数への変更が呼び出しもとの変数に影響します。
さて、まずは次のプログラムで値渡しと参照渡しの基本的な違いを確認しましょう。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click
Dim P1
As Point
'※Pointは座標を表す構造体 P1.X = 1 Call
SetPointVal(P1) MsgBox(P1.ToString)
'(1, 2) End Sub |
Private
Sub SetPointVal(ByVal
P As Point)
'値渡しなのでメンバの変更は呼び出し元に影響しない End Sub |
Private Sub
SetPointRef(ByRef
P As Point)
'参照渡しなのでメンバの変更が呼び出し元に影響する End Sub |
■リスト10
Pointは構造体なので値型です。ともあれ、この例では値型と参照型の区別は重要ではありません。SetPointValの引数はByValで宣言されているの値渡し、SetPointRefの引数はByRefで宣言されているので参照渡しです。この違いが重要です。
この例では変数P2の座標は(30, 40)に書き換わりますが、変数P1の座標は(1, 2)のままです。
これはSetPointValの引数が値渡しのためメソッド内での引数への変更がすべて破棄されたのに対し、SetPointRefの場合は参照渡しなので引数への変更が呼び出し元に反映されたことによる違いです。
この話題にはじめてふれている方はそろそろ読むのが苦痛になってきたでしょうか?正直私もこの話題があまり好きではありません。そもそもメソッドやプロパティの中で引数を変更するという発想が好ましくないのです。
時にはメソッドやプロパティの中で引数を変更する必要があることは認めますが、常に値渡しと参照渡しを気にしながらプログラムするのは嫌です。
みなさんは本当に必要のない限りメソッドやプロパティの中で引数の値を変更するのは避けるようにしましょう。そのような方法でプログラムしていると、プログラムが巨大になるほど収拾がつかなくなってしまいます。
しかし、もしそのようなことをした場合どのようなことが起きるのかは知っておく必要はありますから説明は続行します。
これまでの説明で「値渡しは引数の変更が呼び出し元に影響しない」、「参照渡しは引数の変更が呼び出し元に影響する」という図式はわかっていただけたと思います。
問題を複雑にしているのは、ここで「参照型」が登場するときです。前に説明したように基本的に参照型の変数のコピーを作ることはできません。明示的にCloneメソッドを呼び出せばできますが、Cloneメソッドを持っていないクラスもたくさんあります。
ところが、ByValは変数の「コピー」を引数に渡すのですから、参照型の変数を値渡しした場合に、メソッド・プロパティの内部で引数を変更した場合呼び出し元に影響するのかどうか微妙な情勢です。
先に結論を言うと、参照型の変数を値渡しした場合は、メソッドやプロパティの内部で引数のメンバを変更すると呼び出しもとの変数に反映されます。つまり、ByValの力でも参照型の変数のコピーを作ることはできないということです。
ただし、ByValの効力によって引数自体への変更は呼び出し元に反映されません。次のサンプルを見てください。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click
Dim Ar1
As New ArrayList Ar1.Add("Apple") Ar2.Add("リンゴ") Call
ArrayListVal1(Ar1) MsgBox(Ar1(1))
'XXXXXXX End Sub |
Private
Sub ArrayListVal1(ByVal
Ar As ArrayList)
'引数のメンバを変更する→参照型のコピーは作れないので呼び出し元に影響する End Sub |
Private Sub
ArrayListVal2(ByVal Ar As
ArrayList) Dim NewAr As New ArrayList NewAr.Add("ネルバ") '引数自体を変更する→ByValの効力で呼び出し元に影響しない End Sub |
■リスト11
プログラム中のコメントに書いてある通りなのですが、2つのプロシージャはどちらも引数を値渡しで受け取りますがArrayListVal1の方は呼び出し元に影響を与えることができるのに、ArrayListVal2は呼び出し元に影響を与えることができません。
これを単純に「ByValの効力で」と説明していますが、ByValに特別な効力があるのではなくStringの場合と同じような参照型の機能が働いているのです。
私の説明下手がみなさんを混乱におとしいれたかもしれませんね。最後に表にまとめておきます。
引数自体の変更 | メンバの変更 | ||
値型を | 値渡し | 影響しない | 変更しない |
値型を | 参照渡し | 影響する | 影響する |
参照型を | 値渡し | 影響しない | 影響する |
参照型を | 参照渡し | 影響する | 影響する |
■表1
参照型の変数を値渡ししてメンバを変更したときだけちょっと法則からはみだしているように見える以外は基本どおりなのを確認して置いてください。基本は「値渡し→影響しない」、「参照渡し→影響する」です。
また、途中で説明したようにメソッドやプロパティの中で引数を変更することは極力控えましょう。