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
The repository is here: https://github.com/byme8/DuckInterface
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.
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);
}
}
- 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!