Бөлісу құралы:


Новые возможности библиотек .NET для .NET 9

В этой статье описываются новые возможности библиотек .NET для .NET 9.

Base64Url

Base64 — это схема кодирования, которая преобразует произвольные байты в текст, состоящий из определенного набора 64 символов. Это распространенный метод передачи данных, который уже давно поддерживается с помощью различных методов, таких как Convert.ToBase64String или Base64.DecodeFromUtf8(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32, Boolean). Однако некоторые символы, используемые в нем, делают его менее идеальным для использования в некоторых обстоятельствах, например в строках запроса. В частности, 64 символа, составляющих таблицу Base64, включают "+" и "/", оба из которых имеют собственное значение в URL-адресах. Это привело к созданию схемы Base64Url, которая похожа на Base64, но использует немного другой набор символов, который делает его подходящим для использования в контекстах URL-адресов. .NET 9 включает новый класс Base64Url, который предоставляет множество полезных и оптимизированных методов для кодирования и декодирования с помощью Base64Url в различные типы данных.

В следующем примере показано использование нового класса.

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

Двоичный форматтер

.NET 9 удаляет BinaryFormatter из среды выполнения .NET. API-интерфейсы по-прежнему присутствуют, но их реализации всегда вызывают исключение независимо от типа проекта. Для получения дополнительной информации об удалении и вариантах ваших действий, если это вас касается, см. в руководстве по миграции BinaryFormatter.

Коллекции

Типы коллекций в .NET получают следующие обновления для .NET 9:

Поиск в коллекции по диапазонам

В коде высокой производительности спаны часто используются, чтобы избежать ненужного выделения памяти для строк, а таблицы поиска с типами, как Dictionary<TKey,TValue> и HashSet<T>, часто используются в качестве кэшей. Однако не было безопасного встроенного механизма поиска в этих типах коллекций с диапазонами. С помощью новой функции allows ref struct в C# 13 и новых функций этих типов коллекций в .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>, которая обеспечивает простую и быструю реализацию кучи массива. Одна из проблем с кучами массивов в целом заключается в том, что они не поддерживают обновления приоритета, что делает их запрещенными для использования в алгоритмах, таких как вариации алгоритма 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.

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 атрибут, чтобы триммер сохранял элементы для этого типа. Этот метод следует вызывать один раз на тип и, как правило, на раннем этапе.

Вторичные API имеют FromRegisteredType суффикс, например TypeDescriptor.GetPropertiesFromRegisteredType(Type). В отличие от своих коллег, у которых нет FromRegisteredType суффикса, эти 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 включает несколько статических "one-shot" реализаций хэш-функций и связанных функций. Эти API включают SHA256.HashData и HMACSHA256.HashData. Одноразовые API предпочтительнее использовать, так как они могут обеспечить наилучшую производительность и уменьшить или исключить выделение ресурсов памяти.

Если разработчик хочет предоставить API, поддерживающий хэширование, где вызывающий определяет используемый хэш-алгоритм, он обычно выполняется путем принятия аргумента HashAlgorithmName . Однако использование этого шаблона с однократными API потребует переключения всех возможных HashAlgorithmName и использования соответствующего метода. Чтобы решить эту проблему, .NET 9 представляет API CryptographicOperations.HashData. Этот API позволяет создавать хэш или HMAC над входными данными за раз, где используемый алгоритм определяется HashAlgorithmName.

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

Алгоритм KMAC

.NET 9 предоставляет алгоритм KMAC, указанный NIST SP-800-185. Код проверки подлинности сообщений KECCAK (KMAC) — это псевдорандомная функция и хэш-функция с ключом на основе KECCAK.

Следующие новые классы используют алгоритм KMAC. Используйте экземпляры для аккумулирования данных для создания MAC или используйте статический HashData метод для однократной операции над одним вводом.

KMAC доступен в Linux с OpenSSL 3.0 или более поздней версии, а также на 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.
}

алгоритмы AES-GCM и ChaChaPoly1305, включенные для iOS/tvOS/MacCatalyst

IsSupported и ChaChaPoly1305.IsSupported теперь возвращают значение true при запуске в iOS 13+, tvOS 13+ и Mac Catalyst.

AesGcm Поддерживает только 16-байтовые (128-разрядные) значения тегов в операционных системах Apple.

Загрузка сертификата 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) (и его перегрузки).

Все эти методы использовали определение типа содержимого, чтобы понять, могут ли обрабатываться входные данные, и затем загружали его при возможности. Для некоторых абонентов эта стратегия была очень удобной. Но у него также есть некоторые проблемы:

  • Не все форматы файлов работают в каждой ОС.
  • Это отклонение протокола.
  • Это источник проблем безопасности.

.NET 9 представляет новый класс X509CertificateLoader, который имеет дизайн "один метод, одна цель". В исходной версии он поддерживает только два из пяти форматов, поддерживаемых конструктором X509Certificate2 . Это два формата, которые поддерживались на всех операционных системах.

Поддержка поставщиков OpenSSL

В .NET 8 были внедрены специфичные для OpenSSL API-интерфейсы OpenPrivateKeyFromEngine(String, String) и OpenPublicKeyFromEngine(String, String). Они позволяют взаимодействовать с компонентами OpenSSL ENGINE и использовать аппаратные модули безопасности (HSM), например.

.NET 9 представляет SafeEvpPKeyHandle.OpenKeyFromProvider(String, String), что позволяет использовать поставщиков OpenSSL и взаимодействовать с поставщиками, такими как tpm2 или pkcs11.

Некоторые дистрибутивы удалили 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 произошли некоторые улучшения производительности, а также улучшения во взаимодействии с закрытыми ключами RSA, которые используют компоненты ENGINE.

Безопасность Windows CNG на основе виртуализации

Windows 11 добавлены новые API для защиты ключей Windows с помощью виртуализационной безопасности (VBS). Благодаря этой новой возможности ключи можно защитить от атак на кражу ключей на уровне администратора с незначительным эффектом на производительность, надежность и масштабирование.

.NET 9 добавил соответствующие флаги CngKeyCreationOptions. Добавлены следующие три флага:

  • CngKeyCreationOptions.PreferVbs сопоставление NCRYPT_PREFER_VBS_FLAG
  • CngKeyCreationOptions.RequireVbs совпадение NCRYPT_REQUIRE_VBS_FLAG
  • CngKeyCreationOptions.UsePerBootKey сопоставление NCRYPT_USE_PER_BOOT_KEY_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.
}

Новые перегрузки для TimeSpan.From* — дата и время

Класс TimeSpan предлагает несколько From* методов, которые позволяют создать TimeSpan объект с помощью doubleобъекта. Тем не менее, так как double это формат с плавающей запятой на основе двоичного кода, врожденная неточность может привести к ошибкам. Например, TimeSpan.FromSeconds(101.832) может не точно представлять 101 seconds, 832 milliseconds, а приблизительно 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds. Это несоответствие вызвало частое путаницу, и это также не самый эффективный способ представления таких данных. Для решения этой проблемы .NET 9 добавляет новые перегрузки, которые позволяют создавать объекты TimeSpan из целых чисел. Существуют новые перегрузки из FromDays, FromHours, FromMinutes, FromSeconds, FromMilliseconds, и FromMicroseconds.

В следующем коде показан пример вызова double и одной из новых целочисленных перегрузок.

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 конструктор

Разрешение конструктора для ActivatorUtilities.CreateInstance изменилось в .NET 9. Ранее конструктор, который был явно помечен с помощью ActivatorUtilitiesConstructorAttribute атрибута, может не вызываться в зависимости от порядка конструкторов и количества параметров конструктора. Логика изменилась в .NET 9, так что конструктор, имеющий атрибут, всегда вызывается.

Диагностика

Debug.Assert сообщает о условии утверждения по умолчанию

Debug.Assert обычно используется для проверки условий, которые должны всегда быть истинными. Сбой обычно указывает на ошибку в коде. Существует много перегрузок 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 API AddLink(ActivityLink) позволяет связать объект 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 теперь предоставляет инструмент Gauge<T> в соответствии со спецификацией OpenTelemetry. Инструмент Gauge предназначен для записи неадитивных значений при возникновении изменений. Например, он может измерять фоновый уровень шума, при этом суммирование значений из нескольких комнат было бы бессмысленным. Инструмент Gauge — это универсальный тип, который может записывать любой тип значения, например int, doubleили decimal.

В следующем примере показано использование инструмента 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" } });

Прослушивание с использованием подстановочных знаков вне процесса

Вы уже можете прослушивать метры вне процесса с помощью поставщика источника событий 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

Были введены новые методы CountBy и AggregateBy. Эти методы позволяют агрегировать состояние по ключу без необходимости выделения промежуточных групп с помощью 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, который передается от вызывающего объекта в делегат, выполняющий фактическую запись.

Этот TState параметр String.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 (События с сервера в System.Net)

События, отправленные сервером (SSE), — это простой и популярный протокол для потоковой передачи данных с сервера на клиент. Его использует, например, компания OpenAI для потоковой передачи созданного текста из своих служб искусственного интеллекта (ИИ). Чтобы упростить использование SSE, новая System.Net.ServerSentEvents библиотека предоставляет средство синтаксического анализа для легкого приема событий, отправленных сервером.

Следующий код демонстрирует использование нового класса.

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

Возобновление сеансов TLS с использованием клиентских сертификатов в Linux

Возобновление TLS — это функция протокола TLS, который позволяет возобновить ранее установленные сеансы на сервере. Это позволяет избежать нескольких раунд-трипов и сохраняет вычислительные ресурсы во время процедуры рукопожатия TLS.

Возобновление TLS уже поддерживается в Linux для подключений SslStream без сертификатов клиента. .NET 9 добавляет поддержку возобновления взаимно прошедших проверку подлинности TLS-подключений, распространенных в сценариях "сервер — сервер". Функция включена автоматически.

WebSocket сохраняет связь и время ожидания

Новые API на ClientWebSocketOptions и WebSocketCreationOptions позволяют вам включить отправку WebSocket пингов и завершение подключения, если одноранговый узел не отвечает вовремя.

До сих пор можно было указать KeepAliveInterval, чтобы предотвратить бездействие подключения, но не было встроенного механизма, обеспечивающего ответ однорангового узла.

В следующем примере сервер отправляет запросы каждые 5 секунд и прерывает подключение, если оно не отвечает в течение секунды.

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 класса (пакет параметров), который позволяет настроить синтаксический анализ.

  • Name, FullName, и AssemblyQualifiedName свойства, которые работают точно так же, как их аналоги в System.Type.

  • Несколько свойств и методов, которые предоставляют дополнительные сведения о самом имени:

    • IsArray, IsSZArray (SZ обозначает одномерный, нумерованный массив), IsVariableBoundArrayTypeа также GetArrayRank для работы с массивами.
    • IsConstructedGenericType, GetGenericTypeDefinitionа также GetGenericArguments для работы с именами универсальных типов.
    • IsByRef а также IsPointer для работы с указателями и управляемыми ссылками.
    • GetElementType() для работы с указателями, ссылками и массивами.
    • IsNested и DeclaringType для работы с вложенными типами.
    • AssemblyName, который предоставляет сведения об имени сборки через новый AssemblyNameInfo класс. В отличие от AssemblyName, новый тип неизменяем, а синтаксический анализ культурных имен не создает экземпляры CultureInfo.

Оба типа TypeName и AssemblyNameInfo неизменяемы и не предоставляют способ проверки равенства (они не реализуют IEquatable). Сравнение имен сборок является простым, но в разных сценариях необходимо сравнить только подмножество предоставленных сведений (Name, Version, CultureName, и PublicKeyOrToken).

В следующем фрагменте кода показан пример использования.

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 доступны из пакета NuGet System.Reflection.Metadata, который можно использовать с версиями .NET нижнего уровня.

Регулярные выражения

[GeneratedRegex] в свойствах

.NET 7 представил генератор источника Regex и соответствующий атрибут GeneratedRegexAttribute.

Следующий частичный метод будет автоматически сгенерирован на основе исходного кода со всем необходимым для реализации этого Regex.

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

C# 13 поддерживает частичные properties помимо частичных методов, поэтому начиная с .NET 9 можно также использовать [GeneratedRegex(...)] для свойства.

Следующее частичное свойство является эквивалентом свойства предыдущего примера.

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

Regex.EnumerateSplits

Класс Regex предоставляет метод Split, аналогичный методу String.Split. При помощи String.Split вы предоставляете один или несколько разделителей char или string, и реализация разбивает входной текст по этим разделителям. Вместо указания разделителя в виде 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 позволяет выполнять ту же операцию, но с входными данными на основе `span` и без выделения памяти для результатов. Он принимает 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
//}

Синглтон параметров веб по умолчанию

Если вы хотите сериализовать с помощью параметров по умолчанию, которые используются ASP.NET Core для веб-приложений, используйте новый JsonSerializerOptions.Web singleton.

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

JsonSchemaExporter

JSON часто используется для представления типов в сигнатурах методов в рамках схем удаленного вызова процедур. Он используется, например, как часть спецификаций OpenAPI или как часть вызова инструментов со службами ИИ, такими как OpenAI. Разработчики могут сериализовать и десериализировать типы .NET в формате JSON с помощью System.Text.Json. Но они также должны иметь возможность получить схему JSON, которая описывает форму типа .NET (т. е. описывает форму сериализации и то, что можно десериализировать). System.Text.Json теперь предоставляет тип JsonSchemaExporter, который поддерживает создание схемы JSON, представляющей тип .NET.

Дополнительные сведения см. в разделе "Экспорт схемы 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; }
}

Дополнительные сведения см. в аннотациях nullable.

Требовать не необязательные параметры конструктора

Исторически при 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 теперь поддерживает чтение нескольких документов JSON, разделенных пробелами, из одного буфера или потока. По умолчанию ридер создает исключение, если обнаруживает любые непечатные символы, которые следуют за первым документом верхнего уровня. Это поведение можно изменить с помощью флага AllowMultipleValues .

Дополнительные сведения см. в разделе "Чтение нескольких документов JSON".

Диапазоны

В коде с высокой производительностью spans часто используются, чтобы избежать ненужного выделения памяти для строк. 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[]) второй параметр метода помечен как params. Эту перегрузку можно вызвать с массивом или передать значения по отдельности:

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

До .NET 9 при отдельном передаче значений компилятор C# выдает код, идентичный первому вызову, создав неявный массив вокруг трех аргументов.

Начиная с C# 13, можно использовать params с любым аргументом, который можно создать с помощью выражения коллекции, включая спаны (Span<T> и ReadOnlySpan<T>). Это полезно для удобства использования и производительности. Компилятор C# может хранить аргументы в стеке, оборачивать их в span и передавать в метод, что позволяет избежать неявного выделения массива, которое в противном случае произошло бы.

.NET 9 включает более 60 методов с параметром params ReadOnlySpan<T>. Некоторые из них являются новыми перегрузками, а некоторые из них — существующие методы, которые уже принимали ReadOnlySpan<T>, но теперь этот параметр отмечен как params. Чистый эффект заключается в обновлении до .NET 9 и повторной компиляции кода, вы увидите улучшения производительности без внесения изменений в код. Это связано с тем, что компилятор предпочитает перегрузки, основанные на диапазоне, чем перегрузки, основанные на массиве.

Например, String.Join теперь включает в себя следующую перегрузку, которая реализует новый шаблон: String.Join(String, ReadOnlySpan<String>)

Теперь вызов string.Join(", ", "a", "b", "c") выполняется без выделения массива для передачи аргументов "a", "b" и "c".

Перечисление по char< ReadOnlySpan>. Сегменты split()

string.Split — это удобный метод для быстрого секционирования строки с одним или несколькими предоставленными разделителями. Однако для кода, ориентированного на производительность, профиль string.Split выделения может быть запретительным, так как он выделяет строку для каждого разобранного компонента и string[] для сохранения всех из них. Он также не работает с спанами, поэтому если у вас есть ReadOnlySpan<char>, вам придется выделить еще одну строку при преобразовании в строку, чтобы можно было вызвать string.Split на ней.

В .NET 8 были представлены методы Split и SplitAny для ReadOnlySpan<char>. Вместо того чтобы возвращать новый string[], эти методы принимают итоговый Span<Range>, куда записываются ограничивающие индексы для каждого компонента. Это делает операцию полностью без использования памяти для выделения. Эти методы подходят для использования, если число диапазонов известно и невелико.

В .NET 9 добавлены новые перегрузки Split и SplitAny, чтобы позволить постепенный анализ ReadOnlySpan<T> с a priori неизвестным числом сегментов. Новые методы позволяют перечислять каждый сегмент, который аналогично представлен как 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() Guid создает объект, заполненный в основном криптографически защищенными случайными данными, согласно спецификации версии 4 UUID в RFC 9562. Этот же RFC также определяет другие версии, включая версию 7, которая "содержит поле упорядоченного по времени значения, производное от широко реализованного и хорошо известного источника метки времени эпохи Unix". Другими словами, большая часть данных по-прежнему является случайной, но некоторые из них зарезервированы для данных на основе метки времени, что позволяет этим значениям иметь естественный порядок сортировки. В .NET 9 можно создать Guid в соответствии с версией 7 с помощью новых методов Guid.CreateVersion7() и Guid.CreateVersion7(DateTimeOffset). Вы также можете использовать новое свойство Version, чтобы получить поле версии объекта Guid.

System.IO

Сжатие с помощью zlib-ng

System.IO.Compressionтакие функции, как ZipArchive, DeflateStreamGZipStreamи ZLibStream все они основаны в первую очередь на библиотеке zlib. Начиная с .NET 9, эти функции вместо этого используют zlib-ng, библиотеку, которая обеспечивает более последовательную и эффективную обработку в более широком спектре операционных систем и оборудования.

Параметры сжатия ZLib и Brotli

ZLibCompressionOptionsи BrotliCompressionOptions являются новыми типами для настройки уровня сжатия и стратегии для конкретного алгоритма (Default, , Filtered, HuffmanOnlyRunLengthEncodingили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

Документы XPS, поступающие из виртуального принтера V4 XPS, ранее не были открыты с помощью System.IO.Packaging библиотеки, из-за отсутствия поддержки обработки файлов .piece . Этот разрыв был решен в .NET 9.

System.Numerics

Верхний предел BigInteger

BigInteger поддерживает представление целых значений по существу произвольной длины. Однако на практике длина ограничена ограничениями базового компьютера, например доступной памяти или времени, необходимого для вычисления заданного выражения. Кроме того, существуют некоторые API, которые завершаются сбоем при вводе данных, который приводит к значению, превышающему допустимое. Из-за этих ограничений .NET 9 применяет максимальную длину BigInteger, то есть она может содержать не более (2^31) - 1 (приблизительно 2,14 млрд) битов. Число, которое соответствует выделению около 256 МБ памяти, содержит примерно 646,5 миллиона цифр. Это новое ограничение гарантирует, что все предоставляемые API хорошо работают и согласованы, что, тем не менее, позволяет использовать числа, которые выходят далеко за рамки большинства сценариев использования.

BigMul API

BigMul — это операция, которая создает полный продукт двух чисел. .NET 9 добавляет соответствующие API для BigMul, int, long, и ulong, тип возврата которых является следующим более крупным целочисленным типом, чем типы параметров.

Новые API:

API преобразования векторов

.NET 9 добавляет выделенные API расширения для преобразования между Vector2, Vector3, Vector4, Quaternion и Plane.

Новые API приведены следующим образом:

Для одноразмерных преобразований, таких как между Vector4, Quaternionи Plane, эти преобразования равны нулю стоимости. То же самое можно сказать для сужения преобразований, таких как от Vector4 до Vector2 или Vector3. Для расширяющих преобразований, таких как от Vector2 или Vector3 до Vector4, существует обычный API, который инициализирует новые элементы до 0, и API с суффиксом Unsafe, который оставляет эти новые элементы неопределенными, что позволяет избежать дополнительных затрат.

API создания вектора

Существуют новые Create API, предоставляемые для Vector, Vector2, Vector3 и Vector4, которые эквивалентны API, представленным для типов аппаратных векторов в пространстве имен System.Runtime.Intrinsics.

Дополнительные сведения о новых API см. в следующих статье:

Эти API в первую очередь предназначены для обеспечения удобства и общей согласованности при работе с типами .NET, ускоренными с помощью SIMD.

Дополнительное ускорение

Дополнительные улучшения производительности были внесены для многих типов в пространстве имен System.Numerics, включая BigInteger, Vector2, Vector3, Vector4, Quaternion и Plane.

В некоторых случаях это привело к увеличению скорости на 2-5 раз для основных API, включая Matrix4x4 умножение, создание Plane из ряда вершин, Quaternion объединение и вычисление векторного произведения Vector3.

Существует также поддержка постоянного сворачивания для API SinCos, которая вычисляет как Sin(x), так и Cos(x) в одном вызове, что повышает его эффективность.

Тензоры для искусственного интеллекта

Tensors — это краеугольный камень структуры данных искусственного интеллекта (ИИ). Их часто можно рассматривать как многомерные массивы.

Тензоры используются для:

  • Представляет и кодирует такие данные, как текстовые последовательности (токены), изображения, видео и звук.
  • Эффективнее управлять данными с более высокими размерами.
  • Эффективное применение вычислений для более высоких размерных данных.
  • Храните сведения о весе и промежуточные вычисления (в нейронных сетях).

Чтобы использовать api-интерфейсы .NET tensor, установите пакет NuGet System.Numerics.Tensors.

Новый тип Tensor<T>

Новый тип Tensor<T> расширяет возможности ИИ библиотек и среды выполнения .NET. Этот тип:

  • Обеспечивает эффективное взаимодействие с библиотеками ИИ, такими как ML.NET, TorchSharp и ONNX Runtime с использованием нулевых копий, где это возможно.
  • Строится на основе TensorPrimitives для эффективных математических операций.
  • Позволяет легко и эффективно манипулировать данными, предоставляя операции индексирования и срезов.
  • Не является заменой существующих библиотек ИИ и машинного обучения. Вместо этого он предназначен для предоставления общего набора API для уменьшения дублирования кода и зависимостей, а также для повышения производительности с помощью последних функций среды выполнения.

В следующих кодах показаны некоторые API, включенные в новый Tensor<T> тип.

// 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 помечен как experimental для .NET 9.

TensorPrimitives (Тензорные Примитивы)

Библиотека System.Numerics.Tensors включает TensorPrimitives класс, который предоставляет статические методы для выполнения числовых операций по диапазонам значений. В .NET 9 существенно расширена область методов, предоставляемых TensorPrimitives, увеличившись с 40 в .NET 8 до почти 200 перегрузок. Область поверхности охватывает знакомые числовые операции из типов, таких как Math и MathF. Он также включает универсальные математические интерфейсы, такие как INumber<TSelf>, за исключением обработки отдельного значения, они обрабатывают диапазон значений. Многие операции также были ускорены с помощью оптимизированных для SIMD реализаций для .NET 9.

TensorPrimitives теперь предоставляет универсальные перегрузки для любого типа T , реализующего определенный интерфейс. (Версия .NET 8 включала только перегрузки для управления диапазонами значений float.) Например, новая перегрузка CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) выполняет косинусное сходство двух векторов значений float, double или Half или значений любого типа, реализующего IRootFunctions<TSelf>.

Сравните точность операции сходства косинуса по двум векторам типа float и 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

Нарезание резьбы

Интерфейсы API потоков включают улучшения для итерации задач в приоритетных каналах, которые могут упорядочивать свои элементы не по принципу «первым пришел — первым вышел» (FIFO), и Interlocked.CompareExchange для большего количества типов.

Task.WhenEach

Добавлены различные полезные новые API для работы с Task<TResult> объектами. Новый 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 имен позволяет создавать каналы типа "первым пришел — первым вышел" (FIFO) с помощью методов CreateBounded и CreateUnbounded. С каналами 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.Exchange и Interlocked.CompareExchange были перегрузки для работы с int, uint, long, ulong, nint, nuint, float, double и object, а также универсальную перегрузку для работы с любым ссылочным типом T. В .NET 9 существуют новые перегрузки для атомарной работы с byte, sbyte, short и ushort. Кроме того, было удалено ограничение для универсальных Interlocked.Exchange<T> и Interlocked.CompareExchange<T> перегрузок, поэтому эти методы больше не ограничены тем, что работают только с ссылочными типами. Теперь они могут работать с любым примитивным типом, который включает все перечисленные выше типы, а также bool и char, а также любой тип enum.