When writing C#, I tend to write a lot of value type objects that implement IEquatable<T>
, IComparable<T>
, or both.
For the sake of this proposal, let's assume that I'm writing a fictitious struct called Int256
with equatable and comparable value semantics; for example:
public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable
{
public bool Equals(Int256 other)
{
// TODO : is this equal to other?
}
public int CompareTo(Int256 other)
{
// TODO : how does this compare to other?
}
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Int256 int256) throw new ArgumentException("Obj must be of type Int256.");
return CompareTo(int256);
}
public static bool operator ==(Int256 left, Int256 right)
{
return Equals(left, right);
}
public static bool operator !=(Int256 left, Int256 right)
{
return !Equals(left, right);
}
public static bool operator >(Int256 left, Int256 right)
{
return left.CompareTo(right) is 1;
}
public static bool operator >=(Int256 left, Int256 right)
{
return left.CompareTo(right) is 1 or 0;
}
public static bool operator <(Int256 left, Int256 right)
{
return left.CompareTo(right) is -1;
}
public static bool operator <=(Int256 left, Int256 right)
{
return left.CompareTo(right) is -1 or 0;
}
}
Let's also assume that I were to create some other fictitious structs with the same semantics; for example UInt256
and Decimal256
. While trivial, those operators become tedious to implement for every value type object.
Recently, I've been looking at C# 11's new language features, specifically static interface methods, which I believe is largely what makes the new generic math interfaces possible. With that in mind, I could add some additional interfaces to my implementation, specifically IEqualityOperators<TSelf, TOther, TResult>
and IComparisonOperators<TSelf, TOther, TResult>
; for example:
public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable, IComparisonOperators<Int256, Int256, bool>
{
...
}
For completeness, there is no need to implement IEqualityOperators<TSelf, TOther, TResult>
here, as they are extended by IComparisonOperators<TSelf, TOther, TResult>
anyway.
Ultimately, this doesn't really solve the problem. All these interfaces do is ensure that the operators are implemented.
My proposal is whether interfaces could be designed that auto-implement some of the typical boilerplate code associated with IEquatable<T>
and IComparable<T>
, specifically the operators: ==
, !=
, >
, >=
, <
, <=
; for example:
IAutoEquatable<T>
public interface IAutoEquatable<T> : IEquatable<T>, IEqualityOperators<T, T, bool> where T : IAutoEquatable<T>
{
// Auto-implemented boilerplate.
static virtual bool operator ==(T? left, T? right)
{
return Equals(left, right);
}
// Auto-implemented boilerplate.
static virtual bool operator !=(T? left, T? right)
{
return !Equals(left, right);
}
}
IAutoComparable<T>
public interface IAutoComparable<T> : IComparable<T>, IComparable, IComparisonOperators<T, T, bool> where T : IAutoComparable<T>
{
// Auto-implemented boilerplate.
static virtual bool operator ==(T? left, T? right)
{
return Equals(left, right);
}
// Auto-implemented boilerplate.
static virtual bool operator !=(T? left, T? right)
{
return !Equals(left, right);
}
// Auto-implemented boilerplate.
static virtual bool operator >(T left, T right)
{
return left.CompareTo(right) is 1;
}
// Auto-implemented boilerplate.
static virtual bool operator >=(T left, T right)
{
return left.CompareTo(right) is 1 or 0;
}
// Auto-implemented boilerplate.
static virtual bool operator <(T left, T right)
{
return left.CompareTo(right) is -1;
}
// Auto-implemented boilerplate.
static virtual bool operator <=(T left, T right)
{
return left.CompareTo(right) is -1 or 0;
}
}
The intention here is that the implementor would only require implementation of bool Equals(T other)
and int CompareTo(T other)
respectively, but, given that the operators are implemented on the interface, they get the operators for free!
Given my Int256
example, it might look something like this:
public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable
{
public bool Equals(Int256 other)
{
// TODO : is this equal to other?
}
public int CompareTo(Int256 other)
{
// TODO : how does this compare to other?
}
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Int256 int256) throw new ArgumentException("Obj must be of type Int256.");
return CompareTo(int256);
}
}
But I would still be able to use the operators with it; for example:
Int256 a = 123;
Int256 b = 456;
a == b; // False
a != b; // True
a > b; // False
a >= b; // False
a < b; // True
a <= b; // True
There is a problem, however.
Whilst those interfaces IAutoEquatable<T>
and IAutoComparable<T>
contain implementations for the operators, I'm still expected to implement them in Int256
.
Questions
- Why do
virtual
default implementations in interfaces still require implementation? i.e. why doesn'tInt256
just use the default implementation? - Might it be possible for a future version of C# to address this issue, such that we can use it to alleviate the need to write boilerplate code?
Raised here with the C# language design team: https://github.com/dotnet/csharplang/discussions/7032