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.
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 (URIs8) (perhaps on a per-machine basis). To enable this constraint, the IDistributedSettingsProvider interface is created; it derives from ISettingsProvider.
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, 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, 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.
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 it implies the interface is used for more than polymorphism. Table 8.1 lists the additional language constructs along with some of their important limitations.
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.) The default accessibility for static members on interfaces is public. |
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 Accessible only from members in classes that derive from the defining class.. |
public interface IPerson { // ... protected void Initialize() => { /* ... */ }; } |
private Access Modifier 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 static members on interfaces are also public by default. While static members always have an implementation and map closely to class static members in this regard, in contrast, the purpose of interface instance members is to support polymorphism, so they default to public.
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.
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:
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.
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.
________________________________________