Exceção vs conjunto de resultados vazio quando as entradas são tecnicamente válidas, mas insatisfiáveis

108

Estou desenvolvendo uma biblioteca destinada ao 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 em IEnumerable , a serem lançadas como um pacote NuGet.

Alguns dos métodos nesta biblioteca podem receber parâmetros de entrada insatisfáveis. Por exemplo, nos métodos combinatoriais, existe um método para gerar todos os conjuntos de itens n que podem ser construídos a partir de um conjunto de fontes de itens m . 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 dar a ele um conjunto de três itens e, em seguida, pedir combinações de quatro itens ao definir a opção que diz que só pode usar cada item uma vez. / p>

Neste cenário, cada parâmetro é individualmente válido:

  • A coleção de origem não é nula e contém itens
  • O tamanho solicitado de 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 considerados juntos causa problemas.

Nesse cenário, você esperaria que o método lançasse 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 itens n de um conjunto de itens m em que n > m se você só puder usar cada item uma vez, então essa operação pode ser considerada impossível, portanto InvalidOperationException .
  • O conjunto de combinações de tamanho n que pode ser produzido a partir de itens m 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 evita o encadeamento idiomático de métodos no estilo LINQ quando você está lidando com conjuntos de dados que podem ter tamanho desconhecido. Em outras palavras, você pode querer fazer algo assim:

var result = someInputSet
    .CombinationsOf(4, CombinationsGenerationMode.Distinct)
    .Select(combo => /* do some operation to a combination */)
    .ToList();

Se o seu conjunto de entrada é de tamanho variável, o comportamento deste código é imprevisível. Se .CombinationsOf() gerar uma exceção quando someInputSet tiver menos de 4 elementos, esse código irá algumas vezes falhar em tempo de execução sem alguma pré-verificação. No exemplo acima, essa verificação é trivial, mas se você estiver chamando a metade de uma cadeia mais longa de LINQ, isso pode ser entediante. Se ele retornar um conjunto vazio, então result estará vazio, com o qual você ficará perfeitamente satisfeito.

O argumento para uma exceção

Minha segunda preocupação é que retornar um conjunto vazio pode ocultar problemas - se você estiver chamando esse método a meio caminho de uma cadeia de LINQ e ele retornar silenciosamente um conjunto vazio, você poderá encontrar alguns problemas mais tarde ou localizar-se com um conjunto de resultados vazio, e pode não ser óbvio como isso aconteceu, já que você definitivamente tinha algo no conjunto de entrada.

O que você esperaria, e qual é o seu argumento para isso?

    
por anaximander 30.11.2016 / 13:19
fonte

13 respostas

145

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

    
por 30.11.2016 / 14:54
fonte
80

Em caso de dúvida, pergunte a outra pessoa.

Sua função de exemplo tem uma muito semelhante em Python: itertools.combinations . Vamos ver como funciona:

>>> import itertools
>>> input = [1, 2, 3, 4, 5]
>>> list(itertools.combinations(input, 2))
[(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]
>>> list(itertools.combinations(input, 5))
[(1, 2, 3, 4, 5)]
>>> list(itertools.combinations(input, 6))
[]

E parece perfeitamente bom para mim. Eu estava esperando um resultado que eu pudesse repetir e consegui um.

Mas, obviamente, se você fosse perguntar algo estúpido:

>>> list(itertools.combinations(input, -1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: r must be non-negative

Então, eu diria que, se todos os seus parâmetros forem validados, mas o resultado for um conjunto vazio, retorne um conjunto vazio. Você não é o único a fazê-lo.

Como dito por @ Bakuriu nos comentários, isso também é o mesmo para uma consulta SQL como SELECT <columns> FROM <table> WHERE <conditions> . Contanto que <columns> , <table> , <conditions> sejam bem formados e se refiram a nomes existentes, você pode criar um conjunto de condições que se excluam mutuamente. A consulta resultante apenas renderia nenhuma linha em vez de lançar um InvalidConditionsError .

    
por 30.11.2016 / 14:55
fonte
72

Em termos leigos:

  • Se houver um erro, você deve levantar uma exceção. Isso pode envolver fazer as coisas em etapas, em vez de em uma única chamada encadeada, para saber exatamente onde o erro aconteceu.
  • Se não houver erro, mas o conjunto resultante estiver vazio, não apresente uma exceção, retorne o conjunto vazio. Um conjunto vazio é um conjunto válido.
por 30.11.2016 / 13:32
fonte
53

Concordo com a resposta de Ewan , mas quero adicionar um raciocínio específico.

Você está lidando com operações matemáticas, então 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 (isto é, nCr ) é bem definido para todos os r > n > = 0. É zero. Portanto, retornar um conjunto vazio seria o caso esperado do ponto de vista matemático.

    
por 30.11.2016 / 15:36
fonte
24

Eu acho uma boa maneira de determinar se usar uma exceção, é imaginar pessoas envolvidas na transação.

Como buscar o conteúdo de um arquivo como exemplo:

  1. Por favor, busque o conteúdo do arquivo, "não existe.txt"

    a. "Aqui está o conteúdo: uma coleção vazia de caracteres"

    b. "Hã, há um problema, esse arquivo não existe. Eu não sei o que fazer!"

  2. Por favor, me buscar o conteúdo do arquivo, "existe, mas é empty.txt"

    a. "Aqui está o conteúdo: uma coleção vazia de caracteres"

    b. "Hã, há um problema, não há nada neste arquivo. Eu não sei o que fazer!"

Sem dúvida, alguns irão discordar, mas para a maioria das pessoas, "Erm, há um problema" faz sentido quando o arquivo não existe e retorna "uma coleção vazia de caracteres" quando o arquivo está vazio.

Aplicando a mesma abordagem ao seu exemplo:

  1. Por favor, me dê todas as combinações de 4 itens para {1, 2, 3}

    a. Não há nenhum, 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 fosse oferecido como o conjunto de itens, mas "aqui está um conjunto vazio" parece ser uma resposta sensata à solicitação acima.

Se retornar um valor vazio mascarar um problema (por exemplo, um arquivo ausente, null ), uma exceção geralmente deve ser usada em seu lugar (a menos que o idioma escolhido suporte option/maybe types, às vezes eles fazem mais sentido). Caso contrário, a devolução de um valor vazio provavelmente simplificará o custo e obedecerá melhor ao princípio de menor espanto.

    
por 30.11.2016 / 14:48
fonte
10

Como é para uma biblioteca de propósito geral, meu instinto seria deixar o usuário final escolher.

Assim como temos Parse() e TryParse() disponíveis para nós, podemos ter a opção de usar, dependendo da saída que precisamos da função. Você gastaria menos tempo escrevendo e mantendo um wrapper de função para lançar a exceção do que discutir sobre a escolha de uma única versão da função.

    
por 30.11.2016 / 15:36
fonte
4

Você precisa validar os argumentos fornecidos quando sua função é chamada. E, de fato, você quer saber como lidar com argumentos inválidos. O fato de que vários argumentos dependem um do outro não compensa o fato de você validar os argumentos.

Assim, eu votaria no ArgumentException fornecendo as informações necessárias para o usuário entender o que deu errado.

Como exemplo, verifique o Função public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32) no Linq. Que lança um 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.

    
por 30.11.2016 / 15:04
fonte
2

Eu posso ver argumentos para ambos os casos de uso - uma exceção é ótima se o código downstream espera conjuntos que contenham dados. Por outro lado, simplesmente um conjunto vazio é ótimo se isso for esperado.

Eu acho que depende das expectativas do chamador se isso é um erro ou um resultado aceitável - então eu transferiria a escolha para o chamador. Talvez introduzir uma opção?

.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)

    
por 30.11.2016 / 14:44
fonte
2

Existem duas abordagens para decidir se não há uma 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 "estrito" para indicar se você deseja que os parâmetros sejam estritamente verificados ou não. Por exemplo, o SimpleDateFormat do Java tem um método setLenient para tentar analisar entradas que não correspondem totalmente ao formato. Claro, você teria que decidir qual é o padrão.

por 30.11.2016 / 15:35
fonte
2

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 sempre desejando para usá-lo dessa maneira.

Se você realmente sentir que alguns usuários podem querer forçar retornos não-vazios, forneça a eles uma maneira de perguntar por esse comportamento, em vez de forçá-lo a todos. Por exemplo, você pode:

  • Crie uma opção de configuração em qualquer objeto que esteja realizando a ação para o usuário.
  • Torne um sinalizador que o usuário pode passar opcionalmente para a função.
  • Forneça uma porcentagem deAssertNonempty que eles podem colocar em suas cadeias.
  • Crie duas funções, uma que defina não-vazia e outra não.
por 01.12.2016 / 03:55
fonte
2

Você deve seguir um dos procedimentos a seguir (embora continue a usar problemas básicos, como um número negativo de combinações):

  1. Forneça duas implementações, uma que retorne um conjunto vazio quando as entradas juntas forem sem sentido e uma que seja lançada. Tente chamá-los de CombinationsOf e CombinationsOfWithInputCheck . Ou o que você quiser. Você pode inverter isso para que a verificação de entrada seja o nome mais curto e a lista seja CombinationsOfAllowInconsistentParameters .

  2. Para métodos Linq, retorne o IEnumerable vazio na premissa exata que você descreveu. Em seguida, adicione esses métodos do Linq à sua biblioteca:

    public static class EnumerableExtensions {
       public static IEnumerable<T> ThrowIfEmpty<T>(this IEnumerable<T> input) {
          return input.IfEmpty<T>(() => {
             throw new InvalidOperationException("An enumerable was unexpectedly empty");
          });
       }
    
       public static IEnumerable<T> IfEmpty<T>(
          this IEnumerable<T> input,
          Action callbackIfEmpty
       ) {
          var enumerator = input.GetEnumerator();
          if (!enumerator.MoveNext()) {
             // Safe because if this throws, we'll never run the return statement below
             callbackIfEmpty();
          }
          return EnumeratePrimedEnumerator(enumerator);
       }
    
       private static IEnumerable<T> EnumeratePrimedEnumerator<T>(
          IEnumerator<T> primedEnumerator
       ) {
          yield return primedEnumerator.Current;
          while (primedEnumerator.MoveNext()) {
             yield return primedEnumerator.Current;
          }
       }
    }
    

    Finalmente, use isso assim:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .ThrowIfEmpty()
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    ou assim:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .IfEmpty(() => _log.Warning(
          [email protected]"Unexpectedly received no results when creating combinations for {
             nameof(someInputSet)}"
       ))
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    Por favor, note que o método privado sendo diferente dos públicos é necessário para que o comportamento de lançamento ou ação ocorra quando a cadeia linq é criada, em vez de algum tempo depois, quando é enumerada. Você quer que jogue imediatamente.

    Note, no entanto, que é claro que tem que enumerar pelo menos o primeiro item para determinar se há algum item. Essa é uma possível desvantagem que, na minha opinião, é mitigada pelo fato de que qualquer visualizador futuro pode facilmente argumentar que um método ThrowIfEmpty tem para enumerar pelo menos um item, portanto não < Surpreenda-se com isso. Mas você nunca sabe. Você poderia tornar isso mais explícito em ThrowIfEmptyByEnumeratingAndReEmittingFirstItem . Mas isso parece um exagero gigantesco.

Eu acho que o # 2 é bem, ótimo! 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 terá que lidar com exceções inesperadas.

    
por 01.12.2016 / 21:47
fonte
1

Isso realmente depende do que seus usuários esperam obter. Para um exemplo (um pouco não relacionado) se seu código executa divisão, você pode lançar uma exceção ou retornar Inf ou NaN quando você divide por zero. Nem está certo nem errado, no entanto:

  • se você retornar Inf em uma biblioteca Python, as pessoas o atacarão por ocultar erros
  • se você gerar um erro em uma biblioteca do Matlab, as pessoas o atacarão por não processar dados com valores ausentes

No 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 ser algo com o qual seus usuários esperariam lidar, então retorná-lo parece uma coisa sensata a ser feita. Mas posso estar enganado: você tem uma compreensão muito melhor do contexto do que qualquer outra pessoa aqui, então se você espera que seus usuários confiem que o conjunto está sempre vazio, você 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 de strict deve ser o padrão? "

    
por 01.12.2016 / 11:48
fonte
0

É uma concepção comum (em matemática) que quando você seleciona elementos em um conjunto você não encontra nenhum elemento e, portanto, obtém um conjunto vazio . É claro que você tem que ser consistente com a matemática se você for assim:

Regras comuns:

  • Set.Foreach (predicado); // sempre retorna true para conjuntos vazios
  • Set.Exists (predicado); // sempre retorna false para conjuntos vazios

Sua pergunta é muito sutil:

  • Pode ser que a entrada da sua função tenha de respeitar um contrato : Nesse caso, qualquer entrada inválida deve levantar 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 ser capaz de retornar um conjunto vazio.

Agora, se eu estivesse em você, eu seguiria o caminho "Set", mas com um grande "BUT".

Suponha que você tenha uma coleção que "por hipóteses" deve ter apenas alunas:

class FemaleClass{

    FemaleStudent GetAnyFemale(){
        var femaleSet= mySet.Select( x=> x.IsFemale());
        if(femaleSet.IsEmpty())
            throw new Exception("No female students");
        else
            return femaleSet.Any();
    }
}

Agora, sua coleção não é mais um "conjunto puro", porque você tem um contrato e, portanto, você deve aplicar seu contrato com uma exceção.

Quando você usa suas funções "set" de maneira pura, você não deve lançar exceções no caso de um conjunto vazio, mas se você tiver coleções que não são mais "puros", então você deve lançar exceções onde .

Você sempre deve fazer o que parecer mais natural e consistente: para mim, um conjunto deve aderir às regras definidas, enquanto as coisas que não são conjuntos devem ter regras de pensamento adequadas.

No seu caso, parece uma boa ideia fazer:

List SomeMethod( Set someInputSet){
    var result = someInputSet
        .CombinationsOf(4, CombinationsGenerationMode.Distinct)
        .Select(combo => /* do some operation to a combination */)
        .ToList();

    // the only information here is that set is empty => there are no combinations

    // BEWARE! if 0 here it may be invalid input, but also a empty set
    if(result.Count == 0)  //Add: "&&someInputSet.NotEmpty()"

    // we go a step further, our API require combinations, so
    // this method cannot satisfy the API request, then we throw.
         throw new Exception("you requsted impossible combinations");

    return result;
}

Mas não é realmente uma boa idéia, temos agora um estado inválido que pode ocorrer em tempo de execução em momentos aleatórios, porém isso está implícito no problema, então não podemos removê-lo, com certeza podemos mover a exceção dentro de alguns método utilitário (é exatamente o mesmo código, movido em lugares diferentes), mas isso é errado e basicamente a melhor coisa que você pode fazer está sujeito às regras regulares .

Se você adicionar uma nova complexidade apenas para mostrar que pode escrever métodos de consultas do linq parece não valer para o seu problema , tenho certeza que se o OP puder nos informar mais sobre seu domínio, provavelmente encontraremos o local onde a exceção é realmente necessária (se for possível, o problema não requer nenhuma exceção).

    
por 01.12.2016 / 13:20
fonte