Here is how I have done it in the past. I used a generic specification for a future class. Below TComplex is to specified by the deriving classes as their own, but it can be used in the base class to describe those future classes.
An example implementation of complex and dual numbers is below with algebra, formatting, and some other goodies.
Complex numeric type usage
The numeric types define algebra style operators and most call functions on the common base class. What is different between the specific complex types are the rules of multiplication which are specialized with the Product(TComplex other) method.
static class Program
{
static void Main(string[] args)
{
var a = new ComplexNumber(1.0, -5.0);
var b = new ComplexNumber(1.0, +5.0);
Console.WriteLine($"a = {a}, {a.GetType().Name}");
Console.WriteLine($"b = {b}, {b.GetType().Name}");
//a = 1-5i, ComplexNumber
//b = 1+5i, ComplexNumber
Console.WriteLine($"a + b = {a + b}");
Console.WriteLine($"a - b = {a - b}");
Console.WriteLine($"a * b = {a * b}");
// a + b = 2
// a - b = -10i
// a * b = 26
var c = 2.0 * a + 3.0 * b;
Console.WriteLine($"c = {c}, {c.GetType().Name}");
// c = 5+5i, ComplexNumber
var p = new DualNumber(1.0, -5.0);
var q = new DualNumber(1.0, +5.0);
Console.WriteLine($"p = {p}, {p.GetType().Name}");
Console.WriteLine($"q = {q}, {q.GetType().Name}");
//p = 1 - 5ε, DualNumber
//q = 1 + 5ε, DualNumber
Console.WriteLine($"p + q = {p + q}");
Console.WriteLine($"p - q = {p - q}");
Console.WriteLine($"p * q = {p * q}");
// p + q = 2
// p - q = -10ε
// p * q = 1
}
}
Complex numeric type implementation
Most of the common code is defined in the base class. Each specialized complex type defines its own constructors, random initialization, own equality, the symbol for non-real component, rule for multiplication, and implicit/explicit conversions to and from double.
public abstract class HyperComplexNumber<TComplex>
where TComplex : HyperComplexNumber<TComplex>, new()
{
protected static readonly Random rng = new Random();
protected HyperComplexNumber(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
public double Real { get; private set; }
public double Imaginary { get; private set; }
public abstract string Symbol { get; }
public TComplex Scale(double factor)
{
var t = new TComplex
{
Real = factor * Real,
Imaginary = factor * Imaginary
};
return t;
}
public TComplex Add(TComplex other)
{
var t = new TComplex
{
Real = Real + other.Real,
Imaginary = Imaginary + other.Imaginary
};
return t;
}
public TComplex Subtract(TComplex other)
{
var t = new TComplex
{
Real = Real - other.Real,
Imaginary = Imaginary - other.Imaginary
};
return t;
}
public abstract TComplex Product(TComplex other);
#region Operators
public static TComplex operator +(HyperComplexNumber<TComplex> rhs) => (TComplex)rhs;
public static TComplex operator -(HyperComplexNumber<TComplex> rhs) => rhs.Scale(-1);
public static TComplex operator +(HyperComplexNumber<TComplex> lhs, TComplex rhs) => lhs.Add(rhs);
public static TComplex operator -(HyperComplexNumber<TComplex> lhs, TComplex rhs) => lhs.Subtract(rhs);
public static TComplex operator *(double factor, HyperComplexNumber<TComplex> lhs) => lhs.Scale(factor);
public static TComplex operator *(HyperComplexNumber<TComplex> lhs, double factor) => lhs.Scale(factor);
public static TComplex operator /(HyperComplexNumber<TComplex> lhs, double divider) => lhs.Scale(1/divider);
public static TComplex operator *(HyperComplexNumber<TComplex> lhs, TComplex rhs) => lhs.Product(rhs);
#endregion
#region IEquatable Members
/// <summary>
/// Equality overrides from <see cref="System.Object"/>
/// </summary>
/// <param name="obj">The object to compare this with</param>
/// <returns>False if object is a different type, otherwise it calls <code>Equals(HyperComplexNumber)</code></returns>
public override bool Equals(object obj)
{
if (obj is HyperComplexNumber<TComplex> item)
{
return Equals(item);
}
return false;
}
/// <summary>
/// Checks for equality among <see cref="HyperComplexNumber"/> classes
/// </summary>
/// <returns>True if equal</returns>
public bool Equals(HyperComplexNumber<TComplex> other)
{
return Real.Equals(other.Real)
&& Imaginary.Equals(other.Imaginary);
}
/// <summary>
/// Calculates the hash code for the <see cref="HyperComplexNumber"/>
/// </summary>
/// <returns>The int hash value</returns>
public override int GetHashCode()
{
unchecked
{
int hc = -1817952719;
hc = (-1521134295) * hc + Real.GetHashCode();
hc = (-1521134295) * hc + Imaginary.GetHashCode();
return hc;
}
}
#endregion
public string ToString(string format, IFormatProvider formatProvider)
{
if (Real == 0 && Imaginary == 0) return "0";
string rPart = string.Empty, iPart = string.Empty, sign = string.Empty;
if (Real != 0.0)
{
rPart = Real.ToString(format, formatProvider);
}
if (Imaginary != 0.0)
{
if (Imaginary < 0)
{
sign = "-";
}
else if (Real != 0)
{
sign = "+";
}
iPart = $"{Math.Abs(Imaginary).ToString(format, formatProvider)}{Symbol}";
}
return $"{rPart}{sign}{iPart}";
}
public string ToString(string formatting)
=> ToString(formatting, null);
public override string ToString()
=> ToString("g");
}
public sealed class ComplexNumber :
HyperComplexNumber<ComplexNumber>,
IEquatable<ComplexNumber>
{
public ComplexNumber() : this(0, 0) { }
public ComplexNumber(double real) : this(real, 0) { }
public ComplexNumber(double real, double imaginary) : base(real, imaginary) { }
public static ComplexNumber Random() => new ComplexNumber(rng.NextDouble(), rng.NextDouble());
public static implicit operator ComplexNumber(double real) => new ComplexNumber(real);
public static explicit operator double(ComplexNumber number) => number.Real;
public override string Symbol => "i";
public override ComplexNumber Product(ComplexNumber other)
=> new ComplexNumber(
Real * other.Real - Imaginary * other.Imaginary,
Real * other.Imaginary + Imaginary * other.Real);
public bool Equals(ComplexNumber other)
=> base.Equals(other);
}
public sealed class DualNumber :
HyperComplexNumber<DualNumber>,
IEquatable<DualNumber>
{
public DualNumber() : this(0, 0) { }
public DualNumber(double real) : this(real, 0) { }
public DualNumber(double real, double imaginary) : base(real, imaginary) { }
public static DualNumber Random() => new DualNumber(rng.NextDouble(), rng.NextDouble());
public static implicit operator DualNumber(double real) => new DualNumber(real);
public static explicit operator double(DualNumber number) => number.Real;
public override string Symbol => "ε";
public override DualNumber Product(DualNumber other)
=> new DualNumber(
Real * other.Real,
Real * other.Imaginary + Imaginary * other.Real);
public bool Equals(DualNumber other)
=> base.Equals(other);
}
PS. I recommend using the new record types which are like classes, but immutable and can auto-equate by value, something I have not implemented above.