Tengo un método de extensión de cadena C # que debería devolver uno IEnumerable<int>
de todos los índices de una subcadena dentro de una cadena. Funciona perfectamente para el propósito previsto y se devuelven los resultados esperados (como lo demuestra una de mis pruebas, aunque no la siguiente), pero otra prueba unitaria ha descubierto un problema con ella: no puede manejar argumentos nulos.
Aquí está el método de extensión que estoy probando:
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;
}
}
Aquí está la prueba que señaló el problema:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Cuando la prueba se ejecuta en mi método de extensión, falla, con el mensaje de error estándar de que el método "no arrojó una excepción".
Esto es confuso: claramente he pasado null
a la función, pero por alguna razón la comparación null == null
está regresando false
. Por lo tanto, no se lanza ninguna excepción y el código continúa.
He confirmado que esto no es un error con la prueba: cuando ejecuto el método en mi proyecto principal con una llamada a Console.WriteLine
en el if
bloque de comparación nula , no se muestra nada en la consola y ningún catch
bloque que agregue no detecta ninguna excepción . Además, usar en string.IsNullOrEmpty
lugar de == null
tiene el mismo problema.
¿Por qué falla esta comparación supuestamente simple?
fuente
Respuestas:
Estás usando
yield return
. Al hacerlo, el compilador reescribirá su método en una función que devuelve una clase generada que implementa una máquina de estado.En términos generales, reescribe locales en campos de esa clase y cada parte de su algoritmo entre las
yield return
instrucciones se convierte en un estado. Puede verificar con un descompilador en qué se convierte este método después de la compilación (asegúrese de desactivar la descompilación inteligente que produciríayield return
).Pero la conclusión es: el código de su método no se ejecutará hasta que comience a iterar.
La forma habitual de comprobar las condiciones previas es dividir su método en dos:
Esto funciona porque el primer método se comportará tal como lo espera (ejecución inmediata) y devolverá la máquina de estado implementada por el segundo método.
Tenga en cuenta que también debe verificar el
str
parámetronull
, porque los métodos de extensión se pueden llamar ennull
valores, ya que son simplemente azúcar sintáctico.Si tiene curiosidad acerca de lo que hace el compilador con su código, aquí está su método, descompilado con dotPeek usando la opción Mostrar código generado por el compilador .
Este es un código C # inválido, porque el compilador puede hacer cosas que el lenguaje no permite, pero que son legales en IL, por ejemplo, nombrar las variables de una manera que no podría evitar las colisiones de nombres.
Pero como puede ver, el
AllIndexesOf
único construye y devuelve un objeto, cuyo constructor solo inicializa algún estado.GetEnumerator
solo copia el objeto. El trabajo real se realiza cuando comienzas a enumerar (llamando alMoveNext
método).fuente
str
parámetronull
, porque los métodos de extensión se pueden llamar ennull
valores, ya que son solo azúcar sintáctico.yield return
es una buena idea en principio, pero tiene muchas trampas extrañas. ¡Gracias por sacar este a la luz!MoveNext
es llamado bajo el capó por laforeach
construcción. Escribí una explicación de lo queforeach
hace en mi respuesta explicando la semántica de la colección si desea ver el patrón exacto.Tienes un bloque de iterador. Ninguno de los códigos de ese método se ejecuta fuera de las llamadas a
MoveNext
en el iterador devuelto. Llamar al método no hace notar, pero crea la máquina de estado, y eso nunca fallará (fuera de los extremos, como errores de falta de memoria, desbordamientos de pila o excepciones de aborto de subprocesos).Cuando realmente intente iterar la secuencia, obtendrá las excepciones.
Es por eso que los métodos LINQ realmente necesitan dos métodos para tener la semántica de manejo de errores que desean. Tienen un método privado que es un bloque iterador, y luego un método de bloque no iterador que no hace más que validar el argumento (para que se pueda hacer con entusiasmo, en lugar de diferirlo) y al mismo tiempo diferir todas las demás funciones.
Entonces este es el patrón general:
fuente
Los enumeradores, como han dicho los demás, no se evalúan hasta el momento en que comienzan a ser enumerados (es decir
IEnumerable.GetNext
, se llama al método). Así que esteno se evalúa hasta que comienza a enumerar, es decir
fuente