C# Class Construction Techniques






4.12/5 (9 votes)
An article that describes interfaces, indexers, and delegates.
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
{
}
//Add the interface declaration:
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;
// Constructor that allocates the byte array and sets all bits to 0
public BitArray(Int32 numBits) {
// Validate arguments first.
if (numBits <= 0)
throw new ArgumentOutOfRangeException("numBits must be > 0");
// Save the number of bits.
m_numBits = numBits;
// Allocate the bytes for the bit array.
m_byteArray = new Byte[(m_numBits + 7) / 8];
}
// This is the indexer.
public Boolean this[Int32 bitPos] {
// This is the index property’s get accessor method.
get {
// Validate arguments first
if ((bitPos < 0) || (bitPos >= m_numBits))
throw new ArgumentOutOfRangeException("bitPos",
"bitPos must be between 0 and " + m_numBits);
// Return the state of the indexed bit.
return((m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0);
}
// This is the index property’s set accessor method.
set {
if ((bitPos < 0) || (bitPos >= m_numBits))
throw new ArgumentOutOfRangeException("bitPos",
"bitPos must be between 0 and " + m_numBits);
if (value) {
// Turn the indexed bit on.
m_byteArray[bitPos / 8] =
(Byte)(m_byteArray[bitPos / 8] | (1 << (bitPos % 8)));
}
else {
// Turn the indexed bit off.
m_byteArray[bitPos / 8] =
(Byte)(m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8)));
}
}
}
}
public sealed class Program {
public static void Main() {
// Allocate a BitArray that can hold 14 bits.
BitArray ba = new BitArray(14);
// Turn all the even-numbered bits on by calling the set accessor.
for (Int32 x = 0; x < 14; x++) {
ba[x] = (x % 2 == 0);
}
// Show the state of all the bits by calling the get accessor.
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
{
// Internal state data.
private int currSpeed;
private int maxSpeed;
private string petName;
// Out of gas?
bool dead;
// A car has a radio
private Radio theMusicBox;
public Car()
{
maxSpeed = 100;
dead = false;
// Outer class creates the inner class(es)
// upon start-up.
// NOTE: If we did not, theMusicBox would
// begin as a null reference.
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)
{
// Tell the radio play (or not).
// Delegate request to inner object.
theMusicBox.TurnOn(state);
}
public void SpeedUp(int delta)
{
// If the car is dead, just say so...
if(dead)
{
Console.WriteLine("{0} is out of order....", petName);
}
else // Not dead, speed up.
{
currSpeed += delta;
if(currSpeed >= maxSpeed)
{
Console.WriteLine("{0} has overheated...", petName);
dead = true;
}
else
Console.WriteLine("\tCurrSpeed = {0}", currSpeed);
}
}
// Properties.
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
{
// This class maintains an array of cars.
private Car[] carArray;
// Current position in array.
int pos = -1;
public Cars()
{
carArray = new Car[10];
}
// The indexer.
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;
}
}
// Implementation of IEnumerator.
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]; }
}
// This must be present in order to let the foreach
// expression to iterate over our array.
// IEnumerable implemtation.
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};
// Use the [] operator to access each element.
for(int j = 0; j < myInts.Length; j++)
Console.WriteLine("Index {0} = {1}", j, myInts[j]);
Cars carLot = new Cars();
// Add to car array.
carLot[0] = new Car("Lemon", 200, 0);
carLot[1] = new Car("BadOnGas", 90, 0);
carLot[2] = new Car("BMW", 30, 0);
// Now get and display each.
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
{ // Iterate using IEnumerable.
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);
// add a standard routine: betcha never seen that one
}
Now we want to instantiate the delegate:
static void Main(string[] args)
{
// so we want to create an instance of our delegate to target our PrintMessage method
// which will be invoked whenver we invoke our delegate
// we'll call our instance pm
PrintMessageDelegate pm = new PrintMessageDelegate(PrintMessage);
// the instance of our delegate wil target our method PrintMessage by creating an object
//whenever we invoke pm, it will point to our PrintMessage method
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)
{
// we'll call our instance pm
PrintMessageDelegate pm = new PrintMessageDelegate(PrintMessage);
// the instance of our delegate wil target our method PrintMessage
// by creating an object whenever we invoke pm, it will point
// to our PrintMessage method
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
. Afternew
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 variablepm
(which functioned as an instance of our delegate that when invoked, will point to our methodPrintMessage
). 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);
// The Synchronous Invoke() method
public void virtual Invoke(object Jimmy Hendricks, int volume);
// you also receive an asynchronous version of the same callback
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
{
// This delegate encapsulates a function pointer
// to some method taking a Car and returning void.
// This is represented as Car$CarDelegate (e.g. is nested).
public delegate void CarDelegate(Car c);
// The nested car type.
public class Radio
{
public Radio(){}
public void TurnOn(bool on)
{
if(on)
Console.WriteLine("Jamming...");
else
Console.WriteLine("Quiet time...");
}
}
// Internal state data.
private int currSpeed;
private int maxSpeed;
private string petName;
// Outta gas?
private bool dead;
// NEW! Are we in need of a wash?
private bool isDirty;
// NEW! Are we in need of a wash?
private bool shouldRotate;
// A car has-a radio.
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;
// Outer class creates the inner class(es)
// upon start-up.
// NOTE: If we did not, theMusicBox would
// begin as a null reference.
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)
{
// Tell the radio play (or not).
// Delegate request to inner object.
theMusicBox.TurnOn(state);
}
public void SpeedUp(int delta)
{
// If the car is dead, just say so...
if(dead)
Console.WriteLine("Car is already dead...");
else // Not dead, speed up.
{
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;
// This delegate encapsulates a function pointer
// to some method taking a Car and returning void.
// public delegate void CarDelegate(Car c);
public class Garage
{
// We have some cars.
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));
}
// This method takes a CarDelegate as a parameter.
// Therefore! 'proc' is nothing more than a function pointer...
public void ProcessCars(Car.CarDelegate proc)
{
// Where are we passing the call?
foreach(Delegate d in proc.GetInvocationList())
{
Console.WriteLine("***** Calling: {0} *****",
d.Method.ToString());
}
// Am I calling an object's method or a static method?
if(proc.Target != null)
Console.WriteLine("\n-->Target: " + proc.Target.ToString());
else
Console.WriteLine("\n-->Target is a static method");
// Now call method for each car.
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;
// A helper class
public class ServiceDept
{
// A target for the delegate.
public void WashCar(Car c)
{
if(c.Dirty)
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean...");
}
// Another target for the delgate.
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
{
// A target for the delegate.
/*
public static void WashCar(Car c)
{
if(c.Dirty)
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean...");
}
// Another target for the delgate.
public static void RotateTires(Car c)
{
if(c.Rotate)
Console.WriteLine("Tires have been rotated");
else
Console.WriteLine("Don't need to be rotated...");
}
*/
public static int Main(string[] args)
{
// Make the garage.
Garage g = new Garage();
// Make the service department.
ServiceDept sd = new ServiceDept();
// Wash all dirty cars.
//g.ProcessCars(new Car.CarDelegate(sd.WashCar));
// Rotate the tires.
//g.ProcessCars(new Car.CarDelegate(sd.RotateTires));
// Create two new delegates.
Car.CarDelegate wash = new Car.CarDelegate(sd.WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(sd.RotateTires);
// Store the new delegate for later use.
MulticastDelegate d = wash + rotate;
// Send the new delegate into the ProcessCars() method.
g.ProcessCars((Car.CarDelegate)d);
// Remove the rotate pointer.
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:
// wash all those dirty cars
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;
// Declare a delegate type; instances refer to a method that
// takes an Int32 parameter and returns void.
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)); // "Program." is optional
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 any callbacks are specified, call them
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.