Estou desenvolvendo uma biblioteca destinada a lançamento público. Ele contém vários métodos para operar em conjuntos de objetos - gerando, inspecionando, particionando e projetando os conjuntos em novas formas. Caso seja relevante, é uma biblioteca de classes C # contendo extensões no estilo LINQ IEnumerable
, a ser lançada como um pacote NuGet.
Alguns dos métodos nesta biblioteca podem receber parâmetros de entrada insatisfatórios. Por exemplo, nos métodos combinatórios, existe um método para gerar todos os conjuntos de n itens que podem ser construídos a partir de um conjunto de origem de m itens. Por exemplo, dado o conjunto:
1, 2, 3, 4, 5
e pedir combinações de 2 produziria:
1, 2
1, 3
1, 4
etc ...
5, 3
5, 4
Agora, obviamente, é possível pedir algo que não pode ser feito, como fornecer um conjunto de 3 itens e, em seguida, solicitar combinações de 4 itens enquanto define a opção que diz que ele pode usar cada item apenas uma vez.
Nesse cenário, cada parâmetro é válido individualmente :
- A coleção de origem não é nula e contém itens
- O tamanho solicitado das combinações é um número inteiro diferente de zero positivo
- O modo solicitado (use cada item apenas uma vez) é uma opção válida
No entanto, o estado dos parâmetros, quando reunidos, causa problemas.
Nesse cenário, você esperaria que o método gerasse uma exceção (por exemplo InvalidOperationException
) ou retornasse uma coleção vazia? Qualquer um parece válido para mim:
- Você não pode produzir combinações de n itens a partir de um conjunto de m itens em que n> m se você puder usar cada item apenas uma vez, portanto, essa operação pode ser considerada impossível
InvalidOperationException
. - O conjunto de combinações de tamanho n que pode ser produzido a partir de m itens quando n> m é um conjunto vazio; nenhuma combinação pode ser produzida.
O argumento para um conjunto vazio
Minha primeira preocupação é que uma exceção evite o encadeamento idiomático de métodos no estilo LINQ quando você estiver lidando com conjuntos de dados que podem ter tamanho desconhecido. Em outras palavras, você pode fazer algo assim:
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.Select(combo => /* do some operation to a combination */)
.ToList();
Se o seu conjunto de entradas tiver tamanho variável, o comportamento desse código é imprevisível. Se .CombinationsOf()
lança uma exceção quando someInputSet
possui menos de 4 elementos, esse código às vezes falha no tempo de execução sem pré-verificação. No exemplo acima, essa verificação é trivial, mas se você está chamando pela metade de uma cadeia mais longa de LINQ, isso pode ser entediante. Se retornar um conjunto vazio, ele result
estará vazio, com o qual você poderá estar perfeitamente feliz.
O argumento para uma exceção
Minha segunda preocupação é que o retorno de um conjunto vazio possa ocultar problemas - se você estiver chamando esse método na metade de uma cadeia de LINQ e retornar silenciosamente um conjunto vazio, poderá encontrar alguns problemas mais tarde ou encontrar um vazio. conjunto de resultados e pode não ser óbvio como isso aconteceu, pois você definitivamente tinha algo no conjunto de entradas.
O que você esperaria e qual é o seu argumento para isso?
fonte
Respostas:
Retornar um conjunto vazio
Eu esperaria um conjunto vazio porque:
Existem 0 combinações de 4 números do conjunto de 3 quando só posso usar cada número uma vez
fonte
Em caso de dúvida, pergunte a outra pessoa.
Sua função exemplo tem uma forma muito semelhante em Python:
itertools.combinations
. Vamos ver como isso funciona:E parece perfeitamente bem para mim. Eu esperava um resultado que pudesse repetir e consegui um.
Mas, obviamente, se você perguntasse algo estúpido:
Então, eu diria que, se todos os seus parâmetros forem validados, mas o resultado for um conjunto vazio, retornar um conjunto vazio, você não será o único a fazê-lo.
Como dito por @Bakuriu nos comentários, também é o mesmo para uma
SQL
consulta comoSELECT <columns> FROM <table> WHERE <conditions>
. Enquanto<columns>
,<table>
,<conditions>
são bem formados formados e referem-se a nomes existentes, você pode construir um conjunto de condições que excluem mutuamente. A consulta resultante não produziria nenhuma linha em vez de gerar umInvalidConditionsError
.fonte
n
elementos em uma coleção para contar o número de maneiras que podemos selecioná-los. Se, em algum momento, não houver elementos ao selecioná-los, não haverá (0) maneira de selecionarn
elementos da coleção.1.0 / 0
, o Python não.SELECT
sobre uma tabela produz uma exceção quando o conjunto de resultados está vazio? (spoiler: não!). A "boa formação" de uma consulta depende apenas da própria consulta e de alguns metadados (por exemplo, a tabela existe e possui esse campo (com esse tipo)?) Depois que a consulta estiver bem formada e você a executar, espera-se um (possivelmente vazio) resultado válido ou uma exceção porque o "servidor" teve um problema (por exemplo, o servidor db caiu ou algo assim).Em termos leigos:
fonte
throw
ou retornar um conjunto vazio ...Concordo com a resposta de Ewan, mas quero acrescentar um raciocínio específico.
Você está lidando com operações matemáticas, portanto, pode ser um bom conselho seguir as mesmas definições matemáticas. Do ponto de vista matemático, o número de conjuntos r de um conjunto n (ou seja, nCr ) está bem definido para todos os r> n> = 0. É zero. Portanto, retornar um conjunto vazio seria o caso esperado do ponto de vista matemático.
fonte
Eu acho que uma boa maneira de determinar se deve usar uma exceção é imaginar pessoas envolvidas na transação.
Tomando como exemplo o conteúdo de um arquivo:
Por favor, traga-me o conteúdo do arquivo, "não existe.txt"
uma. "Aqui está o conteúdo: uma coleção vazia de caracteres"
b. "Erm, há um problema, esse arquivo não existe. Eu não sei o que fazer!"
Por favor, traga-me o conteúdo do arquivo "existe, mas está vazio.txt"
uma. "Aqui está o conteúdo: uma coleção vazia de caracteres"
b. "Erm, há um problema, não há nada neste arquivo. Eu não sei o que fazer!"
Sem dúvida, alguns discordarão, mas para a maioria das pessoas, "Erm, há um problema" faz sentido quando o arquivo não existe e retornando "uma coleção vazia de caracteres" quando o arquivo está vazio.
Então, aplicando a mesma abordagem ao seu exemplo:
Por favor, me dê todas as combinações de 4 itens para
{1, 2, 3}
uma. Não há, aqui está um conjunto vazio.
b. Há um problema, não sei o que fazer.
Novamente, "Há um problema" faria sentido se, por exemplo,
null
fossem oferecidos como o conjunto de itens, mas "aqui está um conjunto vazio" parece uma resposta sensata à solicitação acima.Se o retorno de um valor vazio mascarar um problema (por exemplo, um arquivo ausente, a
null
), uma exceção geralmente deverá ser usada (a menos que o idioma escolhido ofereça suporte aoption/maybe
tipos, às vezes eles fazem mais sentido). Caso contrário, o retorno de um valor vazio provavelmente simplificará o custo e obedecerá melhor ao princípio de menor espanto.fonte
Como é para uma biblioteca de uso geral, meu instinto seria deixar o usuário final escolher.
Assim como temos
Parse()
eTryParse()
disponível para nós, podemos ter a opção de que usamos, dependendo de qual saída precisamos da função. Você gastaria menos tempo escrevendo e mantendo um wrapper de função para lançar a exceção do que discutindo sobre a escolha de uma única versão da função.fonte
Single()
eSingleOrDefault()
imediatamente vieram à mente. Single lança uma exceção se houver zero ou> 1 resultado, enquanto SingleOrDefault não lançará e, em vez disso, retornarádefault(T)
. Talvez o OP possa usarCombinationsOf()
eCombinationsOfOrThrow()
.Single
eSingleOrDefault
(ouFirst
eFirstOrDefault
) são uma história completamente diferente, @RubberDuck. Pegando meu exemplo do meu comentário anterior. É perfeitamente bom não responder à pergunta "O que existem primos maiores que dois?" , já que esta é uma resposta matematicamente sólida e sensata. Enfim, se você está perguntando "Qual é o primeiro primo par maior que dois?" não há resposta natural. Você simplesmente não pode responder à pergunta, porque você está pedindo um único número. Não é nenhum (não é um número único, mas um conjunto), não 0. Portanto, lançamos uma exceção.Você precisa validar os argumentos fornecidos quando sua função é chamada. De fato, você deseja saber como lidar com argumentos inválidos. O fato de vários argumentos dependerem um do outro não compensa o fato de você validar os argumentos.
Assim, eu votaria na ArgumentException, fornecendo as informações necessárias para o usuário entender o que deu errado.
Como exemplo, verifique a
public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32)
função no Linq. Que lança uma ArgumentOutOfRangeException se o índice for menor que 0 ou maior que ou igual ao número de elementos na origem. Assim, o índice é validado em relação ao enumerável fornecido pelo chamador.fonte
new[] { 1, 2, 3 }.Skip(4).ToList();
receber um conjunto vazio, o que me faz pensar que retornar um conjunto vazio talvez seja a melhor opção aqui.Você deve seguir um destes procedimentos (embora continue apresentando problemas básicos de maneira consistente, como um número negativo de combinações):
Forneça duas implementações, uma que retorne um conjunto vazio quando as entradas juntas não fizerem sentido e uma que seja lançada. Tente ligar para eles
CombinationsOf
eCombinationsOfWithInputCheck
. Ou o que você quiser. Você pode reverter isso para que a verificação de entrada seja o nome mais curto e a lista sejaCombinationsOfAllowInconsistentParameters
.Para os métodos Linq, retorne o vazio
IEnumerable
na premissa exata que você descreveu. Em seguida, adicione estes métodos Linq à sua biblioteca:Por fim, use o seguinte:
ou assim:
Observe que o método privado diferente dos públicos é necessário para que o comportamento de execução ou ação ocorra quando a cadeia linq é criada, em vez de algum tempo depois, quando é enumerada. Você quer jogar imediatamente.
Observe, no entanto, que é claro que ele precisa enumerar pelo menos o primeiro item para determinar se há algum item. Essa é uma desvantagem potencial que, na minha opinião, é atenuada principalmente pelo fato de que futuros espectadores podem facilmente entender que um
ThrowIfEmpty
método precisa enumerar pelo menos um item, portanto, não deve ser surpreendido por isso. Mas você nunca sabe. Você poderia tornar isso mais explícitoThrowIfEmptyByEnumeratingAndReEmittingFirstItem
. Mas isso parece um exagero gigantesco.Eu acho que # 2 é bastante, bem, incrível! Agora, o poder está no código de chamada, e o próximo leitor do código entenderá exatamente o que está fazendo e não precisará lidar com exceções inesperadas.
fonte
Eu posso ver argumentos para ambos os casos de uso - uma exceção é ótima se o código downstream espera conjuntos que contêm dados. Por outro lado, simplesmente um conjunto vazio é ótimo se isso for esperado.
Eu acho que depende das expectativas do chamador se este for um erro ou um resultado aceitável - então eu transferiria a opção para o chamador. Talvez introduza uma opção?
.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)
fonte
Existem duas abordagens para decidir se não há resposta óbvia:
Escreva o código assumindo primeiro uma opção e depois a outra. Considere qual deles funcionaria melhor na prática.
Adicione um parâmetro booleano "strict" para indicar se você deseja que os parâmetros sejam estritamente verificados ou não. Por exemplo, o Java's
SimpleDateFormat
tem umsetLenient
método para tentar analisar entradas que não correspondem totalmente ao formato. Claro, você teria que decidir qual é o padrão.fonte
Com base em sua própria análise, retornar o conjunto vazio parece claramente correto - você até o identificou como algo que alguns usuários podem realmente querer e não caiu na armadilha de proibir algum uso, porque você não pode imaginar os usuários que desejam usá-lo dessa maneira.
Se você realmente acha que alguns usuários podem querer forçar retornos não vazios, ofereça a eles uma maneira de solicitar esse comportamento, em vez de forçá-lo a todos. Por exemplo, você pode:
AssertNonempty
cheque que eles possam colocar em suas correntes.fonte
Realmente depende do que seus usuários esperam obter. Para um exemplo (um pouco não relacionado), se o seu código executar uma divisão, você poderá lançar uma exceção ou retornar
Inf
ouNaN
quando dividir por zero. Nem está certo ou errado, no entanto:Inf
em uma biblioteca Python, as pessoas o atacarão por ocultar errosNo seu caso, eu escolheria a solução que será menos surpreendente para os usuários finais. Como você está desenvolvendo uma biblioteca que lida com conjuntos, um conjunto vazio parece algo com o qual os usuários esperam lidar, portanto, retornar parece uma coisa sensata a ser feita. Mas eu posso estar enganado: você tem uma compreensão muito melhor do contexto do que qualquer outra pessoa aqui. Portanto, se você espera que seus usuários confiem no conjunto sempre não estando vazio, deve lançar uma exceção imediatamente.
As soluções que permitem ao usuário escolher (como adicionar um parâmetro "estrito") não são definitivas, pois substituem a pergunta original por uma nova equivalente: "Qual valor
strict
deve ser o padrão?"fonte
É concepção comum (em matemática) que, quando você seleciona elementos sobre um conjunto, não encontra nenhum elemento e, portanto, obtém um conjunto vazio . É claro que você deve ser consistente com a matemática se seguir este caminho:
Regras comuns definidas:
Sua pergunta é muito sutil:
Pode ser que a entrada da sua função tenha que respeitar um contrato : nesse caso, qualquer entrada inválida deve gerar uma exceção, é isso, a função não está funcionando sob parâmetros regulares.
Pode ser que a entrada da sua função tenha que se comportar exatamente como um conjunto e, portanto, deve poder retornar um conjunto vazio.
Agora, se eu estivesse em você, seguiria o caminho "Set", mas com um grande "MAS".
Suponha que você tenha uma coleção que "por hipotese" deve ter apenas estudantes do sexo feminino:
Agora sua coleção não é mais um "conjunto puro", porque você tem um contrato e, portanto, deve fazer cumprir seu contrato com uma exceção.
Quando você usa suas funções "set" de maneira pura, não deve lançar exceções no caso de set vazio, mas se você tiver coleções que não são mais "sets puros", deverá lançar exceções onde for apropriado .
Você deve sempre fazer o que parecer mais natural e consistente: para mim, um conjunto deve aderir às regras, enquanto coisas que não são conjuntos devem ter suas regras adequadamente pensadas.
No seu caso, parece uma boa ideia:
Mas não é realmente uma boa ideia, temos agora um estado inválido que pode ocorrer em tempo de execução em momentos aleatórios, no entanto, isso está implícito no problema e, portanto, não podemos removê-lo, com certeza podemos mover a exceção dentro de algum método utilitário (é exatamente o mesmo código, movido em lugares diferentes), mas isso está errado e basicamente a melhor coisa que você pode fazer é seguir as regras estabelecidas regularmente .
De fato, adicionar uma nova complexidade apenas para mostrar que você pode escrever métodos de consultas linq não parece valer o seu problema . Tenho certeza de que, se o OP puder nos dizer mais sobre seu domínio, provavelmente poderemos encontrar o local onde a exceção é realmente necessária (se é possível que o problema não exija nenhuma exceção).
fonte
FemaleClass
menos que ela tenha apenas fêmeas (não é publicamente publicável, apenas a classe Builder pode distribuir uma instância) e possivelmente possui pelo menos uma fêmea . Então, seu código em todo o lugar que levaFemaleClass
como argumento não precisa lidar com exceções porque o objeto está no estado errado.