Работа с разными единицами измерения одинакового типа и их конвертация

от автора

Доброго времени суток.
Столкнулся с интересной задачей в большом проекте, где много расчетов разных расстояний. При том, что даные собираются с разных источников, они могут быть в разных единицах — там метры, там миллиметры. Уследить за всем этим трудно, когда вычисления разбросаны повсюду. А если обьявлена переменная, то зачастую только автору досконально известно, в каких она единицах, так как комментариев в коде почти нет. А автор уволился/забыл/ушёл в запой.

Напрашивается решение описать каждую единицу отдельным типом, например так:

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
ссылка на оригинал статьи https://habrahabr.ru/post/314814/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *