son que propiedades personalizar personalizadas personalizada mensaje manejo las generate excepción excepciones excepcion c# null comparison ienumerable argumentnullexception

c# - que - ¿Por qué este método de extensión de cadena no produce una excepción?



propiedades de las excepciones en c# (3)

Tengo un método de extensión de cadena C # que debería devolver un IEnumerable<int> de todos los índices de una subcadena dentro de una cadena. Funciona perfectamente para su 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 marcó 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 contra 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á devolviendo false . Por lo tanto, no se produce 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 bloque if comparación nula, no se muestra nada en la consola y ningún bloque catch detecta ninguna excepción Añado. Además, usar string.IsNullOrEmpty lugar de == null tiene el mismo problema.

¿Por qué falla esta comparación supuestamente simple?


Estás utilizando el yield return . Al hacerlo, el compilador reescribirá su método en una función que devuelva una clase generada que implemente una máquina de estados.

En términos generales, reescribe los locales en los campos de esa clase y cada parte de su algoritmo entre las instrucciones de yield return 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ía un yield 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 verificar las condiciones previas es dividir su método en dos:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText) { if (str == null) throw new ArgumentNullException("str"); if (searchText == null) throw new ArgumentNullException("searchText"); return AllIndexesOfCore(str, searchText); } private static IEnumerable<int> AllIndexesOfCore(string str, string searchText) { for (int index = 0; ; index += searchText.Length) { index = str.IndexOf(searchText, index); if (index == -1) break; yield return index; } }

Esto funciona porque el primer método se comportará como 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 parámetro str para null , porque los métodos de extensiones se pueden invocar en valores null , ya que son solo azúcar sintáctico.

Si tiene curiosidad acerca de lo que el compilador le hace a su código, este es su método, descompilado con dotPeek usando la opción Mostrar código generado por el compilador .

public static IEnumerable<int> AllIndexesOf(this string str, string searchText) { Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2); allIndexesOfD0.<>3__str = str; allIndexesOfD0.<>3__searchText = searchText; return (IEnumerable<int>) allIndexesOfD0; } [CompilerGenerated] private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable { private int <>2__current; private int <>1__state; private int <>l__initialThreadId; public string str; public string <>3__str; public string searchText; public string <>3__searchText; public int <index>5__1; int IEnumerator<int>.Current { [DebuggerHidden] get { return this.<>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return (object) this.<>2__current; } } [DebuggerHidden] public <AllIndexesOf>d__0(int <>1__state) { base..ctor(); this.<>1__state = param0; this.<>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator() { Test.<AllIndexesOf>d__0 allIndexesOfD0; if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2) { this.<>1__state = 0; allIndexesOfD0 = this; } else allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0); allIndexesOfD0.str = this.<>3__str; allIndexesOfD0.searchText = this.<>3__searchText; return (IEnumerator<int>) allIndexesOfD0; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); } bool IEnumerator.MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; if (this.searchText == null) throw new ArgumentNullException("searchText"); this.<index>5__1 = 0; break; case 1: this.<>1__state = -1; this.<index>5__1 += this.searchText.Length; break; default: return false; } this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1); if (this.<index>5__1 != -1) { this.<>2__current = this.<index>5__1; this.<>1__state = 1; return true; } goto default; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } void IDisposable.Dispose() { } }

Este es un código C # no vá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, AllIndexesOf solo construye y devuelve un objeto, cuyo constructor solo inicializa algún estado. GetEnumerator solo copia el objeto. El verdadero trabajo se realiza cuando comienza a enumerar (llamando al método MoveNext ).


Los enumeradores, como han dicho los demás, no se evalúan hasta el momento en que comienzan a enumerarse (es decir, se llama al método IEnumerable.GetNext ). Así esto

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

no se evalúa hasta que comienza a enumerar, es decir

foreach(int index in indexes) { // ArgumentNullException }


Tienes un bloque iterador. Ninguno de los códigos en 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 memoria insuficiente, desbordamientos de pila o excepciones de cancelación 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 nada más que validar el argumento (para que pueda hacerse con entusiasmo, en lugar de diferirse) mientras difieren todas las demás funcionalidades.

Entonces este es el patrón general:

public static IEnumerable<T> Foo<T>( this IEnumerable<T> souce, Func<T, bool> anotherArgument) { //note, not an iterator block if(anotherArgument == null) { //TODO make a fuss } return FooImpl(source, anotherArgument); } private static IEnumerable<T> FooImpl<T>( IEnumerable<T> souce, Func<T, bool> anotherArgument) { //TODO actual implementation as an iterator block yield break; }