Examples of Interface Implementation

Interfaces contribute to maintainable and well-engineered software. However, they are still an abstract concept for people who haven't used them. A common question people ask is, "If I just declare my type and add the same members as the interface, why do I need an interface?" Sure, it is true that you can code without interfaces, but you lose the benefits they provide. The rest of this section shows how interfaces are used with C# to implement tasks that could not be accomplished as easily without interfaces.

Using the IDisposable Interface

In Chapter 2, I promised to show a better way to provide deterministic destruction of unmanaged resources. This task is accomplished with the IDisposable interface and the using statement. The using statement (described later in this section), which works with the IDisposable interface, is not the same as the using declaration, which is used for including namespaces in a file. Listing 4.9 shows how to implement the IDisposable interface.

Listing 4.9 The IDisposable Interface (IDisposableInterface.cs)
 using System; public class UnmanagedResourceHolder : IDisposable {    bool m_isValid = true;    public void Close()    {       Console.WriteLine("Close.");       Dispose();    }    public void Dispose()    {       Console.WriteLine("Dispose.");       // stop GC from calling destructor       GC.SuppressFinalize(this);       Dispose(true);    }    ~UnmanagedResourceHolder()    {       Console.WriteLine("Destructor.");       Dispose(false);    }    protected virtual void Dispose(bool disposing)    {       Console.WriteLine("Dispose(bool).");       // protect against multiple threads       lock (this)       {          // execute only if not called          //    from destructor          if (disposing)          {             Console.WriteLine("Dispose(true).");             // call other object members of             //  this type that need to be disposed          }          if (m_isValid)          {             Console.WriteLine("m_isValid == true.");             // dispose of unmanaged resources             //  that are still valid             m_isValid = false;          }       }    } } class IDisposableInterface {    static void Main()     {       UnmanagedResourceHolder resHold = null;       try       {          resHold = new UnmanagedResourceHolder();          // do something with resHold       }       catch (Exception ex)       {          // handle some exception       }       finally       {          // guarantee unmanaged resource release          resHold.Close();       }       Console.ReadLine();    } } 

The UnmanagedResourceHolder class in Listing 4.9 implements the Dispose pattern. All methods of this class lead to the protected virtual Dispose method with the disposing parameter, which is where the actual code cleanup occurs. The Close method simply calls the parameterless Dispose method. Both the parameterless Dispose method and the destructor call the Dispose method with the disposing parameter.

When calling Dispose, the destructor sets the disposing parameter to false, indicating that the object is no longer valid and other disposable objects in the class should not be disposed. The reason is that there is no guarantee that those objects are valid, and trying to dispose them may generate an exception.

The parameterless Dispose method will call GC.SuppressFinalize, using this as the parameter to indicate that the destructor should not be executed. This is an optimization that prevents the GC from needlessly executing the destructor when the type's unmanaged resources have already been cleaned up. The parameterless Dispose method then calls the other Dispose, setting its disposing parameter to true. This indicates that it is safe to dispose member objects.

The reason to implement the IDisposable interface is to provide deterministic destruction of unmanaged resources. To accomplish this goal, a caller must invoke a type's Dispose method. When it makes sense to do so, a type will also declare a Close method that calls the Dispose method. For example, it makes more sense to call Close on a file stream than Dispose.

The time to call a Close or Dispose method is when the unmanaged resource is no longer needed. This could be done in a try/catch/finally block, as described in Chapter 3 and demonstrated in Listing 4.9. However, C# provides a simpler method: the using statement. Listing 4.10 shows how to perform deterministic destruction with the using statement.

Listing 4.10 The using Statement (UsingStatement.cs)
 using System; public class UnmanagedResourceHolder : IDisposable {    bool m_isValid = true;    public void Close()    {       Console.WriteLine("Close.");       Dispose();    }    public void Dispose()    {       Console.WriteLine("Dispose.");       // stop GC from calling destructor       GC.SuppressFinalize(this);       Dispose(true);    }    ~UnmanagedResourceHolder()    {       Console.WriteLine("Destructor.");       Dispose(false);    }    protected virtual void Dispose(bool disposing)    {       Console.WriteLine("Dispose(bool).");       // protect against multiple threads       lock (this)       {          // execute only if not called          //    from destructor          if (disposing)          {             Console.WriteLine("Dispose(true).");             // call other object members of             //  this type that need to be disposed          }          if (m_isValid)          {             Console.WriteLine("m_isValid == true.");             // dispose of unmanaged resources             //  that are still valid             m_isValid = false;          }       }    } } class UsingStatement {    static void Main()    {       using (UnmanagedResourceHolder resHold =          new UnmanagedResourceHolder())       {          // do something with resHold       }       Console.ReadLine();    } } 

The using statement in Listing 4.10 only operates on types that implement IDisposable. One or more types, separated by comma operators, may be specified as parameters to a using statement. When all actions within the block of the using statement have executed, the Dispose method will be called on each object specified in the using statement's parameter list.

Using the IEnumerable and IEnumerator Interfaces

A common statement in C# programming is the foreach loop. In Chapter 2, I described how this works and promised to divulge the details here. The reason is that the foreach loop operates by virtue of interfaces. Any type that wants to support iteration of its objects via foreach must implement the IEnumerable interface, which has a single method, GetEnumerator. The foreach loop calls GetEnumerator on the IEnumerable type to receive a reference to another type that implements the IEnumerator interface.

It is the IEnumerator derived object that the foreach statement uses to perform the iteration through a loop. When the foreach loop has the IEnumerator derived object, it calls the MoveNext method to set the position to read the next value from. Then it queries the Current property to get the current value. The foreach loop will continue calling MoveNext and Current until MoveNext returns false, indicating that the last item has been returned. Listing 4.11 shows how to implement the IEnumerable and IEnumerator interfaces to make a type usable in a foreach statement.

Notice that this discussion does not assume that the IEnumerable derived object is a list. This is because any type of object can implement this interface, and the particular implementation is of no concern to the foreach statement. For example, the object passed to foreach could be any type in the System.Collections namespace, such as ArrayList, Hashtable, Queue, SortedList, or Stack.

Listing 4.11 Implementing IEnumerable and IEnumerator (Enumeration.cs)
 using System; using System.Collections; public class MyEnumerableType : IEnumerable {    private MyEnumeratorType m_myEnumerator;    private object[]         m_internalArray;    private int              m_arraySize = 16;    private int              m_count;    public MyEnumerableType()    {       m_internalArray = new object[m_arraySize];       m_myEnumerator  = new MyEnumeratorType(this);    }    public void Add(object obj)    {       m_internalArray[m_count++] = obj;    }    public IEnumerator GetEnumerator()    {       return m_myEnumerator;    }    public int Count    {       get       {          return m_count;       }       set       {          m_count = value;       }    }    private class MyEnumeratorType : IEnumerator    {       private int m_current = -1;       private object[]         m_internalArray;       private MyEnumerableType m_outer;       public MyEnumeratorType(MyEnumerableType outer)       {          m_outer         = outer;          m_internalArray = outer.m_internalArray;       }       public object Current       {          get          {             Console.WriteLine("Current.");             if (m_current == -1)                throw new InvalidOperationException(                   "You tried to access MyEnumerableType when it is empty.");             return m_internalArray[m_current];          }       }       public bool MoveNext()       {          Console.WriteLine("MoveNext.");          m_current++;          int m_arraySize = m_internalArray.Length;          // has array reached max size?          if (m_arraySize == m_current)          {             m_arraySize *= 2;             object[] temp = new object[m_arraySize];             m_internalArray.CopyTo(temp, 0);             m_internalArray = temp;          }          bool shouldContinue =              m_current == m_outer.Count ? false : true;          if (!shouldContinue)             Reset();          return shouldContinue;       }       public void Reset()       {          Console.WriteLine("Reset.");          m_current = -1;       }    } } class Enumeration {    static void Main()    {       MyEnumerableType enType = new MyEnumerableType();       enType.Add(1);       enType.Add(2);       enType.Add(3);       foreach(int obj in enType)       {          Console.WriteLine(obj);       }       foreach(int obj in enType)       {          Console.WriteLine(obj);       }       Console.ReadLine();    } } 

The primary thing to remember from Listing 4.11 is that the foreach statement calls MoveNext and Current for each object it gets. The Reset method is only called when MoveNext reaches the last item to return. This allows subsequent foreach loops to iterate through the items again, as demonstrated by the two foreach loops in the Main method.

The example in Listing 4.11 is doing extra duty in that it demonstrates many aspects of the material covered in the last three chapters and shows a practical application of interfaces. It also shows a few other things that can be done in C#.

You might have noticed that the class that implements IEnumerator, MyEnumeratorType, is a nested class. This class is nested because its sole purpose in life is to support its outer class. No other class will ever need to reference it directly because it doesn't have a use besides supporting its outer class. Sure, the foreach loop uses it, but the foreach statement doesn't access it directly or care where it resides, as long as it implements IEnumerator. Any type that is not reusable beyond directly supporting another type is a candidate for a nested type.

Another important feature to pay close attention to is how the nested class is initialized. During the call to GetEnumerator, MyEnumeratorType is instantiated with its parameter set to this. Whenever code needs to reference the object it is contained in, it can use the this reference. Within the MyEnumeratorType constructor, the parameter is saved as a member variable m_outer, so it can have a reference to its outer class. A nested type does not have visibility into its outer type. Therefore, it must have a reference to its outer type. Furthermore, if the outer type has members that it wants to make available to the nested class, those members must have internal or public access. It is important to remember this pattern because it's more than likely that you will need it in the future.

As has already been discussed, foreach works with types that expose IEnumerable and IEnumerator interfaces. These keep operations safe and make the program more robust. Consider what would happen if the assumption were made that a type passed to foreach would just implement the right methods without having an interface. The application would generate runtime exceptions if a method was forgotten, which could be hard to find. Also, what type would the foreach statement use to reference the object? Would it have to be the Object type or something else? Because C# only has single implementation inheritance, such a requirement would force objects that want to support foreach into a specific hierarchy, limiting support of other inheritance hierarchies. These problems are solved by interfaces. For example, if an object is given to the foreach statement, which doesn't implement IEnumerable, the compiler generates an error. Also, the single implementation inheritance hierarchy is preserved because the interface provides the type that a foreach can reference. Interfaces are available to help you write more robust and well-engineered code.



C# Builder KickStart
C# Builder KickStart
ISBN: 672325896
EAN: N/A
Year: 2003
Pages: 165

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net