Eu tenho um método de extensão de string c # que deve retornar um IEnumerable<int>
de todos os índices de uma substring dentro de uma string. Funciona perfeitamente para a finalidade pretendida e os resultados esperados são retornados (como comprovado por um dos meus testes, embora não o abaixo), mas outro teste de unidade descobriu um problema: ele não pode lidar com argumentos nulos.
Aqui está o método de extensão que estou testando:
public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
if (searchText == null)
{
throw new ArgumentNullException("searchText");
}
for (int index = 0; ; index += searchText.Length)
{
index = str.IndexOf(searchText, index);
if (index == -1)
break;
yield return index;
}
}
Aqui está o teste que sinalizou o problema:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Quando o teste é executado no meu método de extensão, ele falha, com a mensagem de erro padrão de que o método "não lançou uma exceção".
Isso é confuso: eu claramente passei null
para a função, mas por algum motivo a comparação null == null
está retornando false
. Portanto, nenhuma exceção é lançada e o código continua.
Confirmei que este não é um erro no teste: ao executar o método no meu projeto principal com uma chamada Console.WriteLine
no if
bloco de comparação nula , nada é mostrado no console e nenhuma exceção é capturada por nenhum catch
bloco adicionado. Além disso, usar em string.IsNullOrEmpty
vez de == null
tem o mesmo problema.
Por que essa comparação supostamente simples falha?
fonte
Respostas:
Você está usando
yield return
. Ao fazer isso, o compilador reescreverá seu método em uma função que retorna uma classe gerada que implementa uma máquina de estado.Em termos gerais, ele reescreve os locais nos campos dessa classe e cada parte do seu algoritmo entre as
yield return
instruções se torna um estado. Você pode verificar com um descompilador o que esse método se torna após a compilação (desative a descompilação inteligente que produziriayield return
).Mas a linha inferior é: o código do seu método não será executado até que você comece a iterar.
A maneira usual de verificar pré-condições é dividir seu método em dois:
Isso funciona porque o primeiro método se comportará exatamente como você espera (execução imediata) e retornará a máquina de estado implementada pelo segundo método.
Observe que você também deve verificar o
str
parâmetronull
, pois os métodos de extensões podem ser chamados emnull
valores, pois são apenas açúcar sintático.Se você estiver curioso sobre o que o compilador faz com o seu código, aqui está o seu método, descompilado com dotPeek usando a opção Mostrar código gerado pelo compilador .
Este é um código C # inválido, porque o compilador pode fazer coisas que a linguagem não permite, mas que são legais na IL - por exemplo, nomear as variáveis de uma maneira que você não pode evitar colisões de nomes.
Mas como você pode ver, o
AllIndexesOf
único constrói e retorna um objeto, cujo construtor inicializa apenas algum estado.GetEnumerator
apenas copia o objeto. O trabalho real é feito quando você começa a enumerar (chamando oMoveNext
método).fonte
str
parâmetronull
, pois os métodos de extensões podem ser chamados emnull
valores, pois são apenas açúcar sintático.yield return
é uma boa ideia em princípio, mas tem muitas dicas estranhas. Obrigado por trazer este à luz!MoveNext
é chamado sob o capô pelaforeach
construção. Eu escrevi uma explicação do queforeach
faz na minha resposta explicando a semântica da coleção, se você quiser ver o padrão exato.Você tem um bloco iterador. Nenhum código nesse método é executado fora das chamadas
MoveNext
no iterador retornado. A chamada do método não é importante, mas cria a máquina de estado, e isso nunca falha (fora de extremos, como erros de falta de memória, estouros de pilha ou exceções de interrupção de encadeamento).Quando você realmente tenta repetir a sequência, obtém as exceções.
É por isso que os métodos LINQ realmente precisam de dois métodos para obter a semântica de manipulação de erros que desejam. Eles têm um método privado que é um bloco iterador e, em seguida, um método de bloco não iterador que não faz nada além da validação do argumento (para que possa ser feito com entusiasmo, em vez de ser adiado), enquanto adia todas as outras funcionalidades.
Portanto, este é o padrão geral:
fonte
Os enumeradores, como os outros disseram, não são avaliados até o momento em que começam a ser enumerados (ou seja, o
IEnumerable.GetNext
método é chamado). Assim, estenão é avaliado até você começar a enumerar, ou seja,
fonte