Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第9回 変数のスコープと寿命

2020/4/18

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 ◎ 対象です。
VB2017 Visual Basic 2017 ◎ 対象です。
VB2015 Visual Basic 2015 ◎ 対象です。
VB2013 Visual Basic 2013 ◎ 対象です。
VB2012 Visual Basic 2012 ◎ 対象です。
VB2010 Visual Basic 2010 ◎ 対象です。
VB2008 Visual Basic 2008 ◎ 対象です。
VB2005 Visual Basic 2005 ◎ 対象です。
VB.NET 2003 Visual Basic.NET 2003 △ 対象外ですがほとんどの説明があてはまるので参考になります。
VB.NET 2002 Visual Basic.NET (2002) △ 対象外ですがほとんどの説明があてはまるので参考になります。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.変数の適用範囲

1-1.スコープ

変数を宣言しても使える場所と使えない場所があります。どこでどのように変数を宣言するかで使える場所が変わります。変数が使える場所のことを変数の「スコープ」または「適用範囲」(てきようはんい)と呼びます。

変数のスコープには次の3種類があります。

まだ初級講座で登場していない言葉もありますが、ひとまず一覧を紹介します。

 

ブロックスコープ

一番狭いスコープです。このスコープの変数はブロック内でのみ使用できるスコープです。

プロシージャスコープ

同じプロシージャ内ならどこでも有効なスコープです。このスコープの変数はプロシージャ内でのみ使用できます。

クラスレベルのスコープ

クラス・モジュール内で有効なスコープです。このスコープの変数は同じクラス・モジュールで有効です。

マイクロソフトのドキュメントでは「モジュールのスコープ」と表現されていますが、「クラスレベル」という言葉の方が一般的に使われているように思うのでここでは言葉を変えました。

 

参考

Visual Basic におけるスコープ

https://docs.microsoft.com/ja-jp/dotnet/visual-basic/programming-guide/language-features/declared-elements/scope

 

このあと、1つずつ説明しますが、先に概要がわかるプログラムを紹介します。

このプログラムにはまだ初級講座では登場していないIf ~ End If の構文が登場しています。VBにはこのように ○○ ~ End ○○ という構造が多く登場します。Class ~ End Class、Sub ~ End Subなどは今まで意味を解説はしていませんが、サンプルにも何度も登場していたと思います。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Dim x As Integer '←この変数 x は クラスレベルのスコープ

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim y As Integer '←この変数 y はプロシージャスコープ

        If x = y Then
            Dim z As Integer '←この変数 z はブロックスコープ
            z = x + y
        End If

    End Sub

End Class

変数のスコープの問題とは要するにこの○○ ~ End ○○ の間で宣言した変数は、その○○ ~ End ○○の中でしか使用できませんよということなのです。

たとえば、変数 z は If ~ End If の間で宣言されているので、このブロックの外側では使用できません。

今回は説明の都合上初級講座でまだ説明していないキーワードが多数登場しますが、個々の機能については別途説明していきますので知らない機能については心配しないでください。私が主に今回説明したいのはブロックが存在する場合の変数の使い方であり、個々のブロックの機能ではないのです。

 

1-2.ブロックスコープ

○○ ~ End ○○ という構造のほかに、For ~ Nextなどブロックを作る構造はいくつかあります。

VBでブロックを作る代表的なキーワードをまとめると次のようになります。

この表では参考にブロックの機能も書いておきますが、どの機能も初級講座の現時点では説明していないで、具体的なことは今後説明していきます。

ブロックを作るキーワード 機能
If ~ End If 条件に合致する場合に処理を記述します。(間がElse、ElseIfで分割されている場合もあります。)
Try ~ End Try エラーが発生した場合の処理を記述します。(間がCatch、Finallyで分割されている場合もあります。)
While ~ End While 条件が成立するまで繰り返して実行する処理を記述します。
Using ~ End Using リソースが確実に開放される処理を記述します。
SyncLock ~ End SyncLock マルチスレッドで実行できない処理を記述します。
With ~ End With 暗黙にオブジェクトを参照する処理を記述します。
Do ~ Loop 条件に合う場合繰り返して実行する処理を記述します。
For ~ Next 決められた回数実行する処理を記述します。
Select ~ End Select 条件に応じて分岐する処理を記述します。間は Case で分割されます。

知らないキーワードが登場してもVisual Studioではブロックは自動的にインデントされるので、ブロックであることを知ることはできます。

ブロックは入れ子にすることもできます。たとえば、If ~ End If の間に、Do ~ Loop を記述することもできます。

こららのブロック内で宣言した変数はそのブロック内でのみ使用できるブロックスコープになります。複数のブロックをまたいで同じ変数を使いたい場合は、ブロックの外で変数を宣言し、変数のスコープを1段階上にする必要があります。

 

下記にスコープを間違ってエラーになる例を示します。

 VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

この例には3つのブロックが登場し、変数 userName は1つの目のブロック内で宣言されています。それを3つ目のブロック内で使用しようとしている出エラーです。

Visual Studioはこの箇所の userName に赤い波線を表示して、次のようなエラーを出力します。

'userName' は宣言されていません。アクセスできない保護レベルになっています。

 

解決方法の1つはuserNameを上位で宣言することです。たとえば、次のようにします。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim userName As String

    Try
        userName = InputBox("お名前を教えてください。")
    Catch ex As Exception
        'ここに書いた処理はエラー発生時にだけ実行されます。
        MsgBox("エラー発生 " & ex.Message)
    End Try

    If Len(userName) > 0 Then
        MsgBox("こんにちは、" & userName & "さん。")
    End If

End Sub

変数userNameを宣言する場所を変えただけですが、この位置だとすべてのブロックの外側になり、次で説明するプロシージャスコープになります。(要するにSub ~ End Sub内で有効になります。)

そのため、複数のブロックをまたいで使用できるようになります。

メモ メモ  -  でも、緑の波線が・・・
この例のように修正すると確かに赤い波線はなくなって実行できるようになります。でも、Visual StudioはLen(userName)のuserNameに緑の波線を表示して、何かを警告します。[表示]メニューのエラー一覧で確認してみると次のように表示されます。

変数 'userName' は、値が割り当てられる前に使用されています。Null 参照の例外が実行時に発生する可能性があります。

これは変数userNameの初期化がされない状況で Len(userName)が実行される可能性があることを警告するもので、初級講座 第2回 変数と定数 で説明します。一見、userName = InputBox(・・・)の行で値が代入されるので、Len(userName)の時点では初期化が完了しているはずのように見えますが、このプログラムではTryを使っているため、InputBoxの行でエラーが発生すると、初期化が完了しないままLen(userName)が実行される可能性があるのです。Visual Studioはそれを警告しています。

 

発展 発展学習  -  で、どうすれば緑の波線を消せますか?

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

Len関数は初期化されていない変数でも正常に処理してくれることを『私は知っている』ので、問題はないのですが、緑の波線がなんか嫌な感じがするのでVisual Studioをおとなしくさせるためには、Dim userName As Stringの行でDim userName As String = Nothing と書いて無意味な初期化を行います。初期化していない文字列型の値はNothingなので、明示的に = Nothing を書く必要は機能上はないのですが、これを記述することでVisual Studioは初期化はされているものとみなしLen(userName)を警告しなくなります。
ここからは私の推測ですが、書かなくても良い = Nothing をわざわざ記述したのですから、プログラマーは初期化について責任を持つとみなしてもよいはずです。そのためVisual Studioがこれを見て警告しなくなるのは良い動作だと思います。#Disable Warning BC42104 と記述することでこの警告をこの部分だけ無効にすると明示的な指示をすることもできますが、プログラムが汚くなるので、変数1個のためにこれは使いたくないものです。

 

もう1つの解決方法はuserNameを必要とする処理を同じブロック内に集めることです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Try
        Dim userName As String
        userName = InputBox("お名前を教えてください。")

        If Len(userName) > 0 Then
            MsgBox("こんにちは、" & userName & "さん。")
        End If
    Catch ex As Exception
        'ここに書いた処理はエラー発生時にだけ実行されます。
        MsgBox("エラー発生 " & ex.Message)
    End Try

End Sub

これで変数 userName を使う処理はすべて Try ~ Catch の中に集められました。userNameはこのブロック内で有効なので正常に動作します。

このプログラムではブロックの中にブロックが入っているブロックの入れ子状態になっています。この場合親ブロックで宣言された変数は子ブロックでも有効なので問題ありません。

 

ところで、C#では { } を使って特に機能がないブロックを作ることができて、変数のスコープを限定したい目的だけで使うことができます。VBにはこのような機能がないスコープを作る機能はありません。無害なブロックが欲しい場合はWith Me ~ End Withが一番無害です(すべての場所で使用できるわけではありませんが)。もともとWithはプログラムを入力する手間を軽減するだけの存在で、機能は特にありません。下記の例でWith ~ End Withを使って2つのブロックを作り出していますが、ブロック自体には意味はなく、中の変数のスコープを分けているだけです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

With Me
    Dim count As Integer
    count = count + 111
    MsgBox(count) '111と表示される。
End With

With Me
    Dim count As Integer '新しいスコープの変数になる。
    count = count + 222
    MsgBox(count) '222と表示される。
End With

ただ、意味のないWithは混乱しそうなので、私はこのように使うことを特に推奨しているわけではありませんし、このようなことがしたいという欲求を感じたこともほとんどありません。C# 等他の言語で { } を多用していて VBでも同じようなことがしたいというフラストレーションを抱えている方向けの情報だと思ってください。

 

1-3.プロシージャスコープ

プロシージャとはブロック構造の中で大きな構造を指す言葉で、Sub ~ End Sub、Function ~ End Functionが代表です。

言葉が違うだけで構造上はブロックであり、スコープの考え方も同じです。

WindowsフォームアプリケーションではButtonのクリックイベント処理を記述するとButton1_Clickのような名前がついたSub ~ End Subが自動的に作成されます。これはプロシージャです。

このスコープの変数は「ローカル変数」とも呼ばれます。ローカル変数という言葉はプログラムの話や説明をする中でしばしば出てきます。文脈によってはブロックスコープの変数のことも「ローカル変数」と呼ぶ場合もあります。この一連の連鎖危機時ではブロックスコープの変数とプロシージャスコープの変数の両方をローカル変数と呼ぶことにします。

例としてWindowsフォームアプリケーションでButtonを2つ配置してクリックイベントを記述した次のプログラムを考えて見ます。

このプログラムはエラーです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim userName As String
        userName = InputBox("お名前を教えてください。")

    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click

        'ここでエラーです。
        MsgBox("こんにちは、" & userName & "さん。")

    End Sub

End Class

このプログラムでは、1つ目のプロシージャ(Button1_Clickプロシージャ)で宣言しているローカル変数 userName を、別のプロシージャ(Button2_Clickプロシージャ)で使用しようとしています。

プロシージャ内で宣言した変数はそのプロシージャ内でのみで有効なため別のプロシージャで使用することはできません。

 

では、次のようにButton2_ClickプロシージャでもuserName変数を宣言するとどうでしょう?この例でもまだ問題があります。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim userName As String
        userName = InputBox("お名前を教えてください。")

    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click

        Dim userName As String
       
MsgBox("こんにちは、" & userName & "さん。")

    End Sub

End Class

エラーにはなりませんが、Button1を先にクリックして名前を入力してからButton2をクリックしても名前部分は何も表示されないはずです。

変数の名前が同じでも宣言されているプロシージャが異なると別の変数だからです。

このようにプロシージャをまたいで同じ変数を使用したい場合は、変数をもう1段階上のスコープであるクラスレベルのスコープにすることになります。

 

1-4.クラスレベルのスコープ

クラスレベルのスコープという考え方

クラスとは基本的にはClass ~ End Classで囲まれた範囲です。

※クラスの定義は複数のClass ~ End Classに分割されている場合もありますが、この分割には機能上の意味はなく、クラスレベルの変数は分割とは無関係にクラス全体で有効になります。

ここで説明する内容はモジュールにも当てはまります。モジュールはModule ~ End Module で囲まれた範囲を指します。コンソールアプリケーションでは初期状態ではモジュールが作成されます。

 

さて、 次のように変数userNameを宣言する場所を変えると、このクラス(Form1)内のすべての場所(すべてのプロシージャ・すべてのブロック)でこの変数が使用できるようになります。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Dim userName As String

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        userName = InputBox("お名前を教えてください。")

    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click

        MsgBox("こんにちは、" & userName & "さん。")

    End Sub

End Class

 

フィールド

クラスレベルで宣言された変数は、もはや通常の変数ではなく、クラスのメンバーの一員で「フィールド」になります。つまり、クラスレベルのスコープを持つ変数とはフィールドと同じ意味です。

クラスのメンバーは他のクラスからどのように見えるか Private, Protected, Friend, Publicというアクセス指定子と呼ばれるキーワードを使って定義することができます。アクセス指定子を省略したときどのような状態になるのかわかりにくいで、アクセス指定子を必ずつけることを推奨します。

発展 発展学習  -  アクセス指定子の効果

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

詳しい説明は自分でクラスを作成する方法を解説するときに行いますが、Privateがついているものは他のクラスからはアクセスしないもの、Publicがついているものは他のクラスからもアクセスするものです。ProtectedとFriendはアクセスできるクラスに条件をつけるものです。(Protectedは派生クラスからはアクセスする。Friendは同じプロジェクト内のクラスからはアクセスする。)

そのため初級講座の現段階では特に困らないのですが、基本を勉強している段階からクラスレベルの変数はDimではなくPrivate Dim userName As StringやPublic Dim userName As Stringのようにアクセス指定子をつけるようにしましょう。

実際書いてみるとちょっと面白いことがおきます。Dimにアクセス指定子をつけるとVisual Studioは「Dim」の方を省いて省略形にしてしまいます。

たとえば、Private Dim userName As String と書くと、Visual Studioは自動的にDimを省いて、Private userName As String にしてしまいます。なので慣れてくるとはじめからDimを省略して Private userName As Stringと書くことになるでしょう。

結果、まとめると私の非推奨・推奨は次のような記述です。現段階では他のクラスから変数を使用することはないのでアクセス指定子はPrivateにしてください。

× 非推奨 Dim userName As String

○ 推奨 Private userName As String

なお、アクセス指定子の方を省略してDim userName As Stringと書いた場合は、Privateと同じ効果になります。アクセス指定子を省略した場合の効果は変数とそれ以外とでは異なるので、このことは変数についてだけだと覚えておいてください。

 

宣言の順序に意味はない

クラスレベルの変数は変数というよりもクラスのメンバーであるフィールドなのでローカル変数(プロシージャスコープの変数とブロックスコープの変数)とは違うルールが適用される場合がいくつかあります。

たとえば、プロシージャの中では変数を使っている箇所より上で変数を宣言する必要がありますが、クラスレベルの変数はどこで宣言しても良いです。クラスの上の方で宣言しても良いですし、End Classの直前で宣言しても良いです。(End Classの直後とだとクラスの外側になってしまうのでだめです。)

 

型推論はできない

また、ローカル変数では型推論が利用できるのに対し、クラスレベルの変数では型推論は利用できません。

このプログラムではローカル変数である s2 の型推論機能により文字列型になりますが、フィールドであるs1の方では型推論機能が働かず単純に「As 型名」という記述が省略されているだけだとみなされます。「As 型名」が省略されて型推論もできない場合、その変数はObject型になります。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Private s1 = "あいうえお"

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim s2 = "かきくけこ"

    End Sub

End Class

 

2.変数の寿命

変数はそのスコープ内で宣言されたときに生成され、スコープから外れたときに消滅します。

次のプログラムはボタンがクリックされるたびに 1 プラスして、カウントを数えているように見えますが、クリックするたびに「1」と表示されます。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim count As Integer
    count = count + 1

    MsgBox(count)

End Sub

実行がEnd Subに到達した時点で、このプロシージャレベルで宣言されている変数 count は消滅するからです。次にボタンをクリックしたときにまた新しくcount変数が生成されますが、それは新しい変数なので値は 0 です。

この問題を解決するには、やはり変数のスコープを1段階上にあげることになります。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1

    Private count As Integer

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        count = count + 1

        MsgBox(count)

    End Sub

End Class

これでcountはクラスのフィールドになったので、クラスが消滅するまで(フォームの場合、フォームが閉じられるまで)存続します。

この解決方法だと、変数のスコープ自体が変わってしまうのでご注意ください。つまり、本来、プロシージャの中だけでしか使わないのに、消滅すると困るからクラスレベルに格上げしているわけです。そうすると、この変数は他のプロシージャからも見えるようになります。

博士のワンポイントレッスン それってまずいの?
V太 V太:他のプロシージャから見えたっていいじゃないですか?不要なら使わなきゃいいだけですよね?
V太 博士:確かに、使わないようにすれば問題はないの。
B子 B子:じゃあどうして他のプロシージャから見えるようになることに注意しなきゃいけないんですか?
V太 それはのぉ。納得してもらえるかわからんのじゃが、本格的なプログラムをつくるとものすごく大量のクラスや変数を使うことになるのじゃ。話を変数だけにしぼると、長~いプログラムの中に大量の変数があるものだから、間違って変数の値を変更してはいけない箇所で変更してしまうプログラムを書いてしまうことがあるのじゃ。まぁバグってやつじゃがの。
大規模なプロジェクトだと何十人ものプログラマーがいるので管理が行き届かないという事情もある。
V太 ふむふむ。
V太 だから、変更してはいけない変数は、そもそもアクセスできないようになっていたほうがいいのじゃ。そういうわけで、アクセスできなくてもいいはずの変数にアクセスできてしまう状態は注意したほうが良いという意味なのじゃ。
B子 それは・・・。もっと経験をつまないとちゃんとはわからなさそうですね。でも、おっしゃっていることは理解できましたわ。

 

もう1つ回避方法があります。

次のようにDimではなく、Static(読み方:Static = スタティック)を使ってローカル変数を宣言すると、スコープからはずれても変数が消滅しなくなり、意図したとおりカウントアップすることができます。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Static count As Integer
    count = count + 1

    MsgBox(count)

End Sub

こちらの解決方法だと変数のスコープ自体は変化しないので他のプロシージャからアクセスできないのは良いことです。

 

このようにローカル変数であるのに、プロシージャ実行終了時にも消滅しない変数というのは、C#やJava、Pythonなど他のメジャーなプログラミング言語には存在しない機能であり、混乱してしまうプログラマーが多いようです。この機能利用するためのキーワード Static も他の言語では別の意味があり混乱に拍車をかけます。そのため、VBのこの機能は便利ではありますが、使用しないほうがいいのではないかといわれることが多いようです。

VBを勉強中のみなさんも他の原則的なルールの理解が不十分になってしまうため使用しないことをお勧めします。VBのことを十分理解したうえで、この機能を便利に使いたいのであれば止めはしません。

発展 発展学習  -  staticでもマルチスレッドには逆らえません

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

マルチスレッドで値を参照・更新するプログラムは結果がプログラマの意図とは違うものになってしまうことがしばしばあります。複数のスレッドで同じ変数の値を参照・更新する場合は不整合が発生しないように対策が必要ですが、Staticは残念ながらこの対策としては使えません。

たとえば、100回カウントアップする機能を100個のスレッドでほぼ同時に実行してみます。単純計算ではカウントは10000になるはずですが、そうはなりません。実行するたびに違う値になります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim tasks As New List(Of Task)
    For i = 1 To 100
        '100回カウントアップする100個のタスク
        tasks.Add(Task.Factory.StartNew(Sub() CountUp100Times()))
    Next

    Task.WaitAll(tasks.ToArray)

    'もう1回カウントアップして結果を受け取る。10001になっているか?
    Dim last As Integer = CountUp()

    MsgBox(last)
End Sub

Private Sub CountUp100Times()
    For i = 1 To 100
        CountUp()
    Next
End Sub

Private Function CountUp() As Integer
    Static count As Integer
    count = count + 1
    Return count
End Function

もちろん、値の取得・更新のところでSyncLockすればこの部分が同時に実行されることはなくなるので単純計算どおりの結論になります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private locker As New Object

Private Function CountUp() As Integer
    Static count As Integer
    SyncLock locker
        count = count + 1
    End SyncLock
    Return count
End Function