Estratégias de migração anuláveis

Gorjeta

Iniciando um novo projeto? Novos projetos criados a partir de modelos .NET 6 ou posteriores já têm <Nullable>enable</Nullable> definido. Você não precisa de uma estratégia de migração: vá direto para Resolver avisos de anulabilidade.

Mantendo uma base de código existente? Leia Tipos de referência anuláveis primeiro para entender contextos, anotações e o estado de nulidade. Este artigo pressupõe que você esteja familiarizado com esses conceitos e pronto para planejar uma distribuição.

Quando você ativa tipos de referência anuláveis em um projeto grande iniciado antes de tipos de referência anuláveis serem introduzidos, o compilador produz muitos avisos ao mesmo tempo. A migração consiste em sequenciar o trabalho: escolher um contexto padrão, exibir avisos arquivo por arquivo ou seção por seção e convergir para <Nullable>enable</Nullable> em todo o projeto. A sequência certa depende de quão ativa é a base de código e de quanto risco você pode correr em uma única passagem.

O estado final é o mesmo em todos os casos: o projeto define <Nullable>enable</Nullable> e não contém diretivas #nullable de pré-processador.

Escolher um contexto padrão

O contexto anulável tem dois sinalizadores independentes: anotações (se ? declara um tipo de referência anulável) e avisos (se o compilador emite diagnóstico). Defina-os juntos como um único <Nullable> valor:

Valor padrão Annotations Avisos Mais adequado para
disable (implícito) desativado desativado Bibliotecas estáveis que não receberão desenvolvimento de novos recursos nesta etapa.
enable on on Bases de código ativas com novos arquivos frequentes. O novo código inicia a aceitação.
warnings desativado on Migração em duas fases: endereçar avisos primeiro, anotar depois.
annotations on desativado Anote a API pública antes de corrigir os avisos internos.

Escolha a estratégia que melhor corresponde às metas para a migração do projeto:

  • Desative por padrão. Defina <Nullable>disable</Nullable> e adicione #nullable enable na parte superior de cada arquivo à medida que você o migra. Os arquivos existentes permanecem alheios à anulabilidade até que você os modifique. Essa opção causa menos atrito para bibliotecas estáveis, porque o desenvolvimento de novas funcionalidades é raro.
  • Habilite como o padrão. Defina <Nullable>enable</Nullable> e adicione #nullable disable na parte superior de cada arquivo que você ainda não migrou. Cada novo arquivo tem reconhecimento nulo desde o início, portanto, a lista de pendências de migração só pode ser reduzida. Essa opção é melhor quando o desenvolvimento está ativo.
  • Avisos por padrão. Defina <Nullable>warnings</Nullable>. Escolha esse padrão para uma migração de duas fases: endereçar avisos enquanto cada tipo de referência ainda for tratado como alheio e, em seguida, ativar anotações. A divisão de duas fases mantém a diferença de cada etapa focada.
  • Anotações por padrão. Defina <Nullable>annotations</Nullable>. Comece anotando sua API pública (? nos membros que permitem null) antes de lidar com os avisos. O compilador ainda não emite avisos, então você pode definir a superfície da API sem distrações.

O arquivo de projeto controla o padrão global. #nullableAs diretivas de pré-processador substituem esse padrão para uma região de código:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

Dentro dos arquivos de origem, a diretiva opta por uma região dentro ou fora da configuração anulável do projeto:

#nullable disable
public static class LegacyHelper
{
    // This file is nullable-oblivious. Reference types use the legacy rules.
    public static string GetGreeting(string name) =>
        name == null ? "hello" : $"hello {name}";
}
#nullable restore

#nullable enable
public static class MigratedHelper
{
    // This file is fully migrated. Reference types are non-nullable by default.
    public static string GetGreeting(string? name) =>
        name is null ? "hello" : $"hello {name}";
}
#nullable restore

Migrar arquivo por arquivo

A maneira mais previsível de migrar um projeto grande é habilitar avisos ou anotações arquivo por arquivo. O padrão é o mesmo, independentemente de qual padrão você escolher:

  1. Escolha um arquivo. Comece com os tipos folha mais profundos do seu grafo de dependência e depois avance para as camadas externas. Anotar um tipo provoca novos avisos em seus chamadores, portanto trabalhar de baixo para cima minimiza o retrabalho.
  2. Adicione a #nullable diretiva que aceita o arquivo no novo comportamento. Use #nullable enable se quiser os dois sinalizadores. Use #nullable enable warnings somente para avisos.
  3. Resolva os avisos no arquivo usando as técnicas em Resolver avisos anuláveis.
  4. Repita para o próximo arquivo.
  5. Quando cada arquivo no projeto tiver sua diretiva, remova as diretivas e defina <Nullable>enable</Nullable> no nível do projeto.

Se a base de código já tiver <Nullable>enable</Nullable>, você estará dirigindo na direção oposta . Suprima avisos em arquivos não migrados até você estar pronto. Use #nullable disable para excluir arquivos e, em seguida, remova as supressões uma de cada vez.

Migrar em duas fases

Uma migração de duas fases separa os dois tipos de trabalho que os tipos de referência anuláveis envolvem. Você pode sequenciar as fases de qualquer maneira, dependendo de qual forma de estabilidade importa mais para você.

Avisos primeiro, depois anotações

Priorize avisos ao corrigir bugs latentes System.NullReferenceException quando isso for a prioridade:

  1. Fase 1: Endereçar avisos. Defina o padrão do projeto como warnings. Os tipos de referência permanecem alheios à nulabilidade, portanto o sistema de tipos ainda não muda. O compilador emite avisos em todos os lugares em que seu código existente já pode gerar um System.NullReferenceException. Adicione verificações de nulo, reestruture o fluxo ou aplique atributos até que o projeto fique sem avisos. Cada correção torna o código de produção mais resiliente mesmo antes das anotações existirem.
  2. Fase 2: Adicionar anotações. Alterne o padrão do projeto para enable. Os tipos de referência agora são não anuláveis por padrão, e as variáveis locais var se tornam anuláveis. Novos avisos refletem declarações que não correspondem à forma como as variáveis são usadas. Adicione ? aos tipos que devem permitir null. Restrinja as APIs que devem exigir parâmetros não nulos.

Anotações primeiro, depois avisos

Dê prioridade às anotações quando a prioridade for estabilizar a superfície pública da API. Essa sequência atende às bibliotecas: você pode enviar assinaturas anotadas para que os consumidores vejam os contratos certos e fechem os avisos internos em sua própria agenda.

  1. Fase 1: adicionar anotações. Defina o padrão do projeto como annotations. Os tipos de referência se tornam não anuláveis por padrão, mas o compilador não emite avisos, assim o ruído não atrapalha você. Percorra a API pública e adicione ? a cada membro que possa, legitimamente, retornar ou aceitar null. Aperte as assinaturas que não deveriam. Como os avisos estão desativados, você pode definir a estrutura da API em commits focados sem precisar ajustar a implementação ao mesmo tempo.
  2. Fase 2: Avisos de endereço. Alterne o padrão do projeto para enable. As anotações que você adicionou na fase 1 agora alimentam a análise de nulidade, portanto os avisos que o compilador emite são de qualidade superior desde o início: cada um aponta para um trecho de código cujo comportamento não corresponde ao contrato que você já publicou. Resolva-os com as técnicas descritas em Resolver avisos de anulabilidade.

Escolhendo entre as ordenações

Cada ordenação separa as fases em diferenças menores e mais revisíveis. Uma fase altera apenas o comportamento e a outra altera apenas os tipos. A desvantagem é que você visita cada arquivo duas vezes. Para um código maduro e estável em que cada alteração traz risco, as duas passagens geralmente valem a pena. Escolha os avisos primeiro quando você mais quiser proteger o código em execução. Escolha primeiro as anotações quando o que você mais quer é publicar um contrato estável.

O código gerado é excluído

O compilador trata arquivos marcados como gerados como se o contexto anulável estivesse desabilitado, independentemente da configuração do projeto. Um arquivo é considerado gerado quando qualquer uma das seguintes condições é verdadeira:

  • Uma .editorconfig regra define generated_code = true para o arquivo.
  • O primeiro comentário no arquivo contém <auto-generated> ou <auto-generated/>.
  • O nome do arquivo começa com TemporaryGeneratedFile_.
  • O nome do arquivo termina com .designer.cs, .generated.cs, .g.csou .g.i.cs.

Geradores que produzem saída com reconhecimento de valor nulo podem aceitar novamente emitindo #nullable enable na parte superior do arquivo gerado.

Quando terminar

Depois que cada arquivo participa do padrão do projeto e o <Nullable>enable</Nullable> elemento é definido:

  • Remova todas as diretivas #nullable do seu código-fonte.
  • Remova os inicializadores null! e default! que você adicionou apenas para silenciar avisos durante a migração. Substitua-os pela inicialização adequada ou faça do tipo um tipo de referência anulável.
  • Faça uma verificação por amostragem da API pública. Cada membro que retorna ou aceita null deve ser anotado com ?. As anotações fazem parte do seu contrato depois que o pacote é fornecido.

Agora você está no mesmo estado que novos projetos: tipos de referência anuláveis fazem parte do sistema de tipos e quaisquer novos avisos refletem uma incompatibilidade real entre declarações e código. Use Resolver avisos de anulabilidade para tratá-los à medida que surgirem.