Start Debugging

C# 16 transforma unsafe em um contrato para quem chama

C# 16 redesenha a palavra-chave unsafe para que ela propague uma obrigação a quem chama em vez de abrir silenciosamente um contexto unsafe, e agora os blocos unsafe internos são obrigatórios.

Richard Lander apresentou um redesenho da palavra-chave unsafe que muda o seu significado depois de 25 anos. Hoje unsafe marca um detalhe de implementação: você transforma um método ou bloco em um contexto unsafe, escreve seu código de ponteiros e ninguém que chama esse método faz ideia de que está mexendo em código não seguro quanto à memória. O novo modelo, voltado para o C# 16 como versão prévia no .NET 11 e produção no .NET 12, transforma unsafe em um contrato voltado a quem chama. A motivação é em parte a ascensão do código gerado por IA: uma obrigação de segurança que existe apenas como convenção é invisível para quem revisa e para o modelo que escreve o código.

O que a palavra-chave significa agora

Sob as regras atuais, marcar um método como unsafe faz duas tarefas não relacionadas ao mesmo tempo: permite que você escreva operações com ponteiros dentro dele e não exige nada de quem chama. O redesenho separa as duas.

unsafe na assinatura de um membro passa a ser um contrato que se propaga. Quem chama precisa envolver a invocação em um bloco unsafe { }, do mesmo jeito que você precisa envolver a desreferência de um pointer. E dentro de um método unsafe, as próprias operações unsafe ainda precisam de um bloco interno explícito. Não há mais passe livre:

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

O bloco de documentação /// <safety> é o lugar formal para anotar a obrigação, e um analisador sinaliza membros unsafe que não a tenham.

Cumprindo a obrigação

Um método que chama uma API unsafe tem duas escolhas: continuar propagando ao ser unsafe ele mesmo, ou se tornar um limite seguro validando a precondição e então envolvendo a chamada.

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

É exatamente esse o ponto: o bloco unsafe é onde você afirma “verifiquei a precondição”, então ele deve ser pequeno e deliberado, não envolver um método inteiro.

Menor superfície, padrão mais rígido

Algumas outras regras tornam o modelo mais rígido. O modificador de tipo unsafe deixou de existir, então a condição unsafe vive apenas em métodos, propriedades e campos. Tipos ponteiro não tornam mais uma assinatura unsafe por si só; passar um byte* é aceitável, desreferenciá-lo é o ato unsafe. E as declarações extern ganham um novo modificador safe para atestar que uma P/Invoke é segura de chamar:

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

A configuração do projeto também se divide. A propriedade existente <AllowUnsafeBlocks> continua controlando se a palavra-chave unsafe sequer compila, enquanto uma nova propriedade de adesão seleciona o modelo do C# 16 sobre o que conta como unsafe. Omita ambas e você terá a postura mais rígida.

Nada disso torna o código de ponteiros seguro por si só. Como Lander coloca, as palavras-chave “não são a segurança; são o andaime que leva os desenvolvedores a articulá-la e honrá-la”. Se você mantém uma biblioteca com caminhos críticos cheios de ponteiros, a versão prévia é a hora de começar a escrever blocos <safety>, porque no .NET 12 serão os seus chamadores que serão forçados a envolver suas APIs.

Comments

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

< Voltar