I've been able to come up with something that is along the lines of what you're looking for; it's far less elegant than what can be done in C++.
The root of it is a struct
that looks like
namespace TypeTraits
{
struct Number<T, TOperations> : IComparable, IFormattable, IConvertible, IComparable<Number<T, TOperations>>, IEquatable<Number<T, TOperations>>, IComparable<T>, IEquatable<T>
where T : IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
where TOperations : Operations<Number<T, TOperations>>, new()
{
static readonly Operations<Number<T, TOperations>> Operations = new TOperations();
public T Value { get; }
public Number(T value)
{
Value = value;
}
public static implicit operator Number<T, TOperations>(T source) => new Number<T, TOperations>(source);
public static implicit operator T(Number<T, TOperations> source) => source.Value;
public override bool Equals(object obj) => Value.Equals(obj);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public bool Equals(T other) => Value.Equals(other);
public bool Equals(Number<T, TOperations> other) => Equals(other.Value);
public int CompareTo(object obj) => Value.CompareTo(obj);
public int CompareTo(T other) => Value.CompareTo(other);
public int CompareTo(Number<T, TOperations> other) => CompareTo(other.Value);
public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);
public TypeCode GetTypeCode() => Value.GetTypeCode();
public bool ToBoolean(IFormatProvider provider) => Value.ToBoolean(provider);
public byte ToByte(IFormatProvider provider) => Value.ToByte(provider);
public char ToChar(IFormatProvider provider) => Value.ToChar(provider);
public DateTime ToDateTime(IFormatProvider provider) => Value.ToDateTime(provider);
public decimal ToDecimal(IFormatProvider provider) => Value.ToDecimal(provider);
public double ToDouble(IFormatProvider provider) => Value.ToDouble(provider);
public short ToInt16(IFormatProvider provider) => Value.ToInt16(provider);
public int ToInt32(IFormatProvider provider) => Value.ToInt32(provider);
public long ToInt64(IFormatProvider provider) => Value.ToInt64(provider);
public sbyte ToSByte(IFormatProvider provider) => Value.ToSByte(provider);
public float ToSingle(IFormatProvider provider) => Value.ToSingle(provider);
public string ToString(IFormatProvider provider) => Value.ToString(provider);
public object ToType(Type conversionType, IFormatProvider provider) => Value.ToType(conversionType, provider);
public ushort ToUInt16(IFormatProvider provider) => Value.ToUInt16(provider);
public uint ToUInt32(IFormatProvider provider) => Value.ToUInt32(provider);
public ulong ToUInt64(IFormatProvider provider) => Value.ToUInt64(provider);
public static Number<T, TOperations> operator+(Number<T, TOperations> lhs, Number<T, TOperations> rhs) => Operations.Add(lhs, rhs);
}
}
yes, that's a lot of code; but most of it is simple pass-through.
You then have an abstract Operations
class and a couple of concrete implementations. This is the "trick" to get things into a type so it can be used as a generic parameter.
namespace TypeTraits
{
class Operations<T>
where T : IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
{
// names from https://msdn.microsoft.com/en-us/library/ms182355.aspx
public virtual T Add(T a, T b)
{
throw new NotImplementedException();
}
}
sealed class OperationsInt32 : Operations<Int32>
{
public override Int32 Add(Int32 a, Int32 b)
{
return a.Value + b.Value;
}
}
sealed class OperationsDouble : Operations<Double>
{
public override Double Add(Double a, Double b)
{
return a.Value + b.Value;
}
}
}
Finally, at the top-level (outside of namespace
), some using
aliases
using Int32 = TypeTraits.Number<System.Int32, TypeTraits.OperationsInt32>;
using Double = TypeTraits.Number<System.Double, TypeTraits.OperationsDouble>;
With all that in place, you can now write code like:
class Program
{
static void DoSomething<T>(T t)
{
Console.WriteLine("DoSomething<T>");
}
static void DoSomething<T, TOperations>(Number<T, TOperations> t)
where T : IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
where TOperations : Operations<Number<T, TOperations>>, new()
{
Number<T, TOperations> t2 = t;
var t3 = t + t2;
Console.WriteLine("DoSomething<Number<T, TOperations>>");
}
static void Main(string[] args)
{
string s = "314";
Int32 i = 314;
Double d = 3.14;
DoSomething(s);
DoSomething(i);
DoSomething(d);
}
}
It might be possible to simplify this a bit by removing some constraints ...