8. Interfaces

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.

An illustration shows the various subjects involved in the interfaces. It is listed as follows: polymorphism, interface implementation, interface inheritance, extension methods on interfaces, and versioning. Interface implementation constitutes explicit and implicit.

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.

Introducing Interfaces

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.

Listing 8.1: Defining an Interface

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

Polymorphism through Interfaces

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.

Listing 8.2: Implementing and Using Interfaces

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.

Interface Implementation

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.

Listing 8.3: Implementing an Interface

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.

Explicit Member Implementation

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.

Listing 8.4: Calling Explicit Interface Member Implementations

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).

Listing 8.5: Explicit Interface Implementation

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.

Implicit Member Implementation

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.

Explicit versus Implicit Interface Implementation

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.

Converting between the Implementing Class and Its Interfaces

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.

Interface Inheritance

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.

Listing 8.6: Deriving One Interface from Another

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.

Listing 8.7: Explicit Member Declaration without the Containing Interface (Failure)

// 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.

Listing 8.8: Using a Base Interface in the Class Declaration

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.

Multiple Interface Inheritance

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.

Listing 8.9: Multiple Interface Inheritance

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.

Extension Methods on Interfaces

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.

Listing 8.10: Interface Extension Methods

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

Versioning

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.

Interface Versioning Prior to C# 8.0

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.

Listing 8.11: Deriving One Interface from Another

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.)

Interface Versioning in C# 8.0 or Later

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.

Listing 8.12: Versioning Interfaces with Default Interface Members

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 Main method—an entry point into your program.)

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 (Person) does not have the default interface member available unless it is implemented.

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 access modifier.

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 private access modifier restricts a member to be available for invocation only from the interface that declares it. It is designed to support refactoring of default interface members. All private members must include an implementation.

public interface IPerson
{
  string FirstName { get; set; }
  string LastName { get; set; }
  string Name => GetName();
  private string GetName() =>
      $"{FirstName} {LastName}";
}

internal Access Modifier

internal members are only visible from within the same assembly in which they are declared.

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 private and protected; private protected members are visible from within the same assembly and from within other interfaces that derive from the containing interface. Like protected members, classes external to the assembly cannot see protected internal members.

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 private protected member is only possible from the containing interface or interfaces that derive from the implementing interface. Even classes implanting the interface cannot access a private protected member, as demonstrated by the PersonTitle property in Person.

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 virtual, meaning that derived implementations of the method with the same signature will be invoked when the interface member is invoked. As with the public access modifier, however, you can decorate a member as virtual explicitly to provide clarity. For non-implemented interface members, virtual is not allowed. Similarly, virtual is incompatible with private, static, and sealed modifiers.

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 sealed, thus ensuring that the method implementation cannot be modified by derived classes.

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

The abstract modifier is only allowable on members without an implementation, but the keyword has no effect as such members are abstract by default.

All abstract members are automatically virtual and explicitly declaring abstract members as virtual triggers a compile error.

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 ref/out data) and optionally the fully implemented method in a second declaration of the same interface.

Partial methods are always private—they do not support access modifiers

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.

Additional Encapsulation and Polymorphism with Protected Interface Members

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.

Listing 8.13: Carelessly Relying on a Virtual Method Implementation

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.

Listing 8.14: Forcing the Desirable Run() Encapsulation

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.

Extension Methods versus Default Interface Members

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.

Interfaces Compared with Abstract Classes

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.

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 Compared with Attributes

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.255.134