IEnumerator, IEnumerable y otras hierbas

¡Hola!
El otro día en el trabajo salió el tema de cuál es la diferencia entre IEnumerable e IList (entre otros) y, creyendo que tenía clara la respuesta, me di cuenta de que no tanto. Sí sabía que, por ejemplo, IList implementa IEnumerable, y que en un IEnumerable no se puede acceder por índice a un elemento, sino que debemos recorrerlo. Es un momento perfecto para escribir un post y aclarar mis ideas. En concreto vamos a ver las interfaces IEnumerator, IEnumerable, ICollection e IList, empecemos (espero no extenderme demasiado).

IEnumerator/IEnumerable

He decido explicar los dos conjuntamente porque me parece que va a quedar más claro. Esta dupla forma lo que se conoce como el patrón Iterador. Siendo realistas, rara vez va a ser necesario implementar alguna de estas dos interfaces, pero no quita de conocer cuál es la diferencia y cómo los utilizas constantemente de forma más o menos inconsciente. Parece que de forma casi instintiva asociamos estas interfaces con arrays de enteros, o listas de booleanos, por ejemplo; por eso voy a dejar de lado su versión genérica. Estas interfaces van más allá, de lo que se trata es de construir una clase que pueda enumerar cosas, ni más ni menos. Vamos a echar un vistazo a estas dos interfaces:

1
2
3
4
5
6
7
8
9
10
    public interface IEnumerator
    {
        object Current { get; }
        bool MoveNext();        
        void Reset();
    }
    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }

Como vemos, IEnumerator tiene dos métodos y una propiedad. Se utiliza para leer los datos de nuestra colección. De nuevo, insisto, no pienses que nuestra colección de datos sólo puede ser una lista o un array de cosas. Con la propiedad Current, accedemos al elemento actual disponible en nuestro IEnumerator, y con MoveNext se avanza al siguiente elemento de nuestra colección. Como puedes suponer, el método Reset devuelve nuestro IEnumerator a la posición inicial.
La interfaz IEnumerable simplemente se encarga de exponer nuestro IEnumerator. Esto nos ayuda a mover la lógica de cómo se enumera a nuestro IEnumerable, y la única finalidad de IEnumerable GetEnumerator(), típicamente, será la de instanciar nuestro IEnumerator. Suena lioso, ¿verdad?, veamos un ejemplo. Nos vamos a imaginar que tenemos una clase Person con varias propiedades que lo describen, como su nombre, apellido y edad. Esta clase Person implementa IEnumerable; esto quiere decir, para nuestro caso concreto, que va a ser capaz de enumerar las características de la persona, en un orden concreto, a saber, nombre, apellido, edad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public class Person : IEnumerable
    {
        public string Name { get;}
        public string LastName { get;}
        public int Age { get;}
 
        public Person(string name, string lastName, int age)
        {
            Name = name;
            LastName = lastName;
            Age = age;
        }
 
        public IEnumerator GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }

Muy sencillo todo. Un único e importante detalle, todavía no hemos creado nuestro Enumerator. Pues vamos a ello:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    public class EnumeratorPerson : IEnumerator
    {
        private readonly Person person;
        private string currentProperty;
 
        public object Current { get; private set; }
 
        public EnumeratorPerson(Person person)
        {
            this.person = person;
            this.Reset();
        }
 
        public bool MoveNext()
        {
            if (currentProperty.Equals(string.Empty))
            {
                Current = person.Name;
                currentProperty = "Name";
                return true;
            }
            else if (currentProperty.Equals("Name"))
            {
                Current = person.LastName;
                currentProperty = "LastName";
                return true;
            }
            else if(currentProperty.Equals("LastName"))
            {
                Current = person.Age;
                currentProperty = "Age";
                return true;
            }
 
            return false;
        }
 
        public void Reset()
        {
            this.Current = person.Name;
            this.currentProperty = string.Empty;
        }
    }

El código es bastante feo, pero se ve más o menos claro lo que hacemos. Cuando instanciamos un nuevo EnumeratorPerson, nos guardamos el objeto person y reseteamos el estado de nuestra clase. Al resetear el estado de nuestra clase, lo que estamos haciendo es decir que el valor actual del Enumerator, el objeto Current, sea el nombre de la persona (person.Name); además de esto, establecemos como vacía la propiedad actual (currentProperty). Cuando se llama al método MoveNext la primera vez, debe asignar a Current el nombre de la persona, y, además de eso, indica que la propiedad actual (currentProperty) es «Name», de esta manera, la siguiente vez que se llame a MoveNext, puesto que currentProperty es «Name», se va a establecer como Current el apellido de la persona (person.Name) y se va a asignar el valor «LastName» a currentProperty. El mismo proceso se seguiría la siguiente vez que se llame a MoveNext, pero esta vez con la edad de la persona (person.Age). Si recordamos, MoveNext devuelve un booleano que indica si quedan elementos por recorrer en nuestro Enumerator y, tras establecer la edad de la persona en el objeto Current, ya no quedarían posiciones a las que desplazarnos por lo que se devolverá siempre false hasta que se llame al método Reset y se empiece desde el principio.
Ahora que tenemos nuestro EnumeratorPerson creado, ya podemos completar nuestra clase Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public class Person : IEnumerable
    {
        public string Name { get;}
        public string LastName { get;}
        public int Age { get;}
 
        public Person(string name, string lastName, int age)
        {
            Name = name;
            LastName = lastName;
            Age = age;
        }
 
        public IEnumerator GetEnumerator()
        {
            return new EnumeratorPerson(this);        }
    }

Ahora vamos a ver cómo enumerar las características de una persona con un bucle while:

1
2
3
4
5
6
    Person pepito = new Person("Pepito", "Pérez", 45);
    IEnumerator pepitoEnumerable = pepito.GetEnumerator();
    while (pepitoEnumerable.MoveNext())
    {
        Console.WriteLine(pepitoEnumerable.Current);
    }

Lo que hacemos es crear una nueva persona (pepito); puesto que las personas son IEnumerables, podemos obtener a través del método GetEnumerator su IEnumerator. Este IEnumerator lo recorremos en un while hasta que MoveNext devuelva false (cuando ya no queden características de pepito por enumerar). El resultado en la consola es:

Ahora es el momento en el que piensas «si este hombre me decía que lo utilizo constantemente y muy pocas veces he utilizado una expresión como la anterior». Seguramente no, puesto que le has añadido algo de azúcar sintáctico, ¿a que lo siguiente si te suena?

1
2
3
4
5
    Person pepito = new Person("Pepito", "Pérez", 45);
    foreach (var pepitoCharacteristics in pepito)
    {
        Console.WriteLine(pepitoCharacteristics);
    }

Pues en esencia es lo mismo que hacíamos con el while 🙂
Vamos ahora a intentar darle una vuelta a nuestro IEnumerator utilizando la palabra reservada yield y nos vamos a ahorrar la clase EnumeratorPerson de un plumazo. Atentos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public class Person : IEnumerable
    {
        public string Name { get;}
        public string LastName { get;}
        public int Age { get;}
 
        public Person(string name, string lastName, int age)
        {
            Name = name;
            LastName = lastName;
            Age = age;
        }
 
        public IEnumerator GetEnumerator()
        {
            yield return this.Name;            yield return this.LastName;            yield return this.Age;        }
    }

Si no conocías la yield pensarás que es brujería 🙂 Realmente yield está haciendo el trabajo de retener el estado actual que hacíamos con EnumeratorPerson. Vamos a ver el código que realmente estamos generando al utilizar yield return. Para centrarnos en lo que nos interesa, vamos a ver solamente el método GetEnumerator y el Enumerator que se genera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    [IteratorStateMachine(typeof (Program.Person.\u003CGetEnumerator\u003Ed__10))]
    public IEnumerator GetEnumerator()
    {
        Program.Person.\u003CGetEnumerator\u003Ed__10 getEnumeratorD10 = new Program.Person.\u003CGetEnumerator\u003Ed__10(0);
        getEnumeratorD10.\u003C\u003E4__this = this;
        return (IEnumerator) getEnumeratorD10;
    }
    [CompilerGenerated]
    private sealed class \u003CGetEnumerator\u003Ed__10 : IEnumerator<object>, IDisposable, IEnumerator
    {
        private int \u003C\u003E1__state;
        private object \u003C\u003E2__current;
        public Program.Person \u003C\u003E4__this;
 
        [DebuggerHidden]
        public \u003CGetEnumerator\u003Ed__10(int _param1)
        {
          base.\u002Ector();
          this.\u003C\u003E1__state = _param1;
        }
 
        [DebuggerHidden]
        void IDisposable.Dispose()
        {
        }
 
        bool IEnumerator.MoveNext()
        {
          switch (this.\u003C\u003E1__state)
          {
            case 0:
              this.\u003C\u003E1__state = -1;
              this.\u003C\u003E2__current = (object) this.\u003C\u003E4__this.Name;
              this.\u003C\u003E1__state = 1;
              return true;
            case 1:
              this.\u003C\u003E1__state = -1;
              this.\u003C\u003E2__current = (object) this.\u003C\u003E4__this.LastName;
              this.\u003C\u003E1__state = 2;
              return true;
            case 2:
              this.\u003C\u003E1__state = -1;
              this.\u003C\u003E2__current = (object) this.\u003C\u003E4__this.Age;
              this.\u003C\u003E1__state = 3;
              return true;
            case 3:
              this.\u003C\u003E1__state = -1;
              return false;
            default:
              return false;
          }
        }
 
        object IEnumerator<object>.Current
        {
          [DebuggerHidden] get
          {
            return this.\u003C\u003E2__current;
          }
        }
 
        [DebuggerHidden]
        void IEnumerator.Reset()
        {
          throw new NotSupportedException();
        }
 
        object IEnumerator.Current
        {
          [DebuggerHidden] get
          {
            return this.\u003C\u003E2__current;
          }
        }
      }

¿Te suena verdad? Vamos a renombrar un poco las cosas a ver si queda algo más claro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
    public IEnumerator GetEnumerator()
    {
        Program.Person.EnumeratorPerson enumeratorPerson = new Program.Person.EnumeratorPerson(0);
        enumeratorPerson.Person = this;
        return (IEnumerator)enumeratorPerson;
    }
 
    private sealed class EnumeratorPerson : IEnumerator<object>, IDisposable, IEnumerator
    {
        private int state;
        private object current;
        public Program.Person Person;
 
        public EnumeratorPerson(int initialState)
        {
            this.state = initialState;
        }
 
        void IDisposable.Dispose()
        {
        }
 
        bool IEnumerator.MoveNext()
        {
            switch (this.state)
            {
                case 0:
                    this.state = -1;
                    this.current = (object)this.Person.Name;
                    this.state = 1;
                    return true;
                case 1:
                    this.state = -1;
                    this.current = (object)this.Person.LastName;
                    this.state = 2;
                    return true;
                case 2:
                    this.state = -1;
                    this.current = (object)this.Person.Age;
                    this.state = 3;
                    return true;
                case 3:
                    this.state = -1;
                    return false;
                default:
                    return false;
            }
        }
 
        object IEnumerator<object>.Current
        {
            get
            {
                return this.current;
            }
        }
 
        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }
 
        object IEnumerator.Current
        {
            get
            {
                return this.current;
            }
        }
    }

Como digo, lo único que he hecho ha sido renombrar las algunas cosas y vemos que, en cierto modo, es lo mismo que hicimos nosotros a mano cuando creamos EnumeratorPerson, pero esta vez al utilizar yield return el compilador se encarga por nosotros.

ICollection

Esta interfaz implementa IEnumerable y añade algunos métodos más, pero tampoco demasiada cosa. Vamos a echar un vistazo a su firma:

1
2
3
4
5
6
7
8
9
10
    public interface ICollection : IEnumerable
    {
        void CopyTo(Array array, int index);
 
        int Count { get; }
 
        object SyncRoot { get; }
 
        bool IsSynchronized { get; }
    }

Como decíamos, además de lo que nos proporciona IEnumerable, nos proporciona un método CopyTo para copiar los elementos en un Array a partir de un índice que le especifiquemos, y tres propiedades, Count que nos indica el número de elementos de nuestra colección, SyncRoot que nos devuelve un objeto que permite sincronizar el acceso a la colección (con lock) y IsSynchronized que indica si el acceso a la colección es seguro para subprocesos. Vamos a ver cómo implementamos de forma básica ICollection en nuestra clase Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    public class Person : ICollection
    {
        public string Name { get;}
        public string LastName { get;}
        public int Age { get;}
 
        public Person(string name, string lastName, int age)
        {
            Name = name;
            LastName = lastName;
            Age = age;                
        }
 
        public IEnumerator GetEnumerator()
        {
            yield return this.Name;
            yield return this.LastName;
            yield return this.Age;
        }
 
        public void CopyTo(Array array, int index)
        {
            array.SetValue(this.Name, index);
            array.SetValue(this.LastName, index + 1);
            array.SetValue(this.LastName, index + 2);
        }
 
        public int Count
        {
            get { return 3; }
        }
 
        public object SyncRoot
        {
            get { return this; }
        }
 
        public bool IsSynchronized
        {
            get { return false; }
        }
    }

IList

Y vamos con la última y más completa de todas IList. Esta interfaz implementa ICollection y IEnumerable, y tiene el siguiente aspecto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public interface IList : ICollection, IEnumerable
    {
        object this[int index] { get; set; }
 
        int Add(object value);
 
        bool Contains(object value);
 
        void Clear();
 
        bool IsReadOnly { get; }
 
        bool IsFixedSize { get; }
 
        int IndexOf(object value);
 
        void Insert(int index, object value);
 
        void Remove(object value);
 
        void RemoveAt(int index);
    }

Como podemos ver, tiene un montón de métodos y propiedades más que los anteriores. Podemos, por ejemplo, acceder a elementos por índice, añadir o eliminar elementos, etc. Llegados a este punto, implementar IList en nuestra clase Person deja de tener sentido, o bueno, en cierto modo podría tener algo de sentido si añadiésemos características a la persona de forma dinámica… pero no me apetece 🙂 Un buen ejemplo de una clase que implementa IList es ArrayList, que internamente tiene un array de objects (object[] items) y hace el trabajo necesario para poder utilizarlo como si fuese un IList. Un punto fuerte de IList es que podemos acceder a los elementos a través de un índice, cosa que en las anteriores que vimos no era posible, necesitábamos sí o sí recorrer nosotros la colección.

Conclusiones

Al final he hecho más hincapié en IEnumerator/IEnumerable porque creo que si se entienden correctamente, te das cuenta de qué va el asunto. Las 4 interfaces tienen su versión genérica (IEnumerator, IEnumerable, ICollection, IList) que quizá sean las que más utilizamos habitualmente, pero creo que se entiende mucho mejor el concepto habiendo visto sus versiones no genéricas. ¿Cuál debemos utilizar? Pues como siempre que no sé la respuesta o no me quiero arriesgar depende. Lo que está claro es que deberías utilizar la más pequeña que necesites, es decir, si con IEnumerable es suficiente, utiliza esa; tiempo tendrás de cambiarla a ICollection o IList. Como curiosidad, os invito a coger la clase Person que habíamos generado al principio (la que sólo implementaba IEnumerable, en cualquiera de sus versiones) y depurar lo siguiente:

1
    IList<string> pepitoCharacteristics = pepito.Cast<string>().ToList();

Es una buena forma de ver cómo se materializa la lista cuando hacemos ToList(), y los problemas de rendimiento que podríamos tener en determinadas ocasiones por hacer esto.

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *