Стоит ли пользоваться?
Да. Если у меня есть номер передо мной, я хочу знать, что это такое. В любое время дня. Кроме того, это то, что мы обычно делаем. Мы организуем данные в значимую сущность - класс, структуру, вы называете это. Удваивается в координаты, строки в имена и адреса и т. Д. Почему единицы должны быть другими?
Кто-то уже сделал лучше?
Зависит от того, как лучше определять. Есть несколько библиотек, но я их не пробовал, поэтому у меня нет мнения. К тому же это портит удовольствие от пробовать самому :)
Теперь о реализации. Я хотел бы начать с очевидного: бесполезно пытаться воспроизвести [<Measure>]
систему F # в C #. Почему? Поскольку, как только F # позволяет вам использовать /
^
(или что-то еще в этом отношении) непосредственно с другим типом, игра проиграна. Удачи в выполнении этого на C # на struct
или class
. Уровень метапрограммирования, необходимый для такой задачи, не существует, и я боюсь, что он не будет добавлен в ближайшее время - на мой взгляд. Вот почему вам не хватает размерного анализа, о котором Мэтью Крамли упомянул в своем ответе.
Возьмем пример из fsharpforfunandprofit.com: у вас Newton
s определены как [<Measure>] type N = kg m/sec^2
. Теперь у вас есть функция square
, созданная автором, которая возвращает N^2
, что звучит неправильно, абсурдно и бесполезно. Если вы не хотите выполнять арифметические операции, когда в какой-то момент процесса оценки вы можете получить что-то бессмысленное, пока вы не умножите это на какую-то другую единицу и не получите значимый результат. Или, что еще хуже, вы можете захотеть использовать константы. Например, газовая постоянная R
, которая равна 8.31446261815324
J /(K mol)
. Если вы определите соответствующие единицы, тогда F # готов использовать константу R. C # - нет. Вам нужно указать другой тип только для этого, и вы все равно не сможете выполнять какие-либо операции с этой константой.
Это не значит, что не стоит пытаться. Я сделал это и очень доволен результатами. Я начал SharpConvert около 3 лет назад, после того как меня вдохновил именно этот вопрос. Триггером послужила такая история: однажды мне пришлось исправить неприятную ошибку в разработанном мной симуляторе RADAR: самолет падал в земле вместо того, чтобы следовать заранее заданной глиссаде. Это не радовало меня, как вы могли догадаться, и после 2 часов отладки я понял, что где-то в своих расчетах я считал километры морскими милями. До этого момента я думал, что я просто буду «осторожен», что, по крайней мере, наивно для любой нетривиальной задачи.
В вашем коде было бы несколько вещей, которые я бы сделал по-другому.
Сначала я бы превратил реализации UnitDouble<T>
и IUnit
в структуры. Единица - это просто число, и если вы хотите, чтобы они рассматривались как числа, структура является более подходящим подходом.
Тогда я бы избегал new T()
в методах. Он не вызывает конструктор, он использует Activator.CreateInstance<T>()
, и для обработки чисел это будет плохо, так как добавит накладные расходы. Хотя это зависит от реализации, для простого приложения конвертера единиц это не повредит. Для критического по времени контекста избегайте чумы. И не поймите меня неправильно, я использовал его сам, так как не знал лучшего, и на днях я запустил несколько простых тестов, и такой вызов может удвоить время выполнения - по крайней мере, в моем случае. Подробнее см. Рассмотрение ограничения new () в C #: прекрасный пример дырявой абстракции
Я бы также изменил Convert<T, R>()
и сделал его функцией-членом. Я предпочитаю писать
var c = new Unit<Length.mm>(123);
var e = c.Convert<Length.m>();
скорее, чем
var e = Length.Convert<Length.mm, Length.m>(c);
И последнее, но не менее важное: я бы использовал отдельные оболочки для каждой физической величины (время длины и т. Д.) Вместо UnitDouble, так как будет проще добавить функции, специфичные для физических величин, и перегрузки операторов. Это также позволит вам создать Speed<TLength, TTime>
оболочку вместо другого Unit<T1, T2>
или даже Unit<T1, T2, T3>
класса. Так это выглядело бы так:
public readonly struct Length<T> where T : struct, ILength
{
private static readonly double SiFactor = new T().ToSiFactor;
public Length(double value)
{
if (value < 0) throw new ArgumentException(nameof(value));
Value = value;
}
public double Value { get; }
public static Length<T> operator +(Length<T> first, Length<T> second)
{
return new Length<T>(first.Value + second.Value);
}
public static Length<T> operator -(Length<T> first, Length<T> second)
{
// I don't know any application where negative length makes sense,
// if it does feel free to remove Abs() and the exception in the constructor
return new Length<T>(System.Math.Abs(first.Value - second.Value));
}
// You can add more like
// public static Area<T> operator *(Length<T> x, Length<T> y)
// or
//public static Volume<T> operator *(Length<T> x, Length<T> y, Length<T> z)
// etc
public Length<R> To<R>() where R : struct, ILength
{
//notice how I got rid of the Activator invocations by moving them in a static field;
//double mult = new T().ToSiFactor;
//double div = new R().ToSiFactor;
return new Length<R>(Value * SiFactor / Length<R>.SiFactor);
}
}
Заметьте также, что, чтобы спасти нас от ужасного вызова Activator, я сохранил результат new T().ToSiFactor
в SiFactor. Сначала это может показаться неудобным, но, поскольку Length является общим, Length<mm>
будет иметь свою собственную копию, Length<Km>
свою собственную, и так далее и тому подобное. Обратите внимание, что ToSiFactor
- это toBase
вашего подхода.
Проблема, которую я вижу, заключается в том, что пока вы находитесь в сфере простых единиц и вплоть до первой производной времени, все просто. Если вы попытаетесь сделать что-то более сложное, то увидите недостатки такого подхода. Набор текста
var accel = new Acceleration<m, s, s>(1.2);
не будет такой четкой и гладкой, как
let accel = 1.2<m/sec^2>
И независимо от подхода вам нужно будет указать каждую математическую операцию, которая вам понадобится, с большой перегрузкой оператора, в то время как в F # у вас это бесплатно, даже если результаты не имеют смысла, как я писал в начале.
Последний недостаток (или преимущество, в зависимости от того, как вы его видите) этого дизайна заключается в том, что он не может быть независимым от единиц измерения. Если есть случаи, когда вам нужна только длина, вы не сможете ее получить. Каждый раз вам нужно знать, составляет ли ваша длина миллиметры, статутная миля или фут. Я использовал противоположный подход в SharpConvert, и LengthUnit
происходит от UnitBase
, а метры-километры и т. Д. Происходят от этого. Вот почему я, кстати, не мог пойти по пути struct
. Таким образом вы получите:
LengthUnit l1 = new Meters(12);
LengthUnit l2 = new Feet(15.4);
LengthUnit sum = l1 + l2;
sum
будет метрами, но это не должно волновать, если они хотят использовать его в следующей операции. Если они хотят отобразить это, они могут вызвать sum.To<Kilometers>()
или любой другой модуль. Честно говоря, я не знаю, имеет ли отсутствие привязки переменной к конкретному модулю какие-либо преимущества. Возможно, в какой-то момент стоит исследовать это.
person
Stelios Adamantidis
schedule
12.05.2021