Devo aceitar coleções vazias em meus métodos que iteram sobre elas?


40

Eu tenho um método em que toda a lógica é realizada 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 é imprudente.

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

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


43
Você tem certeza de que sua suposição "se alguém está chamando esse método, então eles pretendem passar dados" está correta? Talvez eles estejam apenas passando o que eles têm em mãos para trabalhar com o resultado.
SpaceTrucker

7
BTW: seu método "transform" é geralmente chamado de "map", embora no .NET (e no SQL, de onde o .NET obteve o nome), ele seja chamado "select". "Transformar" é o que o C ++ chama, mas esse não é um nome comum que se possa reconhecer.
Jörg W Mittag

25
Eu jogaria se a coleção estiver, nullmas não se estiver vazia.
CodesInChaos

21
@NickUdell - passo coleções vazias no meu código o tempo todo. É mais fácil o código se comportar da mesma maneira que escrever uma lógica de ramificação especial para os casos em que não pude adicionar nada à minha coleção antes de prosseguir com ela.
theMayer

7
Se você verificar e gerar um erro se a coleção estiver vazia, você estará forçando o chamador a também verificar e não passar uma coleção vazia para o seu método. As validações devem ser feitas nos limites do sistema (e somente nos), se as coleções vazias o incomodarem, você já deve tê-las verificado muito antes de atingir qualquer método utilitário. Agora você tem duas verificações redundantes.
Lie Ryan

Respostas:


175

Os métodos utilitários 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é economizar algum lixo retornando o próprio parâmetro.)

Existem muitas circunstâncias em que um módulo mantém listas de itens que podem ou não estar preenchidos com algo. Ter que verificar o vazio antes de cada chamada 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 serem liberais em suas entradas e conservadores em suas saídas.

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


14
+1 - (mais se eu pudesse). Eu posso até chegar a ponto de também fundir null- se na coleção vazia. Funções com limitações implícitas são uma dor.
Telastyn

6
"Não, você não deveria" ... não deveria aceitá-los ou não deveria lançar uma exceção?
precisa

16
@ Andy - esses não seriam métodos utilitários embora. Eles seriam métodos de negócios ou algum comportamento concreto semelhante.
Telastyn

38
Não acho que reciclar a coleção vazia para devolver é uma boa ideia. Você está economizando apenas um pouquinho de memória; e isso pode resultar em comportamento inesperado no chamador. Exemplo ligeiramente artificial: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); se Populate1 deixasse a coleção vazia e você a retornasse para a primeira chamada TransformNodes, output1 e input seriam a mesma coleção, e quando Populaten2 fosse chamado, se colocasse nós na coleção, você terminaria com sua segunda conjunto de entrada na saída2.
Dan Neely

6
@ Andy Mesmo assim, se o argumento nunca deve estar vazio - se é um erro não ter dados para processar -, sinto que é da responsabilidade do chamador verificar isso, quando está reunindo esse argumento. Ele não apenas mantém esse método mais elegante, mas significa que é possível gerar uma melhor mensagem de erro (melhor contexto) e gerá-la de maneira rápida e sem falhas. Não faz sentido para uma classe jusante para verificar invariantes do chamador ...
Andrzej Doyle

26

Vejo duas perguntas importantes que determinam a resposta para isso:

  1. Sua função pode retornar algo significativo e lógico quando transmitida uma coleção vazia (incluindo nula )?
  2. Qual é o estilo geral de programação neste aplicativo / biblioteca / equipe? (Especificamente, como você está no FP?)

1. Retorno significativo

Essencialmente, se você puder retornar algo significativo, não lance uma exceção. Deixe o chamador 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, retornando uma coleção vazia. Por favor, não jogue nada. O chamador pode ter uma enorme coleção de coleções, algumas vazias, outras não. O chamador deseja qualquer elemento correspondente em qualquer coleção. Exceções apenas tornam a vida do interlocutor mais difícil.
  • Está procurando o maior / menor / melhor ajuste aos critérios da lista. Opa Dependendo da questão do estilo, você pode lançar uma exceção aqui ou retornar nulo . Eu odeio nulo (muito uma pessoa FP), mas é sem dúvida mais significativo aqui e permite reservar exceções para erros inesperados dentro do seu próprio código. Se o chamador não procurar nulo, uma exceção bastante clara resultará de qualquer maneira. Mas você deixa isso para eles.
  • Solicita o enésimo item na coleção ou o primeiro / último n itens. Este é o melhor caso para uma exceção até agora e a que tem menor probabilidade de causar confusão e dificuldade para o chamador. Um caso ainda pode ser nulo se você e sua equipe estiverem acostumados a verificar isso, por todos os motivos mencionados no ponto anterior, mas esse é o caso mais válido ainda para lançar uma exceção DudeYou Know YouShouldCheckTheSizeFirst . Se o seu estilo é mais funcional, nulo ou leia a minha resposta de estilo .

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

2. Estilo

Seu código geral (ou o código do projeto ou da equipe) favorece um estilo funcional? Se não, serão esperadas e tratadas exceções. Se sim, considere retornar um tipo de opção . Com um tipo de opção, você retorna uma resposta significativa ou Nada / Nada . No meu terceiro exemplo acima, Nothing seria uma boa resposta no estilo FP. O fato de a função retornar um tipo de opção sinalizar 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 isso dá ao chamador mais opções (se você perdoar o trocadilho).

F # é onde todas as frias .Net crianças fazem este tipo de coisa, mas C # faz apoiar este modelo.

tl; dr

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


"Melhor ajuste" etc .: você pode ter um método que encontre o objeto de melhor ajuste em uma coleção que corresponda a algum critério. Esse método teria o mesmo problema para coleções não vazias, porque nada pode se encaixar no critério; portanto, não é necessário se preocupar com seleções vazias.
gnasher729

11
Não, foi por isso que eu disse "melhor ajuste" e não "correspondência"; a frase implica a aproximação mais próxima, não uma correspondência exata. As opções maiores / menores deveriam ter sido uma pista. Se apenas foi solicitado o melhor ajuste, qualquer coleção com 1 ou mais membros deve poder retornar um membro. Se houver apenas um membro, esse é o melhor ajuste.
itsbruce

11
Retornar "null" na maioria dos casos é pior do que lançar uma exceção. No entanto, retornar uma coleção vazia é significativo.
26414 Ian Ian

11
Não, se você precisar devolver um único item (min / max / cabeça / cauda). Quanto ao nulo, como eu disse, eu odeio (e prefiro idiomas que não o possuem), mas onde um idioma o possui, você deve ser capaz de lidar com ele e codificar o que o distribui. Dependendo das convenções de codificação locais, pode ser apropriado.
precisa saber é o seguinte

Nenhum de seus exemplos tem algo a ver com uma entrada vazia. São apenas casos especiais de situações em que a resposta pode ser "nada". O que, por sua vez, deve responder à pergunta do OP sobre como "manipular" a coleção vazia.
djechlin

18

Como sempre, depende.

Será que importa que a coleção está 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, é um novo requisito e você precisa decidir o que fazer.

Peça emprestada alguma lógica de teste do mundo do banco de dados: teste para zero itens, um e dois itens. Isso atende aos casos mais importantes (eliminando quaisquer condições de junção internas ou cartesianas mal formadas).


E se é inválido não ter itens em uma coleção, o ideal seria que o argumento não fosse uma classe, java.util.Collectionmas uma com.foo.util.NonEmptyCollectionclasse personalizada , que possa preservar consistentemente essa invariância e impedir que você entre em um estado inválido para começar.
Andrzej Doyle

3
Esta questão está etiquetada com [c #]
Nick Udell 27/11

@NickUdell O C # não suporta programação orientada a objetos ou o conceito de namespaces aninhados? TIL.
John Dvorak

2
Faz. Meu comentário foi um esclarecimento, pois Andrzej deve ter sido confundido (por que mais eles se esforçariam em especificar o espaço de nomes para um idioma que essa pergunta não faz referência?).
Nick Udell

-1 por enfatizar mais o "depende" do que o "quase certamente sim". Não é brincadeira - comunicar a coisa errada danifica aqui.
djechlin

11

Por uma questão de bom design, aceite o máximo possível de variações em suas entradas. Exceções devem ser lançadas quando (entrada inaceitável é apresentada OU erros inesperados ocorrem durante o processamento) E o programa não pode continuar de maneira previsível como resultado.

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

Agora, vamos ao argumento de 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 significativamente em uma saída, e tentar fazer isso introduziria um comportamento inesperado. Isso seria semelhante a dividir por zero em matemática - é completamente sem sentido.


Sua declaração ousada é um péssimo conselho em geral. Eu acho que é verdade aqui, mas você precisa explicar o que há de especial nessa circunstância. (É um mau conselho, em geral, porque promove a falhas silenciosas.)
djechlin

@AAA - claramente não estamos escrevendo um livro sobre como projetar software aqui, mas não acho que seja um mau conselho se você realmente entende o que estou dizendo. O que quero dizer é que, se uma entrada incorreta produz uma saída ambígua ou arbitrária, é preciso lançar uma exceção. Nos casos em que a saída é totalmente previsível (como é o caso aqui), lançar uma exceção seria arbitrário. A chave é nunca tomar decisões arbitrárias em seu programa, pois é daí que vem o comportamento imprevisível.
theMayer

Mas é sua primeira frase e a única destacada ... ninguém entende o que você está dizendo ainda. (Não estou tentando ser muito agressivo aqui, apenas uma das coisas que estamos aqui a fazer é aprender a escrever e comunicar, e faço a perda de precisão em um ponto-chave é prejudicial.)
djechlin

10

A solução certa é muito mais difícil de ver quando você está apenas olhando para sua função isoladamente. Considere sua função como parte de um problema maior . Uma solução possível para esse exemplo é semelhante a esta (em 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 depois filtra para manter apenas os números de quatro dígitos. Sua função pode estar map (_.toInt)no pipeline.

Esse código é bastante simples, porque cada estágio do pipeline manipula apenas uma sequência vazia ou uma coleção vazia. Se você inserir uma string vazia no início, obterá uma lista vazia no final. Você não precisa parar e procurar nulluma exceção após cada chamada.

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


+1 para um exemplo extremamente eficaz (falta nas outras boas respostas).
djechlin

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 é direta:

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

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

Portanto, seu método auxiliar não deve ser "útil" e gerar uma exceção, a menos que seja incapaz de fazer seu trabalho com uma coleção vazia. Deixe o chamador determinar se os resultados podem ser tratados.

Se ele 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 objetivo 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 souber se alguém tem alguma, mas você não sabe que eles não. Mas se isso é importante por algum motivo, provavelmente vale uma variável extra para acompanhá-lo.


1

O método é nomeado TransformNodes. No caso de uma coleção vazia como entrada, recuperar uma coleção vazia é natural e intuitiva e faz sentido matemático.

Se o método foi nomeado Maxe projetado para retornar o elemento máximo, seria natural lançar NoSuchElementExceptionuma coleção vazia, pois o máximo de nada não faz sentido matemático.

Se o método foi nomeado JoinSqlColumnNamese projetado para retornar uma sequência em que os elementos são unidos por uma vírgula para uso em consultas SQL, faria sentido lançar IllegalArgumentExceptionuma coleção vazia, pois o chamador acabaria recebendo um erro SQL de qualquer maneira, se usasse a string em uma consulta SQL diretamente, sem mais verificações e, em vez de procurar uma string vazia retornada, ele realmente deveria ter verificado a coleção vazia.


Maxdo nada é frequentemente infinito negativo.
djechlin

0

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

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

  • presente / retornar / lançar um erro. usando a convenção da sua base de código para essa classe de erro.
  • documento que um valor como zero será retornado
  • documento que um valor inválido designado será retornado (por exemplo, NaN)
  • documentar que um valor mágico será retornado (por exemplo, um mínimo ou um máximo para o tipo ou algum valor potencialmente indicativo)
  • declarar o resultado não especificado
  • declarar a ação indefinida
  • etc.

Eu digo que dê a eles o erro se eles fornecerem uma entrada inválida e a solicitação não puder ser concluída. Quero dizer, um erro grave desde o primeiro dia, para que eles entendam os requisitos do seu programa. Afinal, sua função não está em posição de responder. Se a operação pode falhar (por exemplo, copiar um arquivo), então a sua API deve dar-lhes um erro eles podem lidar.

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

É muito importante que o seu código seja consistente na maneira como 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 à sua - vamos usar uma função que determina se um arquivo existe em um caminho: bool FileExistsAtPath(String). Se o cliente passa uma cadeia vazia, como você lida com esse cenário? Que tal uma matriz vazia ou nula passada para void SaveDocuments(Array<Document>)? Decida sua biblioteca / base de código e seja consistente. Por acaso, considero esses erros de casos e proíbo os clientes de fazer solicitações sem sentido, sinalizando-os como erros (por meio de uma asserção). Algumas pessoas resistem fortemente a essa idéia / ação. Acho essa detecção de erro muito útil. É muito bom para localizar problemas em programas - com boa localidade 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 queimam ciclos em funções que não fazem nada. O código é menor / mais limpo dessa maneira, e as verificações geralmente são enviadas para os locais onde o problema pode ser introduzido.


6
Para o seu último exemplo, não é o trabalho dessa função determinar o que é um absurdo. Portanto, uma cadeia vazia deve retornar false. De fato, essa é a abordagem exata que File.Exists adota.
theMayer 26/11

@ rmayer06 é o seu trabalho. a função referenciada permite cadeias nulas e vazias, verifica caracteres inválidos na cadeia, executa truncamento de cadeias, pode precisar fazer chamadas ao sistema de arquivos, pode precisar consultar o ambiente etc. etc. essas 'conveniências' muitas vezes redundantes em execução alto custo e definitivamente desfocam as linhas de correção (IMO). Eu já vi muitos if (!FileExists(path)) { ...create it!!!... }bugs - muitos dos quais seriam capturados antes do commit, se as linhas de correção não estivessem borradas.
Justin

2
Eu concordo com você, se o nome da função fosse File.ThrowExceptionIfPathIsInvalid, mas quem em sã consciência chamaria essa função?
theMayer

Uma média de 0 itens já retornará NaN ou lançará uma exceção de divisão por zero, apenas devido à forma como a função é definida. Um arquivo com um nome que não pode existir, não existe ou já acionará um erro. Não há motivo convincente para lidar com esses casos especialmente.
cHao 27/11

Pode-se até argumentar que todo o conceito de verificar a existência de um arquivo antes de lê-lo ou escrevê-lo é defeituoso de qualquer maneira. (Existe uma condição de corrida inerente.) Melhor seguir em frente e tentar abrir o arquivo, para leitura ou com configurações somente para criação, se você estiver escrevendo. Já falhará se o nome do arquivo for inválido ou não existir (ou existir, no caso de gravação).
cHao 27/11

-3

Como regra geral, uma função padrão deve ser capaz de aceitar a lista mais ampla de entradas e fornecer feedback sobre ela; existem muitos exemplos em que os programadores usam funções de maneiras que o designer não havia planejado, com isso em mente, acredito que a função deve ser capaz de aceitar não apenas coleções vazias, mas uma ampla variedade de tipos de entrada e retornar um retorno gracioso, mesmo que seja um objeto de erro de qualquer operação realizada na entrada ...


É absolutamente errado que o designer suponha que uma coleção sempre seja aprovada e vá diretamente à iteração sobre a referida coleção, uma verificação deve ser feita para garantir que o argumento recebido esteja em conformidade com o tipo de dados esperado ...
Clement Mark-Aaba

3
"ampla variedade de tipos de entrada" parece irrelevante aqui. A pergunta está marcada com c # , um idioma fortemente tipado. Ou seja, garantias de compilador que a entrada é uma coleção
mosquito

Por favor, google "regra de ouro", a minha resposta foi uma cautela generalizada que, como um programador de fazer argumentos da função espaço para ocorrências imprevistas ou errôneas, um destes pode ser erroneamente passaram ...
Clement Mark-Aaba

11
Isso é errado ou tautológico. writePaycheckToEmployeenão deve aceitar um número negativo como entrada ... mas se "feedback" significa "faz qualquer coisa que possa acontecer a seguir", sim, toda função fará algo que fará a seguir.
djechlin
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.