Привет!
Хочу предложить концепт системы единиц измерения с полной типобезопасностью, хорошей производительностью и полной расширяемостью!
Для нетерпеливых: 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 операторы, почему бы их не попробовать?
Как мы помним, все делалось так, чтобы работала generic math. То есть мы можем подставить любой тип, который реализует необходимые интерфейсы. Например, мы можем взять AngouriMath.Experimental, экспериментальная версия AngouriMath, которая реализует интерфейсы generic math.
Производительность
Не слишком плохо. На самом деле единственный оверхед нашей системы в том, что JIT не промоутит структуры с единственным полем пока что. Поэтому если с float-ами мы передаем из через xmm регистры, то здесь приходится сначала записать значение юнита в память, потом выгрузить на xmm, произвести операцию, и обратно. Тем не менее, быстрее с оберточным типом сделать невозможно, да и потерянное время - это порядок долей наносекунды для одной операции. Больше информации.
Вывод
Вовсе не могу сказать, что это что-то объективно лучшее чем то, что существует. Но как концепт чего-то светлого очень даже. Вот таблица, которая сравнивает мою систему ЕИ, такую у F# и UnitsNet.
Ext. это про расширяемость физических величин и единиц измерения. Таблица здесь.
Гитхаб репозитория и мой гитхаб. Эта же статья на английском.
Спасибо за внимание. Задавайте вопросы, оставляйте фидбек!