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 を実行しました。

判断図。As Animal で宣言され Dog を入れた変数への呼び出しは、Dog 側のメンバーが Overrides か Shadows かで分かれる。Overrides なら実行時の型が決めるので a.Speak() は Dog.Speak を呼び Woof と出る。Shadows なら宣言型が決めるので a.Name() は Animal.Name を呼び Animal と出る。どちらのキーワードも書かないと警告 BC40005 または BC40003 が出て、Shadows と同じ動きになる。
Dim a As Animal = New Dog() で呼んだとき、どのメソッドが動くか。Overrides はオブジェクトに従い、Shadows は変数の宣言型に従う。

よくある間違い:キーワードの書き忘れ

古いコードで一番多いのは、派生側のメソッドにどちらのキーワードも付いていないケースです。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、拡張メソッド 小さく不変な値(座標、金額、範囲) それ以外のほぼすべて
判断図。まず、インスタンスごとの状態が必要か。不要なら、状態を持たない補助関数は Module にする。必要なら、16バイト程度以下の小さな値で、継承もせず、作った後に書き換えないか。そうなら Structure。それ以外は Class で、継承・イベント・IDisposable もこちら。
VB.NET で Module・Structure・Class のどれにするかの決め方。たいていは Class になる。

よくある間違い: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万個作るのに1.9ミリ秒、割り当ては15.3MiB。Class の点を100万個作るのに28.4ミリ秒、割り当ては38.1MiB。作った後に座標を合計する時間は Structure 配列が1.1ミリ秒、Class 配列が1.6ミリ秒。
100万個の点を配列に詰める時間:Structure と Class。ウォームアップ後5回の中央値、Release、.NET 10.0.14、Apple Silicon Mac(M1 Pro)。

割り当て量はメモリ配置どおりです。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)。作った後の合計はほぼ同じです。代わりにコピーの意味論に注意が必要です。

  • #VB.NET
  • #.NET 10
  • #オブジェクト指向
  • #継承
  • #インターフェイス
  • #中級

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑