Polymorphism is available in C# not only via inheritance (as discussed in Chapter 7) but also via interfaces. Unlike abstract classes, interfaces could not include any implementation—until C# 8.0. (But even in C# 8.0, it is questionable whether you should use this capability except for “versioning” the interfaces.) Like abstract classes, however, interfaces define a set of members that callers can rely on being implemented.
By implementing an interface, a type defines its capabilities. The interface implementation relationship is a “can do” relationship. The type can do what the interface requires an implementing type to do. The interface defines the contract between the types that implement the interface and the code that uses the interface. Types that implement interfaces must declare methods with the same signatures as the methods declared by the implemented interfaces. This chapter discusses implementing and using interfaces. It concludes with default implemented members on interfaces, and the host of paradigms (and complexities) that this new feature introduces.
Interfaces are useful because—unlike abstract classes—they enable the complete separation of implementation details from services provided. For a real-world example, consider the “interface” that is an electrical wall socket. How the electrical power gets to the socket is an implementation detail: It might be generated by chemical, nuclear, or solar energy; the generator might be in the next room or far away; and so on. The socket provides a “contract”: It agrees to supply a particular voltage at a specific frequency, and in return it requires that the appliance using that interface provide a compatible plug. The appliance need not care about the implementation details that get power to the socket; all it needs to worry about is providing a compatible plug.
Consider the following example: A huge number of file compression formats are available (.zip, .7-zip, .cab, .lha, .tar, .tar.gz, .tar.bz2, .bh, .rar, .arj, .arc, .ace, .zoo, .gz, .bzip2, .xxe, .mime, .uue, and .yenc, just to name a few). If you created classes for each compression format, you could end up with different method signatures for each compression implementation and no ability to apply a standard calling convention across them. Alternatively, the desired method could be declared as abstract in the base class. However, deriving from a common base class uses up a class’s one and only opportunity for inheritance. It is unlikely that there is any code common to the various compression implementations that can be put in the base class, thereby ruling out the potential benefits of having a base class implementation. The key point is that base classes let you share implementation along with the member signatures, whereas interfaces allow you to share the member signatures without necessarily sharing the implementation.
Instead of sharing a common base class, each compression class needs to implement a common interface. Interfaces define the contract that a class supports to interact with the other classes that expect the interface. If all the classes implemented the IFileCompression interface and its Compress() and Uncompress() methods, the code for calling the algorithm on any particular compression class would simply involve a conversion to the IFileCompression interface and a call to the members. The result is polymorphism because each compression class has the same method signature but individual implementations of that signature.
The IFileCompression interface shown in Listing 8.1 is an example of an interface implementation. By convention—a convention so strong it is universal—the interface name is PascalCase with a capital “I” prefix.
IFileCompression defines the methods a type must implement to be used in the same manner as other compression-related classes. The power of interfaces is that they grant the ability to callers to switch among implementations without modifying the calling code.
Prior to C# 8.0, one of the key characteristics of an interface was that it had no implementation and no data (fields). Method declarations in an interface always had a single semicolon in place of curly braces after the declaration. Properties, while looking like automatically implemented properties, had no backing fields. In fact, fields (data) could not appear in an interface declaration either.
Many of these rules were relaxed in C# 8.0 for the purposes of allowing interfaces to have some level of restricted changes after publishing. However, until the section “Interface Versioning in C# 8.0 or Later” in this chapter, we will ignore the new capabilities and discuss interfaces for the purposes of establishing polymorphism. This is where the real power of interfaces lies, and it is easier to discuss them in that context before opening up the new capabilities and describing the scenario for making an exception. So let’s stick with the simplification that interfaces cannot have any implementation (without mentioning C# 8.0), and postpone the removal of that restriction until we explore the C# 8.0 capabilities.
The declared members of an interface describe the members that must be accessible on an implementing type. The purpose of non-public members is to make those members inaccessible to other code. Therefore, C# does not allow access modifiers on interface members; instead, it automatically defines them as public. 1