Очень типобезопасно! Концепт продвинутой расширяемой системы единиц измерения с generic math для .NET

Привет!

Хочу предложить концепт системы единиц измерения с полной типобезопасностью, хорошей производительностью и полной расширяемостью!

Для нетерпеливых: github.

Пример работыПример работы

Есть несколько существующих решений для ЕИ, например, UnitsNet и Units of Measure in F#. Оба решения популярны и выполняют свою работу. Но мы здесь будет делать полностью расширяемую систему. А еще мы хотим автоматическую конвертацию ЕИ.

Итак, погнали.

Реализация

Основной принцип в том, что мы никак не делим ЕИ на физические величины. У нас нет длин, дистанций, времени, массы, площади, и т. д. Но при этом у каждой ЕИ есть базовая ЕИ и значение.

У ЕИ может быть любая базовая ЕИ. Для простоты я буду брать СИ как базовые ЕИ. Например, для километра базовой ЕИ будет метр (1000 метров в километре). Для грамма — килограмм (0.001 кг в г). Для метра базовая ЕИ — тоже метр (1:1).

Вот так выглядит интерфейс, который реализуется каждой ЕИ:

public interface IBaseUnit
{
    TNumber Base { get; }
    string Postfix { get; }
}

Base — количество базовой ЕИ в нашей. Postfix — просто текстовый эквивалент. Например, так определена минута:

public struct Minute : IBaseUnit, TNumber>
		where TNumber : IMultiplicativeIdentity, IParseable
{
    public string Postfix => "min";
    public TNumber Base => Constants.Number60;
}

TNumber нужен для generic math.

Итак, что насчет арифметических операций? На самом деле для них тоже есть свои единицы измерения. Например, вот так определено деление:

public struct Div
    : IBaseUnit, TNumber>
    where T1Base : struct, IBaseUnit
    where T2Base : struct, IBaseUnit
    where T1 : struct, IBaseUnit
    where T2 : struct, IBaseUnit
    where TNumber : IDivisionOperators
{
    public TNumber Base => new T1().Base / new T2().Base;
    public string Postfix => $"({new T1().Postfix}/{new T2().Postfix})";
}

Немного жирноватое определение, но не в том суть. Div так же реализует IBaseUnit интерфейс, причем базовая ЕИ для него — это деление базовых ЕИ числителя и знаменателя. Например, для ЕИ км/мин базовая ЕИ — м/с.

Так как такая система не зависит от самих единиц и физических величин, мы можем легко создать метод, который конвертирует что угодно в что угодно при условии, что базовая ЕИ совпадает:

Конвертация единиц измерения с общей базовой ЕИКонвертация единиц измерения с общей базовой ЕИ

Т. е. мы просто требуем одну и ту же базовую ЕИ, и отталкиваясь от нее конвертирует любую в любую. А если базовая ЕИ не совпадает, значит нельзя конвертировать!

Конвертация из метров в секунды невозможнаКонвертация из метров в секунды невозможна

Подобным способом, требуя одну базовую ЕИ, мы можем реализовать сложение. К сожалению, оператор + не получится определить, так как у нас не может быть generic оператор. Поэтому я сделал его методом расширения (extension method):

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Unit 
    Add(this Unit a, Unit b)
    where T1 : IBaseUnit
    where T2 : IBaseUnit
    // убрал несколько constraint-ов для облегчения чтения
    => 
        typeof(T1) == typeof(T2)
        ? new(a.Float + b.Float)
        : new((a.Float * new T1().Base + b.Float * new T2().Base) / new T1().Base);

Такой метод автоматически конвертирует методы с одинаковой базовой ЕИ даже если сами ЕИ разные. Например, 20 секунд + 1 минута = 80 секунд. 1 км + 1 миля = 2.6 км. Но попытка сложить секунды и метры не удастся (не скомпилируется).

Пришло время демонстрации результат работы.

Примеры работы

Все подряд:

Большой пример работыБольшой пример работы

В отличии от C#, в F# есть generic операторы, почему бы их не попробовать?

Пример работы библиотеки в F#Пример работы библиотеки в F#

Как мы помним, все делалось так, чтобы работала generic math. То есть мы можем подставить любой тип, который реализует необходимые интерфейсы. Например, мы можем взять AngouriMath.Experimental, экспериментальная версия AngouriMath, которая реализует интерфейсы generic math.

Пример работы символьной алгебры с нашей системой ЕИПример работы символьной алгебры с нашей системой ЕИ

Производительность

Не слишком плохо. На самом деле единственный оверхед нашей системы в том, что JIT не промоутит структуры с единственным полем пока что. Поэтому если с float-ами мы передаем из через xmm регистры, то здесь приходится сначала записать значение юнита в память, потом выгрузить на xmm, произвести операцию, и обратно. Тем не менее, быстрее с оберточным типом сделать невозможно, да и потерянное время — это порядок долей наносекунды для одной операции. Больше информации.

Вывод

Вовсе не могу сказать, что это что-то объективно лучшее чем то, что существует. Но как концепт чего-то светлого очень даже. Вот таблица, которая сравнивает мою систему ЕИ, такую у F# и UnitsNet.

image-loader.svg

Ext. это про расширяемость физических величин и единиц измерения. Таблица здесь.

Гитхаб репозитория и мой гитхаб. Эта же статья на английском.

Спасибо за внимание. Задавайте вопросы, оставляйте фидбек!

© Habrahabr.ru