Visual Basic 初級講座 [改訂版] |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/11/15
この記事が対象とする製品・バージョン
Visual Basic 2019 | ◎ | 対象です。 | |
Visual Basic 2017 | ◎ | 対象です。 | |
Visual Basic 2015 | ◎ | 対象です。 | |
Visual Basic 2013 | ◎ | 対象です。 | |
Visual Basic 2012 | ◎ | 対象です。 | |
Visual Basic 2010 | ◎ | 対象です。 | |
Visual Basic 2008 | ◎ | 対象です。 | |
Visual Basic 2005 | ◎ | 対象です。 | |
Visual Basic.NET 2003 | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 | |
Visual Basic.NET (2002) | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 | |
Visual Basic 6.0 | × | 対象外です。 |
目次
今回から2、3回にわけて、クラスと変数の「参照」(さんしょう)について説明します。このテーマは難しく、初心者の方は挫折してしまうかもしれません。
そうならないように、いままで説明してきたことの復習も含めて順を説明していきます。
まずは、インスタンスについて正しく理解しましょう。
クラスと構造体とで仕組みが違うので、はじめにクラスについて扱います。
コンピューターは内部ではすべてのものを 0 と 1 の集合で表現しています。プログラムで扱っているものもすべてコンピューターにとっては 0 と 1 の集合です。変数やインスタンスもそうです。
この、 0 と 1 の集合 である変数やインスタンスは、通常は、メモリ上に格納されています。8GBのメモリを搭載しているコンピューターの場合、 68,719,476,736個(約687億個)の 0 と 1 を保持できます。
※8GB = 1024 × 1024 × 1024 × 8 バイト。1バイトは 8ビットなのでさらにこの8倍。
次のプログラムを題材に掘り下げて見ます。
Dim persons1
As New List(Of
String) persons1.Add("織田信長") persons1.Add("豊臣秀吉") persons1.Add("徳川家康") MsgBox($"最初の人物は{persons1(0)}です。") 'MsgBox("最初の人物は" & persons1(0) & "です。") '←VB2013以前の場合 |
この例を実行すると「最初の人物は織田信長です。」と表示されます。
このプログラムでは「New」の効果で、 List クラスのインスタンスが1つ生成されます。このインスタンスも、687億個あるメモリのうちどこかに格納されます。どこに格納するかは主にOS(Windows, Linuxなど)が決定します。ちなみに、この例ではListクラスのインスタンスは200~300バイト程度になるようなので、687億個のうち、1600~2400個程度を消費します。
■画像:広大なメモリ空間のどこかにListクラスのインスタンスが配置されます。変数 persons1 を経由してそこにアクセスできます。
この 687億個の広大なメモリ空間は、アドレスと呼ばれる番号で管理されていて、List クラスのインスタンスがメモリ上のどこに保存されたかはアドレスで区別できるようになっています。
このような処理は.NETが自動的にやってくれるので、VBのプログラムで気にする必要はありませんし、制御することもできません。
VBのプログラム上は persons1 という名前をつかって、いつでもこのListクラスのインスタンスにアクセスできます。(このとき、内部では変数persons1が保持しているアドレスを基に、Listクラスのインスタンス本体にアクセスします。)
ここがポイントです。
ここまでの説明はクラスと構造体の両方に当てはまりますが、ここからの説明はクラスと構造体で違いがあります。まずはクラスを前提に考えて見ます。
※上の画像はクラスをイメージしています
クラスのインスタンスを理解するポイントは次の通りです。
この状態を 『persons1 は Listクラスのインスタンスを参照している』と表現することがあります。
persons1 というのは単なる名前です。インスタンスの実体は687億個のメモリ空間のどこかに存在するのです。
このような説明は回りくどいので、この仕組みを無視してよい場合「person1はListクラスのインスタンスです。」と言ってしまう場合もあります。 これは不正確な表現というわけではなく、ある種の国語表現上の文法だと私は思っています。 |
構造体の場合は、事情が異なります。構造体の場合は変数には直接その構造体が格納されています。構造体との違いについては後で触れます。
プログラムを改造して次のようにしてみましょう。
person2という変数を追加し、person2の最初の要素を表示するようにしてみます。
Dim persons1
As New List(Of
String) persons1.Add("織田信長") persons1.Add("豊臣秀吉") persons1.Add("徳川家康") Dim persons2 As List(Of String) persons2 = persons1 MsgBox($"最初の人物は{persons2(0)}です。") 'MsgBox("最初の人物は" & persons2(0) & "です。") '←VB2013以前の場合 |
この新しいプログラムのポイントは次の2つです。
実行すると、さきほどと同じように「最初の人物は織田信長です。」と表示されます。
同じインスタンスをpersons1とpersons2が参照しているので、persons1で追加した内容にpersons2からもアクセスできるというわけです。
この例では New は1つしか登場していないので、インスタンスは1つしか作成されません。
インスタンスを作成するには、コンストラクターを呼び出す必要があり、コンストラクターを呼び出すには New を使うのです。Newを使わずにコンストラクターを呼び出す方法もなくはないのですが、かなりレアで、何か特別な事情があるとき以外は使用しません。 参考:Newを使わずにコンストラクターを呼び出す例。初心者向けではありません。 |
上記のプログラムで Dim person2 As List(Of String) と記述している部分では New が使用されていないので、新しいインスタンスは作成されません。
理解を深めるためにもっと細かく検証してみましょう。
この行が実行された直後はperson2はどのインスタンスのアドレスも持っておらず、イメージで言うと次のような状態になっています。
この何も参照していない状態をVBでは Nothing (ナッシング) と表現します。
この Nothing という言葉は重要なので覚えておいてください。C#や他の言語で null (ヌル) と呼ばれるものと同じものです。
次の行で persons2 = persons1 が実行された段階ではじめて、persons2 は person1と同じインスタンスを参照する状態になります。
意図的に変数が何も参照しない状態にしたいときは Nothing を代入することで実現できます。
Nothingの状態の変数を使ってメソッドやプロパティを呼び出そうとすると NullReferenceException の例外が発生します。呼び出すべき実体が存在しないということです。 参考: |
それでは、次のように persons2 の宣言時に New をつけたら何が起こるでしょうか?
Dim persons1
As New List(Of
String) persons1.Add("織田信長") persons1.Add("豊臣秀吉") persons1.Add("徳川家康") Dim persons2 As New List(Of String) persons2 = persons1 MsgBox($"最初の人物は{persons2(0)}です。") 'MsgBox("最初の人物は" & persons2(0) & "です。") '←VB2013以前の場合 |
「 最初の人物は織田信長です。」と表示される結果は同じですが、途中で行っていることは少し違います。
Dim persons2 As New List(Of String) の行では New が使われているので、インスタンスが生成されます。そして、このインスタンスには 変数persons2 でアクセスできる状態になります。
次のようなイメージです。新しく生成されるインスタンスを黄色で追加してみました。
プログラムの次の行では、persons2 = persons1 が実行されるので、persons2 は persons1 と同じインスタンスを参照することになります。次の状態です。
そして、最後に persons2(0) にアクセスするので、"織田信長" が取得されるというわけです。
この例だと、黄色で表現したインスタンスは、どの変数からもアクセスできない状態で残存します。つまり、何の役にも立たないゴミとして無駄にメモリーを使っている状態になるので、このようなことはやるべきではないのです。
.NETは実行時にこのようなゴミを探し出し、ゴミを削除するガベージコレクター(Garbage Collector、通常 GC)という仕組みを持っています。ガベージコレクターは自動的に動作しますが、それが本当にゴミなのか、何かやるべき処理が残っているか判断したり、やるべき処理が残っているときにそれを実行したり、と結構やることがいろいろあります。ガベージコレクターにまかせないで、プログラマー自身がゴミが発生しないようなプログラムにすることがもっとも効率的です。たとえば、Disposeを持っているクラスであれば、使い終わったときにDisposeを呼び出しておくことでガベージコレクターの負担を減らしてあげることができます。
また、ゴミが大量にある場合などガベージコレクターの処理が追いつかずゴミがたまりすぎてメモリ不足になる場合もあります。
英語で Garbage Collector といえば、ゴミ収集 のような意味のようです。プログラムからガベージコレクターに命令するには GCクラスを使用します。ただ、本文でも書いたようにガベージコレクターの動作は結構複雑で、自動処理に任せておくのが無難です。もし、自分で GCクラスを使ってガベージコレクターに命令したくなったら、そのような状況になっていること事態になにか問題があると考えて、プログラムの設計を見直したほうが良いかもしれません。 |
今度は構造体について説明します。
.NETではメソッドやプロパティをもったオブジェクトがプログラムの主役であり、すべてのオブジェクトはクラスか構造体のどちらかです。
意味的には、何か機能を表しているものがクラスで、データを表しているものが構造体ですが、機能なのか、データなのかの線引きは人によって代わるのでこの区分は目安程度です。結局のところ、そのクラス/構造体を作った人がどういう考えで作ったのかが区別する要素です。
たとえば、Point構造体は座標というデータを表すものなので構造体です。しかし、座標を計算をする機能も持っています。Debugクラスはよく、Debug.WriteLineのように簡単な情報を表示するのに使用しています。これはデバッグ出力を制御する機能を表しているのでクラスです。しかし、デバッグ出力のインデントサイズ(行頭につけるスペースの数)などはデータとして保持しています。
Stringクラスになると微妙で、文字列というデータを表しているので構造体のように思えるかもしれませんが、これはクラスです。おそらく「文字」というデータの集合(=文字列)を操作する機能を表しているのだとおもいます。1文字を表すCharの方は構造体です。
インテリセンスでは、下記のように青い四角が2つ重なったアイコンのものは 構造体です。構造体の場合右側には「Structure」と表示されます。
下記のような黄色いアイコンはクラスです。右側には「Class」と表示されます。
この画像はVisual Studio 2019のものです。Visual Studioのバージョンが違うとアイコンのデザインが異なる場合があります。
新しいフレームワークではインテリセンスが英語で表示されます。待てば日本語に対応してくれるものもあるようですが、最近は新しいものがでるスピードが速くて日本語対応が全然追いついていないようです。2020年11月現在では .NET Framework 4.8 のヒントは英語で表示されます。.NET Framework 4.7.1 は日本で表示されます。 日本語でも、上記の Strucutre や Class はプログラムのキーワードなのでそのまま Structure、Class と表示されます。 |
具体例も挙げておきましょう。
以下のものはすべてクラスです。
以下のものはすべて構造体です。
クラスと構造体の大きな違いは、実体(インスタンス)がどこにあるかです。
構造体の場合は、変数自身が構造体を表しています。
このような実体の持ち方の観点で、変数が直接その実体を持っているものを「値型」(あたいがた)、変数は実体のアドレスを持っているだけものを「参照型」(さんしょうがた)と呼びます。
つまり、構造体は値型で、クラスは参照型です。
このちょっとした意味の違いが、プログラムで大きな違いをもたらします。
Dim p1 As New
Point(4, 5) Dim p2 As Point p2 = p1 p2.X = 123 MsgBox($"Xは{p1.X}です。") 'MsgBox("Xは" & p1.X & "です。") '←VB2013以前の場合 |
このプログラムを実行すると「Xは4です。」と表示されます。
p1 と p2 は実体が別なので、p2に代入した値123は、p1には無関係というわけです。構造体は変数が違うならば、実体も違うのです。
上の方で紹介したクラスの場合は変数は違うのに、実体は同じだから、どちらの変数を使っても同じ値を取得できていました。これが違いです。
この違いをもっと明確にするために自作のクラスと構造体を使って試してみましょう。
まずは自作のクラスを使う例です。
Dataというプロパティを1つだけ持つ MyTestObject というクラスを定義しています。
Testメソッド内で、このクラスの変数obj1とobj2を用意して、obj2を使って値を代入した後、obj1の値が影響を受けているか確認します。
クラスの場合、コンストラクターを呼び出すことで実体(インスタンス)が作成されます。
この例では、変数が2つありますが、コンストラクターの呼び出しは New の1回だけなので実体は1つです。そのため、obj2を使って変更した値はobj1からも取得できます。結果として「DataはXYZです。」と表示されます。
Private Sub Test() Dim obj1 As New MyTestObject obj1.Data = "ABC" Dim obj2 As MyTestObject obj2 = obj1 obj2.Data = "XYZ" MsgBox($"Dataは{obj1.Data}です。") 'MsgBox("Dataは" & obj1.Data & "です。") '←VB2013以前の場合 End Sub Public Class MyTestObject Public Property Data As String End Class |
さて、同じプログラムで、クラスを定義している箇所を構造体の定義に変更します。Public Class ~ End Classという箇所をPublic Structure ~ End Structureにすると構造体の定義になります。他の部分はそのままにします。
Private Sub Test() Dim obj1 As New MyTestObject obj1.Data = "ABC" Dim obj2 As MyTestObject obj2 = obj1 obj2.Data = "XYZ" MsgBox($"Dataは{obj1.Data}です。") 'MsgBox("Dataは" & obj1.Data & "です。") '←VB2013以前の場合 End Sub Public Structure MyTestObject Public Property Data As String End Structure |
構造体の場合、変数がそれぞれ実体(インスタンス)を表していますから、この例では変数が2つあるので、実体(インスタンス)は2つあります。
そのため、obj2に対する操作はobj1に影響しません。
これを実行すると、「DataはABCです。」と表示されます。
Testメソッド内のプログラムはまったく同じなのに、扱っているものがクラスか構造体かによって結果が変わるということです。
博士のワンポイントレッスン 「混乱してきた?」
|
参照型のものをすべて選択して「回答する」をクリックしてください。Visual Studioのインテリセンスを使って調べても良いです。
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 System.ValueTypeクラスを継承しているものが構造体となるので、構造体はクラスの一種であるという考え方も間違いではありません。 しかし、たとえば、哺乳類は魚類から進化した存在だからといって、哺乳類を魚類の一種と表現する人はまずいません。これと同じようなものだと考えておいてください。 |
以上で説明したようにクラスと構造体では変数とインスタンスの扱いがことなっており、 この違いにより、New や Nothing の動作もクラスと構造体で変わります。
主な違いを書きに表でまとめてみます。
クラス (主に機能を表す) |
構造体 (主にデータを表す) |
|
---|---|---|
変数 | 変数はクラスのインスタンスへのアドレスを保持します。 | 変数は構造体自身を表します。 |
宣言の効果 Dim x As Xxx |
変数 x は何も参照していない Nothing になります。 | 既定のコンストラクターが呼び出され、新しいインスタンスが作成されます。 |
Newの効果 Dim x As New Xxx または x = New Xxx など |
コンストラクターが呼び出され、新しいインスタンスが作成されます。 | 既定のコンストラクターが呼び出され、新しいインスタンスが作成されます。 |
Nothingの効果 x = Nothing |
変数 x は何も参照していない Nothing になります。 | 既定のコンストラクターが呼び出され、新しいインスタンスが作成されます。 |
代入の効果 x = y |
x と y は同じインスタンスを参照します。 ※ y が Nothing の場合は、x も Nothingになります。 |
x は y のコピーになります。 ※x と y は値が同じだけれども別物になります。 |
x.y.z という呼び出しで y がクラス/構造体の場合 |
x.y は本体にアクセスします。 x.y.z の値を変更すると本体の値が変更されます。 |
x.y は本体のコピーにアクセスします。 x.y.z の値を変更しても本体の値は変更されません。 |
注意が必要な下記3点です。
表の一番最後の「x.y.zの呼び出しで・・・」は小難しい上に重要なのですが、このせいで間違いをする人が続出したためVisual Studio 2005以上では、この間違いをするとエラーになるようになりました。そのため、このせいで致命的な間違いをすることを現時点(2020年)ではないと思ってよいです。
表の一番最後の「x.y.zの呼び出しで・・・」は小難しい上に重要なのですが、このせいで間違いをする人が続出したためVisual Studio 2005以上では、この間違いをするとエラーになるようになりました。そのため、このせいで致命的な間違いをすることを現時点(2020年)ではないと思ってよいです。 エラーメッセージの「式が値であるため、代入式のターゲットにすることはできません。」はわかりにくいメッセージですが、要するにLocationが構造体なので、Me.Location は本体のコピーを表していて、コピーのプロパティ(X)を変更する操作は無意味であるという意味です。 この例の場合、本体の値を変更したければ次のように修正するのが正解です。
または、
このエラーについての公式ドキュメント https://docs.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/error-messages/expression-is-a-value-and-therefore-cannot-be-the-target-of-an-assignment |
2つの簡単なルールを守れば、このような紛らわしい違いをあまり気にしなくて良くなります。(残念ですが完全に気にしなくて良いようにはなりません。)
ルール1.インスタンスを作成するときには必ず New を使いましょう。
構造体は New つけてもつけなくてもインスタンスを作成できます。必ず New をつけることにして、クラスと構造体を同じ扱いにしましょう。
ルール2.Nothing を代入しない。
x = Nothing のような記述はほとんど必要ないはずでやめましょう。
構造体の値を初期値(たとえば、Integerの場合 0)にする目的で Nothing を代入する人がたまにいますが、止めましょう。
クラスと構造体の大きな違いで、この他に残るのは = による代入です。
クラスを代入すると、同じ実体をもつ変数がもう1つできるだけですが、構造体は代入を実行するとコピーが作成されるという性質があります。
実はこの違いは、とても役に立っており通常は困りません。無理に同じように扱う必要はないのです。
でも、たまにこの性質の違いで何かミスをしてしまうかもしれません。それは個別に気をつけるしかありません。