.NET 9 の新機能

.NET 9 の新機能について学び、その他のドキュメントへのリンクを確認します。

.NET 8 の後継である .NET 9 では、クラウドネイティブ アプリとパフォーマンスに特に重点が置かれています。 標準期間サポート (STS) リリースとして 18 か月間サポートされます.NET 9 はこちらからダウンロードできます

.NET 9 の新機能として、エンジニアリング チームは、.NET 9 プレビューの更新プログラムを GitHub ディスカッションに投稿します。 これは、リリースに関する質問をしたり、フィードバックを提供したりするのに最適な場所です。

この記事は、.NET 9 Preview 2 用に更新されました。 次のセクションでは、.NET 9 のコア .NET ライブラリの更新について説明します。

.NET ランタイム

シリアル化

System.Text.Json では、.NET 9 には、JSON をシリアル化するための新しいオプションと、Web の既定値を使用してシリアル化を容易にする新しいシングルトンがあります。

インデント オプション

JsonSerializerOptions には、書き込まれた JSON のインデント文字とインデント サイズをカスタマイズできる新しいプロパティが含まれています。

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 2,
};

string json = JsonSerializer.Serialize(
    new { Value = 1 },
    options
    );
Console.WriteLine(json);
//{
//                "Value": 1
//}

既定の Web オプション

Web アプリ向けに ASP.NET Core によって使用される既定のオプションでシリアル化する場合は、新しい JsonSerializerOptions.Web シングルトンを使用します。

string webJson = JsonSerializer.Serialize(
    new { SomeValue = 42 },
    JsonSerializerOptions.Web // Defaults to camelCase naming policy.
    );
Console.WriteLine(webJson);
// {"someValue":42}

LINQ

新しいメソッド CountByAggregateBy が導入されました。 これらのメソッドを使用すると、GroupBy を使って中間グループを割り当てる必要なく、キーによって状態を集計できます。

CountBy を使用すると、各キーの頻度をすばやく計算できます。 次の例では、テキスト文字列で最も頻繁に出現する単語を検索します。

string sourceText = """
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Sed non risus. Suspendisse lectus tortor, dignissim sit amet, 
    adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam.
""";

// Find the most frequent word in the text.
KeyValuePair<string, int> mostFrequentWord = sourceText
    .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(word => word.ToLowerInvariant())
    .CountBy(word => word)
    .MaxBy(pair => pair.Value);

Console.WriteLine(mostFrequentWord.Key); // amet

AggregateBy を使用すると、より汎用的なワークフローを実装できます。 次の例は、特定のキーに関連付けられているスコアを計算する方法を示しています。

(string id, int score)[] data =
    [
        ("0", 42),
        ("1", 5),
        ("2", 4),
        ("1", 10),
        ("0", 25),
    ];

var aggregatedData =
    data.AggregateBy(
        keySelector: entry => entry.id,
        seed: 0,
        (totalScore, curr) => totalScore + curr.score
        );

foreach (var item in aggregatedData)
{
    Console.WriteLine(item);
}
//(0, 67)
//(1, 15)
//(2, 4)

Index<TSource>(IEnumerable<TSource>) を使用すると、列挙可能なインデックスの暗黙的なインデックスをすばやく抽出できます。 次のスニペットのようなコードを記述して、自動的にコレクション内の項目のインデックスを作成できるようになりました。

IEnumerable<string> lines2 = File.ReadAllLines("output.txt");
foreach ((int index, string line) in lines2.Index())
{
    Console.WriteLine($"Line number: {index + 1}, Line: {line}");
}

取立

System.Collections.Generic 名前空間の PriorityQueue<TElement,TPriority> コレクション型には、キュー内の項目の優先度の更新に使用できる新しい Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) メソッドが含まれています。

PriorityQueue.Remove() メソッド

.NET 6 では、シンプルで高速な配列ヒープ実装を提供する PriorityQueue<TElement,TPriority> コレクションが導入されました。 配列ヒープの一般的な問題の 1 つは、優先度の更新をサポートしていないため、Dijkstra のアルゴリズムのバリエーションなどのアルゴリズムでの使用が禁止されていることです。

既存のコレクションに効率的な $O(\log n)$ 優先度の更新を実装することはできませんが、新しい PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) メソッドを使用すると、($O(n)$ 時間ではあるものの) 優先順位の更新をエミュレートできます。

public static void UpdatePriority<TElement, TPriority>(
    this PriorityQueue<TElement, TPriority> queue,
    TElement element,
    TPriority priority
    )
{
    // Scan the heap for entries matching the current element.
    queue.Remove(element, out _, out _);
    // Re-insert the entry with the new priority.
    queue.Enqueue(element, priority);
}

このメソッドは、漸近的なパフォーマンスが阻害要因ではないコンテキストで、グラフ アルゴリズムを実装したいユーザーのブロックを解除します。 (このようなコンテキストには、教育とプロトタイプ作成が含まれます)。たとえば、新しい API を使用する Dijkstra のアルゴリズムのおもちゃの実装を次に示します。

暗号

暗号化の場合、.NET 9 では、CryptographicOperations 型に新しいワンショット ハッシュ メソッドが追加されます。 また、KMAC アルゴリズムを使用する新しいクラスも追加されます。

CryptographicOperations.HashData() メソッド

.NET には、ハッシュ関数と関連関数の静的な "ワンショット" 実装がいくつか含まれています。 これらの API には、SHA256.HashDataHMACSHA256.HashData が含まれます。 ワンショット API は、可能な限り最高のパフォーマンスを提供し、割り当てを削減または排除できるため、使用することをお勧めします。

呼び出し元が使用するハッシュ アルゴリズムを定義するハッシュをサポートする API を開発者が提供したい場合、通常は HashAlgorithmName 引数を受け入れることで行われます。 ただし、ワンショット API でそのパターンを使用するには、可能な限り HashAlgorithmName を切り替えてから、適切な方法を使用する必要があります。 この問題を解決するために、.NET 9 では CryptographicOperations.HashData API が導入されています。 この API を使用すると、使用されるアルゴリズムが HashAlgorithmNameによって決定されるワンショットとして、入力に対してハッシュまたは HMAC を生成できます。

static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data)
{
    byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data);
    ProcessHash(hash);
}

KMAC アルゴリズム

.NET 9 は、NIST SP-800-185 で指定された KMAC アルゴリズムを提供します。 KECCAK メッセージ認証コード (KMAC) は、KECCAK に基づく擬似乱数関数およびキー付きハッシュ関数です。

次の新しいクラスでは、KMAC アルゴリズムが使用されます。 データの蓄積にインスタンスを使用して MAC を生成するか、1 つの入力に対するワンショットに静的 HashData メソッドを使用します。

KMAC は、OpenSSL 3.0 以降の Linux および Windows 11 ビルド 26016 以降で使用できます。 静的 IsSupported プロパティを使用して、プラットフォームが希望するアルゴリズムをサポートしているかどうかを判断できます。

if (Kmac128.IsSupported)
{
    byte[] key = GetKmacKey();
    byte[] input = GetInputToMac();
    byte[] mac = Kmac128.HashData(key, input, outputLength: 32);
}
else
{
    // Handle scenario where KMAC isn't available.
}

リフレクション

.NET Core バージョンと .NET 5 から 8 では、アセンブリのビルドと、動的に作成された型のリフレクション メタデータの出力のサポートは、実行可能な AssemblyBuilderに制限されていました。 アセンブリの "保存" に対するサポートの不足は、多くの場合、.NET Framework から .NET に移行するお客様にとって阻害要因でした。 .NET 9 では、出力されたアセンブリを保存するためにパブリック API が AssemblyBuilder に追加されます。

新しい永続化された AssemblyBuilder 実装は、ランタイムとプラットフォームに依存しません。 永続化された AssemblyBuilder インスタンスを作成するには、新しい AssemblyBuilder.DefinePersistedAssembly API を使用します。 既存の AssemblyBuilder.DefineDynamicAssembly API は、アセンブリ名とオプションのカスタム属性を受け入れます。 新しい API を使用するには、基本ランタイム型の参照に使用されるコア アセンブリである System.Private.CoreLib を渡します。 AssemblyBuilderAccess のオプションはありません。 現在、永続化された AssemblyBuilder 実装では、保存はサポートされますが、実行はサポートされません。 永続化された AssemblyBuilder のインスタンスを作成した後、モジュール、型、メソッド、または列挙型の定義、IL およびその他のすべての使用の書き込みに続く手順は変更されません。 つまり、アセンブリを保存するために、既存の System.Reflection.Emit コードをそのまま使用できます。 次のコードは例を示します。

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(
        new AssemblyName("MyAssembly"),
        typeof(object).Assembly
        );
    TypeBuilder tb = ab.DefineDynamicModule("MyModule")
        .DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod(
        "SumMethod",
        MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]
        );
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

public void UseAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    Type type = assembly.GetType("MyType");
    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, [5, 10]));
}

パフォーマンス

.NET 9 には、アプリのパフォーマンス向上を目的とした 64 ビット JIT コンパイラの機能強化が含まれています。 これらのコンパイラの機能強化は次のとおりです。

Arm64 ベクター化は、ランタイムのもう 1 つの新機能です。

ループの最適化

ループのコード生成の改善は .NET 9 の優先事項であり、64 ビット コンパイラには "誘導変数 (IV) の拡大" と呼ばれる新しい最適化が用意されています。

IV は、含まれるループが反復されるたびに値が変化する変数です。 次の for ループ、for (int i = 0; i < 10; i++) では、i は IV です。 コンパイラが、ループの反復を通じて IV の値がどのように変化するかを分析できる場合、関連する式に対してよりパフォーマンスの高いコードを生成できます。

配列を反復する次の例を考えてみましょう。

static int Sum(int[] arr)
{
    int sum = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        sum += arr[i];
    }

    return sum;
}

インデックス変数 (i) のサイズは 4 バイトです。 アセンブリ レベルでは、x64 で配列インデックスを保持するために 64 ビット レジスタが通常使用されます。以前の .NET バージョンでは、コンパイラによって、配列アクセスのために i を 8 バイトにゼロ拡張するコードが生成されましたが、他の場所では i は 4 バイトの整数として引き続き扱われました。 ただし、i を 8 バイトに拡張するには、x64 で追加の命令が必要です。 IV の拡大により、64 ビット JIT コンパイラがループ全体で i を 8 バイトに拡大し、ゼロ拡張を省略します。 配列のループは非常に一般的であり、この命令の削除の利点はすぐに増大します。

ネイティブ AOT のインライン展開の機能強化

64 ビット JIT コンパイラのインライナに対する .NET の目標の 1 つは、メソッドのインライン化を妨げる制限をできるだけ削除することです。 .NET 9 では、Windows x64、Linux x64、Linux Arm64 上の "スレッドローカル静的変数" へのアクセスのインライン展開が可能になります。

static クラス メンバーの場合、そのメンバーのインスタンスはクラスのすべてのインスタンスにわたって 1 つだけ存在し、そのメンバーを "共有" します。 static メンバーの値が各スレッドに対して一意である場合、その値をスレッドローカルにすると、パフォーマンスが向上する可能性があります。これは、static メンバーに、それが含まれるスレッドから安全にアクセスするためのコンカレンシー プリミティブが不要になるためです。

以前は、ネイティブ AOT でコンパイルされたプログラムでスレッドローカル静的変数にアクセスするには、64 ビット JIT コンパイラがランタイムへの呼び出しを生成して、スレッドローカル ストレージのベース アドレスを取得する必要がありました。 コンパイラがこれらの呼び出しをインライン化できるようになったため、このデータにアクセスするための命令がはるかに少なくなります。

PGO の機能強化: 型チェックとキャスト

.NET 8 では、動的ガイド付き最適化のプロファイル (PGO) が既定で有効になりました。 .NET 9 では、64 ビット JIT コンパイラの PGO 実装が拡張され、より多くのコード パターンをプロファイルできるようになりました。 階層型コンパイルが有効になっている場合、64 ビット JIT コンパイラによってプログラムにインストルメンテーションが既に挿入され、その動作がプロファイルされます。 コンパイラは、最適化を使用して再コンパイルする場合、実行時にビルドしたプロファイルを利用して、プログラムの現在の実行に固有の決定を行います。 .NET 9 では、64 ビット JIT コンパイラは PGO データを使用して、"型チェック" のパフォーマンスを向上させます。

オブジェクトの型を決定するにはランタイムを呼び出す必要があり、これにはパフォーマンスの低下が伴います。 ブジェクトの型をチェックする必要がある場合、正確性のために 64 ビット JIT コンパイラによってこの呼び出しが生成されます (コンパイラは通常、可能性が低く見える場合でも、すべての可能性を排除することはできません)。 ただし、PGO データがオブジェクトが特定の型である可能性が高いことを示唆している場合、64 ビット JIT コンパイラは、その型を安価にチェックする "高速パス" を生成し、必要な場合にのみランタイムへの呼び出しの低速パスにフォールバックするようになりました。

.NET ライブラリでの Arm64 ベクター化

新しい EncodeToUtf8 実装では、Arm64 でマルチレジスタの読み込み/保存命令を生成する 64 ビット JIT コンパイラの機能を利用します。 この動作により、プログラムはより少ない命令でより大きなデータチャンクを処理できます。 さまざまなドメインにわたる .NET アプリでは、これらの機能をサポートする Arm64 ハードウェアのスループットの向上が見られます。 一部のベンチマークでは、実行時間が半分以下に短縮されました。

.NET SDK

単体テスト

このセクションでは、.NET 9 での単体テストの更新 (テストの並列実行とターミナル ロガーのテスト出力) について説明します。

テストを並列で実行する

.NET 9 では、dotnet test は MSBuild とより完全に統合されています。 MSBuild では、並列でのビルドがサポートされているため、異なるターゲット フレームワーク間で同じプロジェクトのテストを並列で実行できます。 MSBuild では、並列プロセスの数は既定でコンピューター上のプロセッサの数に制限されています。 -maxcpucount スイッチを使用して、独自の制限を設定することもできます。 並列処理を無効にする場合は、TestTfmsInParallel MSBuild プロパティを false に設定します。

ターミナル ロガー テストの表示

dotnet test のテスト結果レポートが MSBuild ターミナル ロガーで直接サポートされるようになりました。 テストの実行 "中" (実行中のテスト名が表示されます) とテストの完了 "後" (テスト エラーがより適切な方法でレンダリングされます) の両方で、より十分な機能を有するテスト レポートが提供されます。

ターミナル ロガーの詳細については、dotnet build のオプションに関する記事を参照してください。

.NET ツールのロールフォワード

.NET ツールは、フレームワーク依存のアプリで、グローバルまたはローカルにインストールして、.NET SDK とインストールされた .NET ランタイムを使用して実行できます。 これらのツールは、すべての .NET アプリと同様に、.NET の特定のメジャー バージョンを対象としています。 既定では、アプリは "新しい" バージョンの .NET では実行されません。 ツール作成者は、RollForward MSBuild プロパティを設定すると、新しいバージョンの .NET ランタイムでそのツールを実行することを選択できます。 ただし、すべてのツールでそのようにできるわけではありません。

dotnet tool install の新しいオプションを使用すると、"ユーザー" が .NET ツールの実行方法を決定できます。 dotnet tool install を使用してツールをインストールする場合や、dotnet tool run <toolname> を使用してツールを実行する場合は、--allow-roll-forward という新しいフラグを指定できます。 このオプションは、ロールフォワード モードの Major を使用してツールを構成します。 このモードでは、対応する .NET バージョンが使用できない場合に、新しいメジャー バージョンの .NET でツールを実行できます。 この機能により、ツール作成者がコードを変更しなくても、早期導入者が .NET ツールを使用できるようになります。

関連項目