.NET and duck-typing, part 1
Davyd McColl
Posted on October 25, 2019
"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:
- The item being looped over must implement a method called
GetEnumerator()
- The result of
GetEnumerator()
must conform to the expected shape for an enumerator:- Implements a
Current
property which returns an object - Implements a
MoveNext
method which moves the focus of theCurrent
property forward and returns true if the collection is not yet exhausted (otherwise false) - Implements a
Reset()
method with no return (void
) which can start the enumeration again.
- Implements a
The C# compiler is performing this two-tier duck-typing test for you and emitting IL which does:
- Fetch the enumerator
- Call
MoveNext()
-- if the result is false, break out - Feed the value of
Current
into your loop - 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!")
Posted on October 25, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.