Estratégias de migração anuláveis

Tip

A começar um novo projeto? Novos projetos criados a partir de .NET 6 ou modelos posteriores já têm <Nullable>enable</Nullable> definido. Não precisas de uma estratégia de migração: salta para Resolver avisos anuláveis.

Manter uma base de código existente? Leia primeiro os tipos de referência anuláveis para compreender contextos, anotações e estado nulo. Este artigo parte do princípio que está familiarizado com esses conceitos e pronto para planear um lançamento.

Quando ativa os tipos de referência anuláveis num projeto grande que começou antes da introdução dos tipos de referência anuláveis, o compilador produz muitos avisos ao mesmo tempo. A migração consiste em sequenciar o trabalho: escolher um contexto padrão, expor avisos ficheiro a ficheiro ou secção a secção, e convergir para <Nullable>enable</Nullable> todo o projeto. A sequência correta depende de quão ativa é a base de código e do risco que consegues correr numa única passada.

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

Escolha um contexto padrão

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

Valor predefinido Anotações Avisos Melhor para
disable (implícito) off off Bibliotecas estáveis que não aceitam novas funcionalidades trabalham nesta fase.
enable on on Bases de código ativas com ficheiros novos frequentes. O novo código vem ativado por predefinição.
warnings off on Migração em duas fases: abordar primeiro os avisos, anotar depois.
annotations on off Anote a API pública antes de corrigir os avisos internos.

Escolha a estratégia que melhor se adequa aos objetivos da migração do seu projeto:

  • Desativar como padrão. Define <Nullable>disable</Nullable> e adiciona #nullable enable no topo de cada ficheiro à medida que o migras. Os ficheiros existentes permanecem anuláveis — alheios até tocares neles. Esta opção tem a menor fricção para bibliotecas estáveis porque o trabalho de novas funcionalidades é raro.
  • Ativar como padrão. Define <Nullable>enable</Nullable> e adiciona #nullable disable no topo de cada ficheiro que ainda não migraste. Cada novo ficheiro é consciente do nullable desde o início, pelo que o backlog de migração só pode diminuir. Esta escolha é melhor quando o desenvolvimento está ativo.
  • Avisos como padrão. Defina <Nullable>warnings</Nullable>. Escolha este padrão para uma migração em duas fases: enderece avisos enquanto todos os tipos de referência ainda são tratados como alheios, depois ativar as anotações. A divisão em duas fases mantém o diff de cada passo bem delimitado.
  • Anotações por predefinição. Defina <Nullable>annotations</Nullable>. Comece por anotar a sua API pública (? nos membros que permitem null) antes de andar atrás dos avisos. O compilador ainda não emite avisos, por isso podes estabilizar a superfície da API sem distrações.

O teu ficheiro de projeto controla o padrão global. #nullable As diretivas do pré-processador sobrepõem-se a esse padrão para uma região de código:

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

Dentro dos ficheiros fonte, a diretiva opta por uma região dentro ou fora da definiçã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 ficheiro a ficheiro

A forma mais previsível de migrar um projeto grande é ativar avisos ou anotações ficheiro a ficheiro. O padrão é o mesmo independentemente da predefinição que escolher:

  1. Escolha um arquivo. Começa pelos tipos de folhas mais profundos no teu gráfico de dependência, depois avança para fora. Anotar um tipo provoca novos avisos nos seus chamadores, pelo que trabalhar de baixo para cima minimiza a reestruturação.
  2. Adicione a diretiva #nullable que ativa o novo comportamento para o ficheiro. Usa #nullable enable se quiseres ambas as bandeiras. Utilize #nullable enable warnings exclusivamente para advertências.
  3. Resolva os avisos no ficheiro utilizando as técnicas descritas em Resolver avisos anuláveis.
  4. Repete para o próximo ficheiro.
  5. Quando cada ficheiro do projeto tiver a sua diretiva, remova as diretivas e defina <Nullable>enable</Nullable> ao nível do projeto.

Se a tua base de código já tem <Nullable>enable</Nullable>, vais na direção oposta. Suprime avisos em ficheiros não migrados até estares pronto. Utilize #nullable disable para excluir ficheiros e, em seguida, remova as supressões uma a uma.

Migrar em duas fases

Uma migração em duas fases separa os dois tipos de trabalho que os tipos de referência anuláveis envolvem. Podes sequenciar as fases de qualquer forma, dependendo de qual forma de estabilidade é mais importante para ti.

Primeiro os avisos, depois as anotações

A prioridade é começar com avisos ao corrigir bugs System.NullReferenceException latentes:

  1. Fase 1: Abordar os avisos. Defina a predefinição do projeto como warnings. Os tipos de referência continuam sem ter em conta a nulabilidade, pelo que o sistema de tipos ainda não sofre alterações. O compilador emite avisos em todos os locais onde o seu código existente possa já lançar um System.NullReferenceException. Adicionar verificações de nulo, reestruturar o fluxo ou aplicar atributos até que o projeto fique sem avisos. Cada correção torna o código de produção mais resiliente mesmo antes de existirem anotações.
  2. Fase 2: Adicionar anotações. Altere a predefinição do projeto para enable. Os tipos de referência são agora não anuláveis por defeito, e var os locais tornam-se nulos. Novos avisos refletem declarações que não correspondem à forma como as variáveis são usadas. Adicione ? a tipos que deverão permitir null. Restringir as APIs que devem exigir parâmetros não nulos.

Primeiro as anotações, depois os avisos

Dê prioridade às anotações quando a prioridade for estabilizar a superfície pública da API. Esta sequência é adequada às bibliotecas: pode enviar assinaturas anotadas para que os consumidores vejam os contratos corretos, e depois encerrar os avisos internos ao seu próprio ritmo.

  1. Fase 1: Adicionar anotações. Definir a predefinição do projeto como annotations. Os tipos de referência tornam-se não anuláveis por predefinição, mas o compilador não emite avisos, pelo que evita ruído desnecessário. Percorra a API pública e adicione ? a cada membro que possa legitimamente regressar ou aceitar null. Aperte as assinaturas que não devia apertar. Como os avisos estão desligados, podes ajustar a forma da API em commits focados sem desembaraçar a implementação ao mesmo tempo.
  2. Fase 2: Abordar os avisos. Altera o projeto predefinido para enable. As anotações que adicionaste na fase 1 agora alimentam análise de estado nulo, por isso os avisos que o compilador emite são de melhor qualidade desde o início: cada um aponta para código cujo comportamento não corresponde ao contrato que já publicaste. Resolva-os com as técnicas descritas em Resolver avisos de anulabilidade.

Escolher entre as ordenações

Cada ordenação separa as fases em diferenças mais pequenas e mais fáceis de rever. Uma fase muda apenas o comportamento, e a outra muda apenas os tipos. A desvantagem é que se visita cada ficheiro duas vezes. Para código maduro e estável, onde cada alteração implica risco, as duas passagens geralmente valem a pena. Escolhe os avisos primeiro quando mais quiseres reforçar o código em execução. Escolhe as anotações primeiro quando mais quiseres publicar um contrato estável.

O código gerado é excluído

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

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

Geradores que produzem saída consciente do nullable podem optar por voltar a emitir #nullable enable no topo do ficheiro gerado.

Quando terminares

Depois de todos os ficheiros participarem no projeto predefinido e o elemento <Nullable>enable</Nullable> estar definido:

  • Remova todas as diretivas #nullable do seu código-fonte.
  • Remove os inicializadores null! e default! que adicionaste apenas para silenciar os avisos durante a migração. Substitui-os por uma inicialização adequada, ou faça do tipo um tipo de referência anulável.
  • Faça uma verificação pontual da API pública. Cada membro que regressar ou aceitar null deve ser anotado com ?. As anotações fazem parte do teu contrato assim que o pacote for enviado.

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