September 13, 2024

A sober look at interfaces

Consider the statement, "this class implements such and such an interface." Being able to make a statement like this goes to the heart of what interfaces are all about and why they are useful.

Suppose we have some interface X. We can make promises like, "if your class implements interface X, you can pass it to this API." Or perhaps we can go even further and say that if your class implements a certain interface, it can be used by certain C# language features.

Here is a corny example. Suppose there is a "vehicle" interface. It specifies 3 methods, "steer", "brake", and "gas". Now if I put together a class (call it "my_car") that has those methods, I can hand it to some API like:

	Drive.take_me_to ( my_car, "Chicago, Illinois" );
And it will transport me to the given destination. That is the idea. You make your class comply with some Interface, and you can leverage existing useful API. Now interfaces begin to make sense.

The IEnumerable interface

Let's make this concrete and talk about IEnumerable<T>.
Here is some official stuff from Microsoft:

The idea here is that if I write a class that conforms to the IEnumerable<T> interface, I can write code like this:
	IEnumerable<int> ding = new Example.Dingus ( 5 );
            foreach ( int x in ding )
                Console.WriteLine ( $"Value: {x}" );
Let's see if we can pull it off. I replicate what the Microsoft link above shows, which includes a huge amount of boilerplate and ceremony. Some of the methods are there to satisfy ancient C# versions that do not support generic types. I could play with ripping out some of this. All this is sort of humorous because one of my books claims that the IEnumerable interface is just this:
public interface IEnumerable : IEnumerable
{
	IEnumerator GetEnumerator ();
}
public interface IEnumerable
{
	IEnumerator GetEnumerator ();
}
But clearly there is more to it than that, in particular we also need to satisfy the IEnumerator interface.

Something for a future page -- it is also possible to cobble together an IEnumerable using iterators, which feature the "yield" statement.

The output from the code below is as follows.

Value: 1
Value: 2
Value: 3
Value: 4
Value: 5
All done
We have gotten rather distracted from the main point, which is showing how we can write some code to satisfy an interface, then "plug it into" the C# language.
using System;

// This line is absolutely vital
using System.Collections;

namespace Example
{

    // A custom class that implements IEnumerable(T). When you implement IEnumerable(T),
    // you must also implement IEnumerable and IEnumerator(T).
    public class Dingus : IEnumerable
    {
        private int how_many;

        public Dingus ( int arg )
        {
            how_many = arg;
        }

        // Must implement GetEnumerator, which returns a new MyEnumerator.
        public IEnumerator GetEnumerator()
        {
            return new MyEnumerator ( how_many );
        }

        // Must also implement IEnumerable.GetEnumerator, but implement as a private method.
        private IEnumerator GetEnumerator1()
        {
            return this.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator1();
        }
    }
    // When you implement IEnumerable(T), you must also implement IEnumerator(T),
    // which will walk through whatever you want to enumerate
    // Implementing IEnumerator(T) requires that you implement IEnumerator and IDisposable.
    public class MyEnumerator : IEnumerator
    {
        private int _how_many;
        private int _current;

        // Constructor
        public MyEnumerator ( int arg )
        {
            _how_many = arg;
        }

        // Implement the IEnumerator(T).Current publicly, but implement
        // IEnumerator.Current, which is also required, privately.
        public int Current
        {
            get
            {
                return _current;
            }
        }

        private object Current1
        {
            get { return this.Current; }
        }

        object IEnumerator.Current
        {
            get { return Current1; }
        }

        // Implement MoveNext and Reset, which are required by IEnumerator.
        public bool MoveNext()
        {
            _current++;
            if ( _current > _how_many )
                return false;
            return true;
        }

        public void Reset()
        {
            _current = 0;
        }

        // Implement IDisposable, which is also implemented by IEnumerator(T).
        private bool disposedValue = false;

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue)
            {
                if (disposing)
                {
                    // Dispose of managed resources.
                }
                _current = 0;
            }

            this.disposedValue = true;
        }

        ~MyEnumerator()
        {
            Dispose(disposing: false);
        }
    }
}

namespace HogHeaven
{
    public class Program
    {
        public static void Main(string[] args)
        {
            IEnumerable ding = new Example.Dingus ( 8 );
            foreach ( int x in ding )
                Console.WriteLine ( $"Value: {x}" );

            Console.WriteLine ( "All done" );
        }
    }
}

// THE END
Whew! I pulled my hair out with this, until I added the "using System.Collections;" line. The "dispose" and "reset" sections are entirely ceremony, but are supposed to be essential.


Feedback? Questions? Drop me a line!

Tom's Computer Info / [email protected]