Theodore Lief Gannon
Theodore Lief Gannon

Reputation: 163

Is there any way to subclass a C# interface in a way that *restricts* behavior?

public interface IBlackBox<T> {
  bool IsValid();
  T GetValue();
}

public abstract class Box<T>:IBlackBox<T> {
  public bool IsValid() {
    return true;
  }
  public abstract T GetValue();
}

A Box produces a value on demand; could be an on-demand computation, or random, or just a fancy nullable, etc. A BlackBox does the same thing, except that it might expire or otherwise become invalid. Boxes need to fit in BlackBox lists (and emphatically not the inverse), which dictates the inheritance hierarchy.

What I don't like is Box being an abstract class instead of an interface. This causes two headaches. First, it means I can't derive a struct, which would be very much preferred in the case of a Box which contains only a static value.

Second, it means I can't subclass anything else. For instance, one intended use is a Dictionary<string,BlackBox<int>> which is itself a Box and sums its values on demand. There's no good reason this shouldn't just subclass Dictionary, and at least one very good reason it should: not wrapping the IDictionary interface line-for-line! But there are places where I need the Box guarantee of validity, and it seems like I can only have one or the other.

I could have an interface IBox<T> : IBlackBox<T> which is empty and serves only as a "trust me" label. That would mean duplicating Box's one-line IsValid() in every class that implements IBox<T>, but that's considerably less hassle than hand-wrapping IDictionary. I'm hoping for a more concrete solution, though.

Upvotes: 1

Views: 98

Answers (2)

supercat
supercat

Reputation: 81307

I would suggest having an IValidBox<T> : IBlackBox<T> which a contract which specifies that no legitimate implementation may ever have IsValid return false, and that consumers are entitled to call GetValue without checking IsValid. Additionally, to reduce boilerplate, you could define an abstract class ValidBoxBase<T> which implements IValidBox<T>, so that implementations of IValidBox<T> that don't need to inherit anything else could save some boilerplate. Not much in this brief example, but perhaps a significant amount in more fleshed-out scenarios.

Upvotes: 0

Theodore Lief Gannon
Theodore Lief Gannon

Reputation: 163

Here's the best solution I've come up with. This is just the bare bones, but I've put a full implementation (with better names) on GitHub.

public interface IBlackBox
public interface IBlackBox<T> : IBlackBox {
  Func<T> GetValue;
}

public interface IBox { }
public interface IBox<T> : IBlackBox<T>, IBox { }

public interface IWrappedBox { }
public interface IWrappedBox<T> : IBox<IBlackBox<T>>, IWrappedBox { }

public interface IEmptyBox { }
public static class EmptyBox {
  public static EmptyBox<T> Get<T>() {
    return EmptyBox<T>.value;
  }
  sealed class EmptyBox<T> : IBlackBox<T>, IEmptyBox {
    public static readonly EmptyBox<T> value = new EmptyBox<T>();
    public Func<T> GetValue {
      get {
        throw new NotSupportedException
          ("Cannot extract a value from an empty box.");
      }
    }
  }
}

The non-generic interfaces allow testing at the type level, for instance:

IBlackBox something = someWrappedBox.GetValue();
if (something is IEmptyBox)
  HandleEmpty();
else
  HandleSomething(something.GetValue());

This still has to be combined with three rules that the compiler can't check, but the structure makes it easy to conform:

  • IBlackBox<T>, IBlackBox and IEmptyBox cannot be inherited directly.
  • Only IWrappedBox<T>.GetValue() can return base IBlackBox<T>.
  • Return values cannot specifically be any IEmptyBox.

IEmptyBox is guaranteed to show up only when explicitly permitted; IBox<T> is guaranteed to contain a good T; and everything that can be inherited is an interface. Mission accomplished!

Upvotes: 2

Related Questions