次の方法で共有


.NET 9 用 .NET ライブラリの新機能

この記事では、.NET 9 用 .NET ライブラリの新機能について説明します。

Base64のURL

Base64 は、任意のバイトを 64 文字の特定のセットで構成されるテキストに変換するエンコード スキームです。 これはデータを転送するための一般的なアプローチであり、 Convert.ToBase64StringBase64.DecodeFromUtf8(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32, Boolean)など、さまざまな方法で長い間サポートされてきました。 ただし、使用する文字の中には、クエリ文字列など、使用する必要がある状況によっては理想的ではありません。 特に、Base64 テーブルを構成する 64 文字には '+' と '/' が含まれ、どちらも URL で独自の意味を持ちます。 これにより、Base64Url スキームが作成されました。これは Base64 に似ていますが、URL コンテキストでの使用に適した若干異なる文字セットを使用します。 .NET 9 には新しい Base64Url クラスが含まれています。このクラスには、さまざまなデータ型との間で Base64Url を使用してエンコードおよびデコードするための便利で最適化されたメソッドが多数用意されています。

次の例では、新しいクラスの使用を示します。

ReadOnlySpan<byte> bytes = ...;
string encoded = Base64Url.EncodeToString(bytes);

バイナリフォーマッター

.NET 9 では、.NET ランタイムから BinaryFormatter が削除されます。 API はまだ存在しますが、その実装では、プロジェクトの種類に関係なく、常に例外がスローされます。 削除と影響を受ける場合のオプションの詳細については、 BinaryFormatter 移行ガイドを参照してください。

コレクション

.NET のコレクション型は、.NET 9 の次の更新プログラムを取得します。

スパンを使用したコレクション参照

パフォーマンスの高いコードでは、文字列の不必要な割り当てを回避するためにスパンがよく使用され、 Dictionary<TKey,TValue>HashSet<T> などの型を持つ参照テーブルがキャッシュとして頻繁に使用されます。 ただし、スパンを持つこれらのコレクション型に対して参照を実行するための安全な組み込みメカニズムはありません。 C# 13 の新しい allows ref struct 機能と、.NET 9 のこれらのコレクション型の新機能により、これらの種類の参照を実行できるようになりました。

次の例では 、Dictionary<TKey、TValue> の使用を示します。GetAlternateLookup

static Dictionary<string, int> CountWords(ReadOnlySpan<char> input)
{
    Dictionary<string, int> wordCounts = new(StringComparer.OrdinalIgnoreCase);
    Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> spanLookup =
        wordCounts.GetAlternateLookup<ReadOnlySpan<char>>();

    foreach (Range wordRange in Regex.EnumerateSplits(input, @"\b\W+"))
    {
        if (wordRange.Start.Value == wordRange.End.Value)
        {
            continue; // Skip empty ranges.
        }
        ReadOnlySpan<char> word = input[wordRange];
        spanLookup[word] = spanLookup.TryGetValue(word, out int count) ? count + 1 : 1;
    }

    return wordCounts;
}

OrderedDictionary<TKey, TValue>

多くのシナリオでは、順序を維持できる方法 (キーと値のペアの一覧) でキーと値のペアを格納できますが、キーによる高速検索もサポートされます (キーと値のペアのディクショナリ)。 .NET の初期の頃から、 OrderedDictionary 型は、このシナリオをサポートしてきましたが、キーと値が objectとして型指定された非ジェネリックな方法でのみサポートされています。 .NET 9 では、長く要求された OrderedDictionary<TKey,TValue> コレクションが導入されています。これにより、これらのシナリオをサポートするための効率的で汎用的な型が提供されます。

次のコードでは、新しいクラスを使用します。

OrderedDictionary<string, int> d = new()
{
    ["a"] = 1,
    ["b"] = 2,
    ["c"] = 3,
};

d.Add("d", 4);
d.RemoveAt(0);
d.RemoveAt(2);
d.Insert(0, "e", 5);

foreach (KeyValuePair<string, int> entry in d)
{
    Console.WriteLine(entry);
}

// Output:
// [e, 5]
// [b, 2]
// [c, 3]

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 のアルゴリズムのおもちゃの実装 を次に示します。

ReadOnlySet<T>

多くの場合、コレクションの読み取り専用ビューを提供することが望ましいです。 ReadOnlyCollection<T> では、任意の変更可能な IList<T>の周囲に読み取り専用ラッパーを作成でき、 ReadOnlyDictionary<TKey,TValue> では、任意の変更可能な IDictionary<TKey,TValue>の周囲に読み取り専用ラッパーを作成できます。 ただし、以前のバージョンの .NET には、 ISet<T>で同じことを行うための組み込みのサポートがありませんでした。 .NET 9 では、これに対処するための ReadOnlySet<T> が導入されています。

新しいクラスでは、次の使用パターンが有効になります。

private readonly HashSet<int> _set = [];
private ReadOnlySet<int>? _setWrapper;

public ReadOnlySet<int> Set => _setWrapper ??= new(_set);

コンポーネント モデル - トリミングのサポートTypeDescriptor

System.ComponentModel には、コンポーネントを記述するための新しいオプトイン トリマー互換 API が含まれています。 すべてのアプリケーション (特に自己完結型のトリミングされたアプリケーション) では、これらの新しい API を使用してトリミング シナリオをサポートできます。

プライマリ API は、TypeDescriptor.RegisterType クラスのTypeDescriptor メソッドです。 このメソッドには DynamicallyAccessedMembersAttribute 属性があるため、トリマはその型のメンバーを保持します。 このメソッドは型ごとに 1 回呼び出し、通常は早い段階で呼び出す必要があります。

セカンダリ API には、FromRegisteredTypeなどのTypeDescriptor.GetPropertiesFromRegisteredType(Type)サフィックスがあります。 FromRegisteredType サフィックスを持たない対応する API とは異なり、これらの API には[RequiresUnreferencedCode][DynamicallyAccessedMembers]トリマー属性はありません。 トリマー属性がないため、コンシューマーは次の操作を行う必要がなくなります。

  • トリミングの警告を抑制します。これは危険な可能性があります。
  • 厳密に型指定された Type パラメーターを他のメソッドに伝達します。これは、面倒な場合や不可能な場合があります。
public static void RunIt()
{
    // The Type from typeof() is passed to a different method.
    // The trimmer doesn't know about ExampleClass anymore
    // and thus there will be warnings when trimming.
    Test(typeof(ExampleClass));
    Console.ReadLine();
}

private static void Test(Type type)
{
    // When publishing self-contained + trimmed,
    // this line produces warnings IL2026 and IL2067.
    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(type);

    // When publishing self-contained + trimmed,
    // the property count is 0 here instead of 2.
    Console.WriteLine($"Property count: {properties.Count}");

    // To avoid the warning and ensure reflection
    // can see the properties, register the type:
    TypeDescriptor.RegisterType<ExampleClass>();
    // Get properties from the registered type.
    properties = TypeDescriptor.GetPropertiesFromRegisteredType(type);

    Console.WriteLine($"Property count: {properties.Count}");
}

public class ExampleClass
{
    public string? Property1 { get; set; }
    public int Property2 { get; set; }
}

詳細については、 API の提案を参照してください。

暗号

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 を生成するか、静的 HashData メソッドを使用して 1 回の入力で ワンショット します。

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.
}

iOS/tvOS/MacCatalyst で有効になっている AES-GCM および ChaChaPoly1305 アルゴリズム

IsSupported iOS 13 以降、tvOS 13 以降、Mac Catalyst で実行すると、 ChaChaPoly1305.IsSupported true が返されるようになりました。

AesGcm は、Apple オペレーティング システムで 16 バイト (128 ビット) タグ値のみをサポートします。

X.509 証明書の読み込み

.NET Framework 2.0 以降、証明書を読み込む方法が new X509Certificate2(bytes)されました。 また、 new X509Certificate2(bytes, password, flags)new X509Certificate2(path)new X509Certificate2(path, password, flags)X509Certificate2Collection.Import(bytes, password, flags) (およびそのオーバーロード) などの他のパターンもあります。

これらのメソッドはすべて、コンテンツ スニッフィングを使用して、入力が処理できるものであるかどうかを判断し、可能であれば読み込みます。 一部の呼び出し元にとって、この戦略は非常に便利でした。 ただし、次のような問題もあります。

  • すべてのファイル形式がすべての OS で動作するわけではありません。
  • これはプロトコルの偏差です。
  • これはセキュリティの問題の原因です。

.NET 9 では、"1 つのメソッド、1 つの目的" 設計を持つ新しい X509CertificateLoader クラスが導入されています。 初期バージョンでは、 X509Certificate2 コンストラクターでサポートされている 5 つの形式のうち 2 つの形式のみがサポートされます。 これらは、すべてのオペレーション システムで動作した 2 つの形式です。

OpenSSL プロバイダーのサポート

.NET 8 では、OpenSSL 固有の API OpenPrivateKeyFromEngine(String, String)OpenPublicKeyFromEngine(String, String)が導入されました。 OpenSSL ENGINE コンポーネント との対話を有効にし、ハードウェア セキュリティ モジュール (HSM) などを使用します。

.NET 9 では、 SafeEvpPKeyHandle.OpenKeyFromProvider(String, String)が導入されています。これにより、 OpenSSL プロバイダー を使用したり、 tpm2pkcs11などのプロバイダーと対話したりすることができます。

一部のディストリビューションでは ENGINE サポートが廃止 されました。

次のスニペットは、基本的な使用方法を示しています。

byte[] data = [ /* example data */ ];

// Refer to your provider documentation, for example, https://github.com/tpm2-software/tpm2-openssl/tree/master.
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2", "handle:0x81000007"))
using (ECDsa ecdsaPri = new ECDsaOpenSsl(priKeyHandle))
{
    byte[] signature = ecdsaPri.SignData(data, HashAlgorithmName.SHA256);
    // Do stuff with signature created by TPM.
}

TLS ハンドシェイク中のパフォーマンスの向上と、 ENGINE コンポーネントを使用する RSA 秘密キーとの対話の改善があります。

Windows CNG 仮想化ベースのセキュリティ

Windows 11 では、 仮想化ベースのセキュリティ (VBS) で Windows キーをセキュリティで保護するために役立つ新しい API が追加されました。 この新機能を使用すると、管理者レベルのキー盗難攻撃からキーを保護でき、パフォーマンス、信頼性、またはスケールに影響を与える可能性はごくわずかです。

.NET 9 では、一致する CngKeyCreationOptions フラグが追加されました。 次の 3 つのフラグが追加されました。

  • CngKeyCreationOptions.PreferVbs マッチング NCRYPT_PREFER_VBS_FLAG
  • CngKeyCreationOptions.RequireVbs マッチング NCRYPT_REQUIRE_VBS_FLAG
  • CngKeyCreationOptions.UsePerBootKey マッチング NCRYPT_USE_PER_BOOT_KEY_FLAG

次のスニペットは、フラグの 1 つを使用する方法を示しています。

using System.Security.Cryptography;

CngKeyCreationParameters cngCreationParams = new()
{
    Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider,
    KeyCreationOptions = CngKeyCreationOptions.RequireVbs | CngKeyCreationOptions.OverwriteExistingKey,
};

using (CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256, "myKey", cngCreationParams))
using (ECDsaCng ecdsa = new ECDsaCng(key))
{
    // Do stuff with the key.
}

日付と時刻 - 新しい TimeSpan.From* オーバーロード

TimeSpan クラスには、From*を使用してTimeSpan オブジェクトを作成できるdoubleメソッドがいくつか用意されています。 ただし、 double はバイナリベースの浮動小数点形式であるため、 固有の不正確さがエラーにつながる可能性があります。 たとえば、 TimeSpan.FromSeconds(101.832)101 seconds, 832 millisecondsを正確に表すのではなく、ほぼ 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds。 この不一致により、混乱が頻繁に発生し、このようなデータを表す最も効率的な方法ではありません。 これに対処するために、.NET 9 では、整数から TimeSpan オブジェクトを作成できる新しいオーバーロードが追加されています。 FromDaysFromHoursFromMinutesFromSecondsFromMilliseconds、およびFromMicrosecondsからの新しいオーバーロードがあります。

次のコードは、 double と新しい整数オーバーロードの 1 つを呼び出す例を示しています。

TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832);
Console.WriteLine($"timeSpan1 = {timeSpan1}");
// timeSpan1 = 00:01:41.8319999

TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine($"timeSpan2 = {timeSpan2}");
// timeSpan2 = 00:01:41.8320000

依存関係の挿入 - コンストラクターActivatorUtilities.CreateInstance

.NET 9 では、 ActivatorUtilities.CreateInstance のコンストラクターの解決が変更されました。 以前は、コンストラクターの順序とコンストラクター パラメーターの数によっては、 ActivatorUtilitiesConstructorAttribute 属性を使用して明示的にマークされたコンストラクターが呼び出されない場合がありました。 .NET 9 では、属性を持つコンストラクターが常に呼び出されるようにロジックが変更されました。

診断

Debug.Assert は、既定でアサート条件を報告します

Debug.Assert は、常に true であると予想される条件を検証するために一般的に使用されます。 通常、エラーはコード内のバグを示します。 Debug.Assertには多くのオーバーロードがあり、最も単純なものは条件を受け入れるだけです。

Debug.Assert(a > 0 && b > 0);

条件が false の場合、アサートは失敗します。 しかし、歴史的には、このようなアサートは、失敗した条件に関する情報を無効にしていました。 .NET 9 以降では、ユーザーが明示的にメッセージを提供しない場合、アサートには条件のテキスト表現が含まれます。 たとえば、前のアサートの例では、次のようなメッセージが表示されます。

Process terminated. Assertion failed.
   at Program.SomeMethod(Int32 a, Int32 b)

メッセージは次のようになります。

Process terminated. Assertion failed.
a > 0 && b > 0
   at Program.SomeMethod(Int32 a, Int32 b)

以前は、Activityの作成時にのみ、トレース Activityを他のトレース コンテキストリンクできました。 .NET 9 の新機能である AddLink(ActivityLink) API を使用すると、 Activity オブジェクトを作成後に他のトレース コンテキストにリンクできます。 この変更は、 OpenTelemetry の仕様 にも合わせて調整されます。

ActivityContext activityContext = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None);
ActivityLink activityLink = new(activityContext);

Activity activity = new("LinkTest");
activity.AddLink(activityLink);

Metrics.Gauge インストルメント

System.Diagnostics.Metrics OpenTelemetry 仕様に従って Gauge<T> 機器を提供するようになりました。 Gaugeインストルメントは、変更が発生したときに非加法値を記録するように設計されています。 たとえば、バックグラウンド ノイズ レベルを測定できます。ここで、複数の部屋の値を合計すると意味をなさなくなります。 Gaugeインストルメントは、intdoubledecimalなど、任意の値型を記録できるジェネリック型です。

次の例では、 Gauge のインストルメントの使用を示します。

Meter soundMeter = new("MeasurementLibrary.Sound");
Gauge<int> gauge = soundMeter.CreateGauge<int>(
    name: "NoiseLevel",
    unit: "dB", // Decibels.
    description: "Background Noise Level"
    );
gauge.Record(10, new TagList() { { "Room1", "dB" } });

Out-of-proc Meter ワイルドカードリッスン

System.Diagnostics.Metrics イベント ソース プロバイダーを使用してプロセス外のメーターをリッスンすることは既に可能ですが、.NET 9 より前は、完全なメーター名を指定する必要がありました。 .NET 9 では、ワイルドカード文字 *を使用してすべてのメーターをリッスンできます。これにより、プロセス内のすべてのメーターからメトリックをキャプチャできます。 さらに、測定プレフィックスによるリッスンのサポートが追加されるため、名前が指定されたプレフィックスで始まるすべてのメーターをリッスンできます。 たとえば、 MyMeter* を指定すると、 MyMeterで始まる名前ですべてのメーターをリッスンできます。

// The complete meter name is "MyCompany.MyMeter".
var meter = new Meter("MyCompany.MyMeter");
// Create a counter and allow publishing values.
meter.CreateObservableCounter("MyCounter", () => 1);

// Create the listener to use the wildcard character
// to listen to all meters using prefix names.
MyEventListener listener = new MyEventListener();

MyEventListener クラスは次のように定義されます。

internal class MyEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        Console.WriteLine(eventSource.Name);
        if (eventSource.Name == "System.Diagnostics.Metrics")
        {
            // Listen to all meters with names starting with "MyCompany".
            // If using "*", allow listening to all meters.
            EnableEvents(
                eventSource,
                EventLevel.Informational,
                (EventKeywords)0x3,
                new Dictionary<string, string?>() { { "Metrics", "MyCompany*" } }
                );
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // Ignore other events.
        if (eventData.EventSource.Name != "System.Diagnostics.Metrics" ||
            eventData.EventName == "CollectionStart" ||
            eventData.EventName == "CollectionStop" ||
            eventData.EventName == "InstrumentPublished"
            )
            return;

        Console.WriteLine(eventData.EventName);

        if (eventData.Payload is not null)
        {
            for (int i = 0; i < eventData.Payload.Count; i++)
                Console.WriteLine($"\t{eventData.PayloadNames![i]}: {eventData.Payload[i]}");
        }
    }
}

コードを実行すると、出力は次のようになります。

CounterRateValuePublished
        sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
        meterName: MyCompany.MyMeter
        meterVersion:
        instrumentName: MyCounter
        unit:
        tags:
        rate: 0
        value: 1
        instrumentId: 1
CounterRateValuePublished
        sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
        meterName: MyCompany.MyMeter
        meterVersion:
        instrumentName: MyCounter
        unit:
        tags:
        rate: 0
        value: 1
        instrumentId: 1

ワイルドカード文字を使用して、 dotnet-counters などの監視ツールでメトリックをリッスンすることもできます。

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}");
}

ソース ジェネレーターのログ記録

C# 12 では 、プライマリ コンストラクターが導入されました。これにより、クラス宣言でコンストラクターを直接定義できます。 ログ ソース ジェネレーターで、プライマリ コンストラクターを持つクラスを使用したログ記録がサポートされるようになりました。

public partial class ClassWithPrimaryConstructor(ILogger logger)
{
    [LoggerMessage(0, LogLevel.Debug, "Test.")]
    public partial void Test();
}

その他

このセクションでは、次に関する情報を見つけます。

allows ref struct ライブラリで使用される

C# 13 では、 allows ref structを使用してジェネリック パラメーターを制約する機能が導入されています。これにより、そのジェネリック パラメーターに ref struct を使用できることをコンパイラとランタイムに通知します。 これと互換性のある多くの API に注釈が付けられます。 たとえば、 String.Create メソッドにはオーバーロードがあり、スパンとして表されるメモリに直接書き込んで string を作成できます。 このメソッドには、呼び出し元から実際の書き込みを行うデリゲートに渡される TState 引数があります。

TStateString.Create型パラメーターに、allows ref structで注釈が付けられたようになりました。

public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
    where TState : allows ref struct;

この注釈を使用すると、スパン (またはその他の ref struct) をこのメソッドへの入力として渡すことができます。

次の例は、この機能を使用する新しい String.ToLowerInvariant() オーバーロードを示しています。

public static string ToLowerInvariant(ReadOnlySpan<char> input) =>
    string.Create(span.Length, input, static (stringBuffer, input) => span.ToLowerInvariant(stringBuffer));

SearchValues 拡張

.NET 8 では、 SearchValues<T> 型が導入されました。これは、スパン内の特定の文字セットまたはバイトを検索するための最適化されたソリューションを提供します。 .NET 9 では、 SearchValues が拡張され、大きな文字列内の部分文字列の検索がサポートされています。

次の例では、文字列値内で複数の動物名を検索し、最初に見つかった名前にインデックスを返します。

private static readonly SearchValues<string> s_animals =
    SearchValues.Create(["cat", "mouse", "dog", "dolphin"], StringComparison.OrdinalIgnoreCase);

public static int IndexOfAnimal(string text) =>
    text.AsSpan().IndexOfAny(s_animals);

この新機能には、基になるプラットフォームでの SIMD サポートを利用する最適化された実装があります。 また、上位レベルの型を最適化することもできます。 たとえば、 Regex は実装の一部としてこの機能を利用するようになりました。

ネットワーキング

SocketsHttpHandler は HttpClientFactory の既定値です

HttpClientFactoryでは、HttpClientによってサポートされるオブジェクトHttpClientHandler既定で作成されます。 HttpClientHandler はそれ自体が SocketsHttpHandlerによって支えられています。これは、接続の有効期間管理を含め、はるかに構成可能です。 HttpClientFactory では、既定で SocketsHttpHandler を使用し、ファクトリで指定されたローテーション有効期間の制限に一致するように接続有効期間の制限を設定するように構成します。

System.Net.ServerSentEvents (英語)

サーバー送信イベント (SSE) は、サーバーからクライアントにデータをストリーミングするためのシンプルで一般的なプロトコルです。 たとえば、その AI サービスから生成されたテキストのストリーミングの一部として OpenAI によって使用されます。 SSE の使用を簡略化するために、新しい System.Net.ServerSentEvents ライブラリには、サーバー送信イベントを簡単に取り込むためのパーサーが用意されています。

次のコードは、新しいクラスの使用を示しています。

Stream responseStream = new MemoryStream();
await foreach (SseItem<string> e in SseParser.Create(responseStream).EnumerateAsync())
{
    Console.WriteLine(e.Data);
}

Linux 上のクライアント証明書を使用した TLS 再開

TLS 再開 は、以前に確立されたセッションをサーバーに再開できる TLS プロトコルの機能です。 これにより、いくつかのラウンドトリップが回避され、TLS ハンドシェイク中に計算リソースが節約されます。

クライアント証明書を使用しない SslStream 接続では、Linux で TLS 再開が既にサポートされています。 .NET 9 では、サーバー間のシナリオで一般的な相互認証 TLS 接続の TLS 再開のサポートが追加されています。 この機能は自動的に有効になります。

WebSocket キープアライブ ping とタイムアウト

ClientWebSocketOptionsWebSocketCreationOptionsの新しい API を使用すると、WebSocket ping の送信と、ピアが時間内に応答しない場合の接続の中止をオプトインできます。

これまでは、接続をアイドル状態に保つために KeepAliveInterval を指定できましたが、ピアが応答するように強制する組み込みメカニズムはありませんでした。

次の例では、5 秒ごとにサーバーに ping を実行し、1 秒以内に応答しない場合は接続を中止します。

using var cws = new ClientWebSocket();
cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
cws.Options.KeepAliveInterval = TimeSpan.FromSeconds(5);
cws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);

await cws.ConnectAsync(uri, httpClient, cancellationToken);

HttpClientFactory が既定でヘッダー値をログに記録しなくなりました

LogLevel.Trace HttpClientFactoryによってログに記録されたイベントには、既定でヘッダー値が含まれていません。 RedactLoggedHeaders ヘルパー メソッドを使用して、特定のヘッダーの値をログに記録することを選択できます。

次の例では、ユーザー エージェントを除くすべてのヘッダーを編集します。

services.AddHttpClient("myClient")
    .RedactLoggedHeaders(name => name != "User-Agent");

詳細については、 HttpClientFactory のログ記録が既定でヘッダー値を編集する方法に関するページを参照してください。

リフレクション

永続化されたアセンブリ

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

PersistedAssemblyBuilder インスタンスを作成するには、そのコンストラクターを呼び出し、アセンブリ名、コア アセンブリ、System.Private.CoreLibを渡して、基本ランタイム型とオプションのカスタム属性を参照します。 アセンブリにすべてのメンバーを出力した後、 PersistedAssemblyBuilder.Save(String) メソッドを呼び出して、既定の設定でアセンブリを作成します。 エントリ ポイントまたはその他のオプションを設定する場合は、 PersistedAssemblyBuilder.GenerateMetadata を呼び出し、返されるメタデータを使用してアセンブリを保存できます。 次のコードは、永続化されたアセンブリを作成し、エントリ ポイントを設定する例を示しています。

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

    MethodBuilder entryPoint = tb.DefineMethod(
        "Main",
        MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static
        );
    ILGenerator il = entryPoint.GetILGenerator();
    // ...
    il.Emit(OpCodes.Ret);

    tb.CreateType();

    MetadataBuilder metadataBuilder = ab.GenerateMetadata(
        out BlobBuilder ilStream,
        out BlobBuilder fieldData
        );
    PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(
                    imageCharacteristics: Characteristics.ExecutableImage);

    ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                    header: peHeaderBuilder,
                    metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
                    ilStream: ilStream,
                    mappedFieldData: fieldData,
                    entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken)
                    );

    BlobBuilder peBlob = new BlobBuilder();
    peBuilder.Serialize(peBlob);

    using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
    peBlob.WriteContentTo(fileStream);
}

public static 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]));
}

新しい PersistedAssemblyBuilder クラスには、PDB のサポートが含まれています。 シンボル情報を出力し、それを使用して生成されたアセンブリをデバッグできます。 API の形状は .NET Framework の実装と似ています。 詳細については、「シンボルの 出力と PDB の生成」を参照してください。

型名の解析

TypeName は ECMA-335 型名のパーサーであり、 System.Type とほぼ同じ機能を提供しますが、ランタイム環境から切り離されています。 シリアライザーやコンパイラなどのコンポーネントでは、型名を解析して処理する必要があります。 たとえば、ネイティブ AOT コンパイラは、 TypeNameの使用に切り替えました。

新しい TypeName クラスには次のものが用意されています。

  • Parseとして表される入力を解析するための静的なTryParseおよびReadOnlySpan<char>メソッド。 どちらのメソッドも、解析 TypeNameParseOptions カスタマイズできるクラス (オプション バッグ) のインスタンスを受け入れます。

  • NameFullName、および AssemblyQualifiedName プロパティは、 System.Typeの対応するプロパティとまったく同じように動作します。

  • 名前自体に関する追加情報を提供する複数のプロパティとメソッド:

    • IsArrayIsSZArray (SZ は、単一次元、ゼロインデックス配列、 IsVariableBoundArrayType、および配列を操作するための GetArrayRank を表します。
    • IsConstructedGenericTypeGetGenericTypeDefinition、およびジェネリック型名を操作するための GetGenericArguments
    • IsByRef ポインターとマネージド参照を操作するための IsPointer
    • GetElementType() ポインター、参照、および配列を操作する場合。
    • IsNested 入れ子になった型を操作するための DeclaringType
    • AssemblyNameは、新しい AssemblyNameInfo クラスを介してアセンブリ名情報を公開します。 AssemblyNameとは異なり、新しい型は不変であり、カルチャ名を解析してもCultureInfoのインスタンスは作成されません。

TypeName型とAssemblyNameInfo型の両方が不変であり、等しいかどうかを確認する方法は提供されません (IEquatableは実装されません)。 アセンブリ名の比較は簡単ですが、さまざまなシナリオで、公開されている情報のサブセット (NameVersionCultureNamePublicKeyOrToken) のみを比較する必要があります。

次のコード スニペットは、使用例を示しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;

internal class RestrictedSerializationBinder
{
    Dictionary<string, Type> AllowList { get; set; }

    RestrictedSerializationBinder(Type[] allowedTypes)
        => AllowList = allowedTypes.ToDictionary(type => type.FullName!);

    Type? GetType(ReadOnlySpan<char> untrustedInput)
    {
        if (!TypeName.TryParse(untrustedInput, out TypeName? parsed))
        {
            throw new InvalidOperationException($"Invalid type name: '{untrustedInput.ToString()}'");
        }

        if (AllowList.TryGetValue(parsed.FullName, out Type? type))
        {
            return type;
        }
        else if (parsed.IsSimple // It's not generic, pointer, reference, or an array.
            && parsed.AssemblyName is not null
            && parsed.AssemblyName.Name == "MyTrustedAssembly"
            )
        {
            return Type.GetType(parsed.AssemblyQualifiedName, throwOnError: true);
        }

        throw new InvalidOperationException($"Not allowed: '{untrustedInput.ToString()}'");
    }
}

新しい API は、下位レベルの .NET バージョンで使用できる System.Reflection.Metadata NuGet パッケージから入手できます。

正規表現

[GeneratedRegex] プロパティの場合

.NET 7 では、 Regex ソース ジェネレーターとそれに対応する GeneratedRegexAttribute 属性が導入されました。

次の部分メソッドは、この Regexを実装するために必要なすべてのコードでソースが生成されます。

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWord();

C# 13 では、部分メソッドに加えて部分 プロパティ がサポートされているため、.NET 9 以降では、プロパティで [GeneratedRegex(...)] を使用することもできます。

次の部分プロパティは、前の例と同等のプロパティです。

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWordProperty { get; }

Regex.EnumerateSplits

Regex クラスは、Split メソッドの概念に似たString.Split メソッドを提供します。 String.Splitでは、1 つ以上のcharまたはstring区切り記号を指定します。実装では、これらの区切り記号の入力テキストが分割されます。 Regex.Splitでは、区切り記号をcharまたはstringとして指定する代わりに、正規表現パターンとして指定されます。

次の例では、 Regex.Splitを示します。

foreach (string s in Regex.Split("Hello, world! How are you?", "[aeiou]"))
{
    Console.WriteLine($"Split: \"{s}\"");
}

// Output, split by all English vowels:
// Split: "H"
// Split: "ll"
// Split: ", w"
// Split: "rld! H"
// Split: "w "
// Split: "r"
// Split: " y"
// Split: ""
// Split: "?"

ただし、 Regex.Split は入力として string のみを受け入れ、 ReadOnlySpan<char>として提供される入力をサポートしていません。 また、分割の完全なセットを string[]として出力します。これには、結果を保持するために string 配列と各分割の string の両方を割り当てる必要があります。 .NET 9 では、新しい EnumerateSplits メソッドを使用すると、同じ操作を実行できますが、スパンベースの入力を使用して、結果に割り当てを行う必要はありません。 ReadOnlySpan<char>を受け取り、結果を表すRangeオブジェクトの列挙可能な値を返します。

次の例では、Regex.EnumerateSplitsを入力として取得するReadOnlySpan<char>を示します。

ReadOnlySpan<char> input = "Hello, world! How are you?";
foreach (Range r in Regex.EnumerateSplits(input, "[aeiou]"))
{
    Console.WriteLine($"Split: \"{input[r]}\"");
}

シリアル化 (System.Text.Json)

インデント オプション

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 オプションシングルトン

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

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

JsonSchemaエクスポーター

JSON は、リモート プロシージャ呼び出しスキームの一部としてメソッド シグネチャの型を表すためによく使用されます。 たとえば、OpenAPI 仕様の一部として、または OpenAI のような AI サービスを使用したツール呼び出しの一部として使用されます。 開発者は、 System.Text.Jsonを使用して.NET 型を JSON としてシリアル化および逆シリアル化できます。 ただし、.NET 型の形状を記述する JSON スキーマを取得できる必要もあります (つまり、シリアル化される内容と逆シリアル化できる内容の形状を記述します)。 System.Text.Json では、.NET 型を表す JSON スキーマの生成をサポートする JsonSchemaExporter 型が提供されるようになりました。

詳細については、「 JSON スキーマ エクスポーター」を参照してください。

null 許容注釈を考慮する

System.Text.Json プロパティの null 許容注釈を認識し、 RespectNullableAnnotations フラグを使用してシリアル化および逆シリアル化中にプロパティを適用するように構成できるようになりました。

次のコードは、オプションを設定する方法を示しています。

public static void RunIt()
{
    JsonSerializerOptions options = new() { RespectNullableAnnotations = true };

    // Throws exception: System.Text.Json.JsonException: The property or field
    // 'Title' on type 'Serialization+Book' doesn't allow getting null values.
    // Consider updating its nullability annotation.
    JsonSerializer.Serialize(new Book { Title = null! }, options);

    // Throws exception: System.Text.Json.JsonException: The property or field
    // 'Title' on type 'Serialization+Book' doesn't allow setting null values.
    // Consider updating its nullability annotation.
    JsonSerializer.Deserialize<Book>("""{ "Title" : null }""", options);
}

public class Book
{
    public required string Title { get; set; }
    public string? Author { get; set; }
    public int PublishYear { get; set; }
}

詳細については、「 null 許容注釈を考慮する」を参照してください。

オプション以外のコンストラクター パラメーターが必要

これまで、 System.Text.Json では、コンストラクターベースの逆シリアル化を使用する場合、オプション以外のコンストラクター パラメーターを省略可能として扱いました。 新しい RespectRequiredConstructorParameters フラグを使用して、その動作を変更できます。

次のコードは、オプションを設定する方法を示しています。

JsonSerializerOptions options = new() { RespectRequiredConstructorParameters = true };

// Throws exception: System.Text.Json.JsonException: JSON deserialization
// for type 'Serialization+MyPoco' was missing required properties including: 'Value'.
JsonSerializer.Deserialize<MyPoco>("""{}""", options);

MyPoco 型は次のように定義されています。

record MyPoco(string Value);

詳細については、「 オプション以外のコンストラクター パラメーター」を参照してください。

JsonObject プロパティの順序付け

JsonObject型は、明示的なプロパティの順序の操作を有効にする、順序付けされたディクショナリのような API を公開するようになりました。

JsonObject jObj = new()
{
    ["key1"] = true,
    ["key3"] = 3
};

Console.WriteLine(jObj is IList<KeyValuePair<string, JsonNode?>>); // True.

// Insert a new key-value pair at the correct position.
int key3Pos = jObj.IndexOf("key3") is int i and >= 0 ? i : 0;
jObj.Insert(key3Pos, "key2", "two");

foreach (KeyValuePair<string, JsonNode?> item in jObj)
{
    Console.WriteLine($"{item.Key}: {item.Value}");
}

// Output:
// key1: true
// key2: two
// key3: 3

詳細については、「 プロパティの順序を操作する」を参照してください。

列挙型メンバー名をカスタマイズする

新しい System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute 属性を使用して、文字列としてシリアル化される型の個々の列挙型メンバーの名前をカスタマイズできます。

JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2); // "Value1, Custom enum value"

[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
    Value1 = 1,
    [JsonStringEnumMemberName("Custom enum value")]
    Value2 = 2,
}

詳細については、カスタム列挙メンバー名に関するページを参照してください。

複数の JSON ドキュメントをストリーム配信する

System.Text.Json.Utf8JsonReader では、1 つのバッファーまたはストリームから複数の空白で区切られた JSON ドキュメントの読み取りがサポートされるようになりました。 既定では、先頭の最上位ドキュメントの末尾にある空白以外の文字が検出された場合、リーダーは例外をスローします。 この動作は、 AllowMultipleValues フラグを使用して変更できます。

詳細については、 複数の JSON ドキュメントの読み取りを参照してください。

スパン

高パフォーマンスコードでは、文字列を不必要に割り当てないようにするためにスパンがよく使用されます。 Span<T> ReadOnlySpan<T>.NET でのコードの記述方法に革命を起こし続け、各リリースにはスパンで動作するメソッドがますます追加されています。 .NET 9 には、次のスパン関連の更新プログラムが含まれています。

ファイル ヘルパー

File クラスには、ファイルにReadOnlySpan<char>/ReadOnlySpan<byte>ReadOnlyMemory<char>/ReadOnlyMemory<byte>を簡単かつ直接書き込む新しいヘルパーが追加されました。

次のコードは、ファイルに ReadOnlySpan<char> を効率的に書き込みます。

ReadOnlySpan<char> text = ...;
File.WriteAllText(filePath, text);

スパンに対して新しい StartsWith<T>(ReadOnlySpan<T>, T) および EndsWith<T>(ReadOnlySpan<T>, T) 拡張メソッドも追加されているため、 ReadOnlySpan<T> が特定の T 値で開始または終了するかどうかを簡単にテストできます。

次のコードでは、これらの新しい便利な API を使用します。

ReadOnlySpan<char> text = "some arbitrary text";
return text.StartsWith('"') && text.EndsWith('"'); // false

params ReadOnlySpan<T> オーバー ロード

C# では、配列パラメーターを paramsとしてマークする機能が常にサポートされています。 このキーワードを使用すると、簡単な呼び出し構文を使用できます。 たとえば、 String.Join(String, String[]) メソッドの 2 番目のパラメーターは、 paramsでマークされます。 このオーバーロードは、配列で呼び出すか、値を個別に渡すことによって呼び出すことができます。

string result = string.Join(", ", new string[3] { "a", "b", "c" });
string result = string.Join(", ", "a", "b", "c");

.NET 9 より前では、値を個別に渡すと、C# コンパイラは、3 つの引数を囲む暗黙的な配列を生成することで、最初の呼び出しと同じコードを出力します。

C# 13 以降では、スパン (paramsSpan<T>) など、コレクション式を使用して構築できる任意の引数でReadOnlySpan<T>を使用できます。 これは、使いやすさとパフォーマンスに役立ちます。 C# コンパイラは、スタックに引数を格納し、その周囲にスパンをラップし、それをメソッドに渡すことができます。そのため、それ以外の場合に発生する暗黙的な配列の割り当てを回避できます。

.NET 9 には、 params ReadOnlySpan<T> パラメーターを持つ 60 を超えるメソッドが含まれています。 新しいオーバーロードもあれば、既に ReadOnlySpan<T> を受け取ったものの、そのパラメーターが params でマークされている既存のメソッドもあります。 .NET 9 にアップグレードしてコードを再コンパイルすると、コードを変更することなくパフォーマンスが向上します。 これは、コンパイラが配列ベースのオーバーロードよりもスパンベースのオーバーロードにバインドすることを好むからです。

たとえば、 String.Join には、新しいパターンを実装する次のオーバーロードが含まれるようになりました。 String.Join(String, ReadOnlySpan<String>)

これで、string.Join(", ", "a", "b", "c")"a"、および"b"引数を渡す配列を割り当てずに、"c"などの呼び出しが行われます。

ReadOnlySpan<char> を列挙します。Split() セグメント

string.Split は、1 つ以上の区切り記号を使用して文字列をすばやくパーティション分割するための便利な方法です。 ただし、パフォーマンスに重点を置いたコードの場合、 string.Split の割り当てプロファイルは、解析されたコンポーネントごとに文字列を割り当て、それらをすべて格納する string[] が割り当てられるため、非常に大きな可能性があります。 スパンでも機能しないので、 ReadOnlySpan<char>がある場合は、文字列に変換するときにさらに別の文字列を割り当てて、 string.Split を呼び出せるようにする必要があります。

.NET 8 では、Split用に一連のSplitAnyメソッドとReadOnlySpan<char> メソッドが導入されました。 これらのメソッドは、新しい string[]を返すのではなく、各コンポーネントの境界インデックスが書き込まれる宛先 Span<Range> を受け入れます。 これにより、操作は完全に割り当て不要になります。 これらのメソッドは、範囲の数が既知と小の両方の場合に使用するのに適しています。

.NET 9 では、SplitSplitAnyの新しいオーバーロードが追加され、セグメントの数が不明なReadOnlySpan<T>するを段階的に解析できます。 新しいメソッドを使用すると、各セグメントを列挙できます。これは、元のスパンにスライスするために使用できる Range と同様に表されます。

public static bool ListContainsItem(ReadOnlySpan<char> span, string item)
{
    foreach (Range segment in span.Split(','))
    {
        if (span[segment].SequenceEquals(item))
        {
            return true;
        }
    }

    return false;
}

システム.フォーマット

TarEntry オブジェクトの外側のストリーム内のデータの位置またはオフセットがパブリック プロパティになりました。 TarEntry.DataOffset は、エントリの最初のデータ バイトが配置されているエントリのアーカイブ ストリーム内の位置を返します。 エントリのデータは、 TarEntry.DataStream経由でアクセスできるサブストリームにカプセル化され、アーカイブ ストリームに対するデータの実際の位置が非表示になります。 これはほとんどのユーザーにとって十分ですが、柔軟性を高める必要があり、アーカイブ ストリーム内のデータの実際の開始位置を知りたい場合は、新しい TarEntry.DataOffset API を使用すると、非常に大きな TAR ファイルでの同時アクセスなどの機能を簡単にサポートできます。

// Create stream for tar ball data in Azure Blob Storage.
BlobClient blobClient = new(connectionString, blobContainerName, blobName);
Stream blobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);

// Create TarReader for the stream and get a TarEntry.
TarReader tarReader = new(blobClientStream);
System.Formats.Tar.TarEntry? tarEntry = await tarReader.GetNextEntryAsync();

if (tarEntry is null)
    return;

// Get position of TarEntry data in blob stream.
long entryOffsetInBlobStream = tarEntry.DataOffset;
long entryLength = tarEntry.Length;

// Create a separate stream.
Stream newBlobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);
newBlobClientStream.Seek(entryOffsetInBlobStream, SeekOrigin.Begin);

// Read tar ball content from separate BlobClient stream.
byte[] bytes = new byte[entryLength];
await newBlobClientStream.ReadExactlyAsync(bytes, 0, (int)entryLength);

System.Guid

NewGuid()は、RFC 9562 の UUID バージョン 4 仕様に従って、主Guidで満たされたを作成します。 同じ RFC では、バージョン 7 を含む他のバージョンも定義されています。このバージョンは、広く実装され、よく知られている Unix エポック タイムスタンプ ソースから派生した時間順の値フィールドを備えています。 つまり、データの多くは依然としてランダムですが、その一部はタイムスタンプに基づいてデータ用に予約されているため、これらの値は自然な並べ替え順序になります。 .NET 9 では、新しいGuidおよびGuid.CreateVersion7()メソッドを使用して、バージョン 7 に従ってGuid.CreateVersion7(DateTimeOffset)を作成できます。 新しい Version プロパティを使用して、 Guid オブジェクトのバージョン フィールドを取得することもできます。

System.IO

zlib-ng による圧縮

System.IO.Compression ZipArchiveDeflateStreamGZipStreamZLibStreamなどの機能はすべて、主に zlib ライブラリに基づいています。 .NET 9 以降では、これらの機能はすべて zlib-ng を使用します。これは、より広範なオペレーティング システムとハードウェア間でより一貫性のある効率的な処理を実現するライブラリです。

ZLib および Brotli 圧縮オプション

ZLibCompressionOptions BrotliCompressionOptionsは、アルゴリズム固有の圧縮レベルと戦略 (DefaultFilteredHuffmanOnlyRunLengthEncoding、またはFixed) を設定するための新しい種類です。 これらの型は、既存の唯一のオプションである <System.IO.Compression.CompressionLevel> よりも微調整された設定が必要なユーザーを対象としています。

新しい圧縮オプションの種類は、今後拡張される可能性があります。

次のコード スニペットは、使用例を示しています。

private MemoryStream CompressStream(Stream uncompressedStream)
{
    MemoryStream compressorOutput = new();
    using ZLibStream compressionStream = new(
        compressorOutput,
        new ZLibCompressionOptions()
        {
            CompressionLevel = 6,
            CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly
        }
        );
    uncompressedStream.CopyTo(compressionStream);
    compressionStream.Flush();

    return compressorOutput;
}

XPS 仮想プリンターからの XPS ドキュメント

以前、V4 XPS 仮想プリンターから送信された XPS ドキュメントは、System.IO.Packaging ファイルの処理のサポートがないため、 ライブラリを使用して開くことができませんでした。 このギャップは、.NET 9 で対処されています。

システム数値

BigInteger の上限

BigInteger は、基本的に任意の長さの整数値を表します。 ただし、実際には、使用可能なメモリや特定の式の計算にかかる時間など、基になるコンピューターの制限によって長さが制限されます。 さらに、指定された入力に失敗し、値が大きすぎる API がいくつか存在します。 これらの制限により、.NET 9 では最大長の BigIntegerが適用されます。つまり、 (2^31) - 1 (約 21 億 4000 万) ビット以下を含めることができます。 このような数値は、ほぼ 256 MB の割り当てを表し、約 6 億 4,650 万桁を含みます。 この新しい制限により、公開されているすべての API が適切に動作し、一貫性を保ちながら、ほとんどの使用シナリオをはるかに超える数値が許可されます。

BigMul API

BigMul は、2 つの数値の完全な積を生成する操作です。 .NET 9 では、戻り値の型がパラメーター型よりも次に大きいBigMulであるintlonguint、およびulongに専用の API が追加されます。

新しい API は次のとおりです。

ベクター変換 API

.NET 9 では、 Vector2Vector3Vector4QuaternionPlaneの間で変換するための専用の拡張 API が追加されています。

新しい API は次のとおりです。

Vector4QuaternionPlaneの間など、同じサイズの変換の場合、これらの変換はコストがゼロになります。 Vector4からVector2Vector3への変換を縮小する場合も同様です。 Vector2Vector3からVector4への拡大変換の場合、新しい要素を 0 に初期化する通常の API と、これらの新しい要素を未定義のままにしてコストがゼロになるUnsafeサフィックス付き API があります。

ベクター作成 API

CreateVectorVector2、およびVector3用に公開される新しいVector4 API があり、System.Runtime.Intrinsics名前空間で公開されているハードウェア ベクター型に対して同等の API をパリティします。

新しい API の詳細については、以下を参照してください。

これらの API は、主に利便性と全体的な整合性を目的とします。NET の SIMD アクセラレータ型。

追加の高速化

System.NumericsBigIntegerVector2Vector3Vector4Quaternionなど、Plane名前空間の多くの種類に対してパフォーマンスが向上しました。

場合によっては、この結果、 Matrix4x4 乗算、一連の頂点からの Plane の作成、 Quaternion 連結、 Vector3のクロス積の計算など、コア API が 2 から 5 倍高速化されました。

1 回の呼び出しでSinCosSin(x)の両方を計算するCos(x) API の定数折りたたみサポートもあり、より効率的になります。

AI の Tensors

テンソルは人工知能 (AI) の基礎となるデータ構造です。 多次元配列と考えられることがよくあります。

テンソルは次の用途に使用されます。

  • テキスト シーケンス (トークン)、画像、ビデオ、オーディオなどのデータを表し、エンコードします。
  • 高次元データを効率的に操作します。
  • 高次元データに計算を効率的に適用します。
  • 重み情報と中間計算を (ニューラル ネットワークに) 格納します。

.NET tensor API を使用するには、 System.Numerics.Tensors NuGet パッケージをインストールします。

新しい Tensor<T> 型

新しい Tensor<T> 型により、.NET ライブラリとランタイムの AI 機能が拡張されます。 この型:

  • 可能な場合はゼロ コピーを使用して、ML.NET、TorchSharp、ONNX Runtime などの AI ライブラリとの効率的な相互運用を提供します。
  • 効率的な算術演算のために TensorPrimitives に基づいて構築されます。
  • インデックス作成とスライス操作を提供することで、簡単かつ効率的なデータ操作が可能になります。
  • 既存の AI と機械学習ライブラリの代わりではありません。 代わりに、コードの重複と依存関係を減らし、最新のランタイム機能を使用してパフォーマンスを向上させるために、共通の API セットを提供することを目的としています。

次のコードは、新しい Tensor<T> の種類に含まれる API の一部を示しています。

// Create a tensor (1 x 3).
Tensor<int> t0 = Tensor.Create([1, 2, 3], [1, 3]); // [[1, 2, 3]]

// Reshape tensor (3 x 1).
Tensor<int> t1 = t0.Reshape(3, 1); // [[1], [2], [3]]

// Slice tensor (2 x 1).
Tensor<int> t2 = t1.Slice(1.., ..); // [[2], [3]]

// Broadcast tensor (3 x 1) -> (3 x 3).
// [
//  [ 1, 1, 1],
//  [ 2, 2, 2],
//  [ 3, 3, 3]
// ]
var t3 = Tensor.Broadcast<int>(t1, [3, 3]);

// Math operations.
var t4 = Tensor.Add(t0, 1); // [[2, 3, 4]]
var t5 = Tensor.Add(t0.AsReadOnlyTensorSpan(), t0); // [[2, 4, 6]]
var t6 = Tensor.Subtract(t0, 1); // [[0, 1, 2]]
var t7 = Tensor.Subtract(t0.AsReadOnlyTensorSpan(), t0); // [[0, 0, 0]]
var t8 = Tensor.Multiply(t0, 2); // [[2, 4, 6]]
var t9 = Tensor.Multiply(t0.AsReadOnlyTensorSpan(), t0); // [[1, 4, 9]]
var t10 = Tensor.Divide(t0, 2); // [[0.5, 1, 1.5]]
var t11 = Tensor.Divide(t0.AsReadOnlyTensorSpan(), t0); // [[1, 1, 1]]

この API は、.NET 9 の 試験段階 としてマークされています。

テンソルプリミティブ

System.Numerics.Tensors ライブラリには、値のスパンに対して数値演算を実行するための静的メソッドを提供するTensorPrimitives クラスが含まれています。 .NET 9 では、 TensorPrimitives によって公開されるメソッドの範囲が大幅に拡張され、40 (.NET 8) から約 200 個のオーバーロードに拡大しました。 このサーフェス領域には、 MathMathFなどの種類の使い慣れた数値演算が含まれます。 また、個々の値を処理する代わりに、値のスパンを処理する以外は、 INumber<TSelf>などの汎用数学インターフェイスも含まれます。 多くの操作は、.NET 9 用の SIMD 最適化実装によっても高速化されています。

TensorPrimitives では、特定のインターフェイスを実装する任意の型 T のジェネリック オーバーロードが公開されるようになりました。 (.NET 8 バージョンには、 float 値のスパンを操作するためのオーバーロードのみが含まれていました)。たとえば、新しい CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) オーバーロードは、 floatdouble、または Half 値の 2 つのベクトル、または IRootFunctions<TSelf>を実装する他の型の値に対してコサインの類似性を実行します。

float型とdoubleの 2 つのベクトルに対するコサイン類似性演算の精度を比較します。

ReadOnlySpan<float> vector1 = [1, 2, 3];
ReadOnlySpan<float> vector2 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector1, vector2));
// Prints 0.9746318

ReadOnlySpan<double> vector3 = [1, 2, 3];
ReadOnlySpan<double> vector4 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector3, vector4));
// Prints 0.9746318461970762

スレッド

スレッド API には、優先順位付けされたチャネルに対するタスクの反復処理の機能強化が含まれています。このチャネルでは、先入れ先出し (FIFO) ではなく要素を並べ替え、より多くの型に対して Interlocked.CompareExchange できます。

Task.WhenEach

Task<TResult> オブジェクトの操作に役立つさまざまな新しい API が追加されました。 新しい Task.WhenEach メソッドを使用すると、 await foreach ステートメントを使用してタスクの完了時に反復処理を行うことができます。 一連のタスクで Task.WaitAny を繰り返し呼び出して、完了した次のタスクを選択する必要がなくなりました。

次のコードは、複数の HttpClient 呼び出しを行い、完了した結果を操作します。

using HttpClient http = new();

Task<string> dotnet = http.GetStringAsync("http://dot.net");
Task<string> bing = http.GetStringAsync("http://www.bing.com");
Task<string> ms = http.GetStringAsync("http://microsoft.com");

await foreach (Task<string> t in Task.WhenEach(bing, dotnet, ms))
{
    Console.WriteLine(t.Result);
}

優先順位付けされていないチャネル

System.Threading.Channels名前空間を使用すると、CreateBoundedメソッドと CreateUnbounded メソッドを使用して先入れ先出し (FIFO) チャネルを作成できます。 FIFO チャネルでは、要素は書き込まれた順序でチャネルから読み取られます。 .NET 9 では、新しい CreateUnboundedPrioritized メソッドが追加されました。これにより、チャネルから読み取られた次の要素が、 Comparer<T>.Default またはカスタム IComparer<T>に従って最も重要と見なされるように要素を並べ替えます。

次の例では、新しいメソッドを使用して、チャネルに別の順序で書き込まれている場合でも、番号 1 から 5 を順番に出力するチャネルを作成します。

Channel<int> c = Channel.CreateUnboundedPrioritized<int>();

await c.Writer.WriteAsync(1);
await c.Writer.WriteAsync(5);
await c.Writer.WriteAsync(2);
await c.Writer.WriteAsync(4);
await c.Writer.WriteAsync(3);
c.Writer.Complete();

while (await c.Reader.WaitToReadAsync())
{
    while (c.Reader.TryRead(out int item))
    {
        Console.Write($"{item} ");
    }
}

// Output: 1 2 3 4 5

Interlocked.CompareExchange (その他の型)

以前のバージョンの .NET では、 Interlocked.ExchangeInterlocked.CompareExchange には、 intuintlongulongnintnuintfloatdoubleobjectを操作するためのオーバーロードと、任意の参照型 Tを操作するためのジェネリック オーバーロードがありました。 .NET 9 には、 bytesbyteshortushortをアトミックに操作するための新しいオーバーロードがあります。 また、ジェネリック Interlocked.Exchange<T> および Interlocked.CompareExchange<T> オーバーロードに対するジェネリック制約が削除されたため、これらのメソッドは参照型でのみ機能するように制約されなくなります。 前述のすべての型、 boolchar、および任意の enum 型を含む任意のプリミティブ型で動作できるようになりました。