Não é razoável esperar que Any () * not * lance uma exceção de referência nula?

41

Quando você cria um método de extensão, é possível chamá-lo em null .But, ao contrário de uma chamada de método de instância, chamá-lo de nulo não tem para lançar NullReferenceException - > você tem que verificar e jogá-lo manualmente.

Para a implementação do método de extensão do Linq Any() , a Microsoft decidiu que eles deveriam lançar um ArgumentNullException ( link ).

Me incomoda ter que escrever if( myCollection != null && myCollection.Any() )

Estou errado, como cliente deste código, de esperar que, por exemplo, ((int[])null).Any() deve retornar false ?

    
por thisextendsthat 18.09.2018 / 17:09
fonte

13 respostas

152

Eu tenho uma sacola com cinco batatas. Há .Any() batatas no saco?

" Sim ", você diz. <= true

Eu pego todas as batatas e as como. Há .Any() batatas no saco?

" Não ", você diz. <= false

Eu completamente incinerar a bolsa em um incêndio. Há .Any() batatas na sacola agora?

" Não há saco ." <= ArgumentNullException

    
por 18.09.2018 / 18:39
fonte
52

Primeiramente, parece que esse código-fonte lançará ArgumentNullException , não NullReferenceException .

Tendo dito isso, em muitos casos você já sabe que sua coleção não é nula, porque esse código só é chamado pelo código que sabe que a coleção já existe, então você não terá que colocar a verificação nula lá muito frequentemente. Mas se você não sabe que existe, faz sentido verificar antes de chamar Any() .

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Sim. A pergunta que Any() responde é "esta coleção contém algum elemento?" Se esta coleção não existe, então a questão em si é sem sentido; não pode conter nem não conter nada, porque não existe.

    
por 18.09.2018 / 17:23
fonte
23

Nulo significa falta de informação e não de elementos.

Você pode considerar evitar, de forma mais ampla, nulo - por exemplo, use um dos enumeráveis vazios internos para representar uma coleção sem elementos em vez de nulos.

Se você estiver retornando null em algumas circunstâncias, poderá alterar isso para retornar a coleção vazia. (Caso contrário, se você está encontrando null's retornados por métodos de biblioteca (não seus), isso é lamentável, e eu os envolvo para normalizar.)

Veja também link

    
por 18.09.2018 / 17:59
fonte
13

Além da sintaxe condicional nula , há outra técnica para aliviar esse problema: não deixe sua variável permanece null .

Considere uma função que aceita uma coleção como um parâmetro. Se, para fins da função, null e vazio forem equivalentes, você pode garantir que nunca contenha null no início:

public MyResult DoSomething(int a, IEnumerable<string> words)
{
    words = words ?? Enumerable.Empty<string>();

    if (!words.Any())
    {
        ...

Você pode fazer o mesmo quando buscar coleções de algum outro método:

var words = GetWords() ?? Enumerable.Empty<string>();

(Note que nos casos em que você tem controle sobre uma função como GetWords e null é equivalente à coleção vazia, é preferível apenas retornar a coleção vazia em primeiro lugar.)

Agora você pode realizar qualquer operação que desejar na coleção. Isso é especialmente útil se você precisar executar muitas operações que falharão quando a coleção for null e, nos casos em que você obtiver o mesmo resultado passando por loop ou consultando um enumerável vazio, permitirá eliminar totalmente as condições if .

    
por 19.09.2018 / 05:32
fonte
12

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Sim, simplesmente porque você está em C # e esse comportamento é bem definido e documentado.

Se você estivesse criando sua própria biblioteca ou se estivesse usando um idioma diferente com uma cultura de exceção diferente, seria mais razoável esperar que fosse falso.

Pessoalmente, sinto que return false é uma abordagem mais segura que torna seu programa mais robusto, mas é discutível pelo menos.

    
por 18.09.2018 / 18:52
fonte
10

Se as repetidas checagens de nulo incomodarem você, você pode criar seu próprio método de extensão 'IsNullOrEmpty ()', para espelhar o método String com esse nome, e enrolar a checagem nula e a chamada .Any () em um chamada única.

Caso contrário, a solução, mencionada por @ 17 de 26 em um comentário sob sua pergunta, é mais curta que o método 'padrão', e razoavelmente clara para qualquer pessoa familiarizada com a nova sintaxe condicional nula.

if(myCollection?.Any() == true)
    
por 18.09.2018 / 19:59
fonte
8

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Se você se pergunta sobre as expectativas, precisa pensar em intenções.

null significa algo muito diferente de Enumerable.Empty<T>

Como mencionado na resposta de Erik Eidt , há uma diferença de significado entre null e uma coleção vazia .

Vamos primeiro ver como eles devem ser usados.

O livro < em> Diretrizes de Design de Estrutura: Convenções, Expressões e Padrões para Bibliotecas .NET Reutilizáveis, 2ª Edição escrita por arquitetos da Microsoft Krzysztof Cwalina e Brad Abrams indica a seguinte prática recomendada:

X DO NOT return null values from collection properties or from methods returning collections. Return an empty collection or an empty array instead.

Considere a sua chamada como um método que finalmente está obtendo dados de um banco de dados: se você receber uma matriz vazia ou Enumerable.Empty<T> , isso significa que o seu espaço de amostra está vazio, ou seja, seu resultado é um > conjunto vazio . Receber null neste contexto, no entanto, significaria um estado de erro .

Na mesma linha de raciocínio que a analogia da batata de Dan Wilson , faz sentido fazer perguntas sobre seus dados mesmo que seja um conjunto vazio . Mas isso faz muito sentido , se não houver nenhum conjunto .

    
por 19.09.2018 / 10:08
fonte
3

Há muitas respostas explicando por que null e vazio são diferentes e opiniões suficientes tentando explicar por que eles devem ser tratados de forma diferente ou não. No entanto, você está perguntando:

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

É uma expectativa perfeitamente razoável . Você é tão certo quanto alguém que defende o comportamento atual. Concordo com a filosofia de implementação atual, mas os fatores determinantes não são - apenas - baseados em considerações fora do contexto.

Dado que Any() sem predicado é essencialmente Count() > 0 , o que você espera deste snippet?

List<int> list = null;
if (list.Count > 0) {}

Ou um genérico:

List<int> list = null;
if (list.Foo()) {}

Suponho que você espere NullReferenceException .

  • Any() é um método de extensão, deve integrar suavemente com o objeto estendido e lançar uma exceção é a coisa menos surpreendente.
  • Nem toda linguagem .NET suporta métodos de extensão.
  • Você sempre pode chamar Enumerable.Any(null) e você definitivamente espera ArgumentNullException . É o mesmo método e tem que ser consistente com - possivelmente - quase TUDO mais no Framework.
  • Acessar um objeto null é um erro de programação , a estrutura não deve impor nulo como valor mágico . Se você usar dessa maneira, é sua responsabilidade lidar com isso.
  • É opinativo , você pensa de um jeito e eu acho outro jeito. A estrutura deve ser o mais vigorosa possível.
  • Se você tiver um caso especial, deverá ser consistente : terá que tomar decisões cada vez mais opinativas sobre todos os outros métodos de extensão: se Count() parecer uma decisão fácil, então Where() não é. E quanto a Max() ? Ele lança uma exceção para uma lista EMPTY, não deveria lançar também um null one?

O que os designers de biblioteca fizeram antes do LINQ introduzir métodos explícitos quando null é um valor válido (por exemplo, String.IsNullOrEmpty() ), então eles TÊM de ser consistentes com a filosofia de design existente Dito isso, mesmo que seja muito simples escrever, dois métodos EmptyIfNull() e EmptyOrNull() podem ser úteis.

    
por 19.09.2018 / 12:43
fonte
1

Jim deve deixar as batatas em todas as sacolas. Caso contrário, vou matá-lo.

Jim tem uma sacola com cinco batatas. Há .Any() batatas no saco?

"Sim", você diz. < = verdadeiro

Ok, então Jim vive dessa vez.

Jim tira todas as batatas e come-as. Há alguma (...) batata na bolsa?

"Não", você diz. < = falso

Hora de matar Jim.

Jim incinerou completamente o saco em um incêndio. Há alguma (agora) batata na sacola agora?

"Não há saco". < = ArgumentNullException

Jim deve viver ou morrer? Bem, nós não esperamos isso, então eu preciso de uma decisão. Está deixando Jim escapar com um bug ou não?

Você pode usar anotações para sinalizar que você não está agüentando nenhuma travessura nula dessa maneira.

public bool Any( [NotNull] List bag ) 

Mas sua cadeia de ferramentas precisa apoiá-lo. O que significa que você provavelmente vai acabar escrevendo cheques.

    
por 21.09.2018 / 01:52
fonte
0

Se isso te incomoda muito, sugiro um método de extensão simples.

static public IEnumerable<T> NullToEmpty<T>(this IEnumerable<T> source)
{
    return (source == null) ? Enumerable.Empty<T>() : source;
}

Agora você pode fazer isso:

List<string> list = null;
var flag = list.NullToEmpty().Any( s => s == "Foo" );

... e o sinalizador será definido como false .

    
por 20.09.2018 / 04:52
fonte
0

Esta é uma pergunta sobre os métodos de extensão do C # e sua filosofia de design, então acho que a melhor maneira de responder a essa pergunta é citar Documentação do MSDN sobre o propósito dos métodos de extensão :

Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. For client code written in C#, F# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.

In general, we recommend that you implement extension methods sparingly and only when you have to. Whenever possible, client code that must extend an existing type should do so by creating a new type derived from the existing type. For more information, see Inheritance.

When using an extension method to extend a type whose source code you cannot change, you run the risk that a change in the implementation of the type will cause your extension method to break.

If you do implement extension methods for a given type, remember the following points:

  • An extension method will never be called if it has the same signature as a method defined in the type.
  • Extension methods are brought into scope at the namespace level. For example, if you have multiple static classes that contain extension methods in a single namespace named Extensions, they will all be brought into scope by the using Extensions; directive.

Para resumir, os métodos de extensão são projetados para adicionar métodos de instância a um tipo específico, mesmo quando os desenvolvedores não podem fazê-lo diretamente. E como os métodos de instância substituirão sempre os métodos de extensão se presentes (se chamado usando a sintaxe do método da instância), isso deve ser somente se você não puder adicionar diretamente um método ou estender a classe . *

Em outras palavras, um método de extensão deve agir exatamente como um método de instância faria, porque pode acabar sendo feito um método de instância por algum cliente. E como um método de instância deve lançar se o objeto que está sendo chamado for null , o mesmo acontecerá com o método de extensão.

* Como uma nota lateral, esta é exatamente a situação que os projetistas do LINQ enfrentaram: quando o C # 3.0 foi lançado, já havia milhões de clientes usando System.Collections.IEnumerable e System.Collections.Generic.IEnumerable<T> , tanto em suas coleções quanto em foreach loops. Essas classes retornaram IEnumerator objetos que tinham apenas os dois métodos Current e MoveNext , portanto, incluir quaisquer métodos de instância adicionais obrigatórios, como Count , Any etc., estaria quebrando esses milhões de clientes. Assim, para fornecer essa funcionalidade (especialmente porque ela pode ser implementada em termos de Current e MoveNext com relativa facilidade), eles a lançaram como métodos de extensão, que podem ser aplicados a qualquer instância IEnumerable e também pode ser implementado por classes de maneiras mais eficientes. Se os designers do C # tivessem decidido lançar o LINQ no primeiro dia, ele teria sido fornecido como métodos de instância de IEnumerable e provavelmente teriam projetado algum tipo de sistema para fornecer implementações de interface padrão desses métodos.

    
por 21.09.2018 / 02:31
fonte
0

Acho que o ponto principal aqui é que ao retornar false em vez de lançar uma exceção, você está ofuscando informações que podem ser relevantes para futuros leitores / modificadores de seu código.

Se for possível que a lista seja nula, sugiro ter um caminho lógico separado para isso, caso contrário, no futuro, alguém poderá adicionar alguma lógica baseada em lista (como um add) ao else {} do seu if, resultando em uma exceção indesejada de que eles não tinham motivos para prever.

Progresso de legibilidade e facilidade de manutenção "Preciso escrever uma condição extra" o tempo todo.

    
por 21.09.2018 / 08:28
fonte
0

Como regra geral, escrevo a maior parte do meu código para assumir que o chamador é responsável por não me fornecer dados nulos, principalmente pelos motivos descritos em Diga Não ao Nulo (e outros posts semelhantes). O conceito de null geralmente não é bem entendido e, por essa razão, é aconselhável inicializar suas variáveis antes do tempo, tanto quanto possível. Supondo que você esteja trabalhando com uma API razoável, o ideal é nunca obter um valor nulo de volta, portanto, você nunca deve verificar o valor nulo. O ponto de null, como outras respostas afirmaram, é ter certeza de que você tem um objeto válido (por exemplo, um "saco") para trabalhar antes de continuar. Este não é um recurso de Any , mas sim um recurso da própria linguagem . Você não pode fazer a maioria das operações em um objeto nulo, porque ele não existe. É como se eu pedisse para você dirigir até a loja no meu carro, exceto que eu não possuo um carro, então você não tem como usar o meu carro para ir até a loja. Não faz sentido realizar uma operação em algo que literalmente não existe.

Existem casos práticos para usar null, como quando você literalmente não tem as informações solicitadas disponíveis (por exemplo, se eu perguntei qual é a minha idade, a escolha mais provável para você responder neste ponto é "eu não 'know', que é o que é um valor nulo). No entanto, na maioria dos casos, você deve pelo menos saber se suas variáveis foram inicializadas ou não. E se você não fizer isso, recomendo que você precise reforçar seu código. A primeira coisa que faço em qualquer programa ou função é inicializar um valor antes de usá-lo. É raro que eu precise verificar valores nulos, porque posso garantir que minhas variáveis não são nulas. É um bom hábito entrar, e quanto mais frequentemente você se lembrar de inicializar suas variáveis, menos frequentemente precisará verificar a existência de null.

    
por 22.09.2018 / 23:12
fonte