Skip to content

Instantly share code, notes, and snippets.

@byme8
Last active January 5, 2021 09:27
Show Gist options
  • Save byme8/6e6c621198fcae7cf144e68cc45eb37a to your computer and use it in GitHub Desktop.
Save byme8/6e6c621198fcae7cf144e68cc45eb37a to your computer and use it in GitHub Desktop.
DuckInterface

DuckInterface

I was playing with new .Net 5 and the Source Generator lately and got an idea that it is possible to add "duck typing" support to C#. I would say it is purely academic(no one will use it in production I hope), but it is fun stuff so I decided to try.

The nuget package with results you can find here

Nuget

The repository is here: https://github.com/byme8/DuckInterface

How to use it

Let's suppose that you have the next declaration:

public interface ICalculator
{
  float Calculate(float a, float b);
}

public class AddCalculator
{

  float Calculate(float a, float b);
}

It is important to notice that the AddCalculator doesn't implement a ICalculator in any way. It just has an identical method declaration. If we try to use it like in the next snippet we will get a compilation error:

var addCalculator = new AddCalculator();

var result = Do(addCalculator, 10, 20);

float Do(ICalculator calculator, float a, float b)
{
  return calculator.Calculate(a, b);
}

In this case, duck typing can be helpful, because it will allow us to pass AddCalculator with ease. The DuckInterface may help with it. You will need to install the NuGet package and update the interface declaration like that:

[Duckable]
public interface ICalculator
{
  float Calculate(float a, float b);
}

Then we will need to update the Do method. Repace the ICalculator with a DICalculator. The DICalculator is a class that was generated by DuckInterface. The DICalculator has a public interface identical to ICalculator and can contain implicit conversion operators for any class. Those implicit conversion operators are generated by the Source Generator too. The generation happens on a fly as you typing in IDE and depends on the DICalculator usage.

The final snippet:

var addCalculator = new AddCalculator();

var result = Do(addCalculator, 10, 20);

float Do(DICalculator calculator, float a, float b)
{
  return calculator.Calculate(a, b);
}

And it's done. The compilation errors are gone and everything works as expected.

How it works

There are two independent source generators. The first one looks for Duckable attribute and generates a 'base' class for the interface. For example, for the ICalculator it will look like that:

public partial class DICalculator : ICalculator 
{
  [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] 
  private readonly Func<float, float, float> _Calculate;        

  [System.Diagnostics.DebuggerStepThrough]
  public float Calculate(float a, float b)
  {
      return _Calculate(a, b);
  }
}

The second one looks for a method call and variable assignments to understand how the duckable interface may be used. For example, lets look for next snippet:

var result = Do(addCalculator, 10, 20);

The analyzer will see that the Do method has an argument with type DICalculator, then it will check the type of addCalculator variable. If the type has all the required members, the source generator will extend the DICalculator. The extension will look like that:

public partial class DICalculator
{
  private DICalculator(global::AddCalculator value) 
  {
       _Calculate = value.Calculate;
  }

  public static implicit operator DICalculator(global::AddCalculator value)
  {
      return new DICalculator(value);
  }
}

Because the DICalculator is a partial class we can execute this trick as much time as we want. Also this trick applicable for C# properties too. The result will look like this:

[Duckable]
public interface ICalculator
{
    float Zero { get; }
    float Value { get; set; }
    float Calculate(float a, float b);
}
// ....
public partial class DICalculator : ICalculator 
{
    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] 
    private readonly Func<float> _ZeroGetter;

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] 
    private readonly Func<float> _ValueGetter;

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] 
    private readonly Action<float> _ValueSetter;

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] 
    private readonly Func<float, float, float> _Calculate;        

    public float Zero
    {
         [System.Diagnostics.DebuggerStepThrough] get { return _ZeroGetter(); }
    }

    public float Value
    {
         [System.Diagnostics.DebuggerStepThrough] get { return _ValueGetter(); }
         [System.Diagnostics.DebuggerStepThrough] set { _ValueSetter(value); }
    }

    [System.Diagnostics.DebuggerStepThrough]
    public float Calculate(float a, float b)
    {
        return _Calculate(a, b);
    }
}

Current limitations

  • No support for generics
  • No support for ref structs
  • Ducked interface can't be a generic argument

The first one potentially fixable, but the last two require changes in the C# compiler so there is no way to add them via nuget package right now. Any ideas or suggestions are welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment