Доброго времени суток. Столкнулся с интересной задачей в большом проекте, где много расчетов разных расстояний. При том, что даные собираются с разных источников, они могут быть в разных единицах — там метры, там миллиметры. Уследить за всем этим трудно, когда вычисления разбросаны повсюду. А если обьявлена переменная, то зачастую только автору досконально известно, в каких она единицах, так как комментариев в коде почти нет. А автор уволился/забыл/ушёл в запой.
Напрашивается решение описать каждую единицу отдельным типом, например так:
Этим мы увеличили читаемость кода. Но как уберечься от ошибок конвертации? Ведь эти типы совместимы, и их каст не даст ни ошибки, ни ворнинга (см. тут). Я так и не нашел способ выводить ворнинг при касте типов, но мы можем написать автоматическую конвертацию, если такой каст происходит. Вот самый простой пример со сложением и вычитанием:
А вот его использование:
Что даст вот такой результат:
Это решение не идеально, так как делает конвертацию неочевидной, что может породить новые проблемы. Но, если обьявлять все переменные с корректным типом, проблем быть не должно.
Более жестое решение, которое ограничит кастинг типов, это райзить эксепшн при попытке каста, типа так:
В этом случае мы получим ошибку в строке:
Можно развить решение дальше, создав свой тип эксепшна.
Если кто-то сталкивался с подобными проблемами, делитесь опытом в комментариях :) Наверняка есть более элегантные решения, чем описанное выше.
P.S: Тест выполнялся в Delphi 10.1
Напрашивается решение описать каждую единицу отдельным типом, например так:
type
TSizeMeter = single;
TSizeMilliMeter = single;
Этим мы увеличили читаемость кода. Но как уберечься от ошибок конвертации? Ведь эти типы совместимы, и их каст не даст ни ошибки, ни ворнинга (см. тут). Я так и не нашел способ выводить ворнинг при касте типов, но мы можем написать автоматическую конвертацию, если такой каст происходит. Вот самый простой пример со сложением и вычитанием:
interface
type
TSizeMeter = record
value:single;
const units='m';
class operator Add(a, b: TSizeMeter): TSizeMeter;
class operator Subtract(a, b: TSizeMeter): TSizeMeter;
class operator Implicit(a: single): TSizeMeter;
class operator Implicit(a: TSizeMeter): single;
end;
TSizeMiliMeter = record
value:single;
const units='mm';
class operator Add(a, b: TSizeMiliMeter): TSizeMiliMeter;
class operator Subtract(a, b: TSizeMiliMeter): TSizeMiliMeter;
class operator Implicit(a: single): TSizeMiliMeter;
class operator Implicit(a: TSizeMiliMeter): single;
class operator Implicit(a: TSizeMiliMeter): TSizeMeter;
class operator Implicit(a: TSizeMeter): TSizeMiliMeter;
end;
implementation
class operator TSizeMeter.Add(a, b: TSizeMeter): TSizeMeter;
begin
result.value:=a.value+b.value;
end;
class operator TSizeMeter.Subtract(a, b: TSizeMeter): TSizeMeter;
begin
result.value:=a.value-b.value;
end;
class operator TSizeMeter.Implicit(a: single): TSizeMeter;
begin
result.value:=a;
end;
class operator TSizeMeter.Implicit(a: TSizeMeter): single;
begin
result:=a.value;
end;
class operator TSizeMiliMeter.Add(a, b: TSizeMiliMeter): TSizeMiliMeter;
begin
result.value:=a.value+b.value;
end;
class operator TSizeMiliMeter.Subtract(a, b: TSizeMiliMeter): TSizeMiliMeter;
begin
result.value:=a.value-b.value;
end;
class operator TSizeMiliMeter.Implicit(a: single): TSizeMiliMeter;
begin
result.value:=a;
end;
class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): single;
begin
result:=a.value;
end;
class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): TSizeMeter;
begin
result.value:=a.value/1000;
end;
class operator TSizeMiliMeter.Implicit(a: TSizeMeter): TSizeMiliMeter;
begin
result.value:=a.value*1000;
end;
А вот его использование:
var v1:TSizeMeter;
v2:TSizeMiliMeter;
v3:TSizeMeter;
v4:TSizeMiliMeter;
begin
v1:=1.1;
v2:=111.1;
s1:=v1;
s2:=v2;
writeln(formatfloat('0.000',v1.value)+' '+v1.units+' or '+formatfloat('0.000',s1));
writeln(formatfloat('0.000',v2.value)+' '+v2.units+' or '+formatfloat('0.000',s2));
writeln('+');
v3:=v1+v2;
v4:=v1+v2;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('-');
v3:=v1-v2;
v4:=v1-v2;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('cast');
v3:=v2;
v4:=v1;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('mix');
v3:=v2+22.22;
s1:=v1+33.33;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',s1));
end.
Что даст вот такой результат:
1,100 m or 1,100
111,100 mm or 111,100
+
1,211 m
1211,100 mm
?
0,989 m
988,900 mm
cast
0,111 m
1100,000 mm
mix
0,133 m
34,430
Это решение не идеально, так как делает конвертацию неочевидной, что может породить новые проблемы. Но, если обьявлять все переменные с корректным типом, проблем быть не должно.
Более жестое решение, которое ограничит кастинг типов, это райзить эксепшн при попытке каста, типа так:
class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): TSizeMeter;
begin
raise Exception.Create('Typecast not allowed');
end;
class operator TSizeMiliMeter.Implicit(a: TSizeMeter): TSizeMiliMeter;
begin
raise Exception.Create('Typecast not allowed');
end;
В этом случае мы получим ошибку в строке:
v3:=v1+v2;
Можно развить решение дальше, создав свой тип эксепшна.
Если кто-то сталкивался с подобными проблемами, делитесь опытом в комментариях :) Наверняка есть более элегантные решения, чем описанное выше.
P.S: Тест выполнялся в Delphi 10.1
Поделиться с друзьями
Комментарии (9)
nerudo
09.11.2016 17:11Немного из альтернативной вселенной, просто от нечего делать:
type time is range -2147483647 to 2147483647 units fs; ps = 1000 fs; ns = 1000 ps; us = 1000 ns; ms = 1000 us; sec = 1000 ms; min = 60 sec; hr = 60 min; end units;
MrShoor
11.11.2016 01:33Вы производительность такого кода тестировали? Потому что оверхед при перегрузках там есть, и даже с inline-ом. Просто не прикольно, когда сложение двух чисел начинает работать в 8 раз медленнее.
alexeykuzmin0
В c++ бывает очень удобно все переменные всегда хранить в СИ, а для удобства ввода и читаемости кода использовать User-defined literals:
Те же самые user-defined literals очень удобно использовать при работе с комплексными числами, кватернионами, трехмерными векторами, датами и прочей структурированной информацией.
tarasius
Я был бы не против, если бы всё хранилось в СИ, но тут проблема другая — данные хранятся в разных единицах и нужно избежать ошибок конвертации. Самое банальное — забыл конвертировать, и сложил метры с миллиметрами.
NekitoSP
Так реализуйте все вычисления в СИ. Конструкторы замените методами типа Length.FromMeters(...), Length.FromInches(), снаружи вычисления везде производите не с непонятными double и int, а с Length. Ну и собственно при необходимости отобразить где-то значение в виде конкретной единицы реализуйте get-методы типа Meters(), Millimeters(). Подобным образом реализован TimeSpan в C#, и как выяснилось чуть позже — TTimeSpan в Delphi.
tarasius
Ладно еще если метры с миллиметрами где-то перепутал. Перегрузка операторов помогает — можно райзить эксепшн при попытке каста, или конвертировать и хранить всё в СИ.
А если вот где-то ускорение в метрах за секунду в квадрате суммируется с метрами?
Получается что в перегрузке операторов нужно перечислить все возможные типы, в которые оно может кастится.
Жаль, что нельзя принудительно сказать компилятору что эти типы не кастятся, или хотя бы ворнинг выводить.
Deosis
Зачем хранить в разных единицах? На входе преобразовать в одну. На выходе в другую.
Единственный случай когда используются данные сильно разных порядков (т.е. к диаметру Земли прибавить диаметр атома), но такие вычисления часто бессмысленны.
Если не хотите переводить метры в миллиметры, просто не пишите оператор приведения. При попытке одновременной работы вас проверит компилятор, иначе вычисления могут(обязательно) прерваться на последнем этапе.