Devo aceitar coleções vazias em meus métodos que são iteradas sobre elas?

39

Eu tenho um método onde toda a lógica é executada dentro de um loop foreach que itera sobre o parâmetro do método:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

Nesse caso, o envio de uma coleção vazia resulta em uma coleção vazia, mas estou me perguntando se isso é insensato.

Minha lógica aqui é que, se alguém está chamando esse método, eles pretendem passar os dados e passar apenas uma coleção vazia para o meu método em circunstâncias erradas.

Devo pegar esse comportamento e lançar uma exceção para ele, ou a melhor prática é retornar a coleção vazia?

    
por Nick Udell 26.11.2014 / 13:29
fonte

9 respostas

174

Os métodos de utilitário não devem ser lançados em coleções vazias. Seus clientes da API o odiariam por isso.

Uma coleção pode estar vazia; uma "coleção que não deve ser vazia" é conceitualmente uma coisa muito mais difícil de se trabalhar.

Transformar uma coleção vazia tem um resultado óbvio: a coleção vazia. (Você pode até mesmo salvar algum lixo, retornando o próprio parâmetro.)

Existem muitas circunstâncias em que um módulo mantém listas de coisas que podem ou não estar preenchidas com alguma coisa. Ter que verificar se há vazios antes de cada chamada para transform é irritante e tem o potencial de transformar um algoritmo simples e elegante em uma bagunça feia.

Os métodos de utilidade devem sempre se esforçar para ser liberais em suas entradas e conservadores em seus resultados.

Por todas estas razões, pelo amor de Deus, manuseie a coleção vazia corretamente. Nada é mais exasperante do que um módulo auxiliar que pensa que sabe o que você quer melhor do que você.

    
por 26.11.2014 / 13:35
fonte
26

Eu vejo duas questões importantes que determinam a resposta para isso:

  1. Sua função pode retornar algo significativo e lógico quando passou por uma coleção vazia (incluindo null )?
  2. Qual é o estilo geral de programação nesta aplicação / biblioteca / equipe? (Especificamente, como FP é você?)

1. Retorno significativo

Essencialmente, se você puder retornar algo significativo, não lance uma exceção. Deixe o interlocutor lidar com o resultado. Então, se sua função ...

  • Conta o número de elementos na coleção, retorna 0. Isso foi fácil.
  • Procura por elementos que correspondem a critérios específicos e retorna uma coleção vazia. Por favor, não jogue nada. O chamador pode ter uma enorme coleção de coleções, algumas das quais estão vazias, outras não. O chamador deseja todos os elementos correspondentes em qualquer coleção. Exceções só dificultam a vida do interlocutor.
  • Está procurando o maior / menor / melhor ajuste para os critérios na lista. Oops Dependendo da questão de estilo, você pode lançar uma exceção aqui ou retornar null . Eu odeio nulo (muito uma pessoa FP), mas é sem dúvida mais significativo aqui e permite reservar exceções para erros inesperados dentro de seu próprio código. Se o chamador não verificar a existência de null, uma exceção bastante clara resultará de qualquer maneira. Mas você deixa isso para eles.
  • Solicita o enésimo item da coleção ou o primeiro / último n itens. Este é o melhor caso para uma exceção ainda e aquele que é menos provável de causar confusão e dificuldade para o chamador. Um caso ainda pode ser feito para null se você e sua equipe estiverem acostumados a verificar isso, por todas as razões dadas no ponto anterior, mas este é o caso mais válido até agora para lançar um > DudeYou Conheça YouShouldCheckTheSizeFirst exceção. Se o seu estilo é mais funcional, então null ou leia a minha resposta Style .

Em geral, meu viés de FP me diz "retorne algo significativo" e nulo pode ter um significado válido nesse caso.

2. Estilo

O seu código geral (ou o código do projeto ou o código da equipe) favorece um estilo funcional? Se não, exceções serão esperadas e tratadas. Em caso afirmativo, considere a devolução de um tipo de opção . Com um tipo de opção, você retorna uma resposta significativa ou None / Nothing . No meu terceiro exemplo acima, Nothing seria uma boa resposta no estilo FP. O fato de que a função retorna um sinal de tipo de opção claramente para o chamador do que uma resposta significativa pode não ser possível e que o chamador deve estar preparado para lidar com isso. Eu sinto que dá ao chamador mais opções (se você perdoar o trocadilho).

F # é o lugar onde todos os cool .Net kids fazem esse tipo de coisa, mas C # faz suporta este estilo.

tl; dr

Mantenha exceções para erros inesperados em seu próprio caminho de código, não totalmente imprevisível (e legal) de entrada de outra pessoa.

    
por 26.11.2014 / 15:35
fonte
18

Como sempre, isso depende.

Importa que a coleção esteja vazia?
A maioria dos códigos de manipulação de coleções provavelmente diria "não"; uma coleção pode ter qualquer número de itens, incluindo zero.

Agora, se você tem algum tipo de coleção em que é "inválido" não ter itens, então esse é um novo requisito e você precisa decidir o que fazer sobre isso.

Peça alguma lógica de teste do mundo do banco de dados: teste para itens zero , item um e dois itens. Isso serve para os casos mais importantes (eliminar todas as condições de união interna ou cartesiana mal formadas).

    
por 26.11.2014 / 13:39
fonte
11

Por uma questão de bom design, aceite tanta variação em seus insumos quanto possível ou prático. Exceções devem somente ser lançadas quando (entrada inaceitável é apresentada OU erros inesperados acontecem durante o processamento) E o programa não pode continuar de uma forma previsível como resultado.

Nesse caso, é de se esperar que uma coleção vazia seja apresentada e seu código precise manipulá-la (o que ela já faz). Seria uma violação de tudo o que é bom se o seu código lançasse uma exceção aqui. Isso seria semelhante a multiplicar 0 por 0 em matemática. É redundante, mas absolutamente necessário para que funcione da maneira como funciona.

Agora, para o argumento da coleção nula. Nesse caso, uma coleção nula é um erro de programação: o programador esqueceu de atribuir uma variável. Este é um caso em que uma exceção pode ser lançada, porque você não pode processá-la de maneira significativa em uma saída, e tentar fazê-lo introduziria um comportamento inesperado. Isso seria semelhante a dividir por zero em matemática - é completamente sem sentido.

    
por 26.11.2014 / 15:49
fonte
10

A solução certa é muito mais difícil de enxergar quando você está apenas observando sua função isoladamente. Considere sua função como uma parte de um problema maior . Uma solução possível para esse exemplo se parece com isso (no Scala):

input.split("\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Primeiro, você divide a string por não-dígitos, filtra as strings vazias, converte as strings em números inteiros e filtra para manter apenas os números de quatro dígitos. Sua função pode ser o map (_.toInt) no pipeline.

Esse código é bastante simples porque cada estágio no pipeline apenas manipula uma string vazia ou uma coleção vazia. Se você colocar uma string vazia no início, você verá uma lista vazia no final. Você não precisa parar e verificar null ou uma exceção após cada chamada.

Claro, presumir que uma lista de saída vazia não tenha mais de um significado. Se você precisar para diferenciar entre uma saída vazia causada pela entrada vazia e outra causada pela própria transformação, isso muda completamente as coisas.

    
por 26.11.2014 / 21:07
fonte
2

Esta questão é realmente sobre exceções. Se você olhar dessa maneira e ignorar a coleção vazia como um detalhe de implementação, a resposta será direta:

1) Um método deve lançar uma exceção quando não puder prosseguir: seja incapaz de realizar a tarefa designada ou retornar o valor apropriado.

2) Um método deve capturar uma exceção quando puder prosseguir apesar da falha.

Assim, seu método auxiliar não deve ser "útil" e lançar uma exceção, a menos que it não consiga fazer seu trabalho com uma coleção vazia. Deixe o chamador determinar se os resultados podem ser manipulados.

Se retorna uma coleção vazia ou nula, é um pouco mais difícil, mas não muito: coleções anuláveis devem ser evitadas, se possível. O propósito de uma coleção anulável seria indicar (como no SQL) que você não tem as informações - uma coleção de filhos, por exemplo, pode ser nula se você não sabe se alguém tem alguma, mas você não tem saiba que eles não sabem. Mas se isso for importante por algum motivo, provavelmente vale a pena uma variável extra para rastreá-lo.

    
por 27.11.2014 / 20:21
fonte
1

O método é denominado TransformNodes . No caso de uma coleção vazia como entrada, voltar uma coleção vazia é natural e intuitivo, e faz perfeito sentido matemático.

Se o método foi nomeado Max e projetado para retornar o elemento máximo, seria natural lançar NoSuchElementException em uma coleção vazia, pois o máximo de nada faz sentido matemático.

Se o método foi nomeado JoinSqlColumnNames e projetado para retornar uma string onde os elementos são unidos por uma vírgula para usar em consultas SQL, então faria sentido lançar IllegalArgumentException em uma coleção vazia, como o chamador faria. eventualmente, obter um erro de SQL de qualquer maneira, se ele usasse a string em uma consulta SQL diretamente, sem verificações adicionais, e em vez de verificar uma string vazia retornada, ele deveria ter verificado a coleta vazia.

    
por 30.11.2014 / 12:30
fonte
0

Vamos recuar e usar um exemplo diferente que calcule a média aritmética de uma matriz de valores.

Se a matriz de entrada estiver vazia (ou nula), você pode atender razoavelmente a solicitação do chamador? Não. Quais são suas opções? Bem, você poderia:

  • apresentar / retornar / lançar um erro. usando a convenção do seu codebase para essa classe de erro.
  • documentar que um valor como zero será retornado
  • documentar que um valor inválido designado será retornado (por exemplo, NaN)
  • documentar que um valor mágico será retornado (por exemplo, um min ou max para o tipo ou algum valor esperançosamente indicativo)
  • declare que o resultado não é especificado
  • declare que a ação está indefinida
  • etc.

Eu digo para eles darem o erro se eles tiverem dado entrada inválida e a solicitação não puder ser concluída. Quero dizer, um erro difícil desde o primeiro dia, para que eles entendam os requisitos do seu programa. Afinal, sua função não está em condições de responder. Se a operação puder falhar (por exemplo, copiar um arquivo), sua API deverá apresentar um erro com o qual possa lidar.

Isso pode definir como sua biblioteca lida com solicitações e solicitações malformadas que podem falhar.

É muito importante que seu código seja consistente em como ele lida com essas classes de erros.

A próxima categoria é decidir como sua biblioteca lida com solicitações sem sentido. Voltando a um exemplo semelhante ao seu - vamos usar uma função que determina se um arquivo existe em um caminho: bool FileExistsAtPath(String) . Se o cliente passar uma string vazia, como você lida com esse cenário? Que tal uma matriz vazia ou nula passada para void SaveDocuments(Array<Document>) ? Decida sua biblioteca / codebase e seja consistente . Por acaso, considero esses erros de casos e proíbo os clientes de fazerem solicitações sem sentido, sinalizando-os como erros (por meio de uma afirmação). Algumas pessoas resistirão strongmente a essa ideia / ação. Acho essa detecção de erros muito útil. É muito bom para localizar problemas em programas - com boa localização para o programa ofensivo. Os programas são muito mais claros e corretos (considere a evolução da sua base de código) e não queime ciclos dentro de funções que não fazem nada. O código é menor / limpo dessa maneira, e as verificações são geralmente enviadas para os locais onde o problema pode ser introduzido.

    
por 26.11.2014 / 16:43
fonte
-3

Como regra geral, uma função padrão deve ser capaz de aceitar a lista mais ampla de entradas e dar feedback sobre ela, há muitos exemplos em que os programadores usam funções de maneiras que o designer não planejou, com isso em mente Acredite que a função deve ser capaz de aceitar não apenas coleções vazias, mas também uma ampla gama de tipos de entrada e um feedback de retorno, seja um objeto de erro de qualquer operação realizada na entrada ...

    
por 27.11.2014 / 12:28
fonte