Udostępnij przez


Co nowego w bibliotekach platformy .NET dla platformy .NET 9

W tym artykule opisano nowe funkcje w bibliotekach .NET dla platformy .NET 9.

Adres URL Base64

Base64 to schemat kodowania, który tłumaczy dowolne bajty na tekst składający się z określonego zestawu 64 znaków. Jest to typowe podejście do przesyłania danych i od dawna obsługiwane za pośrednictwem różnych metod, takich jak w przypadku Convert.ToBase64String metody lub Base64.DecodeFromUtf8(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32, Boolean). Jednak niektóre używane znaki sprawiają, że jest mniej niż idealny do użycia w niektórych okolicznościach, w przeciwnym razie możesz chcieć go użyć, na przykład w ciągach zapytania. W szczególności 64 znaki składające się z tabeli Base64 zawierają znaki "+" i "/", z których oba mają własne znaczenie w adresach URL. Doprowadziło to do utworzenia schematu Base64Url, który jest podobny do Base64, ale używa nieco innego zestawu znaków, który sprawia, że jest odpowiedni do użycia w kontekstach adresów URL. Platforma .NET 9 zawiera nową Base64Url klasę, która udostępnia wiele przydatnych i zoptymalizowanych metod kodowania i dekodowania Base64Url z do i z różnych typów danych.

W poniższym przykładzie pokazano użycie nowej klasy.

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

Formater binarny

Program .NET 9 usuwa ze środowiska uruchomieniowego platformy BinaryFormatter .NET. Interfejsy API są nadal obecne, ale ich implementacje zawsze zgłaszają wyjątek, niezależnie od typu projektu. Aby uzyskać więcej informacji na temat usuwania i opcji, jeśli wystąpi problem, zobacz BinaryFormatter migration guide (Przewodnik migracji binaryFormatter).

Zbiory

Typy kolekcji na platformie .NET uzyskują następujące aktualizacje dla platformy .NET 9:

Wyszukiwanie kolekcji z zakresami

W kodzie o wysokiej wydajności zakresy są często używane, aby uniknąć niepotrzebnego przydzielania ciągów, a tabele odnośników z typami takimi jak Dictionary<TKey,TValue> i HashSet<T> są często używane jako pamięci podręczne. Nie było jednak bezpiecznego, wbudowanego mechanizmu do wyszukiwania w tych typach kolekcji z zakresami. allows ref struct Nowa funkcja w języku C# 13 i nowych funkcji w tych typach kolekcji na platformie .NET 9 umożliwia teraz wykonywanie tego rodzaju odnośników.

W poniższym przykładzie pokazano użycie TKey słownika,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>

W wielu scenariuszach można przechowywać pary klucz-wartość w sposób, w którym można zachować kolejność (listę par klucz-wartość), ale gdzie szybkie wyszukiwanie według klucza jest również obsługiwane (słownik par klucz-wartość). Od wczesnych dni platformy .NET OrderedDictionary typ obsługuje ten scenariusz, ale tylko w sposób niegeneryczny z kluczami i wartościami wpisanymi jako object. Platforma .NET 9 wprowadza długo żądaną OrderedDictionary<TKey,TValue> kolekcję, która zapewnia wydajny, ogólny typ do obsługi tych scenariuszy.

Poniższy kod używa nowej klasy.

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(), metoda

Platforma .NET 6 wprowadziła PriorityQueue<TElement,TPriority> kolekcję, która zapewnia prostą i szybką implementację stert tablicy. Jednym z problemów z stertami tablicy jest to, że nie obsługują aktualizacji priorytetów, co sprawia, że są one zbyt uciążliwe do użycia w algorytmach, takich jak odmiany algorytmu Dijkstra.

Chociaż nie można zaimplementować wydajnych aktualizacji priorytetów $O(\log n)$ w istniejącej kolekcji, nowa PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) metoda umożliwia emulowanie aktualizacji priorytetów (choć w czasie $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);
}

Ta metoda odblokowuje użytkowników, którzy chcą implementować algorytmy grafu w kontekstach, w których wydajność asymptotyczna nie jest blokowaniem. (Takie konteksty obejmują edukację i tworzenie prototypów). Na przykład poniżej przedstawiono implementację algorytmu toy Dijkstra , który używa nowego interfejsu API.

ReadOnlySet<T>

Często pożądane jest rozdanie widoków kolekcji tylko do odczytu. ReadOnlyCollection<T> Umożliwia utworzenie otoki tylko do odczytu wokół dowolnego modyfikowalnego IList<T>elementu i ReadOnlyDictionary<TKey,TValue> umożliwia utworzenie otoki tylko do odczytu wokół dowolnego modyfikowalnego IDictionary<TKey,TValue>elementu . Jednak wcześniejsze wersje platformy .NET nie miały wbudowanej obsługi tego samego działania w programie ISet<T>. W programie .NET 9 wprowadzono rozwiązanie tego problemu ReadOnlySet<T> .

Nowa klasa umożliwia następujący wzorzec użycia.

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

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

Model składników — TypeDescriptor obsługa przycinania

System.ComponentModel Zawiera nowe interfejsy API zgodne z trymerem do opisywania składników. Każda aplikacja, szczególnie samodzielne przycinane aplikacje, może używać tych nowych interfejsów API, aby ułatwić obsługę scenariuszy przycinania.

Podstawowym interfejsem TypeDescriptor.RegisterType API jest metoda w TypeDescriptor klasie . Ta metoda ma DynamicallyAccessedMembersAttribute atrybut , aby trimmer zachowywał elementy członkowskie dla tego typu. Tę metodę należy wywołać raz na typ i zazwyczaj na początku.

Pomocnicze interfejsy API mają FromRegisteredType sufiks, taki jak TypeDescriptor.GetPropertiesFromRegisteredType(Type). W przeciwieństwie do ich odpowiedników, które nie mają sufiksu FromRegisteredType , te interfejsy API nie mają [RequiresUnreferencedCode] ani [DynamicallyAccessedMembers] atrybutów trymeru. Brak atrybutów trymeru pomaga konsumentom, nie muszą już mieć jednego z następujących elementów:

  • Pomijanie ostrzeżeń dotyczących przycinania, które mogą być ryzykowne.
  • Propagacja silnie typizowanego Type parametru do innych metod, które mogą być kłopotliwe lub niewykonalne.
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; }
}

Aby uzyskać więcej informacji, zobacz propozycję interfejsu API .

Kryptografia

CryptographicOperations.HashData(), metoda

Platforma .NET zawiera kilka statycznych implementacji funkcji skrótu i powiązanych funkcji skrótu. Te interfejsy API obejmują SHA256.HashData i HMACSHA256.HashData. Interfejsy API jednorazowe są preferowane do użycia, ponieważ mogą zapewnić najlepszą możliwą wydajność i zmniejszyć lub wyeliminować alokacje.

Jeśli deweloper chce udostępnić interfejs API, który obsługuje wyznaczanie wartości skrótu, w którym obiekt wywołujący definiuje algorytm wyznaczania wartości skrótu, zwykle odbywa się to przez akceptowanie argumentu HashAlgorithmName . Jednak użycie tego wzorca z interfejsami API jednoskrętowymi wymagałoby przełączenia się na wszystkie możliwe HashAlgorithmName , a następnie przy użyciu odpowiedniej metody. Aby rozwiązać ten problem, platforma CryptographicOperations.HashData .NET 9 wprowadza interfejs API. Ten interfejs API umożliwia utworzenie skrótu lub HMAC dla danych wejściowych jako jednostrzelenia, w którym używany algorytm jest określany przez element HashAlgorithmName.

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

Algorytm KMAC

Platforma .NET 9 udostępnia algorytm KMAC określony przez NIST SP-800-185. KeCCAK Message Authentication Code (KMAC) to funkcja pseudorandom i funkcja skrótu klucza oparta na keCCAK.

Następujące nowe klasy używają algorytmu KMAC. Użyj wystąpień, aby zebrać dane w celu utworzenia komputera MAC lub użyć metody statycznej HashData dla jednego rzutu danych wejściowych.

Funkcja KMAC jest dostępna w systemie Linux z programem OpenSSL 3.0 lub nowszym oraz w systemie Windows 11 Build 26016 lub nowszym. Możesz użyć właściwości statycznej IsSupported , aby określić, czy platforma obsługuje żądany algorytm.

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

algorytmy AES-GCM i ChaChaPoly1305 włączone dla systemów iOS/tvOS/MacCatalyst

IsSupported a ChaChaPoly1305.IsSupported teraz zwraca wartość true podczas uruchamiania w systemach iOS 13+, tvOS 13+i Mac Catalyst.

AesGcm Obsługuje tylko 16-bajtowe (128-bitowe) wartości tagów w systemach operacyjnych firmy Apple.

Ładowanie certyfikatu X.509

Ponieważ program .NET Framework 2.0 umożliwia ładowanie certyfikatu, to new X509Certificate2(bytes). Istnieją również inne wzorce, takie jak new X509Certificate2(bytes, password, flags), new X509Certificate2(path), new X509Certificate2(path, password, flags)i X509Certificate2Collection.Import(bytes, password, flags) (i jego przeciążenia).

Wszystkie metody te używały wąchania zawartości, aby ustalić, czy dane wejściowe były czymś, co może obsłużyć, a następnie załadować je, jeśli może. Dla niektórych rozmówców ta strategia była bardzo wygodna. Ale ma również pewne problemy:

  • Nie każdy format pliku działa w każdym systemie operacyjnym.
  • Jest to odchylenie protokołu.
  • Jest to źródło problemów z zabezpieczeniami.

Platforma .NET 9 wprowadza nową X509CertificateLoader klasę, która ma projekt "jedna metoda, jeden cel". W początkowej wersji obsługuje tylko dwa z pięciu formatów obsługiwanych przez konstruktora X509Certificate2 . Są to dwa formaty, które pracowały nad wszystkimi systemami operacyjnymi.

Obsługa dostawców openSSL

Platforma .NET 8 wprowadziła interfejsy OpenPrivateKeyFromEngine(String, String) API specyficzne dla protokołu OpenSSL i OpenPublicKeyFromEngine(String, String). Umożliwiają one interakcję ze składnikami OpenSSL ENGINE i używają na przykład sprzętowych modułów zabezpieczeń (HSM).

.NET 9 wprowadza program SafeEvpPKeyHandle.OpenKeyFromProvider(String, String), który umożliwia korzystanie z dostawców openSSL i interakcję z dostawcami, takimi jak tpm2 lub pkcs11.

Niektóre dystrybucje usunęły ENGINE obsługę , ponieważ jest ona teraz przestarzała.

Poniższy fragment kodu przedstawia podstawowe użycie:

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

Podczas uzgadniania protokołu TLS wprowadzono pewne ulepszenia wydajności, a także ulepszenia interakcji z kluczami prywatnymi RSA korzystającymi ze ENGINE składników.

Zabezpieczenia oparte na wirtualizacji CNG systemu Windows

System Windows 11 dodał nowe interfejsy API ułatwiające zabezpieczanie kluczy systemu Windows przy użyciu zabezpieczeń opartych na wirtualizacji (VBS). Dzięki tej nowej funkcji klucze mogą być chronione przed atakami kradzieży kluczy na poziomie administratora z niewielkim wpływem na wydajność, niezawodność lub skalę.

Platforma .NET 9 dodała pasujące CngKeyCreationOptions flagi. Dodano następujące trzy flagi:

  • CngKeyCreationOptions.PreferVbs Dopasowania NCRYPT_PREFER_VBS_FLAG
  • CngKeyCreationOptions.RequireVbs Dopasowania NCRYPT_REQUIRE_VBS_FLAG
  • CngKeyCreationOptions.UsePerBootKey Dopasowania NCRYPT_USE_PER_BOOT_KEY_FLAG

Poniższy fragment kodu pokazuje, jak używać jednej z flag:

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

Data i godzina — nowe przeciążenia TimeSpan.From*

Klasa TimeSpan oferuje kilka From* metod, które umożliwiają utworzenie TimeSpan obiektu przy użyciu klasy double. Jednak ponieważ double jest formatem zmiennoprzecinkowym opartym na binarnej, nieodłączną nieprawdopodobnością może prowadzić do błędów. Na przykład może nie być dokładnie reprezentowana TimeSpan.FromSeconds(101.832), 101 seconds, 832 milliseconds ale raczej w przybliżeniu 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds. Ta rozbieżność spowodowała częste zamieszanie i nie jest to również najbardziej wydajny sposób reprezentowania takich danych. Aby rozwiązać ten problem, platforma .NET 9 dodaje nowe przeciążenia, które umożliwiają tworzenie TimeSpan obiektów na podstawie liczb całkowitych. Istnieją nowe przeciążenia z FromDayssystemu , , FromHours, FromMinutesFromSeconds, FromMilliseconds, i FromMicroseconds.

Poniższy kod przedstawia przykład wywoływania double i jednego z nowych przeciążeń liczb całkowitych.

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

Wstrzykiwanie zależności — ActivatorUtilities.CreateInstance konstruktor

Rozdzielczość konstruktora dla ActivatorUtilities.CreateInstance elementu została zmieniona na platformie .NET 9. Wcześniej konstruktor, który został jawnie oznaczony za pomocą atrybutu ActivatorUtilitiesConstructorAttribute , może nie być wywoływany, w zależności od kolejności konstruktorów i liczby parametrów konstruktora. Logika została zmieniona na platformie .NET 9, tak aby konstruktor, który ma atrybut, był zawsze wywoływany.

Diagnostyka

Warunek asertywnego aserowania raportów Debug.Assert domyślnie

Debug.Assert jest często używany do sprawdzania poprawności warunków, które powinny być zawsze prawdziwe. Błąd zazwyczaj wskazuje usterkę w kodzie. Istnieje wiele przeciążeń Debug.Assertmetody , z których najprostsze są tylko zaakceptowanie warunku:

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

Potwierdzenie kończy się niepowodzeniem, jeśli warunek ma wartość false. Historycznie jednak takie asercyjnie były unieważnione wszelkie informacje o tym, jaki warunek się nie powiódł. Począwszy od platformy .NET 9, jeśli żaden komunikat nie zostanie jawnie dostarczony przez użytkownika, asercja będzie zawierać tekstową reprezentację warunku. Na przykład w poprzednim przykładzie potwierdzenia zamiast pojawiać się komunikat podobny do następującego:

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

Komunikat będzie teraz następujący:

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

Wcześniej można było połączyć śledzenie Activity tylko z innymi kontekstami śledzenia podczas tworzenia elementu Activity. Nowy na platformie .NET 9 interfejs AddLink(ActivityLink) API umożliwia łączenie Activity obiektu z innymi kontekstami śledzenia po jego utworzeniu. Ta zmiana jest również zgodna ze specyfikacjami OpenTelemetry .

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

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

Instrument Metrics.Gauge

System.Diagnostics.Metrics teraz udostępnia Gauge<T> instrument zgodnie ze specyfikacją OpenTelemetry. Instrument jest przeznaczony do rejestrowania Gauge wartości innych niż addytywne w przypadku wystąpienia zmian. Na przykład może mierzyć poziom szumu tła, w którym sumowanie wartości z wielu pomieszczeń byłoby niesensowne. Gauge Instrument jest typem ogólnym, który może rejestrować dowolny typ wartości, taki jak int, doublelub decimal.

W poniższym przykładzie pokazano użycie Gauge instrumentu.

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 Miernik wieloznaczny nasłuchiwanie

Istnieje już możliwość nasłuchiwania mierników poza procesem przy użyciu System.Diagnostics.Metrics dostawcy źródła zdarzeń, ale przed platformą .NET 9 trzeba było określić pełną nazwę miernika. Na platformie .NET 9 można nasłuchiwać wszystkich mierników przy użyciu symbolu *wieloznakowego , który umożliwia przechwytywanie metryk z każdego miernika w procesie. Ponadto dodaje obsługę nasłuchiwania według prefiksu miernika, dzięki czemu można nasłuchiwać wszystkich mierników, których nazwy zaczynają się od określonego prefiksu. Na przykład określenie umożliwia nasłuchiwanie MyMeter* wszystkich mierników z nazwami rozpoczynającymi się od 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();

Klasa jest zdefiniowana MyEventListener w następujący sposób.

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

Po wykonaniu kodu dane wyjściowe są następujące:

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

Możesz również użyć symbolu wieloznakowego, aby nasłuchiwać metryk za pomocą narzędzi do monitorowania, takich jak dotnet-counters.

LINQ

Wprowadzono nowe metody CountByAggregateBy . Te metody umożliwiają agregowanie stanu według klucza bez konieczności przydzielania grup pośrednich za pośrednictwem metody GroupBy.

CountBy pozwala szybko obliczyć częstotliwość każdego klucza. W poniższym przykładzie znajduje się słowo, które występuje najczęściej w ciągu tekstowym.

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 umożliwia implementowanie bardziej ogólnych przepływów pracy. W poniższym przykładzie pokazano, jak można obliczyć wyniki skojarzone z danym kluczem.

(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>) umożliwia szybkie wyodrębnianie niejawnego indeksu wyliczalnego. Teraz możesz napisać kod, taki jak poniższy fragment kodu, aby automatycznie indeksować elementy w kolekcji.

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

Generator źródła rejestrowania

Język C# 12 wprowadził konstruktory podstawowe, które umożliwiają definiowanie konstruktora bezpośrednio w deklaracji klasy. Generator źródła rejestrowania obsługuje teraz rejestrowanie przy użyciu klas, które mają podstawowy konstruktor.

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

Różne

W tej sekcji znajdziesz informacje o:

allows ref struct używane w bibliotekach

Język C# 13 wprowadza możliwość ograniczenia parametru ogólnego za pomocą allows ref structpolecenia , który informuje kompilator i środowisko uruchomieniowe, że ref struct można go użyć dla tego parametru ogólnego. Wiele interfejsów API, które są zgodne z tą funkcją, zostało teraz oznaczonych adnotacjami. Na przykład metoda ma przeciążenie, String.Create które pozwala utworzyć string obiekt, zapisując bezpośrednio w pamięci, reprezentowane jako zakres. Ta metoda ma TState argument przekazany z obiektu wywołującego do delegata, który wykonuje rzeczywiste pisanie.

Ten TState parametr typu w parametrze String.Create jest teraz oznaczony adnotacją za pomocą polecenia allows ref struct:

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

Ta adnotacja umożliwia przekazanie zakresu (lub innych ref struct) jako danych wejściowych tej metody.

W poniższym przykładzie przedstawiono nowe String.ToLowerInvariant() przeciążenie, które korzysta z tej możliwości.

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

SearchValues ekspansja

Platforma .NET 8 wprowadziła SearchValues<T> typ, który zapewnia zoptymalizowane rozwiązanie do wyszukiwania określonych zestawów znaków lub bajtów w ramach zakresów. W programie .NET 9 SearchValues rozszerzono o obsługę wyszukiwania podciągów w większym ciągu.

Poniższy przykład wyszukuje wiele nazw zwierząt w ramach wartości ciągu i zwraca indeks do pierwszego znalezionego.

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

Ta nowa funkcja ma zoptymalizowaną implementację, która korzysta z obsługi SIMD na podstawowej platformie. Umożliwia również optymalizowanie typów wyższego poziomu. Na przykład Regex ta funkcja jest teraz używana w ramach implementacji.

Sieć

Funkcja SocketsHttpHandler jest domyślna w elempcie HttpClientFactory

HttpClientFactory domyślnie tworzy HttpClient obiekty wspierane przez HttpClientHandlerelement . HttpClientHandler Jest on wspierany przez SocketsHttpHandlerprogram , który jest znacznie bardziej konfigurowalny, w tym zarządzanie okresem istnienia połączenia. HttpClientFactory teraz używa SocketsHttpHandler domyślnie i konfiguruje go do ustawiania limitów okresów istnienia połączenia, aby dopasować je do okresu obrotu określonego w fabryce.

System.Net.ServerSentEvents

Zdarzenia wysyłane przez serwer (SSE) to prosty i popularny protokół przesyłania strumieniowego danych z serwera do klienta. Jest on używany na przykład przez interfejs OpenAI w ramach przesyłania strumieniowego wygenerowanego tekstu z usług sztucznej inteligencji. Aby uprościć użycie funkcji SSE, nowa System.Net.ServerSentEvents biblioteka udostępnia analizator do łatwego pozyskiwania zdarzeń wysyłanych przez serwer.

Poniższy kod demonstruje użycie nowej klasy.

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

Wznawianie protokołu TLS przy użyciu certyfikatów klienta w systemie Linux

Wznawianie protokołu TLS to funkcja protokołu TLS, która umożliwia wznowienie wcześniej ustanowionych sesji na serwerze. Pozwala to uniknąć kilku rund i oszczędza zasoby obliczeniowe podczas uzgadniania protokołu TLS.

Wznawianie protokołu TLS zostało już obsługiwane w systemie Linux dla połączeń SslStream bez certyfikatów klienta. Platforma .NET 9 dodaje obsługę wznawiania protokołu TLS wzajemnie uwierzytelnionych połączeń TLS, które są typowe w scenariuszach serwer-serwer. Funkcja jest włączana automatycznie.

Polecenie ping i przekroczenie limitu czasu protokołu WebSocket

Nowe interfejsy API włączone ClientWebSocketOptions i WebSocketCreationOptions pozwalają na wysyłanie WebSocket poleceń ping i przerywanie połączenia, jeśli element równorzędny nie odpowiada w odpowiednim czasie.

Do tej pory można określić parametr , KeepAliveInterval aby uniemożliwić zachowanie bezczynności połączenia, ale nie było wbudowanego mechanizmu wymuszania odpowiedzi elementu równorzędnego.

Poniższy przykład wysyła polecenie ping do serwera co 5 sekund i przerywa połączenie, jeśli nie odpowiada w ciągu sekundy.

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

Element HttpClientFactory domyślnie nie rejestruje już wartości nagłówków

LogLevel.Trace zdarzenia rejestrowane przez HttpClientFactory nie zawierają już wartości nagłówka domyślnie. Możesz wyrazić zgodę na rejestrowanie wartości dla określonych nagłówków za pośrednictwem metody pomocniczej RedactLoggedHeaders .

Poniższy przykład redaguje wszystkie nagłówki, z wyjątkiem agenta użytkownika.

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

Aby uzyskać więcej informacji, zobacz HttpClientFactory logging redacts wartości nagłówka domyślnie.

Refleksja

Utrwalone zestawy

W wersjach .NET Core i .NET 5-8 obsługa kompilowania zestawu i emitowania metadanych odbicia dla dynamicznie utworzonych typów była ograniczona do możliwego AssemblyBuilderdo uruchomienia . Brak obsługi zapisywania zestawu był często blokowaniem dla klientów migrujących z programu .NET Framework do platformy .NET. Platforma .NET 9 dodaje nowy typ , PersistedAssemblyBuilderktórego można użyć do zapisania emitowanego zestawu.

Aby utworzyć PersistedAssemblyBuilder wystąpienie, wywołaj jego konstruktor i przekaż nazwę zestawu, podstawowy zestaw, System.Private.CoreLib, w celu odwołania się do podstawowych typów środowiska uruchomieniowego i opcjonalnych atrybutów niestandardowych. Po emisji wszystkich elementów członkowskich do zestawu wywołaj PersistedAssemblyBuilder.Save(String) metodę , aby utworzyć zestaw z ustawieniami domyślnymi. Jeśli chcesz ustawić punkt wejścia lub inne opcje, możesz wywołać PersistedAssemblyBuilder.GenerateMetadata i użyć metadanych zwracanych w celu zapisania zestawu. Poniższy kod przedstawia przykład tworzenia utrwalonego zestawu i ustawiania punktu wejścia.

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

Nowa PersistedAssemblyBuilder klasa zawiera obsługę plików PDB. Możesz emitować informacje o symbolach i używać ich do debugowania wygenerowanego zestawu. Interfejs API ma podobny kształt do implementacji programu .NET Framework. Aby uzyskać więcej informacji, zobacz Emituj symbole i generowanie pdB.

Analizowanie nazw typów

TypeName jest analizatorem dla nazw typów ECMA-335, które zapewniają wiele takich samych funkcji, jak System.Type tylko są oddzielone od środowiska uruchomieniowego. Składniki, takie jak serializatory i kompilatory, muszą analizować i przetwarzać nazwy typów. Na przykład kompilator natywnej AOT przełączył się na użycie polecenia TypeName.

Nowa TypeName klasa zapewnia:

  • Statyczne Parse i TryParse metody analizowania danych wejściowych reprezentowanych jako ReadOnlySpan<char>. Obie metody akceptują wystąpienie TypeNameParseOptions klasy (torbę opcji), która umożliwia dostosowanie analizy.

  • Name, FullNamei AssemblyQualifiedName właściwości, które działają dokładnie tak jak ich odpowiedniki w pliku System.Type.

  • Wiele właściwości i metod, które zawierają dodatkowe informacje o samej nazwie:

    • IsArray, IsSZArray (SZ oznacza jednowymiarową, zero-indeksowaną tablicę), IsVariableBoundArrayTypei GetArrayRank do pracy z tablicami.
    • IsConstructedGenericType, GetGenericTypeDefinitioni GetGenericArguments do pracy z nazwami typów ogólnych.
    • IsByRef oraz IsPointer do pracy ze wskaźnikami i odwołaniami zarządzanymi.
    • GetElementType() do pracy ze wskaźnikami, odwołaniami i tablicami.
    • IsNested i DeclaringType do pracy z typami zagnieżdżonym.
    • AssemblyName, który uwidacznia informacje o nazwie zestawu za pośrednictwem nowej AssemblyNameInfo klasy. W przeciwieństwie do AssemblyNameklasy nowy typ jest niezmienny, a analizowanie nazw kultur nie powoduje utworzenia wystąpień klasy CultureInfo.

Oba TypeName typy i AssemblyNameInfo są niezmienne i nie zapewniają możliwości sprawdzania równości (nie implementują IEquatable). Porównywanie nazw zestawów jest proste, ale różne scenariusze muszą porównywać tylko podzbiór uwidocznionych informacji (Name, Version, CultureNamei PublicKeyOrToken).

Poniższy fragment kodu przedstawia przykładowe użycie.

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

Nowe interfejsy API są dostępne w System.Reflection.Metadata pakiecie NuGet, który może być używany z wersjami platformy .NET na poziomie dół.

Wyrażenia regularne

[GeneratedRegex] na właściwościach

Platforma .NET 7 wprowadziła Regex generator źródła i odpowiadający mu GeneratedRegexAttribute atrybut.

Następująca metoda częściowa zostanie wygenerowana przy użyciu całego kodu niezbędnego do zaimplementowania tego obiektu Regex.

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

Język C# 13 obsługuje właściwości częściowe oprócz metod częściowych, więc począwszy od platformy .NET 9 można również użyć we [GeneratedRegex(...)] właściwości .

Poniższa właściwość częściowa jest odpowiednikiem właściwości poprzedniego przykładu.

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

Regex.EnumerateSplits

Klasa Regex udostępnia metodę podobną Split w koncepcji do String.Split metody . W przypadku String.Splitelementu należy podać co najmniej jeden char separator, string a implementacja dzieli tekst wejściowy na tych separatorach. W Regex.Splitprzypadku elementu zamiast określać separator jako char lub string, jest on określony jako wzorzec wyrażenia regularnego.

W poniższym przykładzie pokazano Regex.Splitpolecenie .

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 Jednak akceptuje string tylko jako dane wejściowe i nie obsługuje danych wejściowych dostarczanych jako ReadOnlySpan<char>. Ponadto zwraca pełny zestaw podziałów jako string[], który wymaga przydzielenia zarówno string tablicy do przechowywania wyników, jak i string dla każdego podziału. Na platformie .NET 9 nowa EnumerateSplits metoda umożliwia wykonywanie tej samej operacji, ale z danymi wejściowymi opartymi na zakresie i bez ponoszenia żadnej alokacji dla wyników. Akceptuje element ReadOnlySpan<char> i zwraca wyliczenie Range obiektów reprezentujących wyniki.

W poniższym przykładzie pokazano Regex.EnumerateSplits, że parametr przyjmuje ReadOnlySpan<char> jako dane wejściowe.

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

Serializacja (System.Text.Json)

Opcje wcięcia

JsonSerializerOptions zawiera nowe właściwości, które umożliwiają dostosowanie znaku wcięcia i rozmiaru wcięcia zapisanego kodu JSON.

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

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

Domyślne opcje sieci Web — pojedynczo

Jeśli chcesz serializować z opcjami domyślnymi, które ASP.NET Core używa dla aplikacji internetowych, użyj nowego JsonSerializerOptions.Web pojedynczego pliku.

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

JsonSchemaExporter

Kod JSON jest często używany do reprezentowania typów w podpisach metod w ramach zdalnych schematów wywoływania procedur. Jest on używany na przykład w ramach specyfikacji interfejsu OpenAPI lub w ramach wywoływania narzędzi z usługami sztucznej inteligencji, takimi jak te z interfejsu OpenAI. Deweloperzy mogą serializować i deserializować typy platformy .NET w formacie JSON przy użyciu polecenia System.Text.Json. Muszą również mieć możliwość uzyskania schematu JSON opisującego kształt typu .NET (czyli opisujący kształt tego, co byłoby serializowane i co można deserializować). System.Text.Json Teraz udostępnia JsonSchemaExporter typ, który obsługuje generowanie schematu JSON, który reprezentuje typ platformy .NET.

Aby uzyskać więcej informacji, zobacz Eksporter schematu JSON.

Uwzględniać adnotacje nullowalne

System.Text.Json Teraz rozpoznaje adnotacje o wartości null właściwości i można je skonfigurować tak, aby były wymuszane podczas serializacji i deserializacji przy użyciu flagi RespectNullableAnnotations .

Poniższy kod pokazuje, jak ustawić opcję:

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

Aby uzyskać więcej informacji, zobacz Szanowane adnotacje dopuszczane do wartości null.

Wymagaj parametrów konstruktora innego niż opcjonalny

W przeszłości System.Text.Json parametry konstruktora nie opcjonalne były traktowane jako opcjonalne podczas używania deserializacji opartej na konstruktorze. To zachowanie można zmienić przy użyciu nowej RespectRequiredConstructorParameters flagi.

Poniższy kod pokazuje, jak ustawić opcję:

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

Typ jest definiowany MyPoco w następujący sposób:

record MyPoco(string Value);

Aby uzyskać więcej informacji, zobacz Parametry konstruktora nie opcjonalnego.

Określanie kolejności właściwości obiektu JsonObject

Typ JsonObject udostępnia teraz uporządkowane interfejsy API, takie jak słowniki, które umożliwiają jawne manipulowanie kolejnością właściwości.

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

Aby uzyskać więcej informacji, zobacz Manipulowanie kolejnością właściwości.

Dostosowywanie nazw elementów członkowskich wyliczenia

Nowy System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute atrybut może służyć do dostosowywania nazw poszczególnych składowych wyliczenia dla typów, które są serializowane jako ciągi:

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

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

Aby uzyskać więcej informacji, zobacz Niestandardowe nazwy składowych wyliczenia.

Przesyłanie strumieniowe wielu dokumentów JSON

System.Text.Json.Utf8JsonReader Obsługuje teraz odczytywanie wielu dokumentów JSON rozdzielonych odstępami z jednego buforu lub strumienia. Domyślnie czytelnik zgłasza wyjątek, jeśli wykryje znaki inne niż białe znaki, które kończą pierwszy dokument najwyższego poziomu. To zachowanie można zmienić przy użyciu flagi AllowMultipleValues .

Aby uzyskać więcej informacji, zobacz Odczytywanie wielu dokumentów JSON.

Zakres

W kodzie o wysokiej wydajności zakresy są często używane, aby uniknąć niepotrzebnego przydzielania ciągów. Span<T> i ReadOnlySpan<T> nadal rewolucjonalizować sposób pisania kodu na platformie .NET, a każda wersja coraz więcej metod jest dodawanych, które działają na zakresach. Program .NET 9 zawiera następujące aktualizacje związane z zakresem:

Pomocnicy plików

Klasa File ma teraz nowe pomocniki do łatwego i bezpośredniego zapisuReadOnlySpan<char>/ReadOnlySpan<byte>i ReadOnlyMemory<char>/ReadOnlyMemory<byte> do plików.

Poniższy kod efektywnie zapisuje element ReadOnlySpan<char> w pliku.

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

Dodano również nowe StartsWith<T>(ReadOnlySpan<T>, T) metody EndsWith<T>(ReadOnlySpan<T>, T) rozszerzeń dla zakresów, co ułatwia testowanie, czy ReadOnlySpan<T> rozpoczyna się, czy kończy się określoną T wartością.

Poniższy kod używa tych nowych interfejsów API wygody.

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

params ReadOnlySpan<T> Przeciążenia

Język C# zawsze obsługiwał oznaczanie parametrów tablicy jako params. To słowo kluczowe umożliwia uproszczoną składnię wywoływania. Na przykład String.Join(String, String[]) drugi parametr metody jest oznaczony znakiem params. To przeciążenie można wywołać za pomocą tablicy lub przekazując wartości indywidualnie:

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

Przed platformą .NET 9 po przekazaniu wartości indywidualnie kompilator języka C# emituje kod identyczny z pierwszym wywołaniem, tworząc niejawną tablicę wokół trzech argumentów.

Począwszy od języka C# 13, można użyć params dowolnego argumentu, który można skonstruować za pomocą wyrażenia kolekcji, w tym zakresów (Span<T> i ReadOnlySpan<T>). Jest to korzystne dla użyteczności i wydajności. Kompilator języka C# może przechowywać argumenty na stosie, opakowować wokół nich zakres i przekazywać je do metody, co pozwala uniknąć niejawnej alokacji tablicy, która w przeciwnym razie doprowadziłaby do tego.

Platforma .NET 9 zawiera ponad 60 metod z parametrem params ReadOnlySpan<T> . Niektóre są zupełnie nowymi przeciążeniami, a niektóre to istniejące metody, które już miały ReadOnlySpan<T> wartość , ale teraz mają ten parametr oznaczony jako params. Efekt net jest, jeśli uaktualnisz do platformy .NET 9 i ponownie skompilujesz kod, zobaczysz ulepszenia wydajności bez wprowadzania żadnych zmian w kodzie. Dzieje się tak dlatego, że kompilator preferuje powiązanie z przeciążeniami opartymi na zakresie niż z przeciążeniami opartymi na tablicy.

Na przykład String.Join teraz obejmuje następujące przeciążenie, które implementuje nowy wzorzec: String.Join(String, ReadOnlySpan<String>)

Teraz wywołanie podobne string.Join(", ", "a", "b", "c") jest wykonywane bez przydzielania tablicy do przekazania "a"argumentów , "b"i "c" .

Wyliczanie za pośrednictwem znaku< ReadOnlySpan>. Segmenty Split()

string.Split jest wygodną metodą szybkiego partycjonowania ciągu z co najmniej jednym dostarczonym separatorem. Jednak w przypadku kodu skoncentrowanego na wydajności profil string.Split alokacji może być zbyt duży, ponieważ przydziela on ciąg dla każdego przeanalizowanego składnika i element , string[] aby przechowywać je wszystkie. Nie działa również z zakresami, więc jeśli masz ReadOnlySpan<char>element , musisz przydzielić kolejny ciąg podczas konwertowania go na ciąg, aby móc go wywołać string.Split .

Na platformie .NET 8 wprowadzono zestaw Split metod i SplitAny .ReadOnlySpan<char> Zamiast zwracać nową string[]metodę, te metody akceptują miejsce docelowe Span<Range> , do którego są zapisywane indeksy ograniczenia dla każdego składnika. Dzięki temu operacja jest w pełni wolna od alokacji. Te metody są odpowiednie do użycia, gdy liczba zakresów jest znana i mała.

Na platformie .NET 9 dodano nowe przeciążenia Split i SplitAny dodano je, aby umożliwić przyrostowe analizowanie ReadOnlySpan<T> elementu z nieznaną wcześniej liczbą segmentów. Nowe metody umożliwiają wyliczanie za pomocą każdego segmentu, który jest podobnie reprezentowany jako element Range , który może służyć do fragmentowania w oryginalnym zakresie.

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

    return false;
}

System.Formats (Formaty systemowe)

Położenie lub przesunięcie danych w otaczającym strumieniu obiektu TarEntry jest teraz właściwością publiczną. TarEntry.DataOffset Zwraca pozycję w strumieniu archiwum wpisu, w którym znajduje się pierwszy bajt danych wpisu. Dane wpisu są hermetyzowane w podstreamie, do którego można uzyskać dostęp za pośrednictwem TarEntry.DataStreamelementu , co ukrywa rzeczywiste położenie danych względem strumienia archiwum. Jest to wystarczające dla większości użytkowników, ale jeśli potrzebujesz większej elastyczności i chcesz poznać rzeczywistą pozycję początkową danych w strumieniu archiwum, nowy TarEntry.DataOffset interfejs API ułatwia obsługę funkcji, takich jak współbieżny dostęp z bardzo dużymi plikami 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() Tworzy Guid wypełniony głównie kryptograficznie bezpiecznymi danymi losowymi, zgodnie ze specyfikacją UUID w wersji 4 w standardzie RFC 9562. To samo RFC definiuje również inne wersje, w tym wersję 7, która "zawiera pole wartości uporządkowane czasowo pochodzące z powszechnie zaimplementowanego i dobrze znanego źródła sygnatury czasowej systemu Unix Epoka". Innymi słowy, większość danych jest nadal losowa, ale niektóre z nich są zarezerwowane dla danych na podstawie sygnatury czasowej, która umożliwia tym wartościom naturalną kolejność sortowania. Na platformie .NET 9 można utworzyć element zgodnie z wersją Guid 7 za pomocą nowych Guid.CreateVersion7() metod i Guid.CreateVersion7(DateTimeOffset) . Możesz również użyć nowej Version właściwości, aby pobrać Guid pole wersji obiektu.

System.IO

Kompresja zlib-ng

System.IO.Compression funkcje takie jak ZipArchive, DeflateStream, GZipStreami ZLibStream są oparte głównie na bibliotece zlib. Począwszy od platformy .NET 9, te funkcje używają biblioteki zlib-ng, która zapewnia bardziej spójne i wydajne przetwarzanie w szerszej gamie systemów operacyjnych i sprzętu.

Opcje kompresji ZLib i Brotli

ZLibCompressionOptions i BrotliCompressionOptions są nowymi typami ustawiania poziomu i strategii specyficznej dla algorytmu (Default, Filtered, HuffmanOnly, , RunLengthEncodinglub Fixed). Te typy są przeznaczone dla użytkowników, którzy chcą bardziej dostroić ustawienia niż tylko istniejąca opcja <System.IO.Compression.CompressionLevel>.

Nowe typy opcji kompresji mogą zostać rozwinięte w przyszłości.

Poniższy fragment kodu przedstawia przykładowe użycie:

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

Dokumenty XPS z drukarki wirtualnej XPS

Nie można było wcześniej otworzyć dokumentów XPS pochodzących z drukarki wirtualnej W WERSJI 4 XPS przy użyciu System.IO.Packaging biblioteki z powodu braku obsługi plików piece . Ta luka została rozwiązana na platformie .NET 9.

System.Numerics

Górna liczba całkowita biginteger

BigInteger program obsługuje reprezentowanie wartości całkowitych o zasadniczo dowolnej długości. Jednak w praktyce długość jest ograniczona przez limity komputera bazowego, takie jak dostępna pamięć lub czas obliczenia danego wyrażenia. Ponadto istnieją pewne interfejsy API, które kończą się niepowodzeniem, biorąc pod uwagę dane wejściowe, które powodują zbyt dużą wartość. Ze względu na te limity platforma .NET 9 wymusza maksymalną długość BigInteger, co oznacza, że może zawierać nie więcej niż (2^31) - 1 (około 2,14 miliarda) bitów. Taka liczba reprezentuje prawie 256 MB alokacji i zawiera około 646,5 miliona cyfr. Ten nowy limit gwarantuje, że wszystkie uwidocznione interfejsy API są dobrze zachowywane i spójne, jednocześnie umożliwiając liczby, które wykraczają daleko poza większość scenariuszy użycia.

BigMul Apis

BigMul jest operacją tworzącą pełny iloczyn dwóch liczb. Platforma .NET 9 dodaje dedykowane BigMul interfejsy API w systemach int, longi uintulong, których typ zwracany jest następnym większym typem liczby całkowitej niż typy parametrów.

Nowe interfejsy API to następujące:

Interfejsy API konwersji wektorów

Platforma .NET 9 dodaje dedykowane interfejsy API rozszerzenia do konwertowania między elementami , , , i Vector2Vector3. Vector4QuaternionPlane

Nowe interfejsy API są następujące:

W przypadku konwersji o takich samych rozmiarach, jak między Vector4, Quaternioni Plane, te konwersje są zerowe. To samo można powiedzieć w przypadku konwersji zawężających, takich jak od Vector4 do lub Vector2Vector3. W przypadku konwersji rozszerzających, takich jak z Vector2 lub Vector3 do Vector4, istnieje normalny interfejs API, który inicjuje nowe elementy do 0, oraz Unsafe sufiks interfejsu API, który pozostawia te nowe elementy niezdefiniowane i dlatego może być zerowy koszt.

Interfejsy API tworzenia wektorów

Istnieją nowe Create interfejsy API uwidocznione dla Vectorelementów , Vector2, Vector3i Vector4 tej parzystości równoważnych interfejsów API uwidocznionych dla typów wektorów sprzętowych uwidocznionych w System.Runtime.Intrinsics przestrzeni nazw.

Aby uzyskać więcej informacji na temat nowych interfejsów API, zobacz:

Te interfejsy API są przeznaczone przede wszystkim dla wygody i ogólnej spójności między . Typy przyspieszone przez sieć SIMD.

Dodatkowe przyspieszenie

Wprowadzono dodatkowe ulepszenia wydajności dla wielu typów w System.Numerics przestrzeni nazw, w tym do BigInteger, , Vector2, Vector3Vector4, , Quaternioni Plane.

W niektórych przypadkach spowodowało to przyspieszenie od 2 do 5 razy do podstawowych interfejsów API, w tym Matrix4x4 mnożenie, tworzenie Plane z serii wierzchołków, Quaternion łączenia i przetwarzania krzyżowego Vector3produktu .

Istnieje również ciągła obsługa składania dla interfejsu SinCos API, który oblicza zarówno, jak Sin(x) i Cos(x) w jednym wywołaniu, co czyni go bardziej wydajnym.

Tensory dla sztucznej inteligencji

Tensors to podstawowa struktura danych sztucznej inteligencji (AI). Często można je traktować jako tablice wielowymiarowe.

Tensors są używane do:

  • Reprezentacja i kodowanie danych, takich jak sekwencje tekstowe (tokeny), obrazy, wideo i dźwięk.
  • Efektywne manipulowanie danymi o wyższych wymiarach.
  • Efektywne stosowanie obliczeń na danych o wyższych wymiarach.
  • Przechowywanie informacji o wadze i obliczeń pośrednich (w sieciach neuronowych).

Aby użyć interfejsów API tensor platformy .NET, zainstaluj pakiet NuGet System.Numerics.Tensors .

Nowy typ tensor<T>

Nowy Tensor<T> typ rozszerza możliwości sztucznej inteligencji bibliotek i środowiska uruchomieniowego platformy .NET. Ten typ:

  • Zapewnia wydajną interakcję z bibliotekami sztucznej inteligencji, takimi jak ML.NET, TorchSharp i ŚRODOWISKO uruchomieniowe ONNX, przy użyciu zerowych kopii tam, gdzie to możliwe.
  • Opiera się na TensorPrimitives dla wydajnych operacji matematycznych.
  • Umożliwia łatwe i wydajne manipulowanie danymi, zapewniając operacje indeksowania i fragmentowania.
  • Nie zastępuje istniejących bibliotek sztucznej inteligencji i uczenia maszynowego. Zamiast tego ma na celu zapewnienie wspólnego zestawu interfejsów API w celu zmniejszenia duplikowania i zależności kodu oraz uzyskania lepszej wydajności przy użyciu najnowszych funkcji środowiska uruchomieniowego.

Poniższe kody zawierają niektóre interfejsy API dołączone do nowego Tensor<T> typu.

// 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]]

Uwaga / Notatka

Ten interfejs API jest oznaczony jako eksperymentalny dla platformy .NET 9.

TensorPrimitives (Prymitywy Tensor)

Biblioteka System.Numerics.Tensors zawiera klasę TensorPrimitives , która udostępnia metody statyczne do wykonywania operacji liczbowych na zakresach wartości. W programie .NET 9 zakres metod udostępnianych przez TensorPrimitives program został znacznie rozszerzony, zwiększając się z 40 (na platformie .NET 8) do prawie 200 przeciążeń. Obszar powierzchni obejmuje znane operacje liczbowe z typów takich jak Math i MathF. Zawiera również ogólne interfejsy matematyczne, takie jak INumber<TSelf>, z wyjątkiem tego, że zamiast przetwarzać pojedynczą wartość, przetwarzają zakres wartości. Wiele operacji zostało również przyspieszonych za pośrednictwem implementacji zoptymalizowanych pod kątem simD dla platformy .NET 9.

TensorPrimitives Teraz uwidacznia ogólne przeciążenia dla dowolnego typu T , który implementuje określony interfejs. (Wersja .NET 8 zawiera tylko przeciążenia do manipulowania float zakresami wartości). Na przykład nowe CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) przeciążenie wykonuje podobieństwo cosinus w dwóch wektorach float, doublelub Half wartości dowolnego innego typu, który implementuje IRootFunctions<TSelf>.

Porównaj precyzję operacji podobieństwa cosinus na dwóch wektorach typu float a double:

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

Wątkowanie

Interfejsy API wątków obejmują ulepszenia iteracji za pośrednictwem zadań, dla kanałów z priorytetem, które mogą porządkować elementy zamiast pierwszego wyjścia (FIFO) i Interlocked.CompareExchange dla większej liczby typów.

Task.WhenEach

Dodano różne przydatne nowe interfejsy API do pracy z obiektami Task<TResult> . Nowa Task.WhenEach metoda umożliwia iterowanie zadań w miarę ich wykonywania przy użyciu instrukcji await foreach . Nie musisz już wykonywać takich czynności, jak wielokrotne wywoływanie Task.WaitAny zestawu zadań, aby odebrać następny, który zostanie ukończony.

Poniższy kod wykonuje wiele HttpClient wywołań i działa na ich wynikach podczas ich wykonywania.

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

Priorytetowo niezwiązany kanał

System.Threading.Channels Przestrzeń nazw umożliwia tworzenie kanałów first-in-first-out (FIFO) przy użyciu CreateBounded metod i CreateUnbounded . W przypadku kanałów FIFO elementy są odczytywane z kanału w kolejności, w której zostały do niego zapisane. W programie .NET 9 dodano nową CreateUnboundedPrioritized metodę, która porządkuje elementy tak, aby następny element odczytany z kanału był uważany za najważniejszy, zgodnie z Comparer<T>.Default elementem niestandardowym lub niestandardowym IComparer<T>.

W poniższym przykładzie użyto nowej metody do utworzenia kanału, który generuje liczby od 1 do 5 w kolejności, mimo że są zapisywane w kanale w innej kolejności.

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 dla większej liczby typów

W poprzednich wersjach platformy .NET Interlocked.Exchange i miał przeciążenia do pracy z elementami Interlocked.CompareExchange, int, uintlongulongnintnuintfloati double, oraz ogólnym przeciążeniem do pracy z dowolnym typem objectodwołania .T Na platformie .NET 9 istnieją nowe przeciążenia do niepodzielnej pracy z systemami byte, sbyte, shorti ushort. Ponadto ogólne ograniczenie dotyczące Interlocked.Exchange<T> ogólnych i Interlocked.CompareExchange<T> przeciążeń zostało usunięte, więc te metody nie są już ograniczone do pracy tylko z typami referencyjnymi. Mogą teraz pracować z dowolnym typem pierwotnym, który obejmuje wszystkie wyżej wymienione typy, a także bool i char, a także dowolny enum typ.