Как стать автором
Обновить

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

Время на прочтение3 мин
Количество просмотров5K

Привет!

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

Для нетерпеливых: 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, то какую библиотеку?
11.11% UnitsNet2
16.67% UoMs в F#3
0% FSharp.UMX0
50% Свою9
22.22% Другое4
Проголосовали 18 пользователей. Воздержались 52 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Хотели бы видеть описанную библиотеку в полностью рабочем виде?
75% Да33
20.45% Нет, так как нет необходимости в ее плюсах9
4.55% Нет (по другой причине)2
Проголосовали 44 пользователя. Воздержались 28 пользователей.
Теги:
Хабы:
Всего голосов 23: ↑23 и ↓0+23
Комментарии15

Публикации

Истории

Работа

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань