Wednesday, March 3, 2010

Why We Still Rely on Dynamic Casts

*I recently updated this post as it was rather unclear.  By instance members I meant fields and properties, but it was pointed out that MTC was already supported on instance methods which are of course instance members.

The year is 2010.  I program in C#, a modern and (ostensibly) strongly-typed programming language with generics.  So why do I find it so difficult to write a program that doesn’t rely on dynamic casts?

Conventional wisdom is that reliance on dynamic casting is usually the sign of a poorly-designed framework.  While this is true very often the root cause of suboptimal framework design is the inability of the programming language to express certain concepts.  Its my aim to show you that with a simple programming language feature we can largely free our programs of dynamic casts.  In the process I also intend to challenge conventional notions about good interface design.

Why We Need Dynamic Casts: A Typical Example

Let’s take a look at a class that must rely on dynamic casts to do its job.

public class CollectionSynchronizer<T>
{
    public ICollection<T> SourceCollection { get; set; }     
    public ICollection<T> TargetCollection { get; set; }
    
    // Implementation omitted
}

This CollectionSynchronizer listens to changes in a source collection and synchronizes its contents with a target collection.  Over the classes lifetime its SourceCollection property might be set to a variety of different concrete collection classes that implement ICollection and INotifyCollectionChanged.  The CollectionSynchronizer class has to rely on a dynamic cast because it must cast its source collection from an ICollection to an INotifyCollectionChanged interface in order to detect any changes to it.

public class CollectionSynchronizer<T>
{
    private ICollection<T> sourceCollection;
    
    public ICollection<T> SourceCollection
    {
        get 
        {
            return sourceCollection;
        }
        set
        {
            sourceCollection = value;
            INotifyCollectionChanged notifyCollectionChanged = sourceCollection as INotifyCollectionChanged;
            if (notifyCollectionChanged != null)
            {
                notifyCollectionChanged += SourceCollectionChanged;
            }
        }
    }
    
    private void SourceCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
    {
        // synchronize changes with target collection.
    }
    // snip...
}

The SourceCollection property cannot be strongly-typed as INotifyCollectionChanged because INCC contains no members for retrieving the objects already present in the collection (INCC doesn’t inherit from IEnumerable).  As a result of being weakly-typed this API is not discoverable to the class consumer - who might inadvertently assign a regular collection to the SourceCollection property and then wonder why the target collection was not getting updated.

“Isn’t INCC just a poorly designed interface?  Shouldn’t it derive from IEnumerable.”

No.  The architects made the right decision when they decided not to derive INCC from IEnumerable.  I’ll explain why later.  First I’ll demonstrate how we can get rid of this dynamic cast using a hypothetical language feature that is more flexible than interface inheritance and has fewer drawbacks.

Multiple Type Constraints

Ask yourself this question: Why can’t properties and fields be more than one type?  At first this question might seem heretical at best and nonsensical at worst.  Is such a thing even possible?  Well it turns out that this feature - which I’ll refer to as Multiple Type Constraints (MTC) - is not only possible to implement, but very useful. 

Let’s take a look at a revised version of our CollectionSynchronizer class that uses MTC (WARNING: the following code does NOT compile)...

public class CollectionSynchronizer // No generic type but...
{
    // ...we're using a generic type in a property definition
    public TCollection SourceCollection { get; set; } 
        where TCollection : ICollection, INotifyCollectionChanged
        
    public ICollection TargetCollection { get; set; }
    
    // Implementation omitted
}

Now objects assigned to the SourceCollection property are not restricted to being a single type but are instead constrained to be multiple types.  If C# were capable of compiling the code above we could write code like this*:

var sourceCollection = new ObservableCollection<Customer>();

// No generic type needed on the class
var synchronizer = new CollectionSynchronizer();

synchronizer.SourceCollection = sourceCollection;
synchronizer.TargetCollection = myListBox.Items;

// Adding an item to the source collection,
// and consequently the ListBox items
sourceCollection.Add(new Customer());

// Now we can change the type of SourceCollection
// to another collection that implements 
// ICollection and INotifyCollectionChanged.
sourceCollection.SourceCollection = new MyCustomCustomerCollection();

*Some folks pointed out in the comments that you could use a generic method setter with type constraints to write the same code as I’ve written above.  While this is true it would mean that I couldn’t set this property via XAML.

The point I’m trying to make is that if a programming language supports multiple inheritance it should support multiple type constraints.  C# does support MTC today, but not on properties and fields.  This causes problems.  I’ll explain further but first a small tangent…

Interface Inheritance is Harmful

I submit that the ability to inherit one interface from another has no place in a programming language.  Now I suspect you’d probably like some sort of justification for cutting a feature that pretty much every modern statically-typed, object-oriented programming language supports. 

Tough.  You’ve got it backwards.  Language features are expensive and risky propositions and therefore it’s the advocate’s responsibility to justify their inclusion - not the other way around.  So let’s ask the question…

Why Inherit?

There are two reasons to use inheritance:

  1. Share implementation
  2. Polymorphism

Obviously we can’t share implementation code by inheriting one interface from another because interfaces don’t contain any implementation code.  That only leaves polymorphism.

Polymorphism seems like a good reason to enable interface inheritance.  After all we can use interface inheritance to create a version of the CollectionSynchronizer class that doesn’t require a dynamic cast.

// Create new interface that inherits from two others
interface IObservableCollection: ICollection, INotifyCollectionChanged
{
}

public class CollectionSynchronizer // No generic type but...
{
    // Now we don't need generic constraints!
    public IObservableCollection SourceCollection { get; set; }         
    public ICollection TargetCollection { get; set; }
    
    // Implementation omitted
}

There’s just one problem with this approach: There aren’t any collections that implement IObservableCollection because we just invented it.  There’s no way of getting existing classes you don’t own to retroactively implement an interface. Even though the set of types in System.Collections.ObjectModel.ObservableCollection’s type hierarchy are a superset of the types in our IObservableCollection interface we can’t polymorph over both types.  Your IObservableCollection interface also won’t be compatible with semantically identical interfaces distributed by third parties.  This example demonstrates the inflexibility of interface inheritance.

The key takeaway here is that if a language supports MTC then interface inheritance is useless because MTC is a more flexible mechanism for achieving polymorphism.  In this context it makes sense to do away with interface inheritance because this feature encourages developers to build bigger, more coupled interfaces.

The Case For Small Interfaces

The larger an interface the more likely it is to be broken.  A broken interface is a big problem because interfaces don’t version.  It’s impossible to add, remove, or otherwise change members of an interface without breaking existing code.

One strategy for avoiding the embarrassment and pain of deprecation is to design many, small interfaces.  If we design interfaces that do one thing and do it well as well as avoid creating large interfaces indirectly via inheritance we  minimize the damage caused by deprecation.

The strategy of designing many small interfaces is only viable with MTC.  The fact that MTC is missing from properties and fields is a blind spot.  Nowadays systems are increasingly being built with declarative languages like XAML which are oriented around property getters and setters.

An Ideal Candidate for Language Inclusion

Property type constraints are an evolutionary concept, not a revolutionary one.  Its already possible to build  methods that specify multiple type constraints (via generics constraints).  MTC could be added to properties and fields by reusing the existing generic constraint syntax used for methods.

Tools/Designers would also benefit from multiple-type constraints on properties.  For example if a method was constrained to be both INotifyCollectionChanged and ICollection then the intellisense listing could include types that implement both.

Under the hood things get a little more complicated.  Just like generics, MTC is an ideal candidate for inclusion in IL.  Any approach that reduces MTC types to  object types or uses some other form of erasure would share the same drawbacks as generic type erasure.  I don’t pretend making the necessary changes to the platform will be easy, but I think it is worth the effort.*

*Update: Someone pointed out to me that this may actually be supported already because properties are just get/set method pairs under the hood.  Haven’t tried to write the IL myself.  Anyone know for sure?

The Way Forward

Multiple inheritance creates a combinatorial explosion of potential new types (ex. IObservableCollection, IObservableStack, IObservableEnumerableQueue, etc). MTC is an important tool for managing this complexity.   Programming languages that provide multiple inheritance without also allowing for multiple type constraints are not strongly-typed in practice.  Not allowing for constraints on properties and fields is an oversight that should be rectified.

About Me

My photo
I'm a software developer who started programming at age 16 and never saw any reason to stop. I'm working on the Presentation Platform Controls team at Microsoft. My primary interests are functional programming, and Rich Internet Applications.