4.3. Defining an Interface

In Section 4.1 we implemented a new instance of an existing interface. In Section 4.2 we simply discovered and used the interface associated with a given type. In this section we introduce an interface definition. This interface supports the generation and display of sequences of numbers based on a unique algorithm. Here is the general set of operations we wish to support (two methods, a property, and an indexer):

  • GenerateSequence(), which generates the unique sequence of elements

  • Display(), which outputs the elements

  • Length, which returns a count of the number of elements

  • Indexer, which allows the user to access a specific element

An interface definition begins with the keyword interface followed by a name. Recall that in .NET, interfaces by convention begin with a capital I. The name follows, beginning with a capital letter, although this convention is not enforced by the compiler. We'll call our interface INumericSequence:

public interface INumericSequence{}

Yes, it is legal to define an empty interface, so this represents a complete interface definition.

The members that an interface is permitted to define are a subset of those permitted for a class. An interface can declare only methods, properties, indexers, and events as members—for example,

public interface INumericSequence
{
      // a method
      bool generate_sequence( int position );

      // a set of overloaded methods
      void display();
      void display( int first );
      void display( int first, int last );

      // a property
      int Length { get; }

      // a one-dimensional indexer
      int this[ int position ]{ get; }
}

All members of an interface are implicitly abstract. We cannot provide a default implementation, however trivial. In addition, the members of the interface are implicitly public. We cannot modify a member with either the abstract or the public keyword.

An interface cannot define instance data members. Nor can it define either a constructor or a destructor. (Without state members, neither is required.) In addition, an interface cannot declare static members. Because a const member is implicitly static, const members are not allowed, and neither are operators nor, not surprisingly, a static constructor.

An interface can inherit from another interface:

public interface INumericSequence : ICollection { ... }

or from multiple interfaces:

public interface INumericSequence
               : ICollection, ICloneable { ... }

but an interface cannot explicitly inherit from a class or struct. All interfaces, however, implicitly inherit from the root Object class.

4.3.1. Implementing Our Interface: Proof of Concept

Once we have defined our interface, how can we tell if it is any good? Typically, in addition to defining the interface, we deliver at least one implementation—as a kind of proof of concept or sanity check to confirm the viability of the interface. Let's provide a Fibonacci implementation to test our interface.

The first two elements of the Fibonacci sequence are both 1. Adding the two previous elements generates each subsequent element. For example, the first eight Fibonacci elements are 1, 1, 2, 3, 5, 8, 13, 21.

So what do we do first?

An interface is implemented by either class or struct inheritance. A struct isn't appropriate in this case: We don't expect to be creating and manipulating lots of Fibonacci objects in time-critical portions of the application. We'll declare Fibonacci as a class then. Because we do not expect it to be subsequently derived from, we'll declare it to be sealed as well:

public sealed class Fibonacci : INumericSequence {}

This empty definition, however, is illegal. A class inheriting from an interface must provide an implementation of every interface member. Leaving out even one member results in a compile-time error. (The exception is any case in which the class implementing the interface is abstract. It can then declare one or more of the interface member functions abstract. This is illustrated in Section 4.5.)

Before we can implement the interface members, we must determine the state information necessary to support the sequence abstraction. In this case we'll need an array to hold the sequence elements, and two additional members to hold the size and capacity of the array. Because the elements of the sequence are invariant, we need only a single array. We'll declare the array and its two supporting members as static.

The implementation of an interface has two main elements: (1) providing the underlying infrastructure, if any, to support the abstraction, and (2) providing a definition for each member of the interface, including the members of all inherited base interfaces. Here is the skeleton of a Fibonacci class design:

public sealed class Fibonacci : INumericSequence
{
   // infrastructure to support sequence abstraction
   private static int  [] m_elements;
   private static short   m_count;
   private static short   m_capacity = 128;

   // Fibonacci-specific methods:
   //    all for infrastructure supports
   static Fibonacci(){ ... }
   private void check_pos( int pos ) { ... }
   private void grow_capacity() { ... }

   // INumericSequence inherited members
   public bool generate_sequence( int pos ) { ... }

   public void  display(){ ... }
   public void  display( int first ) { ... }
   public void  display( int first, int last ) { ... }

   public int Length { get{ return m_count; }}
   public int this[ int position ] { ... }
}

The indexer has to verify the validity of the position, of course. I've factored that verification into a private member function, check_pos(). If the position is invalid, an exception is thrown. Otherwise the indexer returns the element, decrementing the position by one:

public int this[ int position ]
{
   get
   {
           check_pos( position );
           return m_elements[ position-1 ];
   }
}

Because array indexing in C# is zero based—that is, the first element is retrieved with an index of 0—we must adjust the position specified by the user. Why not require the user to make that adjustment explicitly? Whether that's a good decision or not depends on the perceived sophistication of our users. If they are other software developers, requiring the index to be zero based might well make sense. Nonprogrammers, however, often find the notion of a first element being at position zero very unnatural. By encapsulating the adjustment within the class, we shoulder the burden and make using our code safer and more pleasant for our users.

Here is how we might exercise our implementation. First we program an instance directly as a Fibonacci class object. Next we program it indirectly as a generic INumericSequence object:

public static void Main()
{
    // just some magic numbers – used as positions
    const int pos1 = 8, pos2 = 47;

    // let's directly use interface through class object
    Fibonacci fib = new Fibonacci();

    // invokes indexer;
    // indexer invokes generate_sequence( pos1 );
    int elem   = fib[ pos1 ];
    int length = fib.Length;

    string msg = "The length of the INumericSequence is ";
    Console.WriteLine( msg + length.ToString() );
    Console.WriteLine(
           "Element {0} of the Fibonacci Sequence is {1}",
                        pos1, elem );
    fib.display();

    // OK: let's now use interface generically
    INumericSequence ins = fib;

    elem = ins[ pos2 ];
    length = ins.Length;

    Console.WriteLine( msg + length.ToString() );
    Console.WriteLine(
           "Element {1} of the Fibonacci Sequence is {0}",
                        elem, pos2 );

    ins.display( 44, 47 );
}

As it turns out, our implementation is somewhat flawed. (That's the point of testing it.) The problem is self-evident in the program's output:

The length of the INumericSequence is 8
Element 8 of the Fibonacci Sequence is 21
Elements 1 to 8 of the Fibonacci Sequence:
         1 1 2 3 5 8 13 21

The length of the INumericSequence is 47
Element 47 of the Fibonacci Sequence is -1323752223
Elements 44 to 47 of the Fibonacci Sequence:
         701408733 1134903170 1836311903 -1323752223

What's going on here? The program claims that the forty-seventh element of the Fibonacci sequence is -1323752223. That's really not right. If we look at the display of elements 44 through 47, what has happened is pretty clear:

Elements 44 to 47 of the Fibonacci Sequence:
         701408733 1134903170 1836311903 -1323752223

We have declared m_elements to hold elements of type int. Unfortunately the forty-seventh Fibonacci element is too large to be contained within an int. The value overflowed into the sign bit, so the value is incorrectly displayed as negative. To better handle large Fibonacci values, we need to redefine m_elements as a type able to hold larger values.

What type is the most appropriate? In addition to the integral types, there are two floating-point types (the single-precision float and the double-precision double) and the 28-digits-of-precision decimal type. Let's redeclare m_elements to be, in turn, a uint, a ulong, a decimal, and a double. At position 50, only the ulong, decimal, and double are left standing:

Fibonacci Element #50 : (int)     :  -298632863
Fibonacci Element #50 : (uint)    :  3996334433
Fibonacci Element #50 : (ulong)   : 12586269025
Fibonacci Element #50 : (decimal) : 12586269025
Fibonacci Element #50 : (double)  : 12586269025

At position 100, only the decimal and double are capable of representing the value. However, the double begins to lose precision by rounding, which is unacceptable. Only the decimal values do not round off. Our best choice for representing the elements of the Fibonacci sequence is thus the decimal type:

Fibonacci Element #100 : (ulong)   :   3736710778780434371
Fibonacci Element #100 : (decimal) : 354224848179261915075
Fibonacci Element #100 : (double)  :   3.54224848179262E20

Still, however, this is not a complete solution. The decimal type reaches its 28-digit limit at position 139:

Fibonacci Element #139 :(decimal): 50095301248058391139327916261
Fibonacci Element #139 :(double) :         5.0095301248058406E28

When we attempt to calculate position 140, the decimal object overflows and the following exception occurs:

System.OverflowException:

Value is either too large or too small for a Decimal.

at System.Decimal.Add(System.Decimal, System.Decimal)

at System.Decimal.op_Addition(System.Decimal, System.Decimal)

at Project1.Fibonacci.version_decimal(Int32)

at Project1.MainObj.Main()

We can store and display only the first 139 Fibonacci elements using the predefined C# arithmetic types. Unless we plan to implement our own numeric class supporting very large integral values, we'll need to define a limit to the element position a user can request. Because this holds true for any numeric sequence, we should add that limit definition to the INumericSequence interface, as follows:

public interface INumericSequence
{
   // the two new properties
      int Length      { get; }
      int MaxPosition { get; }
      // ... rest the same ...
}

public sealed class Fibonacci : INumericSequence
{
   // infrastructure to support sequence abstraction
   private static int  [] m_elements = new int[ m_maxpos ];
   private static short   m_count;
   private const  short   m_maxpos = 128;

   public int MaxPosition { get{ return m_maxpos; }}

   // ... rest the same, except no longer need to grow array
}

This means that there is an infinite set of Fibonacci element positions that are legitimate but that we can't easily support and, in fact, have chosen not to support. In addition, the user might specify an invalid position—any value less than or equal to zero. How should we handle these two invalid conditions? The convention in both C# and .NET programming is to report all program anomalies through the raising of an exception.

But which exception should it throw? Should it throw separate exceptions for an invalid position and an unsupported position? There is no single right answer. Who should decide? If each interface implementation is allowed to decide whether and what exceptions to throw, if any, it becomes nearly impossible to safely program the interface generically. For example, to write the following code sequence:

INumericSequence ins = o as INumericSequence;
if ( ins != null )
     elem = ins[ pos2 ];

we must know that every implementation of the indexer reports an invalid position in exactly the same way.

This means that the interface definition must decide under what conditions an exception must be thrown and what the exception should be. Unfortunately, we can do that only through documentation. There is no way within C# to directly associate a member with one or a set of exceptions it may throw.

4.3.2. Integrating Our Interface within the System Framework

Is INumericSequence a good interface? In isolation, it seems to do the job. In the context of .NET, the interface probably will be a disappointment. Users will want to enumerate over the elements, for example, both explicitly and using the foreach loop. How do we support that functionality?

We need to have each INumericSequence implementation also provide an implementation of the IEnumerable interface. By having our interface inherit from IEnumerable, we ensure that each implementation of our interface also implements the IEnumerable interface:

public interface INumericSequence : IEnumerable
{ ... }

We do not list any of the IEnumerable base-interface members within our INumericSequence definition. However, when a user attempts to implement INumericSequence, he or she must provide a definition of not only all the INumericSequence members, but all the members of IEnumerable as well. Otherwise the compilation fails with a list of the missing interface members.

What are those members, and what do we need to do to implement them? Again, we have to turn to the documentation, where we discover that IEnumerable has only one member:

IEnumerator GetEnumerator();

GetEnumerator() returns an IEnumerator object. IEnumerator is yet another interface. It is defined in the System.Collections namespace. It encapsulates the knowledge of how to iterate through its associated collection.

Generally, when we implement the IEnumerable interface, we must also implement an associated instance of the IEnumerator interface. However, the instance of IEnumerator that supports iterating over a numeric sequence is implemented as an independent class:

class NSEnumerator : IEnumerator { ... }

rather than as a base interface of INumericSequence. This way, the enumerator need be implemented only once. Multiple implementations of the interface can reuse it. Here is our implementation of GetEnumerator():

public sealed class Fibonacci : INumericSequence
{
    private static int [] m_elements = new int[ m_maxpos ];
    private static short m_count;

    public IEnumerator GetEnumerator()
      { return new NSEnumerator( m_elements, m_count ); }

    // ...
}

How do we implement the NSEnumerator constructor? Again, we need to look the IEnumerator documentation. Here is what it says:

IEnumerator is the base interface for all enumerator types. When an enumerator is instantiated, it takes a snapshot of the current state of the collection.

This is why we pass the array and a count of its size to NSEnumerator. This represents our snapshot.

The IEnumerator interface has three members: a property, Current, that returns the current element in the iteration; and two methods—MoveNext(), which advances Current to the next element of the collection, and Reset(), which sets Current to its initial position. As it happens, special semantics are associated with the initial position of an IEnumerator instance:

The enumerator is initially positioned before the first element in the collection, and it must be advanced before it is used.

This behavior is slightly nonintuitive, and it is easy for beginners to get this wrong. Users learn soon enough, however. Accessing Current before an invocation of MoveNext() results in a runtime exception:

public void iterate( ArrayList al )
{
   IEnumerator it = al.GetEnumerator();

   // oops: this access of Current before MoveNext()
   // generates an exception
   while( it.Current != null )
   {
      Console.WriteLine( it.Current.ToString() );
      it.MoveNext();
   }
}

The correct use of IEnumerator invokes MoveNext() before accessing Current for the first time. If MoveNext() has a next element to access, it returns true. Here is the corrected implementation:

public void iterate( ArrayList al )
{
   IEnumerator it = al.GetEnumerator();

   while( it.MoveNext())
          Console.WriteLine( it.Current.ToString() );
}

The essential point is that whenever we implement an interface that someone else has defined, we must be sure to understand and provide the exact semantics as described for its members. Otherwise, when our implementation is used through an interface object, it will behave unexpectedly. The compiler cannot enforce this level of conformance.

The next example shows a partial implementation of NSEnumerator without providing an implementation of the three interface members yet. This represents just the interface necessary to support iteration across the collection of elements:

class NSEnumerator : IEnumerator
{
   decimal [] m_elems;

   int        m_count;
   int        m_curr;
   public NSEnumerator( decimal [] array, int count )
   {
       // these are exceptions defined within System
       if ( array == null )
            throw new ArgumentNullException( "null array" );
 
       if ( count <= 0 )
            throw new ArgumentOutOfRangeException(
                                         count.ToString() );

       m_elems = array;
       m_count = count;
       m_curr  = -1; // required semantics!
   }

   // ...
}

Current is a property. It returns an object addressing the current element in the collection. If Current is positioned either before or after the last element of the collection, it must throw the InvalidOperationException :

public object Current
{
    get{
       if ( m_curr == -1 || m_curr >= m_count )
            throw new
                  InvalidOperationException( ToString() );

      return m_elems[ m_curr-1 ];
    }
}

Notice that Current does not advance m_curr to address the next element. Why didn't we do that? Because the documentation told us we can't.

Current does not move the position of the enumerator. Consecutive calls to Current will return the same object until either MoveNext() or Reset() is called.

If we had either initialized m_curr to the first sequence element or incremented it within Current, our users would have silently dropped elements as they iterated across our sequences.

To allow Current to represent elements of any program type, the IEnumerable definition defines the type of the element to be object. This is somewhat disappointing. Within NSEnumerator, for example, we know that the element type is decimal. Returning it as an object means that it is implicitly boxed within Current and the user must unbox it with an explicit cast. We'll see how to design around this problem in the next section.

MoveNext() returns true if the enumerator advances to a next element. If the enumerator advances past the last valid element, it returns false:

public bool MoveNext()
      { return m_count < ++m_curr;}

Reset() sets the enumerator to its initial position. Remember that the documentation states that the value of this position must be set as one less than the first element in the collection:

public void Reset(){ m_curr = -1; }

The code to implement the interface is quite straightforward. The challenge of the implementation is in providing interface semantics consistent with its documentation.

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

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