Verificação de parâmetro nulo em C #

88

Em C #, há bons motivos (além de uma mensagem de erro melhor) para adicionar verificações de parâmetro nulo a cada função onde nulo não é um valor válido? Obviamente, o código que usa s lançará uma exceção de qualquer maneira. E essas verificações tornam o código mais lento e difícil de manter.

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}
Kaalus
fonte
13
Eu duvido seriamente que uma simples verificação de nulo irá desacelerar seu código em uma quantidade significativa (ou mesmo mensurável).
Heinzi
Bem, depende do que "Use s" faz. Se tudo o que ele fizer for "retornar s.SomeField", a verificação extra provavelmente tornará o método mais lento em uma ordem de magnitude.
Kaalus
11
@kaalus: Provavelmente? Provavelmente não encaixa no meu livro. Onde estão seus dados de teste? E qual é a probabilidade de que tal mudança seja o gargalo de seu aplicativo em primeiro lugar?
Jon Skeet
@ Jon: você está certo, não tenho dados concretos aqui. Se você desenvolve para Windows Phone ou Xbox em que o inlining JIT será afetado por este "se" extra e onde a ramificação é muito cara, você pode ficar bastante paranóico.
Kaalus
3
@kaalus: Portanto, ajuste seu estilo de código nesses casos muito específicos depois de provar que isso faz uma diferença significativa ou use Debug.Assert (novamente, apenas nessas situações, veja a resposta de Eric) para que as verificações sejam ativadas apenas em certas compilações.
Jon Skeet

Respostas:

167

Sim, existem boas razões:

  • Ele identifica exatamente o que é nulo, o que pode não ser óbvio de um NullReferenceException
  • Isso faz com que o código falhe na entrada inválida, mesmo se alguma outra condição significar que o valor não foi desreferenciado
  • Faz com que a exceção ocorra antes que o método possa ter quaisquer outros efeitos colaterais que você possa alcançar antes da primeira desreferência
  • Isso significa que você pode ter certeza de que, se passar o parâmetro para outra coisa, não estará violando o contrato
  • Ele documenta os requisitos do seu método (usar Contratos de Código é ainda melhor para isso, é claro)

Agora, quanto às suas objeções:

  • É mais lento : você descobriu que esse é realmente o gargalo em seu código ou está adivinhando? As verificações de nulidade são muito rápidas e, na grande maioria dos casos, não serão o gargalo
  • Isso torna o código mais difícil de manter : acho o contrário. Acho que é mais fácil usar o código onde fica bem claro se um parâmetro pode ser nulo ou não, e onde você tem certeza de que essa condição é aplicada.

E para sua afirmação:

Obviamente, o código que usa s lançará uma exceção de qualquer maneira.

Mesmo? Considerar:

void f(SomeType s)
{
  // Use s
  Console.WriteLine("I've got a message of {0}", s);
}

Isso usa s, mas não lança uma exceção. Se for inválido para sser nulo, e isso indicar que algo está errado, uma exceção é o comportamento mais apropriado aqui.

Agora, onde você coloca essas verificações de validação de argumento é uma questão diferente. Você pode decidir confiar em todo o código de sua própria classe, portanto, não se preocupe com métodos privados. Você pode decidir confiar no resto de sua montagem, portanto, não se preocupe com os métodos internos. Você quase certamente deve validar os argumentos dos métodos públicos.

Uma observação lateral: a sobrecarga do construtor de parâmetro único de ArgumentNullExceptiondeve ser apenas o nome do parâmetro, então seu teste deve ser:

if (s == null)
{
  throw new ArgumentNullException("s");
}

Como alternativa, você pode criar um método de extensão, permitindo um pouco mais terser:

s.ThrowIfNull("s");

Na minha versão do método de extensão (genérico), faço com que ele retorne o valor original se não for nulo, permitindo que você escreva coisas como:

this.name = name.ThrowIfNull("name");

Você também pode ter uma sobrecarga que não leva o nome do parâmetro, se você não estiver muito preocupado com isso.

Jon Skeet
fonte
8
+1 para o método de extensão. Fiz exatamente a mesma coisa, incluindo outra validação, como ThrowIfEmptyemICollection
Davy8
5
Por que eu acho que é mais difícil de manter: como acontece com todo mecanismo manual que requer intervenção do programador todas as vezes, isso está sujeito a erros e omissões e bagunça o código. Segurança fragmentada é pior do que nenhuma segurança.
Kaalus
8
@kaalus: Você aplica a mesma atitude aos testes? "Meus testes não vão pegar todos os bugs possíveis, portanto não vou escrever nenhum"? Quando os mecanismos de segurança do trabalho, eles vão torná-lo mais fácil de encontrar o problema e reduzir o impacto em outros lugares (por apanhar o problema anteriormente). Se isso acontecer 9 vezes em 10, ainda é melhor do que acontecer 0 vezes em 10 ...
Jon Skeet
2
@DoctorOreo: Eu não uso Debug.Assert. É ainda mais importante detectar erros na produção (antes que eles estraguem os dados reais) do que no desenvolvimento.
Jon Skeet
3
Agora, com C # 6.0, podemos até usar throw new ArgumentNullException(nameof(s))
hrzafer
52

Eu concordo com Jon, mas gostaria de acrescentar uma coisa a isso.

Minha atitude sobre quando adicionar verificações nulas explícitas baseia-se nestas premissas:

  • Deve haver uma maneira de seus testes de unidade exercitarem todas as instruções de um programa.
  • throwdeclarações são declarações .
  • A consequência de um ifé uma declaração .
  • Portanto, deve haver uma maneira de exercer o throwemif (x == null) throw whatever;

Se não houver uma maneira possível de execução dessa instrução, ela não poderá ser testada e deverá ser substituída por Debug.Assert(x != null);.

Se houver uma maneira possível de execução dessa instrução, escreva a instrução e um teste de unidade que a exercite.

É particularmente importante que os métodos públicos dos tipos públicos verifiquem seus argumentos dessa maneira; você não tem ideia da loucura que seus usuários vão fazer. Dê a eles o "ei seu estúpido, você está fazendo errado!" exceção o mais rápido possível.

Os métodos privados de tipos privados, por outro lado, têm muito mais probabilidade de estar na situação em que você controla os argumentos e podem ter uma forte garantia de que o argumento nunca será nulo; use uma asserção para documentar essa invariante.

Eric Lippert
fonte
23

Estou usando isso há um ano:

_ = s ?? throw new ArgumentNullException(nameof(s));

É um oneliner, e o descarte ( _) significa que não há alocação desnecessária.

Galdin
fonte
8

Sem uma ifverificação explícita , pode ser muito difícil descobrir o que era nullse você não fosse o proprietário do código.

Se você obtiver um NullReferenceExceptionde dentro de uma biblioteca sem código-fonte, provavelmente terá muitos problemas para descobrir o que fez de errado.

Essas ifverificações não tornarão seu código notavelmente mais lento.


Observe que o parâmetro para o ArgumentNullExceptionconstrutor é um nome de parâmetro, não uma mensagem.
Seu código deve ser

if (s == null) throw new ArgumentNullException("s");

Escrevi um snippet de código para tornar isso mais fácil:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Check for null arguments</Title>
            <Shortcut>tna</Shortcut>
            <Description>Code snippet for throw new ArgumentNullException</Description>
            <Author>SLaks</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>Parameter</ID>
                    <ToolTip>Paremeter to check for null</ToolTip>
                    <Default>value</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[if ($Parameter$ == null) throw new ArgumentNullException("$Parameter$");
        $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>
SLaks
fonte
5

Você pode querer dar uma olhada em Contratos de código se precisar de uma maneira mais agradável de ter certeza de não obter nenhum objeto nulo como parâmetro.

AD.Net
fonte
2

O principal benefício é que você está sendo explícito com os requisitos do seu método desde o início. Isso deixa claro para outros desenvolvedores que trabalham no código que é realmente um erro um chamador enviar um valor nulo para o seu método.

A verificação também interromperá a execução do método antes que qualquer outro código seja executado. Isso significa que você não terá que se preocupar com modificações feitas pelo método que não foram concluídas.

Justin Niessner
fonte
2

Ele economiza alguma depuração, quando você atinge essa exceção.

O ArgumentNullException afirma explicitamente que era "s" que era nulo.

Se você não tiver essa verificação e deixar o código explodir, receberá uma NullReferenceException de alguma linha não identificada nesse método. Em uma versão de lançamento, você não obtém números de linha!

Hans Ke st ing
fonte
0

Código original:

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

Reescreva-o como:

void f(SomeType s)
{
  if (s == null) throw new ArgumentNullException(nameof(s));
}

A razão para reescrever usando nameofé que permite uma refatoração mais fácil. Se o nome da sua variável smudar alguma vez, as mensagens de depuração também serão atualizadas, ao passo que, se você apenas codificar o nome da variável, ela acabará ficando desatualizada quando as atualizações forem feitas ao longo do tempo. É uma boa prática utilizada na indústria.

Asher Garland
fonte
-2
int i = Age ?? 0;

Então, para seu exemplo:

if (age == null || age == 0)

Ou:

if (age.GetValueOrDefault(0) == 0)

Ou:

if ((age ?? 0) == 0)

Ou ternário:

int i = age.HasValue ? age.Value : 0;
Muhammad Farooq Khalid
fonte