Yield return en C#: cuando el código funciona "perezosamente" y es genial
\n\nImagina que estás escribiendo un programa que debe procesar un millón de líneas de un archivo de registro. Cargas honestamente todo en una lista List<string>, la pasas a un método de filtrado, y luego —¡pum!— el programa se congela, la memoria se fuga y el usuario presiona nerviosamente Ctrl+Alt+Del.
¿Te suena familiar? Si alguna vez te has enfrentado a esta situación, entonces es hora de que conozcas la "magia perezosa" de C#: el operador yield return. No es solo azúcar sintáctico, sino una herramienta poderosa que puede cambiar radicalmente la forma en que trabajas con colecciones.
En este artículo analizaremos el uso correcto de yield return con ejemplos reales, exploraremos su funcionamiento interno y descubriremos cuándo usarlo y cuándo es mejor mantenerse alejado. ¡Allá vamos!
\n\n¿Qué es yield return y cómo funciona?
\n\nSi lo explicamos de forma sencilla, yield return es una forma de decirle al compilador: "Oye, no quiero devolver toda la colección de una vez. Mejor iré entregando los elementos uno por uno, según se necesiten". Este enfoque se llama evaluación perezosa (lazy evaluation).
Así se ve un ejemplo clásico:
\n\npublic IEnumerable<int> GetNumbers()\n{\n yield return 1;\n yield return 2;\n yield return 3;\n}\n\nAl llamar a este método, no obtienes un arreglo [1, 2, 3]. En su lugar, obtienes un iterador que irá entregando los números uno por uno, cuando se los pidas. Cada llamada a MoveNext() avanza la ejecución del método hasta el siguiente yield return.
Esto es como una línea de ensamblaje: los elementos se producen solo cuando el consumidor los necesita. Sin copias innecesarias, sin listas gigantescas en memoria, solo trabajo limpio y eficiente.
\n\n¿Por qué yield return no es simplemente "devolver una lista"?
\n\nMuchos principiantes piensan: "¿Para qué necesito yield return si puedo simplemente crear un List y devolverlo?". Analicemos esto con un ejemplo concreto.
Imagina que necesitas obtener los primeros 10 números pares de una secuencia infinita. Con yield return se hace de manera elegante:
public IEnumerable<int> GetEvenNumbers()\n{\n int i = 0;\n while (true)\n {\n yield return i;\n i += 2;\n }\n}\n\n// Uso\nvar firstTenEven = GetEvenNumbers().Take(10).ToList();\n\nIntenta hacer lo mismo con List<int> — o entrarás en un bucle infinito, o tendrás que almacenar todos los números generados en memoria. Yield return nos da la capacidad de trabajar con secuencias potencialmente infinitas sin saturar la memoria RAM.
Ejemplo práctico: lectura de un archivo grande
\n\nVolvamos a nuestro archivo de registro de un millón de líneas. Así es como suelen escribir los principiantes:
\n\npublic List<string> ReadLogLines(string path)\n{\n var lines = new List<string>();\n using (var reader = new StreamReader(path))\n {\n string line;\n while ((line = reader.ReadLine()) != null)\n {\n lines.Add(line);\n }\n }\n return lines;\n}\n\nEl problema: todo el archivo se carga en memoria. Si el archivo pesa 500 MB, la lista pesará aproximadamente lo mismo. ¿Y si solo necesitas filtrar las líneas con errores? Igual cargarás todo.
\n\nAquí está el uso correcto de yield return:
public IEnumerable<string> ReadLogLines(string path)\n{\n using (var reader = new StreamReader(path))\n {\n string line;\n while ((line = reader.ReadLine()) != null)\n {\n yield return line;\n }\n }\n}\n\n// Filtrado sin cargar todo el archivo\nvar errorLines = ReadLogLines("log.txt")\n .Where(line => line.Contains("ERROR"))\n .Take(100);