.NET and duck-typing, part 1

fluffynuts

Davyd McColl

Posted on October 25, 2019

.NET and duck-typing, part 1

"If it walks like a duck and it quacks like a duck, then it must be a duck"

Well, close enough.

Duck-Typing is a phenomenon where you can treat an object as if it is of the type you want if it exhibits the properties that you require.

In some languages, like JavaScript, this is just "the way it's done". Whilst under-the-hood, the JavaScript engine will make clearly-defined types for objects you're wishing to represent, you can just go ahead and do:

var animal = {
  walks: "like-a-duck",
  quacks: "like-a-duck",
  doStuff: function() {
    console.log("does stuff like a duck does");
  }
};

function doStuffIfDuck(animal) {
  // if the incoming animal is a good-enough
  // duck-like thing, then we can treat it like
  // a duck!
  if (animal.walks === "like-a-duck" &&
      animal.quacks === "like-a-duck" &&
      typeof(animal.doStuff) === "function") {
    animal.doStuff();
  }
}

All good and well, but JavaScript is not (technically) a strongly-typed language -- although variables do have type, it's quite valid to do:

var foo = 1;
console.log(typeof foo); // logs "number"
foo = "a";
console.log(typeof foo); // logs "string"

where, in a more strongly-typed language like C#, this would just lead to compile errors, mostly because var is just compiler syntax-sugar which says "work out what type this variable would be, and that's the type it is".

But did you know that the C# compiler has been doing duck-typing since, well, forever?

A Tale of Collections

I'm sure everyone has seen code like this in C# before:

// before LINQ, there was a lot more of this, but it
//  still serves as an example:

var total = 0;
foreach (var item in collectionOfNumbers)
{
  total += item;
}

How does this work? You might be tempted to say that, well, the type of collectionOfNumbers implements IEnumerable<int> (ie collectionOfNumbers might be of type int[] or List<int>). But it's also quite possible that collection doesn't implement any interface at all.

Yeah, stop and think about that for a while.

Now have a look at this:

using NUnit.Framework;
using static NExpect.Expectations;
using NExpect;

[TestFixture]
public class EnumerationIsDoneByDuckTyping
{
    [Test]
    public void ShouldBeAbleToEnumerate()
    {
        // Arrange
        var data = new[] { 1, 2, 3 };
        var enumerable = new MyEnumerable<int>(data);
        var collector = new List<int>();
        // Act
        foreach (var item in enumerable)
        {
            collector.Add((int) item);
        }

        // Assert
        Expect(collector).To.Equal(data);
    }

    // technically, does not neet to be generic: this
    // could just deal with objects, but I like a good
    // generic :D
    public class MyEnumerable<T>
    {
        private readonly T[] _data;

        public MyEnumerable(T[] data)
        {
            _data = data;
        }

        public MyEnumerator<T> GetEnumerator()
        {
            return new MyEnumerator<T>(_data);
        }
    }

    public class MyEnumerator<T>
    {
        public T Current => _data[_index];

        private readonly T[] _data;
        private int _index;

        public MyEnumerator(T[] data)
        {
            _data = data;
            Reset();
        }

        public bool MoveNext()
        {
            _index++;
            return (_index < _data.Length);
        }

        public void Reset()
        {
            _index = -1;
        }
    }
}

Not only does this compile, it runs, and the test passes. You're welcome to verify on your own machine (you'll need to install packages NUnit and NExpect). Yes, I know it's a little contrived. Imagine that this was wrapped around sequential HTTP calls, for instance. Or perhaps it wraps around something taking user input on the console, where an empty input signals the end of the collection. You can see an example of this on GitHub

Of course, you could just implement IEnumerable, but there was a bit of a chicken-and-egg situation there In The Beginning, and, at the end of the day, you simply need to satisfy the requirements of the compiler to be able to emit code to enumerate over your collection.

But how does this work?

What is this dark magick?

When the C# compiler observes a foreach loop, it just looks to validate a few small criteria. In essence, there are two duck-typed requirements:

  1. The item being looped over must implement a method called GetEnumerator()
  2. The result of GetEnumerator() must conform to the expected shape for an enumerator:
    1. Implements a Current property which returns an object
    2. Implements a MoveNext method which moves the focus of the Current property forward and returns true if the collection is not yet exhausted (otherwise false)
    3. Implements a Reset() method with no return (void) which can start the enumeration again.

The C# compiler is performing this two-tier duck-typing test for you and emitting IL which does:

  1. Fetch the enumerator
  2. Call MoveNext() -- if the result is false, break out
  3. Feed the value of Current into your loop
  4. Lather, Rinse, Repeat.

This is interesting because it may not be what you expected from a strongly-typed language. It's also interesting because a similar approach was taken when implementing async/await, so it's not just some older design decision: this is a valid way to implement features!

In a later post, I'd like to demonstrate how we could duck-type any valid object onto our own interface, using a library I wrote called PeanutButter.DuckTyping. This library has saved me a lot of time when I want to have a well-defined interface for things like:

  • ConnectionStrings from app.config
  • AppSettings from app.config
  • Arbitrary dictionaries and JSON objects
  • Concrete types coming from other assemblies, where I'd like to be able to fake them out for testing (ie, the case when you say "I wish this had an interface!")
💖 💪 🙅 🚩
fluffynuts
Davyd McColl

Posted on October 25, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

.NET and duck-typing, part 1
net .NET and duck-typing, part 1

October 25, 2019