четверг, ноября 25, 2010

Условная компиляция: defines vs. const

Те кому приходится обеспечивать работоспособность кода на нескольких версиях Delphi не по наслышке знают о “прелестях” условной компиляции. Довольно сложно держать в голове набор фич каждой версии и особенности их работы. Ориентация на CompilerVersion, RTLVersion и VERXXX кажется простым делом в момент написания кода, но превращается в кошмар к моменту его рефакторинга т.к. все детали из головы уже выветрились и скучные выражения вроде {$IF CompilerVersion >= 18.5} ясности не добавляют. Обычно эта проблема решается с помощью включаемых файлов (те, что с расширением .inc), где, основываясь на значениях перечисленных выше сущностей (и возможно некоторых других), создается некий набор определений (defines) с читаемыми именами. Делается это, например, так:

{$IFNDEF FPC}
{$DEFINE DCC}
{$ENDIF}
...
// Anonymous methods
{$IF Defined(DCC) and (CompilerVersion >= 20)}
{$DEFINE HAS_ANON_METHODS}
{$IFEND}


Тут мы видим создаваемое определение HAS_ANON_METHODS, которое в дальнейшем позволит нам не вспоминать о версии компилятора, а просто и удобно писать код:



Type
TMyEvent = {$IFDEF HAS_ANON_METHODS}Reference To {$ENDIF}Procedure(Sender : TObject) Of Object;


Но так ли, на самом деле, это удобно? Судите сами. CodeInsight не обеспечивает подсказки для определений. Написав “HAS_” и нажав Ctrl+Space вы не получите весь список возможных определений, а значит снова должны все держать в голове. Это не удобно. Лично меня это просто убивало. Однако, решение есть (для версий Delphi начиная с 2005):



Unit Common.Features;

Interface

Type

//
Delphi = Record

Type

//
Platform = Record

Const

Windows = {$IF Defined(MSWINDOWS)}True{$ELSE}False{$IFEND};
Linux = {$IF Defined(LNUX)}True{$ELSE}False{$IFEND};
MacOS = {$IF Defined(MACOS)}True{$ELSE}False{$IFEND};

{$REGION ' Platform check '}

{$IF Ord(Windows) + Ord(Linux) + Ord(MacOS) <> 1}

{$MESSAGE FATAL 'Unknown platform'}

{$IFEND}

{$ENDREGION}

x32 = SizeOf(Pointer) = 4;
x64 = SizeOf(Pointer) = 8;

End;
//

//
Language = Record

Const

Unicode = {$IF Declared(UnicodeString)}True{$ELSE}False{$IFEND};
Generics = CompilerVersion > 18.5;
AnonymousMethods = CompilerVersion >= 20;
Attributes = {$IF Declared(TCustomAttribute)}True{$ELSE}False{$IFEND};

End;
//

//
RTL = Record

Type

//
TObject = Record

Const

ToString = RTLVersion >= 20;

End;
//

//
ObjectInvoke = Record

Const

MaxParams = {$IF RTLVersion < 20}10{$ELSE}32{$IFEND};

End;
//

End;
//

End;
//

Implementation

End.


Это не законченное решение, пока это только концепт. Надеюсь, основная идея понятна. Небольшой пример кода:



scode      := E_FAIL;
bstrSource := {$IF Not Delphi.Language.Unicode}UniUtf8Decode{$IFEND}(ClassName);

{$IF Delphi.RTL.TObject.ToString}

bstrDescription := ExceptObject.ToString

{$ELSE}

If ExceptObject Is Exception Then
bstrDescription := Exception(ExceptObject).Message
Else
bstrDescription := {$IF Not Delphi.Language.Unicode}UniUtf8Decode{$IFEND}(ExceptObject.ClassName);

{$IFEND}


 



Помимо того, что тут мы сразу видим иерархию (и это очень помогает), нам еще и CodeInsight будет помогать (только за пределами фигурных скобок).



26.11.2010



Забыл написать о самом серьезном преимуществе такого решения. Компилятор контролирует корректность идентификаторов (правда с некоторыми оговорками, но это не важно), и если где-то ошибиться в написании то такой код просто не скомпилируется.

Комментариев нет: