Interfaces, Indexers, and Delegates
Nearly everything in .NET is an object. The .NET Framework has thousands of classes, and each class has different methods and properties. Keeping track of all of these classes and members would be impossible if the .NET Framework were not implemented consistently. For example, every class derives from System.Object
and therefore has a ToString()
method that performs exactly the same task - converting an instance of a class to a string. Similarly, many classes support the same operators, such as for comparing two instances of a class for equality.
This consistency is possible because of inheritance and interfaces. Use inheritance to create new classes from existing ones. For example, the Bitmap
class inherits from the Image
class and extends it by adding functionality. Therefore you can use an instance of the Bitmap
class in the same ways that you would an instance of the Image
class.
Interfaces, also known as contracts, define a common set (semantically-related) of members that all classes that implement the interface must provide. For example, the IComparable
interface defines the CompareTo
method, which enables two instances of a class to be compared for equality. All classes that implement the IComparable
interface can be compared for equality. IDisposable
is an interface that provides a single method, Dispose
, to enable assemblies that create an instance of a class to free up any resources the instance has consumed. Below is an example of a class declaration. After declaring the class, you add the interface:
class SomeClass
{
}
class SomeClass : IDisposable
{
}
At this point, it is important to introduce the concept of indexers. The C# language supports the capability of building custom classes that may be indexed just like an array of intrinsic types. In short, indexers allow you to access items in an array like fashion. More specifically, properties can either take parameters or not take parameters. The get
accessor methods for the properties accept no parameters. There are parameterful properties whose get
accessor methods accept one or more parameters and whose set
accessor methods accept two or more parameters. In C#, parameterful properties (called indexers) are exposed using array-like syntax. You can think of an indexer as a means to overload the [ ]
operator. Here is an example of a BitArray
class that allows array-like syntax to index into the set of bits maintained by an instance of the class:
using System;
public sealed class BitArray {
private Int32 m_numBits;
public BitArray(Int32 numBits) {
if (numBits <= 0)
throw new ArgumentOutOfRangeException("numBits must be > 0");
m_numBits = numBits;
m_byteArray = new Byte[(m_numBits + 7) / 8];
}
public Boolean this[Int32 bitPos] {
get {
if ((bitPos < 0) || (bitPos >= m_numBits))
throw new ArgumentOutOfRangeException("bitPos",
"bitPos must be between 0 and " + m_numBits);
return((m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0);
}
set {
if ((bitPos < 0) || (bitPos >= m_numBits))
throw new ArgumentOutOfRangeException("bitPos",
"bitPos must be between 0 and " + m_numBits);
if (value) {
m_byteArray[bitPos / 8] =
(Byte)(m_byteArray[bitPos / 8] | (1 << (bitPos % 8)));
}
else {
m_byteArray[bitPos / 8] =
(Byte)(m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8)));
}
}
}
}
public sealed class Program {
public static void Main() {
BitArray ba = new BitArray(14);
for (Int32 x = 0; x < 14; x++) {
ba[x] = (x % 2 == 0);
}
for (Int32 x = 0; x < 14; x++) {
Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
}
}
}
Here is the output:
Bit 0 is On
Bit 1 is Off
Bit 2 is On
Bit 3 is Off
Bit 4 is On
Bit 5 is Off
Bit 6 is On
Bit 7 is Off
Bit 8 is On
Bit 9 is Off
Bit 10 is On
Bit 11 is Off
Bit 12 is On
Bit 13 is Off
Before explaining the code example above, let us look at a more basic example to expand on the indexer. We will build a set of classes and compile them into DLLs using the ‘/target:library’ option of the csc.exe compiler. Here is an example called Car.cs. It will depend on a class file called Radio.cs. We will first declare the Radio
class:
using System;
public class Radio
{
public Radio()
{}
public void TurnOn(bool on)
{
if(on)
Console.WriteLine("How do you hear it...");
else
Console.WriteLine("Silence...");
}
}
This code compiles on the command line with C:\...\v2.0.50727>csc.exe /t:library Radio.cs.
Now, let us write a class called Car
:
using System;
public class Car
{
private int currSpeed;
private int maxSpeed;
private string petName;
bool dead;
private Radio theMusicBox;
public Car()
{
maxSpeed = 100;
dead = false;
theMusicBox = new Radio();
}
public Car(string name, int max, int curr)
{
currSpeed = curr;
maxSpeed = max;
petName = name;
dead = false;
theMusicBox = new Radio();
}
public void CrankTunes(bool state)
{
theMusicBox.TurnOn(state);
}
public void SpeedUp(int delta)
{
if(dead)
{
Console.WriteLine("{0} is out of order....", petName);
}
else
{
currSpeed += delta;
if(currSpeed >= maxSpeed)
{
Console.WriteLine("{0} has overheated...", petName);
dead = true;
}
else
Console.WriteLine("\tCurrSpeed = {0}", currSpeed);
}
}
public string PetName
{
get { return petName; }
set { petName = value;}
}
public int CurrSpeed
{
get { return currSpeed; }
set { currSpeed = value;}
}
public int MaxSpeed
{
get { return maxSpeed; }
set { maxSpeed = value;}
}
}
Note that an indexer’s set
accessor method also contains a hidden parameter, called value
in C#. This parameter indicates the new value desired for the indexed element. This file is compiled in the same way, the source code compiles into a DLL. Because it contains a Radio
object, the compiler must reference that DLL:
C:\..\v2.0.50727>csc /target:library /r:Radio.dll Car.cs
Now here is another class called Cars
(plural). This example depends on the previous class files, and will exemplify the indexer.
using System;
using System.Collections;
public class Cars : IEnumerator, IEnumerable
{
private Car[] carArray;
int pos = -1;
public Cars()
{
carArray = new Car[10];
}
public Car this[int pos]
{
get
{
if(pos < 0 || pos > 10)
throw new IndexOutOfRangeException("Wait a minute! Index out of range");
else
return (carArray[pos]);
}
set
{
carArray[pos] = value;
}
}
public bool MoveNext()
{
if(pos < carArray.Length)
{
pos++;
return true;
}
else
return false;
}
public void Reset()
{
pos = 0;
}
public object Current
{
get { return carArray[pos]; }
}
public IEnumerator GetEnumerator()
{
return (IEnumerator)this;
}
}
C# requires the keyword this
(this[...]
) as the syntax for expressing and indexer in some cases. This class file will only compile by using the references to the previously built DLLs:
C:\..\v2.0.50727>csc /t:library /r:Radio.dll /r:Car.dll Cars.cs
Now, we can build an application that references those DLLs (to make use of the methods) to use an indexer and iterate over the collection of objects to access the items in an array-like fashion:
using System;
using System.Collections;
public class CarApp
{
public static void Main()
{
Console.WriteLine("Basic array iteration...");
int[] myInts = {10, 9, 100, 432, 9874};
for(int j = 0; j < myInts.Length; j++)
Console.WriteLine("Index {0} = {1}", j, myInts[j]);
Cars carLot = new Cars();
carLot[0] = new Car("Lemon", 200, 0);
carLot[1] = new Car("BadOnGas", 90, 0);
carLot[2] = new Car("BMW", 30, 0);
Console.WriteLine("\nUsing indexer...");
for(int i = 0; i < 3; i++)
{
Console.WriteLine("Car number {0}:", i);
Console.WriteLine("Name: {0}", carLot[i].PetName);
Console.WriteLine("Max speed: {0}\n", carLot[i].MaxSpeed);
}
try
{
Console.WriteLine("Using IEnumerable...");
foreach (Car c in carLot)
{
Console.WriteLine("Name: {0}", c.PetName);
Console.WriteLine("Max speed: {0}\n", c.MaxSpeed);
}
}
catch{}
}
}
Notice that the for
loop construct uses the []
(bracket) operator to access each element. We compile this file on the command line with three references to classes that we have built:
C:\..\v2.0.50727>csc.exe /r:Radio.dll /r:Car.dll /r:Cars.dll CarApp.cs
The output:
Basic array iteration...
Index 0 = 10
Index 1 = 9
Index 2 = 100
Index 3 = 432
Index 4 = 9874
Using indexer...
Car number 0:
Name: Lemon
Max speed: 200
Car number 1:
Name: BadOnGas
Max speed: 90
Car number 2:
Name: BMW
Max speed: 30
Using IEnumerable...
Name: Lemon
Max speed: 200
Name: BadOnGas
Max speed: 90
Name: BMW
Max speed: 30
The best way to learn the concepts of delegates is by example, so I’ll write a very basic example. We will define a delegate by using the delegate
keyword:
delegate void PrintMessageDelegate(string name);
What we did was just define a signature of a method, nothing more. Now, let's create a method that matches that signature.
static void PrintMessage(string name)
{
Console.WriteLine("Hello {0}!", name);
}
Now we want to instantiate the delegate:
static void Main(string[] args)
{
PrintMessageDelegate pm = new PrintMessageDelegate(PrintMessage);
pm("Skippy");
}
Now, here is the entire file:
using System;
using System.Collections.Generic;
using System.Text;
class Program {
delegate void PrintMessageDelegate(string name);
static void PrintMessage (string name)
{
Console.WriteLine("Hello {0}!", name);
}
static void Main(string[] args)
{
PrintMessageDelegate pm = new PrintMessageDelegate(PrintMessage);
pm("Skippy");
}
}
Output: Hello Skippy!
An important note: An aspect of this very basic example demonstrates how the CLR requires that every object must be created using the new
operator. Here is what the new
operator does:
- It calculates the number of bytes required by all instance fields defined by the type and all of its base types up to and including
System.Object
. Every object on the heap requires some additional members - called the type object pointer and the sync block index - used by the CLR to manage the object. The bytes for these additional members are added to the size of the object.
- It allocates memory for the object by allocating the number of bytes required for the specified type from the managed heap; all of these bytes are then set to zero.
- It initiates the object's type object pointer and sync block index member.
- The type's instance constructor is called, passing it any arguments specified in the call to
new
. After new
has performed these operations, it returns a reference (or pointer) to the newly created object. In the preceding example, this reference was saved in the variable pm
(which functioned as an instance of our delegate that when invoked, will point to our method PrintMessage
). Below is an example of an anonymous delegate. Notice that in the example above, the method that our delegate has targeted has a name. Rather than pointing at it (PrintMessage
), let's assign it an anonymous delegate (anonymous delegates were introduced in .NET 2.0).
PrintMessageDelegate pm = delegate(string name);
Use the delegate
keyword as if you were writing a method, but you neither specify a name nor a return value. It will match that up depending on what is in the code body of the method definition. That parameter list is the same as the signature of the delegate that your assigning it to.
using System;
using System.Collections.Generic;
using System.Text;
class TestDelegate
{
delegate void PrintMessageDelegate(string name);
static PrintMessageDelegate GetMessagePrinter(string message)
{
return delegate(string name)
{
Console.WriteLine(message, name);
};
}
static void Main(string[] args)
{
PrintMessageDelegate pm = GetMessagePrinter("Hello {0}!");
pm("World");
pm("Joe");
pm("David");
pm = GetMessagePrinter("Goodbye {0}.");
pm("World");
}
}
A Closer Look at Delegates
Under the hood, the keyword delegate
represents a class deriving from the System.MultiCastDelegate
namespace. Thus, if you write:
public delegate void PlayAcidRock( object Jimmy Hendricks, int volume);
the C# compiler produces a new class that looks something like the following:
public class PlayAcidRock : System.MultiCastDelegate
{
PlayAcidRock(object target, int ptr);
public void virtual Invoke(object Jimmy Hendricks, int volume);
public virtual IAsyncResult BeginInvoke(object Jimmy Hendricks,
int volume, AsyncCallback cb, object o);
public virtual void EndInvoke(IAsynchResult result);
}
The Asynchronous Program Model uses “Begin and End” methods that pre-pend to a method name in order for code to complete during an I/O operation without having to wait for the I/O operation to complete, as you have to with synchronous code. On that note, we will update the previous code used to describe interfaces and inheritance for class construction. We will write an application that references the radio.dll, and we will rearrange the Car.cs file, and the Garage.cs file to then compile into DLLs that will be referenced by our application. Here is the Car.cs file:
using System;
public class Car : Object
{
public delegate void CarDelegate(Car c);
public class Radio
{
public Radio(){}
public void TurnOn(bool on)
{
if(on)
Console.WriteLine("Jamming...");
else
Console.WriteLine("Quiet time...");
}
}
private int currSpeed;
private int maxSpeed;
private string petName;
private bool dead;
private bool isDirty;
private bool shouldRotate;
private Radio theMusicBox;
public bool Dirty
{
get{ return isDirty; }
set{ isDirty = value; }
}
public bool Rotate
{
get{ return shouldRotate; }
set{ shouldRotate = value; }
}
public Car()
{
maxSpeed = 100;
dead = false;
theMusicBox = new Radio();
}
public Car(string name, int max, int curr, bool dirty, bool rotate)
{
currSpeed = curr;
maxSpeed = max;
petName = name;
dead = false;
isDirty = dirty;
shouldRotate = rotate;
theMusicBox = new Radio();
}
public void CrankTunes(bool state)
{
theMusicBox.TurnOn(state);
}
public void SpeedUp(int delta)
{
if(dead)
Console.WriteLine("Car is already dead...");
else
{
currSpeed += delta;
if(currSpeed >= maxSpeed)
dead = true;
else
Console.WriteLine("\tCurrSpeed = " + currSpeed);
}
}
}
To compile on the command line: C:.Net> csc.exe /target:library Car.cs.
Here is the Garage.cs file:
using System;
using System.Collections;
public class Garage
{
ArrayList theCars = new ArrayList();
public Garage()
{
theCars.Add(new Car("Viper", 100, 0, true, false));
theCars.Add(new Car("Fred", 100, 0, false, false));
theCars.Add(new Car("BillyBob", 100, 0, false, true));
theCars.Add(new Car("Bart", 100, 0, true, true));
theCars.Add(new Car("Stan", 100, 0, false, true));
}
public void ProcessCars(Car.CarDelegate proc)
{
foreach(Delegate d in proc.GetInvocationList())
{
Console.WriteLine("***** Calling: {0} *****",
d.Method.ToString());
}
if(proc.Target != null)
Console.WriteLine("\n-->Target: " + proc.Target.ToString());
else
Console.WriteLine("\n-->Target is a static method");
foreach(Car c in theCars)
proc(c);
Console.WriteLine();
}
}
We compile this file in the same way, as a DLL. Now, here is our application that will reference Car.dll and Garage.dll:
using System;
public class ServiceDept
{
public void WashCar(Car c)
{
if(c.Dirty)
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean...");
}
public void RotateTires(Car c)
{
if(c.Rotate)
Console.WriteLine("Tires have been rotated");
else
Console.WriteLine("Don't need to be rotated...");
}
}
public class CarApp
{
public static int Main(string[] args)
{
Garage g = new Garage();
ServiceDept sd = new ServiceDept();
Car.CarDelegate wash = new Car.CarDelegate(sd.WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(sd.RotateTires);
MulticastDelegate d = wash + rotate;
g.ProcessCars((Car.CarDelegate)d);
Delegate washOnly = MulticastDelegate.Remove(d, rotate);
g.ProcessCars((Car.CarDelegate)washOnly);
return 0;
}
}
To compile: csc.exe /r:Radio.dll /r:Car.dll /r:Garage.dll Application.cs.
Here is the output:
If you examine the Main
method, you’ll see that it begins by creating an instance of the Garage
type. This class has been configured to delegate all work to other named static functions. So, when we write the following:
g.ProcessCars(new Car.CarDelegate)WashCar));
what you are saying is “Add a pointer to the WashCar
function to the CarDelegate
type, and pass this delegate to Gargage.ProcessCars()
”. So, on the surface, delegates, once one spends time working with them, seem easy to use: you use the C# delegate
keyword, you construct instances by using the new
operator, and you invoke the callback by using the familiar method-call syntax. But, they can get complicated, which is why a lot of C# instructional papers place them in the “Advanced C# Features” section. But, some of these complexities can be clarified. Chaining is a set or collection of delegate objects, and it provides the ability to invoke, or call, all of the methods represented by the delegates in the set. To demonstrate how important chaining is, consider this referenced code that gives a conceptual understanding about delegates:
using System;
using System.Windows.Forms;
using System.IO;
internal delegate void Feedback(Int32 value);
public sealed class Program {
public static void Main() {
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
private static void StaticDelegateDemo() {
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
}
private static void InstanceDelegateDemo() {
Console.WriteLine("----- Instance Delegate Demo -----");
Program p = new Program();
Counter(1, 3, new Feedback(p.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(Program p) {
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program p) {
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value) {
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value) {
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value) {
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
To understand chaining, consider the ChainDelegateDemo1method
that appears in this code from Jeffrey Richter’s book: The CLR via C#, 2nd Edition:
private static void ChainDelegateDemo1(Program p) {
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
In this method, after the Console.WriteLine
method, he constructs three delegate objects, and have the variables fb1
, fb2
, and fb3
refer to each object:
_target null
fb1 ---------à _method ptr FeedbackToConsole
-invocation list null
_target null
fb2 ---------à _method ptr FeedbackToMsgBox
-invocation list null
_target .----------------------àProgram Object
fb3 ---------à _method ptr FeedbackToFile
-invocation list null
The reference variable to a Feedback
delegate object, fbChain
, is intended to refer to a chain or set of delegate objects that wrap methods that can be called back. Herein, we find an underlying principle: we learn how to build a chain of objects and how to invoke all of those objects in that chain. All items in the chain are invoked because the delegate type’s Invoke
method includes code to iterate through all items in the array, invoking each item. Just like collections - classes used for grouping and managing related objects that allow you to iterate over those objects - are a strong tool. Computers are good at dealing with large amounts of data, so it is important to store that data in an orderly way. But, what good is having the data stored if it cannot be accessed? The generality lies in the class construction techniques of accessing class items by inheritance and interfaces.