So many interfaces, part two

So many interfaces, part two

Rate This
  • Comments 11

In my earlier article from April 2011 on interface implementation I noted that C# supports a seldom-used feature called "interface re-implementation". This feature is useful when you need it but unfortunately is one of those features that can bite you if you use it incorrectly or accidentally.

Every interface method of every interface you implement in a class or struct has to be "mapped" to a method in the type (either a method directly implemented by the type or a method that the type obtained via inheritance, doesn't matter) that implements the interface method. That's pretty straightforward. The idea of interface re-implementation is basically that if you re-state on a derived class that you implement an interface that was already implemented by a base class, then when analyzing the derived class, we abandon all the information we had previously computed about the "mappings" in the base class.

This is useful particularly in the case where a base class does an explicit implementation of an interface and you would like to replace its implementation with your own:

interface I
{
  void M();
}
class Echo : I
{
  void I.M() { ... }
}
class Foxtrot : Echo, I
{
  void I.M() { ... }
}

Echo does not have a public, protected, or internal virtual method M that Foxtrot can override; if Foxtrot wants to replace Echo's behaviour then its only chance to do so is via interface re-implementation.

Like I said, that can be useful but you should be aware that this is a sharp tool that can cut you in some situations. Here's a situation we found ourselves in recently on the Roslyn team where we managed to cut ourselves with our own tool!

We have two very similar interfaces, IInternal and IPublic. We intend that our component be used by the public -- you guys -- via the IPublic interface. As an internal implementation detail, we also have an internal interface IInternal that we use to communicate with a particular subsystem that has been provided for us by another team within Microsoft. The methods of IInternal are a superset of the methods of IPublic:

internal interface IInternal
{
  void M();
  void N();
}

public interface IPublic
{
  void M();
}

public abstract class Bravo : IInternal (*)
{
  // These are our internal workings that we use to communicate
  // with our private implementation details;
  // they are explicitly implemented methods of an internal interface,
  // so they cannot be called by the public.
  void IInternal.M() { ... }
  void IInternal.N() { ... }
}

public abstract class Charlie: Bravo, IPublic
{
  // class Charlie, on the other hand, wants to expose M both
  // as a method of Charlie and as an implicit implementation
  // of IPublic.M:
  public void M() { ... }
}

public sealed class Delta : Charlie, IInternal
{
  // Delta is a derived class of Bravo via Charlie; it needs to
  // change how IInternal.N behaves, so it re-implements the
  // interface and provides a new, overriding implementation:
  void IInternal.N() { ... }
}

This is wrong. There are three methods that Delta must provide: IInternal.M, IInternal.N and IPublic.M. IPublic.M is implemented in Delta by Charlie, as usual. But we are doing an interface re-implementation of IInternal, so we start fresh. What is the implementation of IInternal.N? Obviously the explicitly implemented method in Delta. But what is the implementation of IInternal.M? It's the public method in Charlie, not the invisible-to-everyone explicit method in Bravo. When an instance of Delta is passed off to the internal subsystem it will now call the public method Charlie.M instead of the correct code: the explicit implementation in Bravo.

There is no way for Delta to get to Bravo's implementation of IInternal.M; that is a private implementation detail of Bravo. The interface re-implementation pattern is simply a bad technique to use here; the scenario is too complicated for it to work without you cutting yourself accidentally. A better pattern is to have only one implementation of the internal interface that then defers to an internal-only method:

public abstract class Bravo : IInternal
{
  void IInternal.M() { this.InternalM(); }
  internal virtual void InternalM() { ... }
  void IInternal.N() { this.InternalN(); }
  internal virtual void InternalN() { ... }
}

public abstract class Charlie: Bravo, IPublic
{
  public void M() { ... }
}

public sealed class Delta: Charlie
{
  internal override void InternalN() { ... } 
}

And now Delta can even call base.InternalN if it needs to call Bravo.InternalN.

Once more we see that designing for inheritance is trickier than you might think.


(*) People are sometimes surprised that this works. The accessibility domain of a derived class must be a subset of the accessibility domain of its base class; that is, you cannot derive a public class from an internal base class. The same is not true of interfaces; interfaces can be as private as you like. In fact, a public class can implement a private interface nested inside itself!

  • Nice to know about this, I hadn't heard of interface re-implementation previously.

    Could you elaborate a bit on why a public class is permitted to implement a private (even nested) interface?

    For precisely the reason I stated: sometimes you want to have various internal components communicating with each other using internal interfaces. -- Eric

  • Having one class implement two interfaces with methods with the same names that (are supposed to) do different things is a very bad sign.

    It is a bad sign I agree, but sometimes it is the lesser of two evils. We need to expose public methods with names like "IsStatic" and "BaseType" and so on; we also need to expose similar but slightly different functionality to internal helper libraries based on CCI that have similarly-named methods. We'd rather not build adaptor classes just to bridge this gap, and so we are in a cleft stick of our own devising. -- Eric

    Also, I rarely explicitly implement interfaces without it being more than a call to a protected or internal member; that just makes it harder to call the method.

    That's a good habit to be in. -- Eric

  • It was excellent

  • I think there might be a mistake in your first code example with I, Echo and Foxtrot.  Echo should be:

    class Echo : I

    {

     void I.M() { ... }

    }

  • This reminds me of how IDispose is implemented with derived classes.

  • This should be flagged with a compiler warning.  

    We are considering it; if we got bitten by it then maybe other people get bitten by this as well. However, it is dangerous business adding a compiler warning. A compiler warning should warn about code that is technically legal but unlikely to be correct. What is the bright line here that allows us to disambiguate between a behaviour that the user intended and one they caused by accident? That is, suppose we had wanted the Charlie's implementation to match the re-implemented interface method; how would we have expressed that in Delta so as to prevent the warning? (Remember, the code in Charlie or Bravo might not be in source; those types might be in assemblies that the author of Delta cannot change.) A warning that needs a pragma to turn off isn't a very good warning. We'll keep pondering it. -- Eric

  • "In fact, a public class can implement a private interface nested inside itself!"

    That's strange! Wouldn't it create some kind of circular dependency?

    De jure, usually no; the C# specification allows for that (though the specification is somewhat vague on the topic of precisely how to evaluate an identifier in a base type list when doing so might refer to a nested interface type of a base class.) De facto, there are some bugs in the cycle detector such that sometimes we accidentally detect such topologies as circular dependencies and disallow them. -- Eric

  • A compiler warning will let us know that someone, most likely, accidentally overrode an existing interface.  This is similar to how we get warnings of possible data loss when converting a 4 byte long to a 2 byte short.

  • Are you working on a private pilot's license by chance?  Can't help but notice the overt use of the phonetic alphabet.  :)

  • >> That is, suppose we had wanted the Charlie's implementation to match the re-implemented interface method; how would we have expressed that in Delta so as to prevent the warning?

    Maybe something similar to C++'s `using` declaration?

       public sealed class Delta : Charlie, IInternal{

           void IInternal.N(){ ... }

           // WARNING:

           // Class Delta reimplements interface 'IInternal', but doesn't reimplement 'IInternal.M'.

       }

       public sealed class Epsilon : Charlie, IInternal{

           void IInternal.N(){ ... }

           using base.M;

       }

    Using "base" is pretty limited, though.  It would be more useful, I think to specify the class explicitly (as in C#):

       /* This would be equivalent to the above */

       using Charlie.M;

    And if that's the syntax, then perhaps you could get what you really wanted in the first place with:

       using Bravo.M;

    But since M is not actually a member of Bravo, this might not make sense; perhaps instead you'd need something like:

       using Bravo.IInternal.M;

    which, of course, means "use whatever method would be used when a Bravo instance was accessed through the IInternal interface."

    However, this looks confusing, because it looks like we're referring to a member "M" on type "IInternal" that's a *nested* type of "Bravo", which of course is not the case; so perhaps we'd be better off borrowing the scope-resolution operator from C++ as well (which, like the "using" keyword, has the advantage of already existing in the language).

       using Bravo::IInternal.M;

  • >> It would be more useful, I think to specify the class explicitly (as in C#):

    Insert a comma after "think", and change "C#" to "C++".

    Eric, Microsoft might do well to run the MSDN blogs off of Stack Exchange's software.  Trying to post code-containing comments in plain text is no fun.  Not to mention the blog software seems to swallow every first attempt to post, and provides no way to edit.

Page 1 of 1 (11 items)