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” and 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.
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.
interface IFileCompression { void Compress(string targetFileName, string[] fileList); void Uncompress( string compressedFileName, string expandDirectoryName); }
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.
Begin 8.0
Prior to C# 8.0, one of the key characteristics of an interface was that it had no implementation and no data. 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 even 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 nonpublic 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.
End 8.0
Consider another example, as shown in Listing 8.2: IListable
defines the members that a class needs to support if the ConsoleListControl
class is to display it. As such, any class that implements IListable
can use the ConsoleListControl
to display itself. The IListable
interface requires a read-only property, CellValues
.
interface IListable { // Return the value of each cell in the row string?[] CellValues { get; } } public abstract class PdaItem { public PdaItem(string name) {f Name = name; } public virtual string Name{get;set;} } class Contact : PdaItem, IListable { public Contact(string firstName, string lastName, string address, string phone) : base(GetName(firstName, lastName)) { FirstName = firstName; LastName = lastName; Address = address; Phone = phone; } protected string LastName { get; } // ... protected string FirstName { get; } public string? Address { get; } public string? Phone { get; } public static string GetName(string firstName, string lastName) => $"{ firstName } { lastName }"; public string?[] CellValues { get { return new string?[] { FirstName, LastName, Phone, Address }; } } public static string[] Headers { get { return new string[] { "First Name", "Last Name ", "Phone ", "Address " }; } } // ... } class Publication : IListable { public Publication(string title, string author, int year) { Title = title; Author = author; Year = year; } public string Title { get; } public string Author { get; } public int Year { get; } public string?[] CellValues { get { return new string?[] { Title, Author, Year.ToString() }; } } public static string[] Headers { get { return new string[] { "Title ", "Author ", "Year" }; } } // ... } class Program { public static void Main() { Contact[] contacts = new Contact[] { new Contact( "Dick", "Traci", "123 Main St., Spokane, WA 99037", "123-123-1234"), new Contact( "Andrew", "Littman", "1417 Palmary St., Dallas, TX 55555", "555-123-4567"), new Contact( "Mary", "Hartfelt", "1520 Thunder Way, Elizabethton, PA 44444", "444-123-4567"), new Contact( "John", "Lindherst", "1 Aerial Way Dr., Monteray, NH 88888", "222-987-6543"), new Contact( "Pat", "Wilson", "565 Irving Dr., Parksdale, FL 22222", "123-456-7890"), new Contact( "Jane", "Doe", "123 Main St., Aurora, IL 66666", "333-345-6789") }; // Classes are implicitly convertible to // their supported interfaces ConsoleListControl.List(Contact.Headers, contacts); Console.WriteLine(); Publication[] publications = new Publication[3] { new Publication( "The End of Poverty: Economic Possibilities for Our Time", "Jeffrey Sachs", 2006), new Publication("Orthodoxy", "G.K. Chesterton", 1908), new Publication( "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 1979) }; ConsoleListControl.List( Publication.Headers, publications); } } class ConsoleListControl { public static void List(string[] headers, IListable[] items) { int[] columnWidths = DisplayHeaders(headers); for (int count = 0; count < items.Length; count++) { string?[] values = items[count].CellValues; DisplayItemRow(columnWidths, values); } } /// <summary>Displays the column headers</summary> /// <returns>Returns an array of column widths</returns> private static int[] DisplayHeaders(string[] headers) { // ... } private static void DisplayItemRow( int[] columnWidths, string?[] values) { // ... } }
The results of Listing 8.2 appear in Output 8.1.
Output 8.1
First Name Last Name Phone Address Dick Traci 123-123-1234 123 Main St., Spokane, WA 99037 Andrew Littman 555-123-4567 1417 Palmary St., Dallas, TX 55555 Mary Hartfelt 444-123-4567 1520 Thunder Way, Elizabethton, PA 44444 John Lindherst 222-987-6543 1 Aerial Way Dr., Monteray, NH 88888 Pat Wilson 123-456-7890 565 Irving Dr., Parksdale, FL 22222 Jane Doe 333-345-6789 123 Main St., Aurora, IL 66666 Title Author Year The End of Poverty: Economic Possibilities for Our Time Jeffrey Sachs 2006 Orthodoxy G.K. Chesterton 1908 The Hitchhiker's Guide to the Galaxy Douglas Adams 1979
In Listing 8.2, the ConsoleListControl
can display seemingly unrelated classes (Contact
and Publication
). Any class can be displayed provided that it implements the required interface. As a result, the ConsoleListControl.List()
method relies on polymorphism to appropriately display whichever set of objects it is passed. Each class has its own implementation of CellValues
, and converting a class to IListable
still allows the particular class’s implementation to be invoked.
Declaring a class to implement an interface is similar to deriving from a base class: The implemented interfaces appear in a comma-separated list along with the base class. The base class specifier (if there is one) must come first, but otherwise order is not significant. Classes can implement multiple interfaces but may derive directly from only one base class. An example appears in Listing 8.3.
public class Contact : PdaItem, IListable, IComparable { // ... #region IComparable Members /// <summary> /// /// </summary> /// <param name="obj"></param> /// <returns> /// Less than zero: This instance is less than obj /// Zero This instance is equal to obj /// Greater than zero This instance is greater than obj /// </returns> public int CompareTo(object? obj) => obj switch { null => 1, Contact contact when ReferenceEquals(this, contact) => 0, Contact { LastName: string lastName } when LastName.CompareTo(lastName) != 0 => LastName.CompareTo(lastName), Contact { FirstName: string firstName } when FirstName.CompareTo(firstName) != 0 => FirstName.CompareTo(firstName), Contact _ => 0, _ => throw new ArgumentException( $"The parameter is not a value of type { nameof(Contact) }", nameof(obj)) }; #endregion #region IListable Members string?[] IListable.CellValues { get { return new string?[] { FirstName, LastName, Phone, Address }; } } #endregion }
Once a class declares that it implements an interface, all (abstract1) members of the interface must be implemented. An abstract class is permitted to supply an abstract implementation of an interface member. A non-abstract implementation may throw a NotImplementedException
type exception in the method body, but an implementation of the member must always be supplied.
1. The capability of adding nonabstract members to an interface was added in C# 8.0 but is essentially ignored until the end of the chapter.
One important characteristic of interfaces is that they can never be instantiated; you cannot use new
to create an interface, so interfaces do not have instance constructors or finalizers. Interface instances are available only by instantiating a type that implements the interface. Furthermore, interfaces cannot include static members.2 One key interface purpose is polymorphism, and polymorphism without an instance of the implementing type has little value.
2. Before C# 8.0.
Each (non-implemented3) interface member is abstract, forcing the derived class to implement it. Therefore, it is not possible to use the abstract modifier on interface members explicitly.4
3. Only available in C# 8.0 or later.
4. Before C# 8.0.
When implementing an interface member in a type, there are two ways to do so: explicitly or implicitly. So far, we’ve seen only implicit implementations, where the type member that implements the interface member is a public member of the implementing type.
Explicitly implemented methods are available only by calling them through the interface itself; this is typically achieved by casting an object to the interface. For example, to call IListable.CellValues
in Listing 8.4, you must first cast the contact to IListable
because of CellValues
’ explicit implementation.
string?[] values; Contact contact = new Contact("Inigo Montoya"); // ... // ERROR: Unable to call CellValues() directly // on a contact // values = contact.CellValues; // First cast to IListable values = ((IListable)contact).CellValues; // ...
The cast and the call to CellValues
occur within the same statement in this case. Alternatively, you could assign contact
to an IListable
variable before calling CellValues
.
To declare an explicit interface member implementation, prefix the member name with the interface name (see Listing 8.5).
public class Contact : PdaItem, IListable, IComparable { // ... #region IListable Members string?[] IListable.CellValues { get { return new string?[] { FirstName, LastName, Phone, Address }; } } #endregion }
Listing 8.5 implements CellValues
explicitly by prefixing the property name with IListable
. Furthermore, since explicit interface implementations are directly associated with the interface, there is no need to modify them with virtual
, override
, or public
. In fact, these modifiers are not allowed. The method is not treated as a public member of the class, so marking it as public
would be misleading.
Note that even though the override
keyword is not allowed on an interface, we will still use the term “override” when referring to members that implement the interface-defined signature.
Notice that CompareTo()
in Listing 8.5 does not include the IComparable
prefix; it is implemented implicitly. With implicit member implementation, it is necessary only for the member to be public and for the member’s signature to match the interface member’s signature. Interface member implementation does not require use of the override
keyword or any indication that this member is tied to the interface. Furthermore, since the member is declared just like any other class member, code that calls implicitly implemented members can do so directly, just as it would any other class member:
result = contact1.CompareTo(contact2);
In other words, implicit member implementation does not require a cast because the member is not hidden from direct invocation on the implementing class.
Many of the modifiers disallowed on an explicit member implementation are required or are optional on an implicit implementation. For example, implicit member implementations must be public
. Furthermore, virtual
is optional, depending on whether derived classes may override the implementation. Eliminating virtual
will cause the member to behave as though it is sealed
.
The key difference between implicit and explicit member interface implementation lies not in the syntax of the method declaration but rather in the ability to access the method by name through an instance of the type rather than via the interface.
When building a class hierarchy, it’s desirable to model real-world “is a” relationships—a giraffe is a mammal, for example. These are semantic relationships. Interfaces are often used to model mechanism relationships. A PdaItem
“is not a comparable,” but it might well be IComparable
. This interface has nothing to do with the semantic model; instead, it’s a detail of the implementation mechanism. Explicit interface implementation is a technique for enabling the separation of mechanism concerns from model concerns. Forcing the caller to cast the object to an interface such as IComparable
before treating the object as comparable explicitly separates out in the code when you are talking to the model and when you are dealing with its implementation mechanisms.
In general, it is preferable to limit the public surface area of a class to be “all model” with as little extraneous mechanism as possible. (Unfortunately, some mechanisms are unavoidable in .NET. In the real world, for example, you cannot get a giraffe’s hash code or convert a giraffe to a string. However, you can get a Giraffe
’s hash code [GetHashCode()
] and convert it to a string
[ToString()
] in .NET. By using object
as a common base class, .NET mixes model code with mechanism code, even if only to a limited extent.)
Here are several guidelines that will help you choose between an explicit implementation and an implicit implementation.
Is the member a core part of the class functionality?
Consider the CellValues
property implementation on the Contact
class. This member is not an integral part of a Contact
type, but rather a peripheral member probably accessed only by the ConsoleListControl
class. As such, it doesn’t make sense for the member to be immediately visible on a Contact
object, cluttering up what could potentially already be a large list of members.
Alternatively, consider the IFileCompression.Compress()
member. Including an implicit Compress()
implementation on a ZipCompression
class is a perfectly reasonable choice: Compress()
is a core part of the ZipCompression
class’s behavior, so it should be directly accessible from the ZipCompression
class.
Is the interface member name appropriate as a class member?
Consider an ITrace
interface with a member called Dump()
that writes out a class’s data to a trace log. Implementing Dump()
implicitly on a Person
or Truck
class would result in confusion as to which operation the method performs. Instead, it is preferable to implement the member explicitly so that the Dump()
method can be called only from a data type of ITrace
, where the meaning is clearer. Consider using an explicit implementation if a member’s purpose is unclear on the implementing class.
Does a class member with the same signature already exist?
Explicit interface member implementation does not add a named element to the type’s declaration space. Therefore, if there is already a potentially conflicting member of a type, a second one can be provided with the same name or signature as long as it is an explicit interface member.
Much of the decision making regarding implicit versus explicit interface member implementation comes down to intuition. However, these questions provide suggestions about which issues to consider when making your choice. Since changing an implementation from implicit to explicit results in a version-breaking change, it is better to err on the side of defining interfaces explicitly, allowing them to be changed to implicit implementations later. Furthermore, since the decision between implicit and explicit does not have to be consistent across all interface members, defining some methods as explicit and others as implicit is fully supported.
Just as with a derived type and a base class, a conversion from an implementing type to its implemented interface is an implicit conversion. No cast operator is required because an instance of the implementing type will always provide all the members in the interface; therefore, the object can always be converted successfully to the interface type.
Although the conversion will always be successful from the implementing type to the implemented interface, many different types could implement a particular interface. Consequently, you can never be certain that a “downward” cast from an interface to one of its implementing types will be successful. Therefore, converting from an interface to one of its implementing types requires an explicit cast.
Interfaces can derive from each other, resulting in an interface that inherits all the members5 in its base interfaces. As shown in Listing 8.6, the interfaces directly derived from IReadableSettingsProvider
are the explicit base interfaces.
5. Except C# 8.0’s introduced non-private members.
interface IReadableSettingsProvider { string GetSetting(string name, string defaultValue); } interface ISettingsProvider : IReadableSettingsProvider { void SetSetting(string name, string value); } class FileSettingsProvider : ISettingsProvider { #region ISettingsProvider Members public void SetSetting(string name, string value) { // ... } #endregion #region IReadableSettingsProvider Members public string GetSetting(string name, string defaultValue) { // ... } #endregion }
In this case, ISettingsProvider
is derived from IReadableSettingsProvider
and therefore inherits its members. If IReadableSettingsProvider
also had an explicit base interface, ISettingsProvider
would inherit those members as well, and the full set of interfaces in the derivation hierarchy would simply be the accumulation of base interfaces.
Note that if GetSetting()
is implemented explicitly, it must be done using IReadableSettingsProvider
. The declaration with ISettingsProvider
in Listing 8.7 will not compile.
// ERROR: GetSetting() not available on ISettingsProvider string ISettingsProvider.GetSetting( string name, string defaultValue) { // ... }
The results of Listing 8.7 appear in Output 8.2.
Output 8.2
'ISettingsProvider.GetSetting' in explicit interface declaration is not a member of interface.Inigo Montoya: Enough to survive on
This output appears in addition to an error indicating that IReadableSettingsProvider.GetSetting()
is not implemented. The fully qualified interface member name used for explicit interface member implementation must reference the interface name in which it was originally declared.
Even though a class implements an interface (ISettingsProvider
) that is derived from a base interface (IReadableSettingsProvider
), the class can still declare an implementation of both interfaces overtly, as Listing 8.8 demonstrates.
class FileSettingsProvider : ISettingsProvider, IReadableSettingsProvider { #region ISettingsProvider Members public void SetSetting(string name, string value) { // ... } #endregion #region IReadableSettingsProvider Members public string GetSetting(string name, string defaultValue) { // ... } #endregion }
In this listing, there is no change to the interface’s implementations on the class. Although the additional interface implementation declaration on the class header is superfluous, it provides for better readability.
The decision to provide multiple interfaces rather than just one combined interface depends largely on what the interface designer wants to require of the implementing class. By providing an IReadableSettingsProvider
interface, the designer communicates that implementers are required only to implement a settings provider that retrieves settings; they do not have to be able to write to those settings. This reduces the implementation burden by not imposing the complexities of writing settings as well.
In contrast, implementing ISettingsProvider
assumes that there is never a reason to have a class that can write settings without reading them. The inheritance relationship between ISettingsProvider
and IReadableSettingsProvider
, therefore, forces the combined total of both interfaces on the ISettingsProvider
class.
One final but important note: Although inheritance is the correct term, conceptually it is more accurate to say that an interface represents a contract, and one contract can specify that the provisions of another contract must also be followed. So, the code ISettingsProvider : IReadableSettingsProvider
conceptually states that the ISettingsProvider
contract requires also respecting the IReadableSettingsProvider
contract, rather than that the ISettingsProvider
“is a kind of” IReadableSettingsProvider
. That being said, the remainder of the chapter will continue using the inheritance relationship terminology in accordance with the standard C# terminology.
Just as classes can implement multiple interfaces, so interfaces can inherit from multiple interfaces. The syntax used for this purpose is consistent with class derivation and implementation, as shown in Listing 8.9.
interface IReadableSettingsProvider { string GetSetting(string name, string defaultValue); } interface IWriteableSettingsProvider { void SetSetting(string name, string value); } interface ISettingsProvider : IReadableSettingsProvider, IWriteableSettingsProvider { }
It is unusual to have an interface with no members, but it is a reasonable choice when implementing both interfaces together. The difference between Listing 8.9 and Listing 8.6 is that it is now possible to implement IWriteableSettingsProvider
without supplying any read capability. Listing 8.6’s FileSettingsProvider
is unaffected. If it used explicit member implementation, however, specifying the interface to which a member belongs changes slightly.
Begin 3.0
Perhaps one of the most important features of extension methods is the fact that they work with interfaces in addition to classes. The syntax used is identical to that used for extension methods for classes. The extended type (the first parameter and the parameter prefixed with this
) is the interface that we extend. Listing 8.10 shows an extension method for IListable()
that is declared on the Listable
class.
class Program { public static void Main() { Contact[] contacts = new Contact[] { new Contact( "Dick", "Traci", "123 Main St., Spokane, WA 99037", "123-123-1234") // ... }; // Classes are implicitly converted to // their supported interfaces contacts.List(Contact.Headers); Console.WriteLine(); Publication[] publications = new Publication[3] { new Publication( "The End of Poverty: Economic Possibilities for Our Time", "Jeffrey Sachs", 2006), new Publication("Orthodoxy", "G.K. Chesterton", 1908), new Publication( "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 1979) }; publications.List(Publication.Headers); } } static class Listable { public static void List( this IListable[] items, string?[] headers) { int[] columnWidths = DisplayHeaders(headers); for (int itemCount = 0; itemCount < items.Length; itemCount++) { string?[] values = items[itemCount].CellValues; DisplayItemRow(columnWidths, values); } } // ... }
In this example, the extension method is not for an IListable
parameter (although it could have been), but rather for an IListable[]
parameter. This demonstrates that C# allows extension methods not only on an instance of a particular type but also on a collection of those objects. Support for extension methods is the foundation on which the Language Integrated Query (LINQ) capability is implemented. IEnumerable
is the fundamental interface that all collections implement. By defining extension methods for IEnumerable
, LINQ support was added to all collections. This radically changed programming with collections. We explore this topic in detail in Chapter 15.
End 3.0
Begin 8.0
Prior to C# 8.0, when creating a new version of a component or application that other developers have programmed against, you should not change interfaces. Because interfaces define a contract between the implementing class and the class using the interface, changing the interface is equivalent to changing the contract, which will possibly break any code written against the interface.
Changing or removing an interface member signature is obviously a code-breaking change, as any call to that member will no longer compile without modification. The same is true when you change public or protected member signatures on a class. However, unlike with classes, adding members to an interface could also prevent code from compiling without additional changes. The problem is that any class implementing the interface must do so entirely, and implementations for all members must be provided. With new interface members, the compiler will require that developers add new interface members to the class implementing the interface.
With C# 8.0, the “don’t change interfaces” rule changes slightly. C# 8.0 added a mechanism for enabling a default implementation for an interface member, such that adding a member (you still can’t remove or modify an existing member in a version-compatible way) will not trigger compiler errors on all implementations. Prior to C# 8.0, there is a way to achieve a similar result to changing an interface by adding an additional interface. In this section, we discuss both approaches.
DO NOT add abstract members to an interface that has already been published.
The creation of IDistributedSettingsProvider
in Listing 8.11 serves as a good example of extending an interface in a version-compatible way. Imagine that initially only the ISettingsProvider
interface is defined (as it was in Listing 8.6). In the next version, however, it is determined that settings could be distributed to multiple resources (URIs7) (perhaps on a per-machine basis). To enable this constraint, the IDistributedSettingsProvider
interface is created; it derives from ISettingsProvider
.
7. Universal resource identifiers.
interface IDistributedSettingsProvider : ISettingsProvider { /// <summary> /// Get the settings for a particular URI. /// </summary> /// <param name="uri"> /// The URI the setting is related to.</param> /// <param name="name">The name of the setting.</param> /// <param name="defaultValue"> /// The value returned if the setting is not found.</param> /// <returns>The specified setting.</returns> string GetSetting( string uri, string name, string defaultValue); /// <summary> /// Set the settings for a particular URI. /// </summary> /// <param name="uri"> /// The URI name the setting is related to.</param> /// <param name="name">The name of the setting.</param> /// <param name="value">The value to be persisted.</param> /// <returns>The specified setting.</returns> void SetSetting( string uri, string name, string value); }
The important issue is that programmers with classes that implement ISettingsProvider
can choose to upgrade the implementation to include IDistributedSettingsProvider
, or they can ignore it.
If, instead of creating a new interface, the URI-related methods are added to ISettingsProvider
, then classes implementing this interface will potentially throw an exception at runtime and certainly will not successfully compile with the new interface definition. In other words, changing ISettingsProvider
is a version-breaking change, both at the binary level and at the source code level.
Changing interfaces during the development phase is obviously acceptable, although perhaps laborious if implemented extensively. However, once an interface is published, it should not be changed. Instead, a second interface should be created, possibly deriving from the original interface (Listing 8.11 includes XML comments describing the interface members, as discussed further in Chapter 10.)
Until now, we have ignored the new C# 8.0 interface features except to mention that they exist. In this section, we abandon that restriction and describe the C# 8.0 feature set known as default interface members. As described earlier, changing a published interface in any way prior to C# 8.0 will break any code that implements the interface; therefore, published interfaces should not be changed. However, starting with C# 8.0 and .NET Core 3.0, Microsoft introduced a new C# language feature that allows interfaces to have members with implementation—that is, concrete members, not just declarations. Consider, for example, the CellColors
property included in Listing 8.12.
public interface IListable { // Return the value of each cell in the row string?[] CellValues { get; } ConsoleColor[] CellColors { get { var result = new ConsoleColor[CellValues.Length]; // Using generic Array method to populate array // (see Chapter 12) Array.Fill(regit sult, DefaultColumnColor); return result; } } public static ConsoleColor DefaultColumnColor { get; set; } } public class Contact : PdaItem, IListable { //... #region IListable public string[] CellValues { get { return new string[] { FirstName, LastName, Phone, Address }; } } // *** No CellColors implementation *** // #endregion IListable { public class Publication : IListable { //... #region IListable string?[] IListable.CellValues { get { return new string?[] { Title, Author, Year.ToString() }; } } ConsoleColor[] IListable.CellColors { get { string?[] columns = ((IListable)this).CellValues; ConsoleColor[] result = ((IListable)this).CellColors; if (columns[YearIndex]?.Length != 4) { result[YearIndex] = ConsoleColor.Red; } return result; } } #endregion IListable // ... }
In this listing, notice the addition of the CellColors
property getter. As you can see, it includes an implementation even though it is the member of an interface. The feature is called a default interface member because it provides a default implementation of the method so that any class that implements the interface will already have a default implementation—so that code will continue to compile without any changes even though the interface has additional members. The Contact
class, for example, has no implementation for the CellColors
property getter, so it relies on the default implementation provided by the IListable
interface.
Not surprisingly, you can override a default implementation of the method in the implementing class to provide a different behavior that makes more sense to the class. This behavior is all consistent with the purpose of enabling polymorphism as outlined at the beginning of the chapter.
However, the default interface member feature includes additional features. The primary purpose of these features is to support refactoring of default interface members (though some would debate this interpretation). To use them for any other purpose likely indicates a flaw in the code structure, because they imply the interface is used for more than polymorphism. Table 8.1 lists the additional language constructs along with some of their important limitations.
Table 8.1: Default Interface Refactoring Features
C# 8.0–Introduced Interface Construct |
Sample Code |
Static Members The ability to define static members on the interface including fields, constructors, and methods. (This includes support for defining a static |
public interface ISampleInterface { private static string? _Field; public static string? Field { get => _Field; private set => _Field = value; } static IsampleInterface() => Field = "Nelson Mandela"; public static string? GetField() => Field; } |
Implemented Instance Properties and Methods You can define implemented properties and members on interfaces. Since instance fields are not supported, properties cannot work against backing fields. Also, without instance fields support, there is no automatically implemented property support. Note that to access a default implemented property, it is necessary to cast to the interface containing the member. The class ( |
public interface IPerson { // Standard abstract property definitions string FirstName { get; set; } string LastName { get; set; } string MiddleName { get; set; } // Implemented instance properties and methods public string Name => GetName(); public string GetName() => $"{FirstName} {LastName}"; } public class Person { // ... } public class Program { public static void Main() { Person inigo = new Person("Inigo", "Montoya"); Console.Write( ((IPerson)inigo).Name); } } |
public Access Modifier The default for all instance interface members. Use this keyword to help clarify the accessibility of the code. Note, however, that the compiler-generated CIL code is identical with or without the |
public interface IPerson { // All members are public by default string FirstName { get; set; } public string LastName { get; set; } string Initials => $"{FirstName[0]}{LastName[0]}"; public string Name => GetName(); public string GetName() => $"{FirstName} {LastName}"; } |
protected Access Modifier See the “Protected Access Modifier” section. |
|
private Access Modifier The default for static members. The |
public interface IPerson { string FirstName { get; set; } string LastName { get; set; } string Name => GetName(); private string GetName() => $"{FirstName} {LastName}"; } |
internal Access Modifier
|
public interface IPerson { string FirstName { get; set; } string LastName { get; set; } string Name => GetName(); internal string GetName() => $"{FirstName} {LastName}"; } |
private protected Access Modifier A super set of |
public interface IPerson { string FirstName { get; set; } string LastName { get; set; } string Name => GetName(); protected internal string GetName() => $"{FirstName} {LastName}"; } |
private protected Access Modifier Accessing a |
class Program { static void Main() { IPerson? person = null; // Non-deriving classes cannot call // private protected member. // _ = person?.GetName(); Console.WriteLine(person); } } public interface IPerson { string FirstName { get; } string LastName { get; } string Name => GetName(); private protected string GetName() => $"{FirstName} {LastName}"; } public interface IEmployee: IPerson { int EmpoyeeId => GetName().GetHashCode(); } public class Person : IPerson { public Person( string firstName, string lastName) { FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName)); LastName = lastName ?? throw new ArgumentNullException(nameof(lastName)); } public string FirstName { get; } public string LastName { get; } // private protected interface members // are not accessible in derived classes. // public int PersonTitle => // GetName().ToUpper(); } |
virtual Modifier By default, an implemented interface member is |
public interface IPerson { // virtual is not allowed on members // without implementation /* virtual */ string FirstName { get; set; } string LastName { get; set; } virtual string Name => GetName(); private string GetName() => $"{FirstName} {LastName}"; } |
sealed Modifier To prevent a derived class from overriding a method, mark it as See Listing 8.13 for more information. |
public interface IWorkflowActivity { // Private and, therefore, not virtual private void Start() => Console.WriteLine( "IWorkflowActivity.Start()..."); // Sealed to prevent overriding sealed void Run() { try { Start(); InternalRun(); } finally { Stop(); } } protected void InternalRun(); // Private and, therefore, not virtual private void Stop() => Console.WriteLine( "IWorkflowActivity.Stop().."); } |
abstract Modifier
All abstract members are automatically |
public interface IPerson { // virtual is not allowed on members // without implementation /* virtual */ abstract string FirstName { get; set; } string LastName { get; set; } // abstract is not allowed on members // with implementation /* abstract */ string Name => GetName(); private string GetName() => $"{FirstName} {LastName}"; } |
Partial Interfaces and Partial Methods It is now possible to provide partial implementations of a method with no outgoing data (returns or Partial methods are always |
public partial interface IThing { string Value { get; protected set; } void SetValue(string value) { AssertValueIsValid(value); Value = value; } partial void AssertValueIsValid(string value); } public partial interface IThing { partial void AssertValueIsValid(string value) { // Throw if value is invalid. switch(value) { case null: throw new ArgumentNullException( nameof(value)); case "": throw new ArgumentException( "Empty string is invalid", nameof(value)); case string _ when string.IsNullOrWhiteSpace(value): throw new ArgumentException( "Can't be whitespace", nameof(value)); }; } } |
There are a couple of points to highlight in Table 8.1. First, it is important to note that automatically implemented property support is not available because instance fields (which back an automatically implemented property) are not supported. This is a significant difference from abstract classes, which do support instance fields and automatically implemented properties.
Second, notice that the default accessibility changes between instance and static members. Static members are private
by default, whereas instance members are public
by default. This difference occurs because static members always have an implementation and map closely to class static members, which are also private
by default. In contrast, the purpose of interface instance members is to support polymorphism, so they default to public
, in keeping with the traditional behavior before C# 8.0.
When creating a class, programmers should be careful about choosing to allow overriding of a method, since they cannot control the derived implementation. Virtual methods should not include critical code because such methods may never be called if the derived class overrides them.
Listing 8.13 includes a virtual Run()
method. If the WorkflowActivity
programmer calls Run()
with the expectation that the critical Start()
and Stop()
methods will be called, then the Run()
method may fail.
public class WorkflowActivity { private void Start() { // Critical code } public virtual void Run() { Start(); // Do something... Stop(); } private void Stop() { // Critical code } }
In overriding Run()
, a developer could perhaps not call the critical Start()
and Stop()
methods.
Now consider a fully implemented version of this scenario with the following encapsulation requirements:
It should not be possible to override Run()
.
It should not be possible to invoke Start()
or Stop()
, as the order in which they execute is entirely under the control of the containing type (which we will name IWorkflowActivity
).
It should be possible to replace whatever executes in the “Do something …” code block.
If it were reasonable to override Start()
and Stop()
, then the class implementing them should not necessarily be able to invoke them—they are part of the base implementation.
The deriving types should be allowed to provide a Run()
method, but it should not be invoked when the Run()
method is invoked on IWorkflowActivity
.
To meet all these requirements and more, C# 8.0 provides support for a protected interface member, which has some significant differences from a protected member on a class. Listing 8.14 demonstrates the differences, and Output 8.3 shows the results.
public interface IWorkflowActivity { // Private and, therefore, not virtual private void Start() => Console.WriteLine( "IWorkflowActivity.Start()..."); // Sealed to prevent overriding sealed void Run() { try { Start(); InternalRun(); } finally { Stop(); } } protected void InternalRun(); // Private and, therefore, not virtual private void Stop() => Console.WriteLine( "IWorkflowActivity.Stop().."); } public interface IExecuteProcessActivity : IWorkflowActivity { protected void RedirectStandardInOut() => Console.WriteLine( "IExecuteProcessActivity.RedirectStandardInOut()..."); // Sealed not allowed when overriding /* sealed */ void IWorkflowActivity.InternalRun() { RedirectStandardInOut(); ExecutProcess(); RestoreStandardInOut(); } protected void ExecutProcess(); protected void RestoreStandardInOut() => Console.WriteLine( "IExecuteProcessActivity.RestoreStandardInOut()..."); } class ExecuteProcessActivity : IExecuteProcessActivity { public ExecuteProcessActivity(string executablePath) => ExecutableName = executablePath ?? throw new ArgumentNullException(nameof(executablePath)); public string ExecutableName { get; } void IExecuteProcessActivity.RedirectStandardInOut()=> Console.WriteLine( "ExecuteProcessActivity.RedirectStandardInOut()..."); void IExecuteProcessActivity.ExecutProcess() => Console.WriteLine( $"ExecuteProcessActivity.IExecuteProcessActivity. ExecutProcess()..."); public void Run() { ExecuteProcessActivity activity = new ExecuteProcessActivity("dotnet"); // Protected members cannot be invoked // by the implementing class even when // implemented in the class. // ((IWorkflowActivity)this).InternalRun(); // activity.RedirectStandardInOut(); // activity.ExecuteProcss(); Console.WriteLine( @$"Executing non-polymorphic Run() with process '{ activity.ExecutableName}'."); } } public class Program { public static void Main() { ExecuteProcessActivity activity = new ExecuteProcessActivity("dotnet"); Console.WriteLine( "Invoking ((IExecuteProcessActivity)activity).Run()..."); // Output: // Invoking ((IExecuteProcessActivity)activity).Run()... // IWorkflowActivity.Start()... // ExecuteProcessActivity.RedirectStandardInOut()... // ExecuteProcessActivity.IExecuteProcessActivity.ExecutProcess()... // IExecuteProcessActivity.RestoreStandardInOut()... // IWorkflowActivity.Stop().. ((IExecuteProcessActivity)activity).Run(); // Output: // Invoking activity.Run()... // Executing non-polymorphic Run() with process 'dotnet'. Console.WriteLine(); Console.WriteLine( "Invoking activity.Run()..."); activity.Run(); } }
Output 8.3
Invoking ((IExecuteProcessActivity)activity).Run()... IWorkflowActivity.Start()... ExecuteProcessActivity.RedirectStandardInOut()... ExecuteProcessActivity.IExecuteProcessActivity.ExecutProcess()... IExecuteProcessActivity.RestoreStandardInOut()... IWorkflowActivity.Stop().. Invoking activity.Run()... Executing non-polymorphic Run() with process 'dotnet'.
Let’s consider how Listing 8.14 meets the requirements outlined earlier.
Notice that IWorkflowActivity.Run()
is sealed and, therefore, not virtual. This prevents any derived types from changing its implementation. Any invocation of Run()
, given a IWorkflowActivity
type, will always execute the IWorkflowActivity
implementation.
IWorkflowActivity
’s Start()
and Stop()
methods are private, so they are invisible to all other types. Even though IExecutProcessActivity
seemingly has start/stop-type activities, IWorkflowActivity
doesn’t allow for replacing its implementations.
IWorkflowActivity
defines a protected InternalRun()
method that allows IExecuteProcessActivity
(and ExecuteProcessActivity
, if desirable) to overload it. However, notice that no member of ExecuteProcessActivity
can invoke InternalRun()
. Perhaps that method should never be run out of sequence from Start()
and Stop()
, so only an interface (IWorkflowActivity
or IExecuteProcessActivity
) in the hierarchy is allowed to invoke the protected member.
All interface members that are protected can override any default interface member if they do so explicitly. For example, both the RedirectStandardInOut()
and RestoreStandardInOut()
implementations on ExecuteProcessActivity
are prefixed with IExecuteProcessActivity
. And, like with the protected InternalRun()
method, the type implementing the interface cannot invoke the protected members; for example, ExecuteProcessActivity
can’t invoke RedirectStandardInOut()
and RestoreStandardInOut()
, even though they are implemented on the same type.
Even though only one of them is explicitly declared as virtual, both RedirectStandardInOut()
and RestoreStandardInOut()
are virtual (virtual is the default unless a member is sealed). As such, the most derived implementation will be invoked. Therefore, when IExecuteProcessActivity.InternalRun()
invokes RedirectStandardInOut()
, the implementation on ExecuteProcessActivity()
will execute instead of the implementation from IExecuteProcessActivity
.
A derived type’s implementation can potentially provide a method that matches a sealed signature in the parent. For example, if ExecuteProcessActivity
provides a Run()
method that matches the signature of Run()
in IWorkflowActivity
, the implementation associated with the type will execute, rather than the most derived implementation. In other words, Program.Main()
’s invocation of ((IExecuteProcessActivity)activity).Run()
calls IExecuteProcessActivity.Run()
, while activity.Run()
calls ExecuteProessActivity.Run()
—where activity
is of type ExecuteProcessActivity
.
In summary, the encapsulation available with protected interface members, along with the other member modifiers, provides a comprehensive mechanism for encapsulation—albeit an admittedly complicated one.
When it comes to extending a published interface with additional functionality, when is a default interface member preferable to creating an extension method or creating a second interface that derives from the first and adds additional members? The following factors should be considered when making this decision:
Both conceptually support overriding by implementing a method of the same signature on an instance of the interface.
Extension methods can be added from outside the assembly that contains the interface definition.
While default interface properties are allowed, there is no instance storage location available (fields are not allowed) for the property value, limiting the applicability to calculated properties.
While there is no support for extension properties, calculations can be provided with “getter” extension methods (e.g., GetData()
), without limiting them to .NET Core 3.0 or later frameworks.
Providing a second derived interface supports defining both properties and methods without introducing a version incompatibility or a framework limitation.
The derived interface approach requires implementing types to add the new interface and take advantage of the new capability.
Default interface members can be invoked only from the interface type. Even implementing objects don’t have access to the default interface members without casting to the interface. In other words, default interface members behave like explicitly implemented interface members unless the base class provides an implementation.
Protected virtual members can be defined on interfaces, but they are only available as such to deriving interfaces—not to classes implementing the interface.
A default interface member may be overridden by the implementing classes, thereby allowing each class to define the behavior if desired. With extension methods, the binding is resolved based on the extension method being accessible at compile time. As a result, the implementation is determined at compile time instead of at runtime. With extension methods, therefore, the implementing class author can’t provide a different implementation for the method when it’s called from libraries. For example, System.Linq.Enumerable.Count()
provides a special implementation index-based collection by casting to the list implementation to retrieve the count. As a result, the only way to take advantage of the improved efficiency is to implement a list-based interface. In contrast, with a default interface implementation, any implementing class could override this method to provide a better version.
In summary, adding property polymorphic behavior is only possible with a second interface or default interface members. And, if support for frameworks prior to .NET Core 3.0 is needed, creating a new interface is the preferred solution when adding properties. When only methods, and not properties, are part of the updated interface, extension methods are preferred.
CONSIDER using extension methods or an additional interface in place of default interface members when adding methods to a published interface.
DO use extension methods when the interface providing the polymorphic behavior is not under your control.
DO use an additional interface when properties are necessary for extending polymorphic behavior for framework support prior to .NET Core 3.0.
Interfaces introduce another category of data types. (They are one of the few categories of types that don’t extend System.Object
.8) Unlike classes, however, interfaces can never be instantiated. An interface instance is accessible only via a reference to an object that implements the interface. It is not possible to use the new
operator with an interface; therefore, interfaces cannot contain any instance constructors or finalizers. Prior to C# 8.0, static members are not allowed on interfaces.
8. The others are pointer types and type parameter types. However, every interface type is convertible to System.Object
, and it is permissible to call the methods of System.Object
on any instance of an interface, so perhaps this is a hairsplitting distinction.
Interfaces are similar to abstract classes, sharing such features as the lack of instantiation capability. Table 8.2 lists additional comparisons. Given that abstract classes and interfaces have their own sets of advantages and disadvantages, you must make a cost–benefit decision based on the comparisons in Table 8.2 and the guidelines that follow to make the right choice.
Table 8.2: Comparing Abstract Classes and Interfaces
Abstract Classes |
Interfaces |
Cannot be instantiated directly, but only by instantiating a non-abstract derived class. |
Cannot be instantiated directly, but only by instantiating an implementing type. |
Derived classes either must be abstract themselves or must implement all abstract members. |
Implementing types must implement all abstract interface members. |
Can add additional non-abstract members that all derived classes can inherit without breaking cross-version compatibility. |
Can add additional default interface members in C# 8.0/.NET Core 3.0 that all derived classes can inherit without breaking cross-version compatibility. |
Can declare methods, properties, and fields (along with all other member types, including constructors and finalizers). |
Instance members are limited to methods and properties, not fields, constructors, or finalizers. All static members are possible, including static constructors, static events, and static fields. |
Members may be instance or static, and optionally abstract, and may provide implementations for non-abstract members that can be used by derived classes. |
Starting with C# 8.0/.NET Core 3.0, members may be instance, abstract, or static, and may provide implementations for non-abstract members that can be used by derived classes. |
Members may be declared as virtual or not. Members that should not be overridden (see Listing 8.13) would not be declared as virtual. |
All (non-sealed) members are virtual, whether explicitly designated as such or not; therefore, there is no way for an interface to prevent overriding the behavior. |
A derived class may derive from only a single base class. |
An implementing type may arbitrarily implement many interfaces. |
CONSIDER interfaces over abstract classes for polymorphic behavior starting with in C# 8.0/.NET Core 3.0 and abstract classes prior to C# 8.0.
CONSIDER defining an interface if you need to support its functionality on types that already inherit from some other type.
In summary, assuming a .NET Core 3.0 framework is acceptable, C# 8.0 (or later) defined interfaces have all the capabilities of abstract classes except the ability to declare an instance field. Given that an implementing type can override an interface’s property to provide storage, which the interface then leverages, interfaces virtually provide a superset of what an abstract class provides. Furthermore, interfaces support a more encapsulated version of protected access in addition to multiple inheritance. Therefore, it makes sense to favor using interfaces for all polymorphic behavior, thereby decoupling the contracts (what the type does) from the implementation details (how the type does it) for C# 8.0 and .NET Core 3.0 or later scenarios.
End 8.0
Interfaces with no members at all, inherited or otherwise, are sometimes used to represent information about a type. For example, you might create a marker IObsolete
interface to indicate that a type has been replaced by another type. This is generally considered to be an abuse of the interface mechanism: Interfaces should be used to represent which functions a type can perform, not to indicate facts about particular types. Instead of marker interfaces, use attributes for this purpose. See Chapter 18 for more details.
AVOID using “marker” interfaces with no members; use attributes instead.
Interfaces are a key element of object-oriented programming in C#. They provide polymorphic capabilities like abstract classes without using up the single-inheritance option, because classes can implement multiple interfaces. Starting with C# 8.0/.NET Core 3.0, interfaces can include implementation via the use of default interface members, almost giving them a superset of the capabilities of abstract classes if backward compatibility is not required.
In C#, the implementation of interfaces can be either explicit or implicit, depending on whether the implementing class is used to expose an interface member directly or only via a conversion to the interface. Furthermore, the granularity of whether the implementation is explicit or implicit is determined at the member level: One member may be implicitly implemented, while another member of the same interface is explicitly implemented.
Chapter 9 looks at value types and discusses the importance of defining custom value types. In addition, it points out the subtle problems that such types can introduce.
18.225.255.134