Типизация километров и миль для корректности расчётов

Никак не могу поймать концепт.

Есть система учета расстояния перевозки грузов

void savePathToDb(stping cargold, int milesPath, int KmPath)
{
}

var id = "test";
var miles = 109;
var kilosl = 46;
var kilos2 = 86;
SavePathToDB(id, kilosl, miles + kilos2);

И у нас возникла проблема. Из за того что часть товаров возят по дорогам и меряют километрами, а часть по морю и меряют морскими милями, часто получается ситуация когда разработчики по ошибке путают мили с километрами и получают неверные данные.

Как нам используя знания о конструировании типов создать типизацию при которой такая ошибка будет невозможна?

Была мысль написать отдельные структуры для мили и километра как единицы. Но в этом случае всё равно можно будет присвоить некорректное значение. Буду рад любым подсказкам!


Ответы (1 шт):

Автор решения: VladD

У меня в проекте в аналогичных случаях делается как-то так:

Определяем типизированную длину. Чтобы не возникало соблазна сконструировать непонятно из каких единиц, прячем конструктор, вместо него выставляем наружу так называемые именованные конструкторы.

struct Distance : IComparable<Distance>, IEquatable<Distance>
{
    readonly double meters;
    private Distance(double meters) => this.meters = meters;

    const double metersInKm = 1000.0;
    const double metersInMile = 1609.344;

    public static Distance FromKilometers(double km) => new Distance(km * metersInKm);
    public static Distance FromMiles(double miles) => new Distance(miles * metersInMile);

    public double Meters => meters;

    public static Distance operator + (Distance d1, Distance d2) =>
        new Distance(d1.meters + d2.meters);

    #region Equals, GetHashCode and operator ==
    public override bool Equals(object? obj) => obj is Distance other && Equals(other);
    public bool Equals(Distance other) => meters == other.meters;
    public override int GetHashCode() => HashCode.Combine(meters);
    public static bool operator == (Distance left, Distance right) => left.Equals(right);
    public static bool operator != (Distance left, Distance right) => !(left == right);
    #endregion

    #region CompareTo and operator <
    public int CompareTo(Distance other) => meters.CompareTo(other.meters);

    public static bool operator <  (Distance left, Distance right) =>
            left.CompareTo(right) < 0;
    public static bool operator <= (Distance left, Distance right) =>
            left.CompareTo(right) <= 0;
    public static bool operator >  (Distance left, Distance right) =>
            left.CompareTo(right) > 0;
    public static bool operator >= (Distance left, Distance right) =>
            left.CompareTo(right) >= 0;
    #endregion
}

Теперь ваш метод SavePathToDB должен принимать на вход не int, а Distance.

При этом Distance.FromKilometers(2) + Distance.FromMiles(1.5) вполне работает и даёт верный результат, а вот Distance.FromKilometers(2) + Weight.FromKilograms(3) не компилируется.


Кода получилось много, так что если у вас в проекте много таких вот структур, имеет смысл воспользоваться кодогенерацией.


Кстати, в братском F# единицы длины с аналогичными свойствами (включая автоматическое создание производных единиц наподобие m/s^2) встроены прямо в язык.

→ Ссылка