- C# and Java: Two Sides of the Same Coin?
- A Simple C# Interface
- C# Interfaces and Inheritance
- Conclusion
C# Interfaces and Inheritance
Using interfaces as in Listing 1 is useful because it allows you to define little pockets of functionality. Any implementing classes can provide the code to fulfill the interface contract. However, the real power of interfaces comes from inheritance, as illustrated in Listing 2.
Listing 2 Interfaces and inheritance.
public interface IMyBaseInterface { void DoSomethingBasic(); } public interface IMyInterface : IMyBaseInterface { void DoSomethingOrOther(); } class MainClass : IMyInterface { public void DoSomethingOrOther() { Console.WriteLine("Now doing something"); Console.ReadLine(); } public void DoSomethingBasic() { Console.WriteLine("Now doing something basic"); Console.ReadLine(); } public static void Main(string[] args) { Console.WriteLine("Hello World!"); MainClass mainClass = new MainClass(); mainClass.DoSomethingOrOther(); mainClass.DoSomethingBasic(); }
Notice the way in which the MainClass class implicitly acquires the base interface IMyBaseInterface. This is because the other interface automatically pulls that interface along with it, and the implementing class must then provide code for the methods in both interfaces. This example illustrates the important point that interfaces are lightweight contract entities, and client code must provide the contents; in other words, the method code.
It's perhaps a little surprising that C# interfaces can also be treated as parameters, as shown in Listing 3.
Listing 3 An interface treated as a parameter.
Console.WriteLine("Hello World!"); MainClass mainClass = new MainClass(); mainClass.DoSomethingOrOther(); mainClass.DoSomethingBasic(); IMyBaseInterface iMyBaseInterface; iMyBaseInterface = mainClass; iMyBaseInterface.DoSomethingBasic();
In Listing 3, we can treat the IMyBaseInterface interface as a parameter because the MainClass class implements this interface. Because of this relationship, we know that an object or instance of this class (such as the mainClass object) also implements the interface. This gives rise to the somewhat unexpected syntax in Listing 3.
Another important aspect of interfaces is versioning. If you define an interface and one or more classes that implement it, it's a good practice to include version data. Listing 4 shows a modified interface that includes a version number.
Listing 4 A versioned interface.
public interface IMyInterface : IMyBaseInterface { void DoSomethingOrOther(); byte Version{get;} }
To use this new interface, you simply implement the accessor in the MainClass class (see Listing 5).
Listing 5 A versioned interface.
class MainClass : IMyInterface { public void DoSomethingOrOther() { Console.WriteLine("Now doing something"); Console.ReadLine(); } public byte Version { get { return 1;} } public void DoSomethingBasic() { Console.WriteLine("Now doing something basic"); Console.ReadLine(); } public static void Main(string[] args) { Console.WriteLine("Hello World!"); MainClass mainClass = new MainClass(); mainClass.DoSomethingOrOther(); mainClass.DoSomethingBasic(); Console.WriteLine("Version data: {0}", mainClass.Version); } }
Versioning interfaces in this way allows for the interface contract to be upheld. Rather than just changing an interface, you can inherit from the interface and create a new one. Also, the version number can be updated in the classes that implement the extended interface. Let's see how this might work in practice.
Suppose we have our existing interfaces as in Listings 1–5, and now we want to add another method to IMyInterface. If the existing version of the code has been released, we should assume that there are classes that implement the interfaces. If we change the interfaces now, the existing client code will break—that is, it will no longer compile. The correct way to update the interface is to create a new one (called IMyUpdatedInterface in Listing 6) that derives from the interface we want to modify (in this case, IMyBaseInterface).
Listing 6 Adding code to an existing interface.
public interface IMyBaseInterface { void DoSomethingBasic(); } public interface IMyInterface : IMyBaseInterface { void DoSomethingOrOther(); byte Version{get;} } public interface IMyUpdatedInterface : IMyInterface { void DoSomethingNew(); }
With this change, any client code that needs the new interface can implement it without breaking existing code. The interface version number is entirely optional, but it might be useful as a marker.
What about situations where your C# codebase has many interfaces, and you want to determine which interfaces a given object implements? A few mechanisms exist for handling this problem. We'll look at those next.
Determining Whether a Type Supports an Interface
Suppose you want find out whether a given object supports one of your interfaces. This is possible using the as keyword (see Listing 7).
Listing 7 Determining whether a type supports a given interface.
class MainClass : IMyUpdatedInterface { public void DoSomethingOrOther() { Console.WriteLine("Now doing something"); Console.ReadLine(); } public byte Version { get { return 1;} } public void DoSomethingNew() { Console.WriteLine("Now doing something new"); Console.ReadLine(); } public void DoSomethingBasic() { Console.WriteLine("Now doing something basic"); Console.ReadLine(); } public static void Main(string[] args) { Console.WriteLine("Hello World!"); MainClass mainClass = new MainClass(); mainClass.DoSomethingOrOther(); mainClass.DoSomethingBasic(); Console.WriteLine("Version data: {0}", mainClass.Version); IMyBaseInterface iMyBaseInterface; iMyBaseInterface = mainClass; iMyBaseInterface.DoSomethingBasic(); mainClass.DoSomethingNew(); IMyUpdatedInterface iUpdatedIf = mainClass as IMyUpdatedInterface; if (iUpdatedIf != null) { Console.WriteLine("Interface is supported"); iUpdatedIf.DoSomethingBasic(); } else Console.WriteLine("Interface not supported"); } }
Notice that I changed the very first line of Listing 7 so that the class MainClass implements IMyUpdatedInterface, to ensure that the object of MainClass implements the required interface. Without this code change, the attempt t o obtain a reference to the interface would fail.
Another approach to the same problem is to use the is keyword, as illustrated in Listing 8.
Listing 8 Determining interface support by using the is keyword.
IMyUpdatedInterface iUpdatedIf1 = mainClass; if (iUpdatedIf1 is IMyUpdatedInterface) { Console.WriteLine("This interface is also supported"); iUpdatedIf.DoSomethingBasic(); } else Console.WriteLine("Interface not supported");
The use of the is keyword is a handy alternative to the as keyword. Whichever method you choose, the important point is that you don't risk throwing an exception. The invalid cast in Listing 9 would cause a System.InvalidCastException to be thrown.
Listing 9 This invalid cast causes an exception.
IMyCompletelyNewInterface iItf = (IMyCompletelyNewInterface) mainClass;
In Listing 9, the interface IMyCompletelyNewInterface is not implemented by the MainClass class, so we get an exception. However, using the techniques with the is or as keywords avoids the exception problem. In general, it's far better to avoid throwing exceptions when you can. Exception-free code tends to run faster and requires less code (try-catch blocks).
C# Inheritance
One of the main merits of inheritance is the way you can use it to hide complex details. A properly designed class hierarchy helps in pushing common code up the way; in other words, the subclasses can inherit only what they need. Common code can live at the top of the hierarchy, while more specific code lives lower down the hierarchy. This separation helps to reduce clutter in subclasses. An additional benefit is less need for duplicated code. One important design question occurs, though: Why would you use C# interfaces instead of abstract base classes? Let's consider that issue next.
Inheritance: C# Interfaces and C# Abstract Classes
Perhaps the nicest thing about C# interfaces is their very lightweight nature. You simply define an interface to represent some item of functionality. There's no need to define any additional boilerplate code. The situation isn't the same for abstract base classes, because a class by nature tends to be a more heavyweight entity.
Listing 10 illustrates an abstract base class and an implementing concrete class.
Listing 10 Abstract base class and concrete class.
public abstract class AnAbstractClass { public abstract void DoSomethingBasic(); public abstract void DoSomethingOrOther(); } public class AConcreteClass : AnAbstractClass { public override void DoSomethingBasic() { Console.WriteLine("Now doing something in the concrete class"); Console.ReadLine(); } public override void DoSomethingOrOther() { Console.WriteLine("Now doing something or other in the concrete class"); Console.ReadLine(); } }
As is the case with interfaces, abstract base classes are placeholders for code. The abstract methods must be implemented by subclasses—in this case, AConcreteClass. Why would you use an abstract base class instead of an interface? Well, the choice is partly style, but it also depends on whether you want a more heavyweight base entity. If you want to define a very specific type of base entity, an abstract base class may be a good choice. One point to bear in mind, however, is that the base class may itself inherit from other classes.