.NET 10 の VB.NET:動くもの・C# 連携で詰まるもの

.NET 10 で VB.NET を検証。Mac で WinForms をビルド、VB で使えない C# 機能の実際の BC エラー、Span の実行時クラッシュ、オーバーフロー検査で約3割の差。

シリーズ:VB.NET 講座 6/6(上級)。 VB.NET の資産を抱え、.NET 10 でどう扱うかを決めなければならないチーム向けです。読み終えると、最新の C# から VB が何を使えて何を使えないかをコンパイラの出力で把握でき、書き直さずに C# を横に足す方法と、唯一はっきり測れた性能差の大きさが分かります。

日本では、業務システムや社内ツールの VB.NET 資産がまだたくさん動いています。保守を担当していると、いつか「C# に作り直すべきでは?」という話が出ます。たいていは印象論で終わりますが、この記事はビルドログで答えます。最新機能を詰め込んだ C# 14 のクラスライブラリを作り、.NET 10 の VB プロジェクトから参照して、出た BC エラー、黙って通ったもの、そして警告ゼロでビルドが通ったのに Main の 1 行目すら出力せずに落ちたプログラムを記録しました。最後に同じループを両言語で測ります。

環境: .NET SDK 10.0.403(Roslyn 5.9.0)、ランタイム 10.0.14、Visual Basic 17.13 と C# 14(SDK の既定)、Apple Silicon Mac(M1 Pro)。時間は JIT ウォームアップ後の Release ビルドです。

2026 年の VB の立ち位置

Microsoft の方針は Microsoft Learn の記事 「.NET Managed languages strategy」(.NET のマネージド言語戦略)に書かれています。要点を言い換えると、Visual Basic は分かりやすく安定した設計を保つこと、C# やランタイムが言語サポートを要する機能を追加しても VB は基本的に「利用のみ(consumption-only)」で新しい構文は避けること、VB を新しいワークロードには広げないこと、そして Visual Studio での体験と C# との相互運用には、特に Windows Forms とライブラリの分野で投資を続けること、です。同じ記事には、VB が対応していないワークロードでは VB 開発者が C# を使うのが一般的だ、という趣旨の記述もあります。

この Mac でも、それを裏づけるものが 2 つ見えました。1 つはコンパイラが知っている言語バージョンの一覧です。

$ dotnet vbc.dll /langversion:?
サポートされる言語バージョン: default 9 10 11 12 14 15 15.3 15.5 16 16.9 17.13 (default) latest

既定は 17.13 ですが、この記事の範囲ではそれが必要な場面はありませんでした。後で出てくる動作例は LangVersion 16.9 でもビルドでき、required のチェックも同じように効きました。もう 1 つは dotnet new list --language VB の結果です。コンソール、クラスライブラリ、WinForms(アプリ・コントロールライブラリ・クラスライブラリ)、WPF(4 種)、MSTest/NUnit/xUnit があり、ASP.NET Core・Blazor・MAUI・Worker Service の VB テンプレートはありません。

つまり、VB はデスクトップアプリとライブラリ向けに保守されている安定した言語で、新しいワークロードは C# の担当です。今回のテストに、これと矛盾する結果はありませんでした。

.NET 10 でビルドできるもの(実測)

コンソールとクラスライブラリ: dotnet new console -lang VB は net10.0 を対象にし、このシリーズの 1〜5 回目はすべてその上で動かしました。

WinForms はビルドのみ。 実行には Windows が必要ですが、ビルドはほかの OS でもできます。追加のプロパティがないと SDK に断られます。

$ dotnet new winforms -lang VB -o wf && cd wf
$ dotnet build
…targets(120,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.
$ dotnet build -p:EnableWindowsTargeting=true
  wf -> ~/demo/wf/bin/Debug/net10.0-windows/wf.dll
Build succeeded.
    0 Warning(s)
    0 Error(s)

テンプレートは net10.0-windows、<UseWindowsForms>true</UseWindowsForms>、<StartupObject>Sub Main</StartupObject> という構成です。ビルドはしましたが、実行はしていません。macOS で DLL を起動すると「Framework: 'Microsoft.WindowsDesktop.App'」が見つからないというエラーで止まります。macOS 用の Windows Desktop ランタイムは存在しないので当然です。Mac や Linux の CI でビルドする意味は、ビルドエラーを早く見つけることにあり、フォームに触るテストはやはり Windows で回す必要があります。

テスト:C# 14 ライブラリを VB から使う

C# 側は dotnet new classlib に 1 ファイルだけです。型ごとに、VB が理解できないかもしれない機能を 1 つずつ入れました。

using System;
using System.Numerics;

namespace ModernLib;

public class Person
{
    public string Name { get; init; } = "";       // C# 9 の init 専用セッター
    public int Age { get; set; }
}

public class Order
{
    public required string Id { get; set; }       // C# 11 の required メンバー
    public decimal Total { get; set; }
}

public ref struct Cursor                          // ref struct(C# 7.2)
{
    private ReadOnlySpan<char> _rest;
    public Cursor(ReadOnlySpan<char> text) { _rest = text; }
    public bool TryNext(out int value)
    {
        value = 0;
        if (_rest.IsEmpty) return false;
        int comma = _rest.IndexOf(',');
        var token = comma < 0 ? _rest : _rest[..comma];
        _rest = comma < 0 ? default : _rest[(comma + 1)..];
        value = int.Parse(token);
        return true;
    }
}

public static class Spans
{
    public static int SumDigits(ReadOnlySpan<char> s)
    {
        int t = 0;
        foreach (var c in s) if (char.IsAsciiDigit(c)) t += c - '0';
        return t;
    }
    public static int CountCommas(ReadOnlySpan<char> s) => s.Count(',');
    // VB 向けの入口:string を受けて int を返す。Span は C# の中だけ。
    public static int SumDigits(string s) => SumDigits(s.AsSpan());
    public static int SumCsv(string csv)
    {
        var cur = new Cursor(csv);
        int total = 0;
        while (cur.TryNext(out var v)) total += v;
        return total;
    }
}

public interface IParsable2<TSelf> where TSelf : IParsable2<TSelf>
{
    static abstract TSelf Parse(string s);          // C# 11 の static abstract
}

public readonly record struct Meters(double Value) : IParsable2<Meters>
{
    public static Meters Parse(string s) => new(double.Parse(s.TrimEnd('m')));
}

public static class Generic
{
    public static T ParseAll<T>(string s) where T : IParsable2<T> => T.Parse(s);
    public static T Sum<T>(T[] xs) where T : INumber<T>     // ジェネリック数値演算(.NET 7)
    {
        T t = T.Zero;
        foreach (var x in xs) t += x;
        return t;
    }
}

public interface IGreeter
{
    string Name { get; }
    string Greet() => $"Hello, {Name}";            // C# 8 の既定インターフェイスメソッド
}

public static class TextExtensions
{
    extension(string s)                            // C# 14 の拡張ブロック
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // 拡張プロパティ
        public string Shout() => s.ToUpperInvariant() + "!";    // 拡張メソッド
    }
}

VB のコンソールプロジェクトから dotnet add reference ../ModernLib/ModernLib.csproj で参照します。どちらも net10.0 で、dotnet build 1 回で両方ビルドされます。

VB で使えないもの:実際の BC エラー

まず、VB 開発者が素直に書きそうなコードです。

Option Strict On
Imports ModernLib

Module Program
    Sub Main()
        Dim p As New Person With {.Name = "Kim"}
        p.Name = "Lee"                                   ' 生成後に init 専用へ代入
        Dim span As ReadOnlySpan(Of Char) = "a1b2".AsSpan()
        Dim c As New Cursor("1,2,3")
        Dim blank = "  ".IsBlank
    End Sub
End Module

Class VbMeters
    Implements IParsable2(Of VbMeters)
End Class

Class VbGreeter
    Implements IGreeter
    Public ReadOnly Property Name As String Implements IGreeter.Name
        Get
            Return "VB"
        End Get
    End Property
End Class

日本語 UI の SDK では、最初のビルドでクラス単位の 2 件だけが出ました。

Program.vb(17,16): error BC37315: Class 'VbMeters' には、共有された抽象または仮想 'Function Parse(s As String) As VbMeters' が含まれているため、インターフェイス 'IParsable2(Of VbMeters)' を実装できません。
Program.vb(21,16): error BC30149: Class 'VbGreeter' は 'Function Greet() As String' をインターフェイス 'IGreeter' に実装しなければなりません。

2 つのクラスを消して初めて、メソッド本体のエラーが出てきました。新しい C# ライブラリに VB を向けると、エラーは「波」になってやって来ると覚えておくと慌てずに済みます。

Program.vb(7,9): error BC37311: 初期化専用プロパティ 'Name' は、オブジェクト メンバー初期化子を使用する場合か、インスタンス コンストラクター内の 'Me'、'MyClass'、'MyBase' でのみ割り当てることができます。
Program.vb(8,21): error BC30668: 'ReadOnlySpan(Of Char)' は廃止されています: 'Types with embedded references are not supported in this version of your compiler.'。
Program.vb(11,22): error BC30668: 'Cursor' は廃止されています: 'Types with embedded references are not supported in this version of your compiler.'。
Program.vb(12,21): error BC30456: 'IsBlank' は 'String' のメンバーではありません。

BC30668 が「廃止されています」と言うのは、C# コンパイラが ref struct に古いコンパイラ向けの Obsolete 属性を付けているからで、Span が非推奨になったわけではありません。日本語メッセージだけ見ると誤解しやすいところです。

そして、エラーより意外だったのが動いたものです。

Dim p As New Person With {.Name = "Kim", .Age = 30}   ' 初期化子なら init 専用も OK
Dim o As New Order With {.Id = "A-1"}                  ' required を設定
Console.WriteLine($"Span wrapper : {Spans.SumDigits("a1b2c3")} / {Spans.SumCsv("10,20,30")}")
Console.WriteLine($"Span-only    : {Spans.CountCommas("a,b,c")}")
Dim m = Generic.ParseAll(Of Meters)("12.5m")
Console.WriteLine($"generic math : {Generic.Sum(New Integer() {1, 2, 3})} {Generic.Sum(New Double() {0.5, 0.25})}")
Console.WriteLine($"ext. method  : {"hi".Shout()}")
Console.WriteLine($"ext. property: {TextExtensions.get_IsBlank("  ")}")
init-only    : Kim
required     : Id = A-1
Span wrapper : 6 / 60
Span-only    : 2
static abstr.: 12.5
generic math : 6 0.75
ext. method  : HI!
ext. property: True

まとめるとこうなります(すべてこのコンパイラで確認)。

C# の機能 .NET 10 の VB 根拠
init セッター With {…} の中だけ設定可 後からの代入は BC37311。LangVersion 16 では初期化子も BC36716
required メンバー チェックされる New Order() で BC37321(LangVersion 16.9 でも)
Span/ReadOnlySpan・ref struct を型名で書く 不可 BC30668
ReadOnlySpan(Of Char) 引数のメソッドに String を渡す 可 CountCommas("a,b,c") = 2
static abstract 制約付きジェネリックメソッドの呼び出し 可 ParseAll(Of Meters) = 12.5
static abstract メンバーを持つインターフェイスの実装 不可 BC37315
ジェネリック数値演算(INumber(Of T)) 呼び出しは可 Integer と Double の Sum
既定インターフェイスメソッド 結局自分で実装が必要 BC30149
C# 14 拡張ブロック内の拡張メソッド 可 "hi".Shout()
C# 14 拡張プロパティ プロパティとしては不可 BC30456。TextExtensions.get_IsBlank(s) なら動くが読みにくい

required の行は「VB は required を無視する」という思い込みを訂正するものです。VB コンパイラは BC37321(必要なメンバーは、オブジェクト初期化子または属性引数に設定する必要があります)を出しました。

よくある間違い:VB で Span が「動いた」あと落ちる

BC30668 は Span の型名を書いたときに出ます。型推論に任せると、コンパイラは通してしまいます。

Dim inferred = "a1b2".AsSpan()                   ' 型名を書かない
Console.WriteLine($"Length = {inferred.Length}")   ' 出力:Length = 4

抜け道に見えますが罠です。VB は ref struct の安全規則を知らないからです。この変数をラムダで捕まえてみます。

Sub Main()
    Console.WriteLine("start")
    Dim inferred = "a1b2".AsSpan()
    Dim f = Function() inferred.Length              ' ラムダが Span をキャプチャ
    Console.WriteLine(f())
End Sub

実際に起きること: ビルドはエラー 0・警告 0 で通り、プログラムは "start" すら出力せずに落ちました。

Unhandled exception. System.TypeLoadException: A ByRef or ByRef-like type cannot be used as the type for an instance field in a non-ByRef-like type.
   at VbErr.Program.Main()

ラムダによって inferred はコンパイラが作るクロージャクラスのフィールドになり、ランタイムは Span 型のフィールドを持つクラスの読み込みを拒否します。失敗するのは行を実行したときではなく Main が JIT コンパイルされるときなので、1 行目の Console.WriteLine も実行されません。C# コンパイラならビルド時に弾いていたコードです。Await をまたいで生かした場合も同じく状態マシンのフィールドになるはずですが、そのケースは実行していません。

直し方: Span を VB のコードに一切渡さないこと。上の SumDigits(String)・SumCsv(String) のように、C# ライブラリ側に VB 向けのオーバーロードを用意して、Span は C# の中に閉じ込めます。

混在ソリューション:VB を残して C# を横に足す

この直し方が、そのまま構成の答えになります。「全部 VB」か「全面書き直し」かの二択ではありません。

図。既存の VB.NET アプリ(コンソールや WinForms)は C# クラスライブラリをプロジェクト参照している。ライブラリの公開 API は String・配列・Task・普通のクラスといった VB で扱いやすい型だけを使い、例えば SumCsv(String) As Integer のような形になる。ライブラリの内側では ReadOnlySpan、ref struct のカーソル、static abstract インターフェイスメンバー、拡張ブロックを使う。これらは VB プロジェクト側には出てこない。両プロジェクトとも net10.0 で、同じ dotnet build でビルドされる。
今回組んだ混在ソリューションの形。VB アプリはそのまま残し、普通の型だけを公開する小さな C# ライブラリを呼ぶ。Span・ref struct・新しい C# 機能は C# の内側に閉じ込める。
  1. VB プロジェクトはそのまま、対象を net10.0(WinForms なら net10.0-windows)に上げる。
  2. 同じソリューションに C# クラスライブラリを追加し、VB から C# へプロジェクト参照する。
  3. 新しい機能が必要なコード(Span での解析、非同期ストリーム、ソースジェネレーター、static abstract インターフェイス)はそのライブラリに書く。
  4. 公開するのは VB が扱いやすい型だけにする:String、配列、List(Of T)、Task(Of T)、普通のプロパティや init を持つクラス。非同期ストリームは IAsyncEnumerable ではなく Task(Of List(Of T)) やコールバックで渡す。VB には Await For Each がないからです(第 5 回参照)。
  5. VB のモジュールを C# に移すのは、どうせ手を入れるときだけにする。

逆向き(C# プロジェクトから VB ライブラリを使う)は特別なことは不要です。VB のクラスライブラリも普通の .NET メタデータだからです。注意点として、ビルドのプロパティは参照先にも伝わります。VB プロジェクトで dotnet build -p:LangVersion=17 とすると、C# プロジェクトの側で error CS1617 が出て失敗しました。コマンドラインの -p: はビルド内の全プロジェクトに効くので、LangVersion は各プロジェクトファイルに書きます。

実測:同じループを VB と C# で

どちらのコンパイラも同じランタイム向けの IL を出すので、問題は IL が違うかどうかだけです。MethodBody.GetILAsByteArray() と小さなオペコード表でメソッド本体を解読しました。

Function SumAllVb(a As Integer()) As Long
    Dim total As Long = 0
    For i = 0 To a.Length - 1
        total += a(i)
    Next
    Return total
End Function
public static long SumAll(int[] a)
{
    long total = 0;
    for (int i = 0; i < a.Length; i++) total += a[i];
    return total;
}

偶数だけを足す SumEven では、解読結果はこうでした。

C#  SumEven: 33 bytes IL, ops: add rem
VB  SumEven: 37 bytes IL, ops: add.ovf rem

違いは add.ovf です。VB は <RemoveIntegerChecks>true</RemoveIntegerChecks> を指定しない限り、整数の加算ごとにオーバーフローを検査します。Random(42) で作った 1,000 万個の Integer に対し、同じプロセス内で両方のメソッドを、ウォームアップ 5 回の後 30 回測った中央値を、プロセス 3 回分記録しました。

既定のビルド:              SumAll 10M ints: C# 6.50 / 6.50 / 6.45 ms | VB 8.49 / 8.42 / 8.36 ms
RemoveIntegerChecks=true:  SumAll 10M ints: C# 6.46 / 6.46 / 6.63 ms | VB 6.46 / 6.45 / 6.66 ms
1,000 万個の整数を合計する時間の中央値の棒グラフ。C#:6.50 ms。オーバーフロー検査つきの加算命令を出す既定設定の VB:8.42 ms。RemoveIntegerChecks を true にしてビルドした VB:6.46 ms。
1,000 万個の Integer を Long に合計する同じループを、C# と VB で同じプロセス内で実行(ウォームアップ後 30 回の中央値)。VB 既定のオーバーフローチェックでここでは約 3 割遅く、RemoveIntegerChecks で同じになる。.NET 10.0.14、Release、Apple M1 Pro。

この単純なループでは VB の既定設定が約 3 割遅く、検査を外すと同じになりました。分岐の多いループ(偶数だけ合計、分岐予測ミスが支配的)では差は約 2% に縮みました(C# 33.7〜34.2 ms、VB 34.1〜35.3 ms)。実際のアプリは I/O やライブラリで時間を使うので言語の差はまず効きません。例外はこういう数値計算の内側のループです。

オーバーフロー検査は、移植時の正しさにも関わります。C# から写したハッシュ関数です。

Function HashVb(a As Integer()) As Integer
    Dim h = 17
    For Each x In a
        h = h * 31 + x
    Next
    Return h
End Function
C# Hash = -2025638595
VB Hash -> OverflowException: Arithmetic operation resulted in an overflow.

C# は黙って桁あふれし、VB は例外を投げます。RemoveIntegerChecks にすると両方とも -2025638595 でした(第 1 回でも Integer.MaxValue + 1 で同じことを見ました)。プロジェクト単位で検査を外すのは、どのコードが桁あふれ前提かを把握しているときだけにしましょう。ハッシュのような計算は C# ライブラリ側に置くか、Long で計算してマスクする方が安全です。

移行の判断表

判断図。既存の VB.NET コードから始める。最初の問い:今もよく変更されるか。いいえなら VB のまま .NET 10 に載せ替えるだけ。はいなら次の問い:新機能に VB が扱えないもの(Span 系 API、非同期ストリーム、Web フロントエンドなど)が必要か。いいえなら VB で書き続ける。はいなら VB プロジェクトの横に C# クラスライブラリを足し、モジュールを少しずつ移す。全面書き直しは小さなコードか、どうせ作り直す場合だけ。
残す・横に足す・書き直す。判断表で決め手になった問い。
状況 おすすめ 理由
変更の少ない安定した WinForms/コンソールアプリ VB のまま 残す(.NET 10 に載せ替え) ビルドも実行もできる。方針として言語の安定と Windows Forms 支援が明言されている
活発に改修中、新機能は普通の業務ロジック VB で 書き続ける 普通のクラス・LINQ・async・ジェネリックは今回のテストで何も詰まらなかった
活発に改修中、Span 系 API・非同期ストリーム・ソースジェネレーターが必要 段階的:VB の横に C# ライブラリ VB は Span の型名を書けず(BC30668)、IAsyncEnumerable をループできない
最新の BCL インターフェイス(INumber(Of T)、IParsable(Of T))を実装したい 段階的:その型だけ C# で VB は static abstract を実装できない(BC37315)
Web API・Blazor・MAUI のフロントが必要 段階的:新規 C# プロジェクト+VB ライブラリを共有 VB のテンプレートがなく、方針でも新ワークロードには広げないとしている
小さいコード、またはどうせ再設計する C# で 書き直す 混在には「2 言語をレビューできる人」というコストがある
大規模コードを「VB は終わったから」で それだけを理由に書き直さない ランタイム・ライブラリ・IL 性能は共通。測れた差はオーバーフロー検査だけ

正直な限界とバージョンの注記

  • すべて macOS 上の .NET SDK 10.0.403/ランタイム 10.0.14 で実行しました。WinForms はビルドのみ(-p:EnableWindowsTargeting=true。Windows Desktop の参照パックはローカルの NuGet キャッシュに既にあったもの)で、実行はしていません。WPF はビルドしていません。
  • 言語バージョンによる違いは記載した箇所だけ確認しました。init 専用の利用は LangVersion 16 で失敗(BC36716)、16.9 で成功。required のチェックは 16.9 と 17.13 の両方で働きました。そのほかの行は既定の 17.13 でのみ確認しています。
  • 方針の要約は Microsoft Learn の「.NET Managed languages strategy」を 2026 年 11 月時点で読んで言い換えたものです。正確な文言は原文で確認してください。
  • ラムダでの Span クラッシュは私たちの再現です。VB で ref struct が漏れる経路(Await をまたぐ、イテレーター、ボックス化など)をすべて試したわけではないので、どれも危険と考えてください。
  • 時間は Apple M1 Pro 1 台、Release、ウォームアップ 5 回後 30 回の中央値をプロセス 3 回分。比率の目安であって絶対速度ではありません。ビルド時間は測っていません。

前回: VB.NET 講座 5/6:Async/Await。これで VB.NET シリーズは最終回です。最初から読むなら 第 1 回:Option Strict と型 へ。C# 側の基礎は C# のインターフェイス、C# のジェネリック、C# の構造体 を、ほかの言語の上級編は Java:JVM・JIT・GC と Python のプロファイリングと最適化 をどうぞ。

よくある質問

VB.NET は .NET 10 でもまだ使えますか?

使えます。.NET 10 SDK には VB コンパイラ(既定の言語バージョン 17.13)と、コンソール・クラスライブラリ・WinForms・WPF・テスト用の VB テンプレートが入っています。Microsoft Learn の「.NET Managed languages strategy」では、VB は安定した設計を保ち、C# やランタイムの新機能は「利用のみ」で取り込み、新しいワークロードには広げないとしています。ASP.NET Core の VB テンプレートはありません。

init や required プロパティを持つ C# ライブラリを VB から使えますか?

どちらも使えます。init 専用プロパティはオブジェクト初期化子で設定でき(後から代入すると BC37311)、required メンバーも VB がチェックします。Id を設定しない New Order() は BC37321 になりました。VB でできないのは、Span や ref struct の型名を書くこと(BC30668)、static abstract メンバーを持つインターフェイスの実装(BC37315)、C# 14 の拡張プロパティ(BC30456)、既定のインターフェイスメソッドの継承(BC30149)です。

VB.NET は C# より遅いのですか?

IL はほぼ同じで、違いは VB が既定で整数オーバーフローを検査することです。1,000 万個の Integer の合計は VB 8.42 ms、C# 6.50 ms でした(30 回の中央値、Release、Apple M1 Pro)。RemoveIntegerChecks を true にすると VB も 6.46 ms で C# と同じになりました。

  • #VB.NET
  • #.NET 10
  • #C#
  • #相互運用
  • #移行
  • #上級

t.laboratory

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

運営者情報 · アプリ

↑