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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет!

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

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

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

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

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

Реализация

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

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

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

public interface IBaseUnit<T, TNumber>
{
    TNumber Base { get; }
    string Postfix { get; }
}

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

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

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

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

public struct Div<T1, T2, T1Base, T2Base, TNumber>
    : IBaseUnit<Div<T1Base, T2Base, T1Base, T2Base, TNumber>, TNumber>
    where T1Base : struct, IBaseUnit<T1Base, TNumber>
    where T2Base : struct, IBaseUnit<T2Base, TNumber>
    where T1 : struct, IBaseUnit<T1Base, TNumber>
    where T2 : struct, IBaseUnit<T2Base, TNumber>
    where TNumber : IDivisionOperators<TNumber, TNumber, TNumber>
{
    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<T1, TBase, TNumber> 
    Add<T1, T2, TBase, TNumber>(this Unit<T1, TBase, TNumber> a, Unit<T2, TBase, TNumber> b)
    where T1 : IBaseUnit<TBase, TNumber>
    where T2 : IBaseUnit<TBase, TNumber>
    // убрал несколько 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.

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

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Если используете единицы измерения в .net, то какую библиотеку?
0% UnitsNet 0
0% UoMs в F# 0
0% FSharp.UMX 0
0% Свою 0
0% Другое 0
Никто еще не голосовал. Воздержались 3 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Хотели бы видеть описанную библиотеку в полностью рабочем виде?
100% Да 1
0% Нет, так как нет необходимости в ее плюсах 0
0% Нет (по другой причине) 0
Проголосовал 1 пользователь. Воздержался 1 пользователь.
Источник: https://habr.com/ru/post/597437/


Интересные статьи

Интересные статьи

.NET nanoFramework — это бесплатная платформа с открытым исходным кодом, основанная на .NET и предназначена для малых встраиваемых устройств, микроконтроллеров. С ее помощью можно раз...
Железнодорожные вокзалы России могут оборудовать автоматическими системами измерения температуры у пассажиров. Такая система уже работает на Ленинградском вокзале Москвы. Об этом ...
Перевод статьи подготовлен специально для студентов курса «Разработчик Python». Когда вы пишете на низкоуровневом языке, таком как С, вы беспокоитесь о выборе правильного типа данных и специ...
Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...
«Битрикс» — кошмар на костылях. Эта популярная характеристика системы среди разработчиков и продвиженцев ныне утратила свою актуальность.