Professional Documents
Culture Documents
<<
<<
En este trabajo se ilustra la utilidad de los enumeradores y del ciclo foreach en C#. Se analizan cules son las limitaciones de estos enumeradores que justifican la importancia de la nueva inclusin de iteradores en el venidero C# 2.0. Se ejemplifican las ventajas de estos iteradores en C# 2.0 para escribir cdigo ms elegante, legible y menos propenso a errores. Finalmente se proporciona una implementacin de iteradores por medio de hebras que, adems de ilustrar la utilizacin de las hebras, ofrece una forma concreta de usar iteradores en el actual C#.
El mtodo Reset permite que haciendo c.Reset() se deje disponible a c para una nueva iteracin. En este sentido una instancia de C hace las veces de coleccin porque se pueden recorrer los elementos de C siguiendo el patrn anterior.
Colecciones Virtuales
La coleccin de elementos a recorrer a travs del enumerador puede ser virtual, es decir
25
<<dotNetMana
<< dnm.plataforma
los elementos que se procesan como resultado de la iteracin no tienen que estar fsicamente contenidos en una estructura contenedor (sea un array, una lista, etc). Por ejemplo con la siguiente clase se permitira recorrer los enteros pares de un intervalo.
class EvenEnumerator: IEnumerator { bool moveOK; int currentValue, lower, upper; public EvenEnumerator(int lowerBound, int upperBound) { if (lowerBound > upperBound) throw new ArgumentException( Wrong interval); if (lowerBound % 2 != 0) lower = lowerBound-1; else lower = lowerBound 2; upper = upperBound; Reset(); } public bool MoveNext() { if (currentValue > upper) return false; currentValue = currentValue+2; moveOK = currentValue<=upper; return moveOK; } public object Current{ get { if (moveOK) return currentValue; else throw new InvalidOperationException( There is no current element); } } public void Reset() { moveOK = false; currentValue = lower; } }
El ciclo foreach
Asociado al patrn enumerador C# ofrece un recurso importante: el ciclo foreach. Si una clase Items implementa la interface IEnumerable
interface IEnumerable{ IEnumerator GetEnumerator(); } class Items: IEnumerable{ }
pues nos protegemos de errores en la inicializacin de la variable de control del ciclo for (derecha), la pregunta de control y el incremento de la variable. El azcar sintctico que nos ofrece este ciclo foreach aumenta la productividad, previene cometer errores por un uso inadecuado de los patrones tradicionales y favorece la escritura de un cdigo ms elegante. Sin embargo, desafortunadamente C# no introduce una notacin similar para ser utilizada en expresiones lgicas (de tipo bool). Podra ser muy til disponer de dos operadores lgicos como:
IEnumerator e = list.GetEnumerator(); while(e.MoveNext()) { object x = e.Current; ... Process x; } Tabla1. Cdigo generado para el ciclo foreach
forall (object x in list): <expresin bool que usa a x> exist (object x in list): <expresin bool que usa a x>
T[] list = new T[10]; for (int i=0; i<list.Length; i++) { ... Process list[i] }
T[] list = new T[10]; for (int i=0; i<list.Length; i++) { ... Process list[i] } Tabla2. Ciclos foreach y for sobre un array
<<dotNetMana
Para apreciar mejor la utilidad del recurso de iteracin que se propondr ms adelante es importante comprender este concepto de que los elementos que se recorren en una iteracin no tienen que haber sido previamente almacenados fsicamente en alguna parte.
compilador lo expandir en un cdigo equivalente al cdigo a la derecha. Los arrays (la familiar construccin primitiva para almacenar colecciones de elementos) pueden ser tratados tambin con un ciclo foreach (Tabla 2 cdigo a la izquierda) en lugar del tradicional ciclo for (Tabla 2 cdigo a la derecha) De este modo si no nos interesase la posicin de los elementos dentro del array es ms cmodo y seguro recorrerlos con el ciclo foreach (izquierda)
Tales recursos, unidos a las capacidades de atributos, reflection y CodeDom podra aumentar las capacidades de C# como lenguaje no slo de implementacin sino tambin de especificacin1.
1 La inclusin de un recurso de este estilo ser tratado por los autores en un prximo trabajo para incluir aserciones lgicas en C#.
26
<< dnm.plataforma
un array o una lista, o cuando se recorre una coleccin virtual simple como la del caso de intervalo de nmeros pares ilustrado anteriormente. En cualquier caso la implementacin del par MoveNext-Current dentro del enumerador debe mantener el estado interno del recorrido entre una llamada a MoveNext y otra, de manera de poder retomar este estado en cada nueva llamada. Mantener tal estado no es nada simple cuando se quiere hacer un recorrido recursivo complicado sobre una estructura como un rbol o para una coleccin virtual que produce sus elementos por algn algoritmo recursivo (por ejemplo los movimientos de los discos del juego de las Torres de Hanoi, lo que se ver en el ejemplo de la seccin final). Considere a continuacin una versin simplificada de una clase Tree
class Tree { object nodeValue; public ArrayList Nodes { get{} } }
nos devuelva los nodos del rbol en preorden. Una solucin simple podra ser:
class Tree { object nodeValue; public ArrayList Nodes { get{} } public IEnumerable NodesInPreOrder { get { ArrayList nodes = new ArrayList(); ListInPreOrder(this, nodes); return nodes; } } private void ListInPreOrder(Tree t, ArrayList nodes) { nodes.Add(t.nodeValue); foreach (Tree t1 in t.Nodes) ListInPreOrder(t1, nodes); } }
Si se quisieran listar en preorden los elementos de un rbol se podra incluir dentro de la clase un mtodo ListInPreOrder.
class Tree { object nodeValue; public ArrayList Nodes { get{} } public void ListInPreOrder() { ListInPreOrder(this); } private void ListInPreOrder(Tree t) { Console.WriteLine( t.nodeValue.ToString()); foreach (Tree t1 in t.Nodes) ListInPreOrder(t1); } }
Sin embargo, esta solucin tiene el inconveniente que obliga a colocar todos los nodos en un contenedor fsico (ArrayList en este caso) para luego recorrer dicho contenedor. Esto no es una solucin muy eficiente, ms an si es posible que el cdigo cliente (como se muestra a continuacin) puede decidir abortar el recorrido porque en este caso de todos modos el verdadero recorrido de los nodos en el rbol ya se habra realizado.
Tree t = new Tree(); ... foreach (object x in t.NodesInPreOrder) { if (some condition on x) break; else ...Process x ... }
Como se puede observar este recorrido usando la recursin es muy simple pero fuerza a que el procesamiento a realizar con los elementos (listarlos en este caso) est encapsulado dentro de la clase Tree. Para dar ms facilidad a que sea el cdigo cliente el que decida qu quiere hacer con los elementos que recorre en un determinado orden, sera deseable tener dentro de la clase Tree un enumerador que
Otra solucin sera programar directamente el enumerador, pero en este caso habra que implementar el mantener el estado interno del recorrido recursivo. Una implementacin para este caso se muestra a continuacin. Note la complicacin del cdigo porque para conservar el estado de la iteracin en preorden, entre una llamada a MoveNext y otra, ha sido necesario utilizar una pila como estructura auxiliar.
27
<<dotNetMana
<< dnm.plataforma
get{} } public IEnumerable NodesInPreOrder { get { return new PreOrderEnumerable(this);} } //Inner class class PreOrderEnumerable:IEnumerable { Tree t; public PreOrderEnumerable(Tree t){this.t=t;} public IEnumerator GetEnumerator() { return new PreOrderEnumerator(t); } class PreOrderEnumerator: IEnumerator { Tree t; Stack s; object current; bool moveOK; public PreOrderEnumerator(Tree t) { if (t==null) throw new InvalidArgumentException( Null parameter); this.t=t; s = new Stack(); Reset(); } public bool MoveNext() { if (s.Empty) return false; else { Tree t = s.Pop(); current = t.nodeValue; moveOK = true; if (t.Nodes!=null) for (int k = t.Nodes.Count-1; k>=0, k) s.Push(t.Nodes[k]); return moveOK; } } public object Current { if (moveOK) return current; else throw new InvalidOperationException( Enumeration is out of limits); } public void Reset() { s.Clear(); s.Push(t); } } } }
Inspirados en lenguajes como CLU, Sather y otros, el venidero C# 2.0 [1] incluir un recurso muy til de programacin: iteradores
ya en su definicin la utilizacin de una operacin especial yield return expresin. Este operador yield actuar como un return pero con la diferencia de que al volver a invocarse al mtodo, como consecuencia de un nuevo paso en la iteracin del ciclo foreach, la ejecucin del mtodo empezar a continuacin del ltimo yield return. En este sentido el mtodo que incluye el yield actuar como corutina de quien lo llam. De este modo la definicin de un iterador para recorrer el rbol en preorden puede ser
class Tree: IEnumerable { object nodeValue; public ArrayList Nodes { get{...} } ... public IEnumerator GetEnumerator()
<<dotNetMana
2 Tambin habamos propuesto una idea similar para incluir iteradores en el lenguaje Eiffel (ver [2], [3])
28
<< dnm.plataforma
{ yield nodeValue; foreach (Tree t in Nodes) foreach (object x in t) yield return x; } }
y
foreach (object x in t.PostOrder)
public IEnumerable PostOrder { get { return new PostOrderEnumerable(this); } } class PostOrderEnumerable:IEnumerable { Tree t; public new PostOrderEnumerable(Tree t) { this.t=t; } public IEnumerator GetEnumerator() { return new PostOrderEnumerator(t); } } class PostOrderEnumerator:IEnumerator { /* ...latosa implementacin de los mtodos MoveNext y Current */ } }
Note el foreach (object x in t) yield return x ; anidado dentro del foreach ms externo, aqu estamos usando recursivamente el propio concepto de iterador ahora aplicado a cada uno de los hijos del rbol original.
Si queremos que se pueda hacer un recorrido foreach (object x in t) directamente sobre la propia variable rbol, supongamos se asume en este caso el recorrido en preorden como recorrido predeterminado (by default), se puede definir la clase Tree del modo siguiente (note que incluso mantenemos el recorrido con nombre a travs de la propiedad PreOrder)
class Tree: IEnumerable { object nodeValue; public ArrayList Nodes { get{} } public IEnumerator GetEnumerator() { yield return nodeValue; foreach (Tree t in Nodes) foreach (object x in t) yield return x; } public IEnumerable PreOrder { get { return this; } } public IEnumerable PostOrder { get { foreach (Tree t in Nodes) foreach (object x in t) yield x; yield nodeValue; } } }
29
<<dotNetMana
Note la implementacin de la propiedad PostOrder en el ejemplo anterior de Tree, en el get hemos incluido directamente el cdigo del iterador. Con esto nos evitamos tener que escribir el par de clases internas PostOrderEnumerable y PostOrderEnumerator como se muestra en el extracto de cdigo a continuacin:
El mtodo que se asocie a este objeto delegate ser el que encierre el verdadero proceso de iteracin (que puede incluir la complejidad de una recursin). Cuando producto de aplicar el patrn que se muestra en la Tabla 1, se hace el primer MoveNext, ste echar a andar una hebra asociada al delegate anterior. Esta hebra sincronizar con la hebra que la cre (aquella del cdigo cliente que ha desencadenado el ciclo foreach) mediante un objeto especial de tipo IYield que se usar dentro del cuerpo del mtodo del delegate.
<< dnm.plataforma
public interface IYield { void Yield(object result); } Hacer y.Yield(x) significa poner a x como valor del Current del enumera-
dor y detener la hebra en ese punto hasta que por demanda del cdigo cliente a travs del foreach se vuelva a invocar a un prximo MoveNext. El cdigo fuente 1 ilustra cmo incluir en una clase Tree un iterador en preorden. La instruccin en la lnea 12
de la hebra hasta que se vuelva a invocar a un prximo MoveNext (ver ms adelante el cdigo fuente 2 con la implementacin de la clase Iterator) producto de una nueva iteracin del cdigo cliente (instruccin lnea 30). En el cdigo fuente 2 se muestra la implementacin de la clase Iterator, note que el mtodo MoveNext (lnea 34) es quien desencadena la hebra cuando es llamado por primera vez al aplicar el cdigo cliente el patrn de la tabla 2.
1. class Tree 2. { 3. object nodeValue; 4. ... 5. public ArrayList Nodes 6. { 7. get{...} 8. } 9. public IEnumerable PreOrder 10. { 11. get 12. {return new Iterator(new IteratorMethod(PreOrderMethod)); 13. } 14. } 15. void PreOrderMethod(IYield y) 16. { 17. PreOrderMethod(y, this); 18. } 19. void PreOrderMethod(IYield y, Tree t) 20. { 21. y.Yield(t.nodeValue); 22. foreach (Tree t1 in t.Nodes) PreOrderMethod(y, t1); 23. } 24. } 25. class TreeTest 26. { 27. public static void Main() 28. { 29. Tree t = ...; 30. foreach (object x in t.PreOrder) 31. Console.WriteLine(x); 32. } 33. }
El mtodo runMethod de la hebra es quien llama al delegate con la instruccin it(this) (lnea 30) que es quien realmente hace la iteracin y usa al propio objeto this (que es de tipo IYield ) para ejecutar las acciones Yield. El mtodo MoveNext queda en espera de la seal calculatedValue (lnea 47) que es enviada por el mtodo Yield cuando se ha calculado un nuevo valor (lnea 67) o por el propio mtodo runMethod cuando se ha terminado la iteracin (lnea 32) y no hay ms objetos que calcular para la iteracin. El mtodo Reset (lnea 72) manda a abortar la hebra. Esto ocurrira en el caso en que el cdigo cliente haga un Reset an cuando no se haya llegado al final de la iteracin. Las hebras son recursos limitados de modo que hay que garantizar que si el cdigo cliente aborta un ciclo foreach (por ejemplo con una instruccin break o return) la hebra de la iteracin no quede viva innecesariamente, por esta razn la clase CoroutineEnumerator se ha definido como IDisposable . Note que en la definicin de Dispose tambin se manda a abortar la hebra (lnea 77).
<<dotNetMana
asocia al iterador el delegate formado por el mtodo de la lnea 15 (que a su vez llama al mtodo de la lnea 19). Note la instruccin y.Yield(t.nodeValue); de la lnea 21, el lector debe imaginar como si aqu se detuviera la ejecucin
30
<< dnm.plataforma
1.public class Iterator: IEnumerable 2.{ 3. IteratorMethod it; 4. public Iterator(IteratorMethod it) 5. { 6. if (it == null) 7. throw new ArgumentNullException(); 8. this.it = it; 9. } 10. public IEnumerator GetEnumerator() 11. { 12. return new CoroutineEnumerator(it); 13. } 14. //Inner class 15. class CoroutineEnumerator:IEnumerator, IDisposable,IYield 16. { 17. IteratorMethod it; 18. Thread itThread; 19. object current; 20. bool finish; 21. object proceedIteration = new object(); 22. object calculatedValue = new object(); 23. public CoroutineEnumerator(IteratorMethod iterator) 24. { 25. this.iterator = iterator; 26. } 27. void runMethod() { 28. finish = false; 29. //Initialize a thread with the handler 30. it(this); 31. finish = true; 32. lock(calculatedValue) {Monitor.Pulse(calculatedValue);} 33. } 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. } public object Current { get { if (finish) throw new InvalidOperationException( "Invalid Current. Out of the collection"); else return current; } }
60. void IYield.Yield(object result) 61. { 62. lock (proceedIteration) 63. { 64. lock (calculatedValue) 65. { 66. current = result; 67. Monitor.Pulse(calculatedValue); 68. } 69. Monitor.Wait(proceedIteration); 70. } 71. } 72. public void Reset() 73. { 74. try 75. { 76. if (itThread.IsAlive) 77. itThread.Abort(); 78. } 79. catch(ThreadAbortException){}; 80. itThread = null; 81. finish = true; 82. } 83. ~CoroutineEnumerator() 84. { 85. Dispose(); 86. }
31
<<dotNetMana
34. public bool MoveNext() 87. public void Dispose() 35. { 88. { 36. lock(calculatedValue) 89. if (itThread != null) 37. { 90. { 38. if (itThread == null) 91. try 39. { { 40. itThread = new Thread(new ThreadStart(runMethod )); 92. 93. itThread.Abort(); 41. itThread.Start(); 94. } 42. } 95. catch(ThreadAbortException){}; 43. lock (proceedIteration) 96. itThread = null; 44. { 97. } 45. Monitor.Pulse(proceedIteration); 98. } 46. } 99. } 47. Monitor.Wait(calculatedValue); 100. } 48. } 101.} 49. return !finish;
<< dnm.plataforma
Un iterador para los movimientos de las Torres de Hanoi
El juego de Las Torres de Hanoi consiste en disponer de tres torres (como se ilustra en la Figura 1). En una de las torres hay un conjunto de discos de mayor a menor. Se quieren pasar todos los discos desde una torre origen hacia una torre destino, pudiendo usarse la tercera torre como auxiliar, pero con la restriccin de que slo puede moverse un disco por vez y que nunca se puede poner un disco mayor sobre uno menor. Como se muestra a continuacin un mtodo para listar todos los movimientos es muy simple usando recursin
void Hanoi(int disks, string source, string target, string aux) { if (disks == 1) Console.WriteLine( Move from + source + to + target); else { Hanoi(disks-1,source, aux, target); Console.WriteLine(Move from + source + a + target); Hanoi(disks-1,aux, target, source); } } new IteratorMethod(HanoiMethod));} } void HanoiMethod(IYield y) { HanoiMethod(y, disks, source, target, aux); } void HanoiMethod(IYield y, int disks, string source, string target, string aux) { if (disks==1) y.Yield(Move from + source + to + target); else { HanoiMethod(y, disks-1,source, aux, target); y.Yield(Move from + source + to + target); HanoiMethod(y, disks-1,aux, target, source); } } }
los movimientos. Note que tendra prcticamente que programar toda la maquinaria que el proceso recursivo anterior esconde. La solucin utilizando el tipo Iterator es muy simple:
class Hanoi { int disks; string target, source, aux; public Hanoi(int disks, string source, string target, string aux) { this.disks=disks; this.source=source; this.target=target; this.aux=aux; }
<<dotNetMana
No hay ningn problema en anidar el uso de un iterador dentro de otro, el siguiente cdigo listara lo que se muestra en la figura 3.
32
<< dnm.plataforma
llado en este trabajo, deba ser la solucin general de implementacin para tener iteradores ya que las hebras son un recurso caro y limitado. Las descripciones sobre cmo ser implementado este recurso de iteradores por el compilador de C# 2.0 de VS C# 2005 (ver [1] y [4]) indican que el nuevo compilador de Microsoft para C# 2.0 generar clases anidadas que encapsularn la mquina de estado de la iteracin, lo cual es realmente la solucin adecuada pero que slo puede hacerse sobre la base de desarrollar un nuevo compilador. Sin embargo, la implementacin mediante hebras que hemos mostrado en este artculo, adems de ilustrar la utilizacin de las hebras, permite que los programadores que no dispongan an de C# 2.0 puedan mientras tanto irse habituando al til nivel de expresividad que los iteradores proporcionan, evitando el trabajo y los errores que la complejidad de la programacin manual de la maquinaria recursiva de una iteracin puede ocasionar. Esta implementacin con hebras trabaja de modo sncrono pero pudiera desarrollarse una solucin asncrona. Es decir que, si el cdigo cliente lo desea, entonces el iterador pueda ir calculando asncronamente el prximo elemento mientras el cliente procesa el anterior lo cual puede ser una alternativa interesante para aplicaciones distribuidas.
HanoiIterator hanoi1 = new HanoiIterator(2, A, B,C); HanoiIterator hanoi2 = new HanoiIterator(2, Left, Rigth, Center); foreach (string s1 in hanoi1) { Console.WriteLine(s1); foreach (string s2 in hanoi2) Console.WriteLine(- + s2); }
significa que el compilador estticamente garantizar que el objeto que devuelva un yield return tenga que ser de tipo T y que los elementos sobre los que se itere en el foreach sean de tipo T. Lo cual redunda en mayor legibilidad, ms robustez y ms eficiencia al disminuir la necesidad de operaciones
Conclusiones
La uniformidad que propone .NET en el uso de enumeradores es importante para el tratamiento de colecciones y es usado ampliamente en sus propias bibliotecas. El aporte del ciclo foreach es una comodidad sintctica considerable y menos propensa a errores. La anunciada inclusin de iteradores para C#2.0 ser muy bienvenida porque aumentar la expresividad y elegancia del cdigo. Sera deseable que se considerase la inclusin de operadores lgicos forall y exists asociados a este concepto de iteradores ya que esto redundara en beneficio de las capacidades de abstraccin y diseo utilizando C#. En C# 2.0 esta capacidad de iteradores se potencia al mximo cuando sea utilizada en combinacin con la genericidad, nuevo esperado recurso que tambin est incluido en C# 2.0. Con genericidad un iterador podr devolver un objeto de tipo IEnumerator<T> lo que
La anunciada inclusin de iteradores para C#2.0 ser muy bienvenida porque aumentar la expresividad y elegancia del cdigo. Sera deseable que se considerase la inclusin de operadores lgicos forall y exists asociados a este concepto de iteradores...
de casting en tiempo de ejecucin. La genericidad es tambin un tema muy importante a tratar por futuros artculos de dotNetMana pero que por razones de espacio no ha sido abordado ahora aqu. No creemos que una solucin usando hebras, como la que hemos desarro-
Referencias
[1] The C# 2.0 Specification , http://download.microsoft.com/download/8/1/6/81682478-4018-48fe-9e5ef87a44af3db9/SpecificationVer2.doc [2] Katrib M, Martnez I, Collections and Iterators in Eiffel, Journal of Object Oriented Programming, Vol 6, No 7, Nov/Dec 1993. [3] Coira J, Katrib M, Improving Eiffel assertions using quantified iterators, Journal of Object Oriented Programming, Vol 10, No 7, Nov/Dec 1997. [4] Lowy Juval, Create Elegant Code with Anonymous Methods, Iterators, and Partial Classes, MSDN Magazine, May 2004.
33
<<dotNetMana