Start Debugging

C# 16 превращает unsafe в контракт для вызывающего кода

C# 16 переосмысливает ключевое слово unsafe так, что оно передаёт обязательство вызывающему коду, а не молча открывает unsafe-контекст, и теперь внутренние блоки unsafe обязательны.

Ричард Ландер представил переработку ключевого слова unsafe, которая меняет его смысл спустя 25 лет. Сегодня unsafe помечает деталь реализации: вы переводите метод или блок в unsafe-контекст, пишете код с указателями, и никто из вызывающих этот метод даже не подозревает, что имеет дело с небезопасным по памяти кодом. Новая модель, нацеленная на C# 16 как предварительную версию в .NET 11 и продакшен в .NET 12, превращает unsafe в контракт, обращённый к вызывающему коду. Мотивация отчасти связана с ростом кода, сгенерированного ИИ: обязательство по безопасности, существующее лишь как соглашение, невидимо для проверяющего и для модели, которая пишет код.

Что теперь означает ключевое слово

По текущим правилам пометка метода как unsafe выполняет сразу две несвязанные задачи: позволяет писать внутри операции с указателями и ничего не требует от вызывающих. Переработка разделяет их.

unsafe в сигнатуре члена становится контрактом, который распространяется дальше. Вызывающие должны обернуть вызов в блок unsafe { }, так же как вы должны оборачивать разыменование pointer. А внутри метода unsafe сами небезопасные операции по-прежнему требуют явного внутреннего блока. Никакого свободного пропуска:

/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="ofs"/> must address
/// a byte the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int ofs)
{
    // SAFETY: relies on caller obligation.
    unsafe { return ((byte*)ptr)[ofs]; }
}

Блок документации /// <safety> служит формальным местом, где записывается обязательство, а анализатор помечает члены unsafe, у которых его нет.

Выполнение обязательства

У метода, который вызывает API unsafe, есть два варианта: продолжать распространение, будучи unsafe самому, или стать безопасной границей, проверив предусловие и затем обернув вызов.

void Caller2()
{
    if (!ObligationSatisfied()) throw new InvalidOperationException();
    unsafe { ReadByte(ptr, ofs); } // the guard discharges the contract
}

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

Меньшая поверхность, более строгое значение по умолчанию

Несколько других правил делают модель строже. Модификатор типа unsafe исчез, поэтому свойство unsafe живёт только у методов, свойств и полей. Типы указателей больше не делают сигнатуру unsafe сами по себе; передача byte* допустима, а его разыменование и есть небезопасное действие. А объявления extern получают новый модификатор safe, удостоверяющий, что P/Invoke безопасен для вызова:

[LibraryImport("libc")]
internal static safe partial int getpid();

Конфигурация проекта тоже разделяется. Существующее свойство <AllowUnsafeBlocks> по-прежнему определяет, компилируется ли вообще ключевое слово unsafe, тогда как новое свойство выбора задаёт модель C# 16 относительно того, что считается unsafe. Опустите оба, и вы получите самую строгую позицию.

Ничто из этого не делает код с указателями безопасным само по себе. Как говорит Ландер, ключевые слова «это не безопасность; это строительные леса, которые побуждают разработчиков сформулировать её и соблюдать». Если вы сопровождаете библиотеку с насыщенными указателями критическими путями, предварительная версия и есть время начать писать блоки <safety>, потому что в .NET 12 именно ваши вызывающие будут вынуждены оборачивать ваши API.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад