Skip to content

JavaScript Envy in C# - Duck Typing

Published: at 12:31 PM

Source Generators

This is the second post in the JavaScript envy series. Today we’re going to talk about duck typing.

Table of contents

Open Table of contents

What’s duck typing?

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

When using Duck typing, even if two different classes don’t share any common interface or type explicitly, they might be considered the same duck type as long as they both have the same set of methods and properties.

In JavaScript, since there are no interfaces, duck typing is trivial:

class Duck {
  quack() {
    console.log("Quack!");
  }
}

class Derek {
  quack() {
    console.log("Quack...");
  }
}

let duck = new Duck();
let derek = new Derek();
[duck, derek].forEach(d => d.quack());

How do we Duck Type in C#?

Since C# 4 (way back in 2010) this is also fairly trivial in C#, by using dynamic types:

var duck = new Duck();
var derek = new Derek();
var ducks = new dynamic[] {duck, derek}; // <-- note the use of dynamic
foreach (var d in ducks)
{
    d.Quack();
}

class Duck
{
    public void Quack() => Console.WriteLine("Quack!");
}

class Derek
{
    public void Quack() => Console.WriteLine("Quack...");
}

And that’s cool… it works. But I’d be pretty reluctant to do that kind of thing in a production codebase. It would be very easy for someone one week, one month or one year later to make a change to to one of those classes, like this:

var duck = new Duck();
var derek = new Derek();
var ducks = new dynamic[] {duck, derek};
foreach (var d in ducks)
{
    d.Quack();
}

class Duck
{
    public void Quack() => Console.WriteLine("Quack!");
}

class Derek
{
    public void Woof() => Console.WriteLine("Woof!"); // <-- Quack changed to Woof!
}

The modified code compiles without error, but generates a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException at runtime:

RuntimeBinderException: ‘Derek’ does not contain a definition for ‘Quack’

In dynamically typed languages you have to write lots of unit tests to avoid such problems. In a statically typed language like C# though, it seems a shame to leave discovery of such things until runtime.

I think we can do better.

Better duck typing in C#?

Another way to do this in C# is to use an Adapter Pattern.

Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.

Here’s an example of how we could write adapters for Derek and Duck:

public interface IDuck
{
    public void Quack();
}

class DuckDuck(Duck instance) : IDuck
{
    public void Quack() => instance.Quack();
}

class DerekDuck(Derek instance) : IDuck
{
    public void Quack() => instance.Quack();
}

The two new adapter classes explicitly implement a new IDuck interface, so we can now do this:

var duck = new Duck();
var derek = new Derek();
IDuck[] ducks = [ new DuckDuck(duck), new DerekDuck(derek) ];
foreach (var d in ducks)
{
    d.Quack();
}

Doing it this way involves way more code (which we’ll deal with in a second) but has the advantage of being statically typed, so our compiler will let us know when things break.

Best of both worlds

Using dynamic is concise, easy to read and easy to write… but incurs performance overheads and is more likely to results in runtime errors.

Using the Adapter Pattern preserves static type checking… but involves writing lots of tedious code.

Luckily, with the introduction of source generators in .NET 6.0, we don’t have to write tedious code anymore. Instead, we can write really complex source generators 😂

There are a couple of big upsides to a source generators:

  1. They’re more fun to write than tedious repetitive code
  2. They can be shared as NuGet packages (to save others the effort)

And of course, if I was going to write a blog about using Source Generators to solve this problem, I actually had to build said source generator! You can get the result as a NuGet package or the source code is available at https://github.com/mentaldesk/ducktype.

I’ll dig into the source code in a future post. For the time being, here’s what it looks like in action:

using MentalDesk.DuckType;

[assembly: DuckType<Duck, IDuck>]
[assembly: DuckType<Derek, IDuck>]

public interface IDuck
{
    public void Quack();
}

You can see a couple of DuckType<TClass, TInterface> assembly attributes. These are marker attributes that are use to trigger and control the source generators. All the rest of the code is generated automatically:

Source Generators

And we can use those source generated classes like this:

var duck = new Duck();
var derek = new Derek();
IDuck[] ducks = [ duck.AsIDuck(), derek.AsIDuck() ];
foreach (var d in ducks)
{
    d.Quack();
}

You can find a full sample program on GitHub.

The generated source code is almost exactly the same code we wrote for our hand crafted adapters. The only tweak that you can see here is the addition of a couple of AsIDuck extension methods, to make it easier to cast Duck and Derek to IDuck.

Although it was initially a bit painful to get a good workflow for building generators, now that it’s done I’m quite chuffed. As I say, much more fun than writing tedious repetitive code! 🤗

Conclusion

Due to the dynamic keyword in C#, duck typing is equally easy in both JavaScript and C#… however the dynamic nature of the solution, in both cases, runs the risk of runtime surprises.

Adapters can help mitigate that risk but are pretty tedious to write and maintain by hand.

With .NET source generators, a Goldilocks solution is possible where we can reduce the amount of boilerplate code without sacrificing C#‘s static type checking goodness.

In the next post, we’ll dive into the mechanics of how that DuckType source generator works.

Until then, happy coding and keep your types safe!