CHAPTER 18

image

Indexers, Enumerators, and Iterators

It is sometimes useful to be able to treat an object as if it were an array and access it by index. This can be done by writing an indexer for the object. In the same way that a property looks like a field but has accessors to perform get and set operations, an indexer looks like an array but has accessors to perform array-indexing operations.

Pretty much every object with an indexer will also have an enumerator. Enumerators and iterators provide two ways of returning a sequence of values from an object.

Indexing with an Integer Index

A class that contains a database row might implement an indexer to access the columns in the row.

using System;
using System.Collections.Generic;
class DataValue
{
    public DataValue(string name, object data)
    {
       Name = name;
       Data = data;
    }
    public string Name { get; set; }
    public object Data { get; set; }
}
class DataRow
{
    public DataRow()
    {
       m_row = new List < DataValue > ();
    }

    public void Load()
    {
       /* load code here */
       m_row.Add(new DataValue("Id", 5551212));
       m_row.Add(new DataValue("Name", "Fred"));
       m_row.Add(new DataValue("Salary", 2355.23 m));
    }
       // the indexer - implements a 1-based index.
    public DataValue this[int column]
    {
       get { return(m_row[column - 1]); }
       set { m_row[column - 1] = value; }
    }
    List < DataValue > m_row;
}
class Test
{
    public static void Main()
    {
       DataRow row = new DataRow();
       row.Load();
       Console.WriteLine("Column 0: {0}", row[1].Data);
       row[1].Data = 12;    // set the ID
    }
}

The DataRow class has functions to load a row of data, functions to save the data, and an indexer function to provide access to the data. In a real class, the Load() function would load data from a database.

The indexer function is written the same way that a property is written, except that it takes an indexing parameter. The indexer is declared using the name this since it has no name.1

Indexing with a String Index

A class can have more than one indexer. For the DataRow class, it might be useful to be able to use the name of the column for indexing.

using System;
using System.Collections.Generic;
class DataValue
{
    public DataValue(string name, object data)
    {
       Name = name;
       Data = data;
    }
    public string Name { get; set; }
    public object Data { get; set; }
}
class DataRow
{
    public DataRow()
    {
       m_row = new List < DataValue > ();
    }
    public void Load()
    {
       /* load code here */
       m_row.Add(new DataValue("Id", 5551212));
       m_row.Add(new DataValue("Name", "Fred"));
       m_row.Add(new DataValue("Salary", 2355.23 m));
    }
    public DataValue this[int column]
    {
       get { return(m_row[column - 1]); }
       set { m_row[column - 1] = value; }
    }
    int FindColumn(string name)
    {
       for (int index = 0; index < m_row.Count; index++)
       {
            if (m_row[index].Name == name)
            {
                return(index + 1);
            }
       }
       return(−1);
    }
    public DataValue this[string name]
    {
       get { return(this[FindColumn(name)]); }
       set { this[FindColumn(name)] = value; }
    }
    List < DataValue > m_row;
}
class Test
{
    public static void Main()
    {
       DataRow row = new DataRow();
       row.Load();
       DataValue val = row["Id"];
       Console.WriteLine("Id: {0}", val.Data);
       Console.WriteLine("Salary: {0}", row["Salary"].Data);
       row["Name"].Data = "Barney";    // set the name
       Console.WriteLine("Name: {0}", row["Name"].Data);
    }
}

The string indexer uses the FindColumn() function to find the index of the name and then uses the int indexer to do the proper thing.

Indexing with Multiple Parameters

Indexers can take more than one parameter to simulate a multidimensional virtual array. The following example implements a chessboard that can be accessed using standard chess notation (a letter from A to H followed by a number from 1 to 8). The first indexer is used to access the board using string and integer indices, and the second indexer uses a single string like “C5.”

using System;

public class Player
{
    string m_name;

    public Player(string name)
    {
       m_name = name;
    }
    public override string ToString()
    {
       return(m_name);
    }
}

public class Board
{
    Player[,] board = new Player[8, 8];

    int RowToIndex(string row)
    {
       string temp = row.ToUpper();
       return((int) temp[0] - (int) 'A'),
    }

    int PositionToColumn(string pos)
    {
       return(pos[1] - '0' - 1);
    }

    public Player this[string row, int column]
    {
       get
       {
            return(board[RowToIndex(row), column - 1]);
       }
       set
       {
            board[RowToIndex(row), column - 1] = value;
       }
    }

    public Player this[string position]
    {
       get
       {
            return(board[RowToIndex(position),
                      PositionToColumn(position)]);
       }
       set
       {
            board[RowToIndex(position),
                      PositionToColumn(position)] = value;
       }
    }
}
class Test
{
    public static void Main()
    {
       Board board = new Board();

       board["A", 4] = new Player("White King");
       board["H", 4] = new Player("Black King");

       Console.WriteLine("A4 = {0}", board["A", 4]);
       Console.WriteLine("H4 = {0}", board["H4"]);
    }
}

Design Guidelines for Indexers

Indexers should be used only in situations where the class is arraylike.2

Object Enumeration

A class that contains values can implement the IEnumerable < T > alias, which specifies that this class can generate an ordered sequence of values. Enumerators and iterators are two ways (one old, one new) of implementing object enumeration.

Enumerators and Foreach

To understand what is required to enable foreach, it helps to know what goes on behind the scenes.

When the compiler sees the following foreach block:

foreach (string s in myCollection)
{
    Console.WriteLine("String is {0}", s);
}

it transforms this code into the following:3

IEnumerator enumerator = ((IEnumerable) myCollection).GetEnumerator();
while (enumerator.MoveNext())
{
    string s = (string) enumerator.Current();
    Console.WriteLine("String is {0}", s);
}

The first step of the process is to cast the item to iterate to IEnumerable. If that succeeds, the class supports enumeration, and an IEnumerator interface reference to perform the enumeration is returned. The MoveNext() and Current members of the class are then called to perform the iteration.

Enabling Enumeration

To make a class enumerable, you will implement the IEnumerable interface on a class. To do that, you need a class that can walk through a list (this uses the IntList example from Chapter 17).

public class IntListEnumerator : IEnumerator
{
    IntList m_intList;
    int m_index;
    internal IntListEnumerator(IntList intList)
    {
       m_intList = intList;
       Reset();
    }
    public void Reset()
    {
       m_index = −1;
    }
    public bool MoveNext()
    {
       m_index++;
       return m_index < m_intList.Count;
    }
    public object Current
    {
       get { return (m_intList[m_index]); }
    }
}

The IntList class can then use this enumerator class.

public class IntList: IEnumerable
{
    int m_count = 0;
    int[] m_values;
    public IntList(int capacity)
    {
       m_values = new int[capacity];
    }
    public void Add(int value)
    {
       m_values[m_count] = value;
       m_count++;
    }
    public int this[int index]
    {
       get { return m_values[index]; }
       set { m_values[index] = value; }
    }
    public int Count { get { return m_count; } }
    public IEnumerator GetEnumerator()
    {
       return new IntListEnumerator(this);
    }
}

The user can now write the following:

IntList intList = new IntList(3);
intList.Add(1);
intList.Add(2);
intList.Add(4);

foreach (int number in intList)
{
    Console.WriteLine(number);
}

The IntListEnumerator class is a simple state machine that keeps track of where it is in the list, returning items in enumeration order.

ENUMERATION HISTORY

Because enumeration was added to C# in several stages, there are no less than four ways to implement enumerations in C#. They are, in order of introduction:

  1. The enumeration class can implement the IEnumerator interface. This is like the previous example, but the type of the Current property is object (so it can be generic in a world without generics).
  2. The approach shown in the previous example can be used, but modified so that the type of the Current property is not object. This pattern-based approach works in C# and VB but might not work in all .NET languages. In the early days of C#, many classes used this approach and implemented IEnumerator implicitly, effectively implementing both of these approaches.
  3. Implement the generic IEnumerator < T > interface. Many classes implemented the approaches of #1 and #2 as well.
  4. Iterators can be used.

The first two were in C# 1.0, the third one was introduced when generic types showed up in C# 2.0, and the last one is the current approach. It’s more than a little confusing. Thankfully, most classes use iterators, which, as you will see, are the simplest approach.

Iterators

Before the introduction of iterators, writing enumerators was an onerous task.4 You had to write all the boilerplate code and get it correct, and if you had a data structure such as a tree, you had to write the traversal method that would keep track of where you were in the tree for each call.

Making that sort of thing easier is exactly what compilers are good at, and C# therefore provides support to make this easier, in a feature known as an iterator. Iterators automate the boilerplate code and, more importantly, let you express the state machine as if it were normal procedural code.

public class IntListNew: IEnumerable
{
    int m_count = 0;
    int[] m_values;
    public IntListNew(int capacity)
    {
       m_values = new int[capacity];
    }
    public void Add(int value)
    {
       m_values[m_count] = value;
       m_count++;
    }
    public int this[int index]
    {
       get { return m_values[index]; }
       set { m_values[index] = value; }
    }
    public int Count { get { return m_count; } }
    public IEnumerator GetEnumerator()
    {
       for (int index = 0; index < m_count; index++)
       {
            yield return m_values[index];
       }
    }
}

The iterator GetEnumerator() works as follows:

  1. When the class is first enumerated, the code in the iterator is executed from the start.
  2. When a yield return statement is encountered, that value is returned as one of the enumeration values, and the compiler remembers where it is in the code in the enumerator.
  3. When the next value is asked for, execution of code continues immediately after the previous yield return statement.

The compiler will do all the hard work of creating the state machine that makes that possible.5 In more complex classes, such as a tree class, it is common to have several yield return statements in a single iterator.

image Note  So, why is the statement yield return and not just yield? It might have been, if iterators were in the first version of C#, but using yield by itself would have required that yield be a new keyword, which would have broken any existing code that used yield as a variable name. Putting it next to return made it a contextual keyword, preserving the use of the identifier yield elsewhere.

Named Iterators

It is possible for a class to support more than one method of iterating. A named iterator can be added to your class.

public IEnumerable ReversedItems()
{
    for (int index = m_count - 1; index > = 0; index--)
    {
       yield return m_values[index];
    }
}

And it can used as follows:

foreach (int number in intList.ReversedItems())
{
    Console.WriteLine(number);
}

NAMED ITERATORS OR LINQ METHODS?

C# provides multiple ways of doing a list reversal and other such transformations; an iterator method can be defined on the class or a Linq method, as described in Chapter 28.

There are some cases where the choice is obvious. A tree class might want to support pre-order, in-order, post-order, and breadth-first searches, and the only way to do that is through a named iterator. On the other hand, if you are dealing with a class somebody else wrote, you can’t add a named iterator, so you will need to rely on Linq.

In the cases where there is a choice, my recommendation is to start with a single iterator and use Linq methods. If profiling shows a performance bottleneck in that code, then go back and add the named iterator.

Iterators and Generic Types

Generic iterators are defined by implementing IEnumerable < T > and returning IEnumerator < T > .

public class MyListNew < T> : IEnumerable < T>
{
    int m_count = 0;
    T[] m_values;
    public MyListNew(int capacity)
    {
       m_values = new T[capacity];
    }
    public void Add(T value)
    {
       m_values[m_count] = value;
       m_count++;
    }
    public T this[int index]
    {
       get { return m_values[index]; }
       set { m_values[index] = value; }
    }
    public int Count { get { return m_count; } }
    IEnumerator IEnumerable.GetEnumerator()
    {
       return GetEnumerator();
    }
    public IEnumerator < T > GetEnumerator()
    {
       for (int index = 0; index < m_count; index++)
       {
            yield return m_values[index];
       }
    }
    public IEnumerable < T > ReversedItems()
    {
       for (int index = m_count - 1; index > = 0; index--)
       {
            yield return m_values[index];
       }
    }
}

This is very straightforward, with two small caveats:

  • In addition to implementing the generic version of GetEnumerator(), you need to explicitly implement the nongeneric version as well (see the bold code). This allows languages that don’t support generics to iterate over your class.
  • A using System.Collections; statement is required to use the nongeneric IEnumerable type.

Iterators and Resource Management

An iterator might hold a valuable resource, such as a database connection or a file. It would be very useful for that resource to be released when the iterator has completed, and in fact, the foreach statement will ensure that resources are released by calling Dispose() if the enumerator implements IDisposable.

Iterators do their part as well. Consider the following code:

    class ByteStreamer
    {
       string m_filename;
       public ByteStreamer(string filename)
       {
            m_filename = filename;
       }
       public IEnumerator < byte > GetEnumerator()
       {
            using (FileStream stream = File.Open(m_filename, FileMode.Open))
            {
                yield return (byte) stream.ReadByte();
            }
       }
    }

This looks just like the normal pattern with the using statement. The compiler will take any cleanup required by the stream instance and make sure it is performed when Dispose() is called at the end of the enumeration.

This does not, however, extend to exception handling; a yield return can appear only in a try block that does not have a catch block. It can never appear in a catch or finally block.

1 The choice of using this as a keyword was a bit controversial, but it was chosen because there weren’t any better choices apparent and because reusing an existing keyword doesn’t take away a name that developers would like to use elsewhere.

2 I’ve seen cases where a class that was not arraylike implemented an indexer instead of a method that took an integer. It’s very weird and difficult to understand.

3 This is a bit oversimplified. If the IEnumerator implements IDisposable, the compiler will wrap the enumeration in a try-finally statement to ensure that the Dispose method is called. More on this later.

4 But we did it. And we had to trudge through waist-high snowdrifts to get to our offices, and our characters only had 7 bits (the last part is true, actually).

5 There is a considerable amount of code generation magic going on here. I recommend using a decompiler to look at the generated code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.9.118