VB.NET のクラス・継承・インターフェイス(.NET 10 実行)
Overrides と Shadows の違いを実出力で確認。BC40003/40004/40005 の警告、Implements 句、WithEvents、Using、Structure と Class の実測まで。
シリーズ:VB.NET 講座 3/6(中級)。 対象は、プロシージャは書けるようになって、次に型を設計する段階の人です。業務システムの VB.NET 保守を引き継いだ人や、VB6 から移ってきた人も想定しています。読み終えると、Class・Structure・Module の使い分けを決められ、継承したメソッドのどれが呼ばれるかを予測でき、VB が出す BC4xxxx 警告の意味が読めるようになります。
VB.NET の保守案件では、派生クラスのメソッドが「呼ばれるはず」なのに呼ばれない、という不具合によく出会います。原因の多くは Overrides の書き忘れです。VB はこのコードをエラーにせず警告で通すので、テストで基底クラスの動きが出て初めて気づきます。この講座では VB のオブジェクト指向機能を .NET 10 で一通り実行し、実際の出力と、日本語環境の SDK が出すコンパイラ警告・エラー(BC40003、BC40004、BC40005、BC30149、BC30387 など)をそのまま載せます。
環境: .NET SDK 10.0.403、ランタイム 10.0.14、VB 言語バージョン 17.13(SDK の既定値)、Apple Silicon Mac(M1 Pro)。すべてのファイルの先頭は Option Strict On。時間計測は Release ビルドです。メッセージは DOTNET_CLI_UI_LANGUAGE=ja で日本語表示にしました。
クラス・プロパティ・コンストラクター
プロジェクトを作って Program.vb を差し替えます。この講座のコードブロックを上から順につなげると1つのファイルになります。
dotnet new console -lang VB -o oop
cd oop
dotnet run
最初のブロックは基底クラスと派生クラスです。「自分自身」を指す書き方が3つあることに注目してください。Me(このインスタンス。仮想呼び出し)、MyBase(親クラスの実装)、MyClass(このクラスの実装。サブクラスのオーバーライドを無視)。C# には this と base はありますが、MyClass に当たるものはありません。
Option Strict On
Imports System
' --- 1. Shadows vs Overrides ---
Public Class Animal
Public Overridable Function Speak() As String
Return "..."
End Function
Public Function Name() As String
Return "Animal"
End Function
Public Sub Describe()
Console.WriteLine($" Me.Speak()={Me.Speak()} MyClass.Speak()={MyClass.Speak()}")
End Sub
End Class
Public Class Dog
Inherits Animal
Public Overrides Function Speak() As String
Return "Woof (base said " & MyBase.Speak() & ")"
End Function
Public Shadows Function Name() As String
Return "Dog"
End Function
End Class
コンストラクターは Sub New です。ReadOnly の自動実装プロパティはコンストラクター内でだけ代入できます。MustInherit は C# の abstract class、MustOverride は抽象メンバーです。
' --- 2. MustInherit / constructors ---
Public MustInherit Class Shape
Public ReadOnly Property Label As String
Protected Sub New(label As String)
Me.Label = label
Console.WriteLine($" Shape.New({label})")
End Sub
Public MustOverride Function Area() As Double
End Class
Public Class Circle
Inherits Shape
Public Property Radius As Double
Public Sub New(r As Double)
MyBase.New("circle")
Radius = r
Console.WriteLine($" Circle.New({r})")
End Sub
Public Overrides Function Area() As Double
Return Math.PI * Radius * Radius
End Function
End Class
Me.Label = label は書き間違いではありません。VB は大文字と小文字を区別しないので Label と label は同じ識別子で、コンストラクターの中では引数が優先されます。Me. を付けてプロパティを明示しています。C# から来た人が label と Label を別物のつもりで書いて混乱する、典型的な場所です。
Overrides と Shadows:意外な出力
最後に載せる Main は、同じオブジェクトを Dog 型の変数と Animal 型の変数から呼びます。
1) Overrides vs Shadows
Dog var : Speak=Woof (base said ...), Name=Dog
Animal var: Speak=Woof (base said ...), Name=Animal
Me.Speak()=Woof (base said ...) MyClass.Speak()=...
Speak は Overrides なので実行時の型が決め、どちらの行も "Woof" です。Name は Shadows なので変数の宣言型が決め、中身は Dog なのに Animal 型の変数からは "Animal" が返ります。さらに Describe の中の MyClass.Speak() はオーバーライドを無視して Animal.Speak を実行しました。
よくある間違い:キーワードの書き忘れ
古いコードで一番多いのは、派生側のメソッドにどちらのキーワードも付いていないケースです。VB はこれを拒否しません。何が起きるかを確かめるため、キーワードなしの Cat と、基底の「関数」Name に対して「プロパティ」Name を宣言した Bird を書きました。
Public Class Cat
Inherits Animal
Public Function Speak() As String ' no Overrides
Return "Meow"
End Function
Public Function Name() As String ' no Shadows
Return "Cat"
End Function
End Class
Public Class Bird
Inherits Animal
Public Property Name As String = "Bird" ' property vs base function
End Class
Program.vb(12,21): warning BC40005: function 'Speak' は、ベース class 'Animal' のオーバーライド可能なメソッドをシャドウします。ベース メソッドをオーバーライドするには、このメソッドは 'Overrides' に宣言されていなければなりません。
Program.vb(15,21): warning BC40003: function 'Name' は、ベース class 'Animal' で宣言されたオーバーロード可能なメンバーをシャドウします。ベース メソッドをオーバーロードする場合、このメソッドは 'Overloads' に宣言されていなければなりません。
Program.vb(29,21): warning BC40004: property 'Name' は、function 'Name' とベース class 'Animal' で競合しています。'Shadows' として宣言されなければなりません。
警告コードは3種類に分かれました。BC40004 は「種類の違うメンバーが同じ名前」(関数に対するプロパティ、メソッドに対するフィールド)のときだけで、同じシグネチャのメソッドに Overrides を付け忘れたときは BC40005 です。BC40004 で検索しても自分の状況と合わないのはこのためです。実行結果は次のとおりです。
... / Animal <- Dim a As Animal = New Cat()
Meow / Cat <- Dim c As New Cat()
基底型の変数からは Cat の実装が一度も呼ばれません。対策: ポリモーフィズムが目的なら Overrides を書きます。Shadows は本当に隠したいときだけ使い、理由をコメントに残します。プロジェクト単位では .vbproj に <WarningsAsErrors>40003,40004,40005</WarningsAsErrors> を書けば、これらをビルドエラーにできます。
逆に、基底側が Overridable でないメソッドに Overrides を付けるのは警告ではなくエラーです。
error BC31086: 'Overridable' として宣言されていないため、'Public Overrides Function Name() As String' で 'Public Function Name() As String' をオーバーライドすることはできません。
コンストラクターと MustInherit のエラー集
以下はすべて、Protected Sub New(label As String) しか持たない上の Shape を少しずつ変えて出したものです。
| 書いたコード | コード | メッセージ(抜粋) |
|---|---|---|
New Shape("x") |
BC30569 | 'MustInherit' として宣言されたクラスでは、'New' を使用することはできません |
Area を実装しない派生クラス |
BC30610 | 'MustInherit' として宣言するか、…'MustOverride' メンバーをオーバーライドしなければなりません |
Sub New のない派生クラス |
BC30387 | 基底クラス 'Shape' に引数なしで呼び出せるアクセス可能な 'Sub New' が指定されていないため… |
MyBase.New の前に Console.WriteLine |
BC30148 + BC30282 | コンストラクターの呼び出しは、インスタンス コンストラクター内の最初のステートメントとしてのみ有効です |
Structure に引数なし Sub New() |
BC30629 | 構造体は、パラメーターなしの非共有の 'Sub New' を宣言できません |
Structure に Inherits |
BC30628 | 構造体に 'Inherits' ステートメントを指定することはできません |
コンストラクターは基底から順に実行されます。
2) MustInherit + constructor chain
Shape.New(circle)
Circle.New(2)
circle area = 12.57
インターフェイス:C# と違う明示的な Implements
C# では、名前とシグネチャがインターフェイスのメンバーと一致する public メソッドが自動的に実装扱いになります。VB ではそうなりません。実装するメンバーすべてに Implements 句が必要で、その代わり C# にない自由があります。メソッド名を変えられる、Private にできる、1つのメソッドで複数のインターフェイスのメンバーをまとめて実装できる、の3点です。
' --- 3. Interfaces with explicit Implements ---
Public Interface IPrinter
Sub Print(text As String)
End Interface
Public Interface ILogger
Sub Write(text As String)
End Interface
Public Class Console2
Implements IPrinter, ILogger
' one method, two interface members, any name we like
Private Sub Output(text As String) Implements IPrinter.Print, ILogger.Write
Console.WriteLine(" [out] " & text)
End Sub
End Class
3) Implements
[out] via IPrinter
[out] via ILogger
ここから2つのことが言えます。句を書かずに Public Sub Print(text As String) としても、名前の一致は助けになりません。
error BC30149: Class 'P' は 'Sub Print(text As String)' をインターフェイス 'IPrinter' に実装しなければなりません。
また、メソッドが Private で名前も Output なので、クラス自体には Print がありません。c.Print("x") は BC30456: 'Print' は 'Console2' のメンバーではありません。 で止まり、CType(c, IPrinter).Print(...) のようにインターフェイス経由で呼ぶ必要があります。C# の「明示的なインターフェイス実装」と同じ形ですが、VB ではこれが唯一の書き方です。
Shared・Module・イベント・Using
Shared は C# の static です。Module はメンバーがすべて共有でインスタンスを作れないクラスで、メンバーはプロジェクトの名前空間に取り込まれるため、Shout(...) と前置きなしで呼べます。VB6 の標準モジュール(.bas)の後継と考えると分かりやすいでしょう。
' --- 4. Shared + Module ---
Public Class Counter
Public Shared Property Created As Integer
Public Sub New()
Created += 1
End Sub
End Class
Module TextUtil
Function Shout(s As String) As String
Return s.ToUpperInvariant() & "!"
End Function
End Module
共有メンバーをインスタンス経由で読む(k.Created)とコンパイルは通りますが、警告が出ます。
warning BC42025: 共有メンバー、定数メンバー、列挙型メンバー、または入れ子にされた型にインスタンス経由でアクセスしています。正規の式は評価されません。
「式は評価されません」が要点です。MakeCounter().Created(MakeCounter は1行表示する関数)で試すと、7 だけが出て "MakeCounter called" は表示されませんでした。呼び出しそのものが省かれます。
イベントの購読は2通りあります。WithEvents + Handles は宣言的で、フィールドに代入した時点で自動的につながります。AddHandler / RemoveHandler は動的で、外すこともできます。
' --- 5. Events ---
Public Class Downloader
Public Event Progress(percent As Integer)
Public Sub Run()
For p = 50 To 100 Step 50
RaiseEvent Progress(p)
Next
End Sub
End Class
Public Class Screen
Private WithEvents dl As New Downloader()
Private Sub OnProgress(percent As Integer) Handles dl.Progress
Console.WriteLine($" Handles: {percent}%")
End Sub
Public Sub Start()
dl.Run()
End Sub
End Class
フィールドから WithEvents を消すと BC30506: Handles 句には、それを含む型または基本型の 1 つで定義された WithEvents 変数が必要です。 になります。また Public Event Progress(percent As Integer) と書くだけで Downloader.ProgressEventHandler というデリゲート型も生成され、Main ではそれを AddHandler に使っています。
Using は例外が起きても Dispose を保証します。1つの文に2つ書くと、逆順に破棄されます。
' --- 6. IDisposable / Using ---
Public Class TempResource
Implements IDisposable
Private ReadOnly _name As String
Public Sub New(name As String)
_name = name
Console.WriteLine($" open {name}")
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Console.WriteLine($" dispose {_name}")
End Sub
End Class
Module Program
Sub Main()
Console.WriteLine("1) Overrides vs Shadows")
Dim d As New Dog()
Dim a As Animal = d
Console.WriteLine($" Dog var : Speak={d.Speak()}, Name={d.Name()}")
Console.WriteLine($" Animal var: Speak={a.Speak()}, Name={a.Name()}")
d.Describe()
Console.WriteLine("2) MustInherit + constructor chain")
Dim s As Shape = New Circle(2)
Console.WriteLine($" {s.Label} area = {s.Area():F2}")
Console.WriteLine("3) Implements")
Dim c As New Console2()
CType(c, IPrinter).Print("via IPrinter")
DirectCast(c, ILogger).Write("via ILogger")
Console.WriteLine("4) Shared + Module")
Dim x As New Counter(), y As New Counter()
Console.WriteLine($" Counter.Created = {Counter.Created}")
Console.WriteLine($" {Shout("module call")}")
Console.WriteLine("5) Events")
Dim scr As New Screen()
scr.Start()
Dim dl2 As New Downloader()
Dim h As Downloader.ProgressEventHandler = Sub(p) Console.WriteLine($" AddHandler: {p}%")
AddHandler dl2.Progress, h
dl2.Run()
RemoveHandler dl2.Progress, h
dl2.Run()
Console.WriteLine(" (after RemoveHandler: nothing printed)")
Console.WriteLine("6) Using")
Using r1 As New TempResource("A"), r2 As New TempResource("B")
Console.WriteLine(" body")
End Using
End Sub
End Module
4) Shared + Module
Counter.Created = 2
MODULE CALL!
5) Events
Handles: 50%
Handles: 100%
AddHandler: 50%
AddHandler: 100%
(after RemoveHandler: nothing printed)
6) Using
open A
open B
body
dispose B
dispose A
RemoveHandler の落とし穴:デリゲートは変数(上の h)に保持してください。同じラムダを2回書くと別のデリゲートになり、何も外れません。VB はこれを警告してくれます。
warning BC42326: ラムダ式はこのイベント ハンドラーから削除されません。ラムダ式を変数に割り当て、その変数を使用してイベントを追加および削除してください。
警告を無視して実行すると、RemoveHandler の後もハンドラーが "ping" を表示し続けました。
Class・Structure・Module の判断表
| 要件 | Module | Structure | Class |
|---|---|---|---|
| 状態を持つインスタンス | 不可 | 可 | 可 |
継承(Inherits) |
不可 | 不可(BC30628) | 可 |
| インターフェイス実装 | 不可 | 可(インターフェイスとして扱うとボックス化) | 可 |
引数なし Sub New |
— | 不可(BC30629) | 可 |
| 代入でコピーされるもの | — | 値全体 | 参照 |
Nothing を代入すると |
— | 既定値に戻る | オブジェクトなし |
| 向いている用途 | 状態のない補助関数、Main、拡張メソッド |
小さく不変な値(座標、金額、範囲) | それ以外のほぼすべて |
よくある間違い:Structure のコピーを書き換える
値型の意味論は実際の不具合につながります。Public Structure PointS : Public X As Integer で試しました。
Dim arr() As PointS = {New PointS With {.X = 1}}
Dim p = arr(0) ' copy
p.X = 99 ' arr(0).X is still 1
arr(0).X = 99 ' array element: edits in place
For Each q In arr
q.X = 5 ' compiles, but edits a copy
Next
Structure: arr(0).X = 1
Structure via index: arr(0).X = 99
After For Each: arr(0).X = 99
For Each の中の代入はエラーにも警告にもならず、黙ってコピーを書き換えます。List(Of PointS) の場合は list(0).X = 99 自体がコンパイルできません。インデクサーがコピーを返すからです:BC30068: 式が値であるため、代入式のターゲットにすることはできません。 対策は、取り出して変更して書き戻す(Dim t = list(0) : t.X = 99 : list(0) = t)か、構造体を不変にしてこの問題自体をなくすことです。
実測:Structure 100万個と Class 100万個
Double フィールド2つの点を100万個、Structure と Class でそれぞれ配列に詰め、その後フィールドを合計しました。Release ビルド、ウォームアップ1回、各回の前に GC.Collect()、割り当て量は GC.GetAllocatedBytesForCurrentThread() で測定。5回の結果です。
| 回 | Structure 作成 | Structure 合計 | Class 作成 | Class 合計 |
|---|---|---|---|---|
| 1 | 2.01 ms | 1.19 ms | 27.84 ms | 1.22 ms |
| 2 | 1.92 ms | 1.20 ms | 30.44 ms | 1.57 ms |
| 3 | 4.04 ms | 1.01 ms | 28.35 ms | 1.57 ms |
| 4 | 1.73 ms | 1.12 ms | 26.61 ms | 1.06 ms |
| 5 | 0.76 ms | 1.01 ms | 31.32 ms | 1.55 ms |
| 割り当て | 15.3 MiB | 38.1 MiB |
割り当て量はメモリ配置どおりです。Structure の配列は 100万 × 16 バイトの1つの塊。Class 版は 8 バイトの参照の配列に加えて、32 バイト(オブジェクトヘッダー 16 バイト + フィールド 16 バイト)のオブジェクトが100万個で、8MB + 32MB ≒ 38.1MiB になります。作成は約15倍遅くなりました。合計がほぼ同じなのは、ループで連続して作ったオブジェクトがメモリ上でも隣り合っているからです。長く動くアプリでオブジェクトが別々のタイミングで作られると、この局所性は保証されません。同じプログラムを別のセッションで動かしたときは Class 作成が 25〜42 ミリ秒だったので、絶対値ではなく比率を結論として読んでください。
正直な限界とバージョンの注記
- 上記はすべて .NET SDK 10.0.403 / ランタイム 10.0.14、既定の言語バージョン 17.13 でコンパイル・実行しました。
-p:LangVersionで古いバージョンも試し、ReadOnly自動実装プロパティと$"…"は 10 で失敗(BC36716: Visual Basic 10.0 は 読み取り専用の自動実装プロパティ をサポートしません。)、14 から通りました。計測コードの桁区切り1_000_000は 15 が必要です。 - VB.NET は保守モードです。新しい .NET でもコンパイルできるよう維持されていますが、言語機能は追加されません。C# の
record、init、プライマリコンストラクターに当たる VB 構文はありません。 - 計測は Stopwatch による手書きループで、BenchmarkDotNet ではありません(NuGet パッケージの追加は今回の範囲外)。桁は信頼できますが、1桁台の差は誤差の範囲です。
- Windows Forms はここでは試していません(Windows が必要)。
WithEventsの動きはコンソールアプリでも同じで、実行したのはコンソール版です。
C# 側から同じ概念を確認したい場合は、C# のクラス継承、C# のインターフェイス、C# の構造体、C# のイベントも参照してください。
前回: VB.NET 講座 2:プロシージャ、ByVal/ByRef、制御構文 · 次回: VB.NET 講座 4:LINQ とコレクション
よくある質問
VB.NET の Shadows と Overrides は何が違いますか?
Overrides はオブジェクトの実際の型に従い、As Animal で宣言した変数に Dog を入れても Dog のメソッドが呼ばれます。Shadows は変数の宣言型に従うので、同じ変数から Animal のメソッドが呼ばれます。どちらも書かないと警告(BC40005 または BC40003)が出て、Shadows 扱いになります。
なぜ VB.NET はメソッドごとに Implements 句が必要なのですか?
VB はインターフェイスのメンバーを名前ではなく明示的に対応付けるからです。メソッド名は自由で、Private にもでき、1つのメソッドで複数のインターフェイスのメンバーを実装できます。名前が一致していても句がなければ BC30149 になります。
Structure は Class より速いですか?
小さな値を大量に作る場面では速いです。Double 2つの点を100万個配列に詰めると、Structure は約1.9ミリ秒・15.3MiB、Class は約28ミリ秒・38.1MiB でした(Release、.NET 10、M1 Pro)。作った後の合計はほぼ同じです。代わりにコピーの意味論に注意が必要です。