C # 8 e validade nula. Como vivemos com isso

Olá colegas! É hora de mencionar que temos planos de lançar o livro fundamental de Ian Griffiths sobre C # 8:


Enquanto isso, em seu blog, o autor publicou dois artigos relacionados, nos quais considera os meandros de novos fenômenos, como anulabilidade, esquecimento nulo e conscientização nula. Traduzimos os dois artigos sob um título e sugerimos discuti-los.

O novo recurso mais ambicioso do C # 8.0 é chamado de referências anuláveis .

O objetivo desse novo recurso é suavizar os danos causados ​​por uma coisa perigosa, que o cientista da computação Tony Hoar chamou de " erro de bilhão de dólares ". C # tem uma palavra-chavenull(o equivalente a isso é encontrado em muitos outros idiomas), e as raízes dessa palavra-chave podem ser rastreadas até o idioma Algol W, no desenvolvimento do qual Hoar participou. Nesse idioma antigo (que apareceu em 1966), variáveis ​​que se referem a instâncias de um determinado tipo podem ter um significado especial, indicando que, no momento, essa variável não é referenciada em nenhum lugar. Essa oportunidade foi amplamente emprestada e hoje muitos especialistas (inclusive o próprio Hoar) acreditam que ela se tornou a maior fonte de erros de software dispendiosos de todos os tempos.

O que há de errado em assumir zero? Em um mundo em que qualquer link pode apontar para zero, você deve considerar isso onde quer que qualquer link seja usado em seu código; caso contrário, você corre o risco de ser recusado em tempo de execução. Às vezes, não é muito oneroso; se você inicializar uma variável com uma expressão newno mesmo local em que a declara, saberá que essa variável não é igual a zero. Mas mesmo um exemplo tão simples está repleto de alguma carga cognitiva: antes do lançamento do C # 8, o compilador não sabia se você está fazendo algo que possa converter esse valor null. Mas, assim que você começa a costurar diferentes fragmentos de código, fica muito mais difícil julgar com certeza sobre essas coisas: qual a probabilidade de que essa propriedade que estou lendo agora possa retornar null? É permitido transmitirnullnesse método? Em que situações posso ter certeza de que o método que estou chamando definirá esse argumento outnão para null, mas para um valor diferente? Além disso, o assunto não se limita a lembrar de verificar essas coisas; não está totalmente claro o que você deve fazer se chegar a zero.

Com tipos numéricos em C #, não existe esse problema: se você escrever uma função que recebe alguns números como entrada e retorna um número como resultado, não precisa se perguntar se os valores transmitidos são realmente números e se alguma coisa entre eles pode ser misturada. Ao chamar essa função, não é necessário pensar se ela pode retornar algo em vez de um número. A menos que esse desenvolvimento de eventos lhe interesse como uma opção: nesse caso, você pode declarar parâmetros ou resultados do tipoint?, indicando que, nesse caso específico, você realmente deseja permitir a transmissão ou o retorno de um valor nulo. Assim, para tipos numéricos e, em um sentido mais geral, tipos significativos, a tolerância zero sempre foi uma daquelas coisas que são feitas voluntariamente, como uma opção.

Quanto aos tipos de referência, anteriores ao C # 8.0, a permissibilidade de zero não era apenas definida por padrão, mas também não podia ser desativada.

De fato, por razões de compatibilidade com versões anteriores, a validade zero continua a operar por padrão, mesmo no C # 8.0, porque os novos recursos de idioma nesta área permanecem desativados até que você os solicite explicitamente.

No entanto, assim que você habilita esse novo recurso - tudo muda. A maneira mais fácil de ativá-lo é adicioná-lo <Nullablegt;enable</Nullablegt;dentro do elemento.<PropertyGroup>no seu arquivo .csproj. (Observo que mais controle de filigrana também está disponível. Se você realmente precisar dele, poderá configurar o comportamento a ser permitido nullseparadamente em cada linha. Mas, quando recentemente decidimos incluir esse recurso em todos os nossos projetos, verificou-se que ele seria ativado em uma escala um projeto de cada vez é uma tarefa factível.)

Quando os links permitidos no C # 8.0 são nulltotalmente ativados, a situação muda: agora, por padrão, presume-se que os links não permitam nulo somente se você não especificar o oposto, exatamente como nos tipos significativos ( até a sintaxe é a mesma: você pode escrever int ?, se você realmente deseja que o valor inteiro seja opcional. Agora, você escreve string ?, se quer dizer que deseja um link para uma string ounull.)

Essa é uma alteração muito significativa e, em primeiro lugar, devido ao seu significado, esse novo recurso é desativado por padrão. A Microsoft poderia ter projetado esse recurso de linguagem de maneira diferente: você poderia deixar os links padrão anuláveis ​​e introduzir uma nova sintaxe que permitiria especificar que você deseja garantir que isso não seja permitido null. Talvez isso reduza a fasquia ao explorar essa possibilidade, mas a longo prazo essa solução estaria incorreta, pois na prática a maioria dos links na grande massa de código C # não foi projetada para apontar null.

Assumir que zero é uma exceção, não uma regra, e é por isso que, quando esse novo recurso de idioma é ativado, impedir que nulo se torne um novo padrão. Isso é refletido mesmo no nome do recurso original: "referências anuláveis". O nome é curioso, pois os links podem apontar nullpara o C # 1.0. Mas os desenvolvedores optaram por enfatizar que agora a suposição nula entra na categoria de coisas que precisam ser explicitamente solicitadas.

O C # 8.0 facilita o processo de introdução de links permissivos null, pois permite que você introduza esse recurso gradualmente. Não é preciso fazer uma escolha sim ou não. Isso é bem diferente do recurso async/awaitadicionado no C # 5.0, que costumava se espalhar: de fato, operações assíncronas obrigam o chamador a serasynce, portanto, o código que chama esse chamador deve estar async, e assim por diante, no topo da pilha. Felizmente, os tipos que permitem nullsão construídos de maneira diferente: eles podem ser implementados de maneira seletiva e gradual. Você pode trabalhar com os arquivos um por um, ou mesmo linha por linha, se necessário.

O aspecto mais importante dos tipos que permitemnull(graças à qual a transição para eles é simplificada), é que, por padrão, eles estão desativados. Caso contrário, a maioria dos desenvolvedores se recusaria a usar o C # 8.0, pois essa transição causaria avisos em quase qualquer base de código. No entanto, pelas mesmas razões, o limite de entrada para o uso desse novo recurso parece bastante alto: se um novo recurso faz alterações tão drásticas que é desativado por padrão, é provável que você não queira mexer nele, mas há problemas associados à mudança para ele sempre parecerá um aborrecimento desnecessário. Mas isso seria uma pena, porque o recurso é muito valioso. Ajuda a encontrar erros no código antes que os usuários façam isso por você.

Portanto, se você está pensando em introduzir tipos que permitemnull, lembre-se de poder introduzir esse recurso passo a passo.

Somente avisos

O nível mais rígido de controle sobre todo o projeto após um simples ligar / desligar é a capacidade de ativar avisos, independentemente das anotações. Por exemplo, se eu ativar totalmente a suposição zero para Corvus.ContentHandling.Json em nosso repositório Corvus.ContentHandling , adicionando <Nullablegt;enable</Nullablegt;ao grupo de propriedades no arquivo do projeto, em seu estado atual, 20 avisos do compilador aparecerão imediatamente. No entanto, se eu usá-lo, <Nullablegt;warnings</Nullablegt;receberei apenas um aviso.

Mas espere! Por que menos avisos serão mostrados para mim? No final, aqui eu apenas pedi avisos. A resposta a essa pergunta não é totalmente óbvia: o fato é que algumas variáveis ​​e expressões podem ser nullnulas.

Neutralidade nula O

C # suporta duas interpretações de validade nula. Em primeiro lugar, qualquer variável de um tipo de referência pode ser declarada como admitindo ou não nulle, em segundo lugar, o compilador nullsempre que possível conclui logicamente se essa variável pode ou não estar em qualquer ponto específico do código. Este artigo trata apenas da primeira variedade de admissibilidadenull, ou seja, sobre o tipo estático de uma variável (na verdade, isso se aplica não apenas a variáveis ​​e parâmetros e campos próximos a eles em espírito; a admissibilidade estática e logicamente dedutível é nulldeterminada para cada expressão em C #.) De fato, a admissibilidade nullem seu primeiro sentido , o que estamos considerando é uma extensão do sistema de tipos.

No entanto, acontece que, se focarmos apenas na admissibilidade nula para um tipo, a situação não será tão coerente quanto se poderia supor. Este não é apenas um contraste entre "validade nula" e "inválidanull" De fato, existem mais duas possibilidades. Existe uma categoria de "desconhecido", que é obrigatória devido à disponibilidade de genéricos; se você tiver um parâmetro de tipo ilimitado, não será possível descobrir nada sobre a validade null: código usando o método ou tipo generalizado apropriado pode substituir um argumento neles, permitindo ou não null. Você pode adicionar restrições, mas muitas vezes essas restrições são indesejáveis, pois restringem o escopo do tipo ou método generalizado. Portanto, para variáveis ​​ou expressões de algum parâmetro de tipo ilimitado, a T(não) validade de zero deve ser desconhecida; talvez, em cada caso, a questão da admissibilidadenullserá decidido separadamente para eles, mas não sabemos qual opção aparecerá no código genérico, pois dependerá do argumento de tipo.

A última categoria é chamada "neutra". Pelo princípio da "neutralidade", tudo funcionava antes do advento do C # 8.0, e isso funcionará se você não ativar a capacidade de trabalhar com links anuláveis. (Basicamente, este é um exemplo de retroatividade . Embora a idéia de neutralidade nula tenha sido introduzida pela primeira vez no C # 8.0 como um estado natural de código antes de ativar a validade nula para referências, os designers de C # insistiram que essa propriedade nunca era realmente estranha ao C #.

Talvez você não precise explicar o que "neutralidade" significa nesse caso, pois é nesse sentido que o C # sempre funcionou, para que você entenda tudo ... embora, talvez, isso seja um pouco desonesto. Portanto, ouça: em um mundo onde se sabe sobre admissibilidade null, a característica mais importante das nullexpressões -eutrais é que elas não causam avisos sobre aceitabilidade nula. Você pode atribuir uma expressão nula-neutra como uma nullvariável permitida , mas não permitida. Nullvariáveis ​​variáveis ​​(assim como propriedades, campos, etc.), você pode atribuir expressões que o compilador considerou "possível null" ou "não null".

Por isso, se você ativar os avisos, não haverá muitos alertas novos. Todo o código permanece no contexto de anotações de validade desabilitadas null, portanto, todas as variáveis, parâmetros, campos e propriedades serão nullneutros, o que significa que você não receberá nenhum aviso se tentar usá-lo em conjunto com qualquer entidade que leve em consideração null.

Por que, então, recebo avisos? Um motivo comum é a tentativa de fazer amigos de uma maneira inaceitável dois pedaços de código que levam em consideração null. Por exemplo, suponha que eu tenha uma biblioteca em que os links permissivos sejam totalmente incluídos nulle essa biblioteca tenha a seguinte classe profundamente inventada:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

Além disso, em outro projeto, eu posso escrever esse código no contexto em que os avisos de validade nula são ativados, mas as anotações correspondentes estão desabilitadas:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

Como as anotações sobre validade nula estão desativadas, o parâmetro xaqui é nullneutro. Isso significa que o compilador não pode determinar se esse código é verdadeiro ou não. Se o compilador emitiu avisos nos casos em que nullexpressões neutras são misturadas com aquelas que levam em consideração null, uma proporção significativa desses avisos pode ser considerada duvidosa - portanto, os avisos não são emitidos.

Com esse invólucro, eu realmente escondi o fato de que o código leva em consideração a validade null. Isso significa que agora eu posso escrever assim:

	int x = UseString(NullableAwareClass.GetNullable());

O compilador sabe o que GetNullablepode retornar null, mas desde que chamei o método com um parâmetro nulo-neutro, o programa não sabe se isso está certo ou errado. Usando o nullwrapper -neutral, desarmei o compilador, que agora não vê nenhum problema aqui. No entanto, se eu combinasse esses dois métodos diretamente, tudo seria diferente:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

Aqui eu passo o resultado GetNullablediretamente para RequireNonNull. Se eu tentasse fazer isso em um contexto em que hipóteses nulas estão ativadas, o compilador geraria um aviso, independentemente de eu ativar ou desativar o contexto das anotações correspondentes. Nesse caso em particular, o contexto das anotações não importa, pois não há declarações com um tipo de referência. Se você ativar avisos sobre a suposição de nulo, mas desativar as anotações correspondentes, todas as declarações se tornarão nullneutras, o que, no entanto, não significa que todas as expressões se tornem tais. Portanto, sabemos que o resultado GetNullableé nulo. Portanto, recebemos um aviso.

Resumindo: como todas as declarações no contexto de anotações desativadas que permitem nullsãonull- neutro, não receberemos muitos avisos, pois a maioria das expressões será - nullneutra. Mas o compilador ainda poderá capturar erros relacionados à suposição nullnos casos em que as expressões não passam por algum intermediário nulo-neutro. Além disso, o maior benefício nesse caso será detectar erros associados a tentativas de desreferenciar possíveis valores nulos usando ., por exemplo:

int z = NullableAwareClass.GetNullable().Length;

Se seu código foi bem projetado, não deve haver um grande número de erros desse tipo.

Anotação gradual de todo o projeto

Depois de dar o primeiro passo - basta ativar os avisos, você pode prosseguir para a ativação gradual das anotações, arquivo por arquivo. É conveniente incluí-los imediatamente em todo o projeto, ver em quais avisos os arquivos aparecem - e depois selecionar um arquivo em que haja relativamente poucos avisos. Novamente, desative-os no nível de todo o projeto e escreva na parte superior do arquivo selecionado #nullable enable. Isso ativará totalmente a suposição null(tanto para avisos quanto para anotações) em todo o arquivo (a menos que você as desative novamente usando outra diretiva#nullable) Em seguida, você pode percorrer o arquivo inteiro e garantir que todas as entidades que provavelmente são nulas sejam anotadas como permitidas null(ou seja, adicionar ?) e, em seguida, lidar com avisos nesse arquivo, se houver algum.

Pode acontecer que adicionar todas as anotações necessárias seja o suficiente para eliminar todos os avisos. O inverso também é possível: você pode notar que, quando anota um arquivo sobre validadenull, outros avisos apareceram em outros arquivos que o utilizam. Como regra geral, não existem muitos avisos e você tem tempo para corrigi-los rapidamente. Mas, se por algum motivo, após esta etapa, você se afogar em avisos, ainda terá algumas soluções. Primeiramente, você pode simplesmente cancelar a seleção, deixar este arquivo e escolher outro. Em segundo lugar, você pode desativar seletivamente anotações para os membros que você acha que estão causando mais problemas. ( #nullableVocê pode usar a diretiva quantas vezes quiser, para controlar as configurações de validade nula, linha por linha, se desejar.) Talvez se você retornar a esse arquivo mais tarde, quando já ativar a validade nula na maior parte do projeto, verá menos avisos do que a primeira vez.

Há momentos em que os problemas não podem ser resolvidos de maneira tão direta. Portanto, em certos cenários relacionados à serialização (por exemplo, ao usar o Json.NET ou o Entity Framework), o trabalho pode ser mais difícil. Eu acho que esse problema merece um artigo separado.

Os links com a suposição nullmelhoram a expressividade do seu código e aumentam as chances de o compilador detectar seus erros antes que os usuários os encontrem. Portanto, é melhor incluir esse recurso, se possível. E, se você incluí-lo seletivamente, seus benefícios começarão a parecer mais rápidos.

All Articles