4. SynCommons单元

  首先,我们将介绍一些在Synopse源代码中无处不在的基础功能,即使您不需要深入了解实现细节,它也可以帮助您理解在框架源代码及其文档中遇到的一些类和类型。

  使用一些从底层构建的自定义类型、类和函数而不是调用Delphi官方RTL是一种设计选择。

  这样做的好处可能是:

  • 跨平台和跨编译器支持(如使用内存模型或RTTI的特性);
  • Unicode支持所有版本的Delphi,包括Delphi 2009之前版本,及FPC;
  • 优化处理速度、多线程友好性和可重用性;
  • 共享最常见的功能(如文字、数据处理);
  • 亲和、一致的设计。

  为了使用Synopse mORMot框架,您最好能熟悉其中一些定义。

  首先是提供的**Synopse.inc**包含文件,它出现在框架的很多单元中:

{$I Synopse.inc} // define HASINLINE USETYPEINFO CPU32 CPU64

  它定义了一些条件,以帮助编写高效可移植的代码。

  接下来,我们将说明框架的底层部分的一些主要功能,这些功能主要位于**syncommon.pas**中:

  • Unicode和UTF-8;
  • Currency货币类型;
  • 动态数组封装器(TDynArrayTDynArrayHash);
  • 用于存储动态无模式对象或数组的TDocVariant自定义变体类型。

  SynTests.pasSynLog.pas中的其他共享特功能在稍后详细介绍,详见测试和日志记录部分。

4.1. Unicode和UTF-8

  我们的mORMot框架具有100%的UNICODE兼容性,可使用Delphi 2009和更高版本(包括最新的Delphi 10.2 Tokyo修订版)编译。为了与这些编译器的String=UnicodeString范式兼容,对代码进行了深度重写和测试,这些代码也可以安全地支持旧版本的Unicode处理,即从Delphi 6到Delphi 2007。

  因为我们的框架原生支持UTF-8(这是SAX-like模式下更好的JSON数据流快速字符编解码方式,也是SQLite3引擎原生支持的),我们必须为框架建立安全使用字符串的方法,以处理所有版本的Delphi(包括pre-Unicode版本,特别是喜欢使用Delphi 7的人还很多),并为FreePascal编译器提供兼容性。

  注:SAX,Simple API for XML,简单应用程序接口。

  定义了一些字符串类型,并在代码中使用以获得最佳的编译效率(避免诸多格式之间的转换):

  • RawUTF8用于内部数据,因为SQLite3和JSON都需要UTF-8编码;
  • WinAnsiString用于WinAnsi编码的AnsiString(代码页1252);
  • i18n通用字符串(如mORMoti18n单元),可在VCL中使用的文本,既可作为AnsiString(用于Delphi 2到2007),也可作为UnicodeString(用于Delphi 2009及以后);
  • RawUnicode用于一些专用领域(如在Win32环境Delphi 7直接调用*W() API),这种类型不兼容Delphi 2009及以后的UnicodeString
  • 用于字节存储的RawByteString(如用于FileFromString()函数);
  • SynUnicode是最快的Unicode原生字符串类型,与使用的编译器有关(如在Delphi 2009之前使用的是WideString,之后使用的是UnicodeString);
  • 一些特殊的转换函数将用于Delphi 2009+ UnicodeString(在{$ifdef UNICODE}...{$endif}块中定义);
  • 永远不要直接使用AnsiString,而是使用上述类型之一。

  在框架的TSQLRecord中定义文本属性和处理所有内部数据时,RawUTF8是首选的字符串类型。只有当您触达用户界面层时,才使用Language.UTF8ToString方法(mORMoti18n.pas单元)或下列SynCommon.pas中的函数显式地将RawUTF8内容转换为VCL通用字符串类型。

/// convert any UTF-8 encoded String into a generic VCL Text
// - it's prefered to use TLanguageFile.UTF8ToString() in mORMoti18n.pas,
// which will handle full i18n of your application
// - it will work as is with Delphi 2009+ (direct unicode conversion)
// - under older version of Delphi (no unicode), it will use the
// current RTL codepage, as with WideString conversion (but without slow
// WideString usage)
function UTF8ToString(const Text: RawUTF8): string;

  当然,StringToUTF8方法函数可以将文本转换回ORM层。
  在SynCommon.pas中还包含了许多专用的转换函数(包括数值之间的转换),它们针对速度和多线程功能进行了优化,并避免了涉及临时字符串变量的隐式转换。

  我们不允许在编译过程中出现警告信息,特别是在Delphi的Unicode版本中(例如Delphi 2010),上述类型的所有字符串转换在框架的代码中都是显式进行的,以避免任何未知的数据丢失。

  如果您使用的是旧版本的Delphi,并且代码库包含许多WideString变量,那么您可以看看SynFastWideString.pas单元,将此单元添加到.dpr引用子句的顶部,将使得所有WideString字符串进程都使用Delphi堆及其非常有效的FastMM4内存管理器,而不是速度慢得多的BSTR Windows API。如果存在使用大量WideString字符串变量的代码,性能提升可能超过50倍。但注意使用这个单元将破坏与BSTR/COM/OLE类型字符串的兼容性,因此不能与COM对象一起使用,在这种情况下,如果您需要对旧版本的Delphi提供Unicode支持,可以考虑使用我们的RawUTF8类型,它与我们的框架集成得更好。

4.2. Currency处理

  比较两个货币值的更快、更安全的方法是将变量映射为内部Int64二进制形式,如:

function CompCurrency(var A,B: currency): Int64;
var A64: Int64 absolute A;
    B64: Int64 absolute B;
begin
  result := A64-B64;
end;

  这将避免在比较时出现各种舍入错误(通过使用*10000后的整数值),并且将比使用FPU(或x64体系结构下的SSE2)指令的默认实现更快。

  您可以通过SynCommons.pas单元直接处理货币,它将绕过使用FPU,因此非常快。

  下列函数涉及Int64二进制转换(通过PInt64(@aCurrencyVar)^或者absolute语法访问):

  • function Curr64ToString(Value: Int64): string;
  • function StrToCurr64(P: PUTF8Char): Int64;
  • function Curr64ToStr(Value: Int64): RawUTF8;
  • function Curr64ToPChar(Value: Int64; Dest: PUTF8Char): PtrInt;
  • function StrCurr64(P: PAnsiChar; const Value: Int64): PAnsiChar;

  使用这些函数进行文本转换要比使用标准的FloatToText()实现快得多,并通过回归测试对它们进行了验证。

  当然,在通常的代码中,不需要都使用Int64二进制来表示currency,而是基于编译器RTL默认实现。在各种情况下,ORM数据处理函数确实在速度和精度方面进行了优化,包括访问外部SQL数据库

4.3. TDynArray动态数组封装

  1.13版本的SynCommons.pas单元引入了两种封装函数:

  • 用于处理记录类型的底层RTTI函数:RecordEqualsRecordSaveRecordSaveLengthRecordLoad
  • TDynArrayTDynArrayHashed对象,它们是所有动态数组的封装器。

  通过TDynArray您可以访问各种类似于TList动态数组的属性和方法(如TIntegerDynArray = array of integer),这些属性和方法包括CountAddInsertDeleteClearIndexOfFindSort和一些新方法,如LoadFromStreamSaveToStreamLoadFromSaveToSliceReverseAddArray。它包括字符串、记录等各种动态数组的快速二进制序列化,还可以使用CreateOrderedIndex方法根据动态数组内容创建单独的索引,如果需要,还可以将数组内容序列化为JSON。

  动态数组的一个优点是它们是引用计数的,所以不需要使用Create/try..finally…Free代码块,Delphi编译器就可以很好地处理它们(进行了访问优化,一次分配所有数组内容,因此减少了因内存碎片和CPU缓存的性能下降)。

  没有将TDynArray替换为TCollectionTList(这是标准和有效的存储类实例的方式,自框架的1.13版本它们也支持发布属性),是因为他们非常方便的内容列表或数据字典方式,而不是使用先前的类及属性定义。

  您可以以纯Delphi的方式查看类似Python的列表、元组(通过记录处理)和字典(通过Find方法,特别是使用专用的TDynArrayHashed封装器),我们的新方法(搜索和序列化)允许在Delphi代码中使用这些脚本类型的结构。

  为了在ORM中处理动态数组,设计了一些基于RTTI的动态数组结构。由于需要使用动态记录数组,因此使用RTTI实现了对记录内容的一些底层快速访问(比Delphi 2010之后版本增强的新RTTI快得多)。

4.3.1. TList类似属性

  下面是如何通过方法驱动来访问动态数组:

type
   TGroup: array of integer;
var
   Group: TGroup;
   GroupA: TDynArray;
   i, v: integer;
begin
  GroupA.Init(TypeInfo(TGroup),Group); // associate GroupA with Group
  for i := 0 to 1000 do
  begin
    v := i+1000; // need argument passed as a const variable
    GroupA.Add(v);
  end;
  v := 1500;
  if GroupA.IndexOf(v)<0 then // search by content
    ShowMessage('Error: 1500 not found!');
  for i := GroupA.Count-1 downto 0 do
    if i and 3=0 then
      GroupA.Delete(i); // delete integer at index i
end;

  这个TDynArray封装器也可以用于array of stringarray of record

  记录、字符串、变体不需要定义引用计数字段(byteintegerdouble,目前还不支持Interface嵌套),TDynArray能够处理record中的record,或者record中的动态数组。

  是的,您的理解是对的,它能处理record动态数组,其中包含字符串或您需要的各种数据。

  IndexOf()方法将搜索指定内容,也就是说,对于array of record,必须匹配所有记录字段内容(包括string属性)。

  注意,TDynArray只是一个现有动态数组变量的包装。在上面的代码中,AddDelete方法是修改Group变量的内容。因此,您可以根据需要初始化TDynArray封装器,以便更有效地访问各种Delphi原生动态数组。TDynArray不包含任何数据,元素存储在动态数组变量中,而不是存储在TDynArray实例中。

4.3.2. 增强功能

  TDynArray封装器中还定义了一些TList没有的方法,有了这些方法,我们更接近于一些原生泛型实现:

  • 现在您可以从流或字符串中存取动态数组内容(使用LoadFromStream/SaveToStreamLoadFrom/ SaveTo方法),并将使用专用且快速的二进制流方案;
  • 你可以采用两种方式对动态数组内容进行排序:要么内部(即交换数组元素内容,在这种情况下使用Sort 方法),要么通过外部整数索引查找数组(使用CreateOrderedIndex方法,在这种情况下,对于相同的数据可能有几种排序结果);
  • 您可以指定任何自定义的比较函数,并且有一个新的Find 方法可以用于快速搜索二进制数据。

  以下是这些新方法的工作原理:

var
  Test: RawByteString;
...
  Test := GroupA.SaveTo;
  GroupA.Clear;
  GroupA.LoadFrom(Test);
  GroupA.Compare := SortDynArrayInteger;
  GroupA.Sort;
  for i := 1 to GroupA.Count-1 do
    if Group[i]<Group[i-1] then
      ShowMessage('Error: unsorted!');
  v := 1500;
  if GroupA.Find(v)<0 then // fast binary search
    ShowMessage('Error: 1500 not found!');

  还可以使用一些独特的模仿自著名的Python的方法,如SliceReverseAddArray

  这些方法更接近于通用泛型,支持Delphi 6到Delphi 10.2 Tokyo,不需要使用低速的增强版RTTI,也没有大的可执行文件开销和泛型的编译问题……

4.3.3. 使用外部计数处理容量

  使用TDynArray的一个常见的速度问题是,当您改变动态数组长度时,就会重新分配内存缓冲区,就像普通的Delphi动态数组一样。

  也就是说,无论何时调用AddDelete方法,都会执行对SetLength(DynArrayVariable)的内部调用,这可能会很慢,因为它总是执行一些额外的代码,包括调用ReallocMem

  为了避免这种情况,可以定义一个外部整数Count变量。

  在这种情况下,Length(DynArrayVariable)将是动态数组的内存容量,实际的存储项数量可从该Count变量获得。TDynArray发布的Count属性总是反映动态数组存储项的数量,如果这样定义,Count属性也指向外部Count 变量,否则他就与通常的Length(DynArrayVariable)一样。TDynArray发布的Capacity 属性将反映动态数组的容量,与Count变量不同,它对应Length(DynArrayVariable)

  因此,添加或删除项目会快很多。

var
   Group: TIntegerDynArray;
   GroupA: TDynArray;
   GroupCount, i, v: integer;
begin
  GroupA.Init(TypeInfo(TGroup),Group,@GroupCount);
  GroupA.Capacity := 1023; // reserver memory
  for i := 0 to 1000 do
  begin
    v := i+1000; // need argument passed as a const variable
    GroupA.Add(v); // faster than with no external GroupCount variable
  end;
  Check(GroupA.Count=1001);
  Check(GroupA.Capacity=1023);
  Check(GroupA.Capacity=length(Group));

4.3.4. JSON序列化

  TDynArray封装器具有一些JSON序列化原生功能,TTextWriter.AddDynArrayJSONTDynArray.LoadFromJSON方法可用于动态数组的UTF-8 JSON序列化。

  有关此独特功能的所有详细信息,请参见动态数组序列化部分。

4.3.5. 日常使用

  TTestLowLevelCommon._TDynArray_TDynArrayHashed方法实现了与这些封装器相关的统一的自动化测试。

  您将找到处理动态数组的示例和一些更高级的功能,包括各种类型的数据(从简单的TIntegeryDynArray到记录嵌套记录)。

TDynArrayHashed封装器允许使用记录动态数组实现数据字典。如在SynSQLite3.pas中,以下代码将处理准备语句缓存:

  TSQLStatementCache = record
    StatementSQL: RawUTF8;
    Statement: TSQLRequest;
  end;
  TSQLStatementCacheDynArray = array of TSQLStatementCache;

  TSQLStatementCached = object
    Cache: TSQLStatementCacheDynArray;
    Count: integer;
    Caches: TDynArrayHashed;
    DB: TSQLite3DB;
    procedure Init(aDB: TSQLite3DB);
    function Prepare(const GenericSQL: RaWUTF8): PSQLRequest;
    procedure ReleaseAllDBStatements;
  end;

  上述代码定义了一个存储TSQLRequest的动态数组及其相关的SQL语句,并带有一个外部Count变量以提供更好的速度。

  它将在TSQLRestServerDB中这样使用:

constructor TSQLRestServerDB.Create(aModel: TSQLModel; aDB: TSQLDataBase);
begin
  fStatementCache.Init(aDB);
 (...)

  在对象构造函数中初始化封装器:

procedure TSQLStatementCached.Init(aDB: TSQLite3DB);
begin
  Caches.Init(TypeInfo(TSQLStatementCacheDynArray),Cache,nil,nil,nil,@Count);
  DB := aDB;
end;

  TDynArrayHashed.Init方法识别到TSQLStatementCache的第一个字段是RawUTF8,因此默认设置第一个字段为AnsiString散列值(我们可以指定自定义散列函数或散列内容来覆盖默认的nil参数)。

  因此,我们可以直接指定一个GenericSQL变量作为FindHashedForAdding的第一个参数,因为这个方法只访问第一个字段的RawUTF8内容,而不会处理整个记录内容。事实上,FindHashedForAdding方法将用于散列的创建、搜索和添加新项,只需一步。注意,此方法只在prepare中处理添加操作,代码在创建条目后需显式设置StatementSQL内容:

function TSQLStatementCached.Prepare(const GenericSQL: RaWUTF8): PSQLRequest;
var added: boolean;
begin
  with Cache[Caches.FindHashedForAdding(GenericSQL,added)] do begin
    if added then begin
      StatementSQL := GenericSQL; // need explicit set the content
      Statement.Prepare(DB,GenericSQL);
    end else begin
      Statement.Reset;
      Statement.BindReset;
    end;
    result := @Statement;
  end;
end;

  TSQLStatementCached最后一个方法只是循环每个语句并释放它们,您可能注意到,这段代码像平常一样使用动态数组:

procedure TSQLStatementCached.ReleaseAllDBStatements;
var i: integer;
begin
  for i := 0 to Count-1 do
    Cache[i].Statement.Close; // close prepared statement
  Caches.Clear; // same as SetLength(Cache,0) + Count := 0
end;

  生成的代码执行起来非常快,并且易于阅读、维护。

4.4. TDocVariant自定义变体类型

  我们在框架的1.18修订版引入了两种新的自定义变体类型:

  • TDocVariant变体类型;
  • TBSONVariant变体类型。

  第二个自定义类型(它处理针对MongoDB的扩展,如ObjectID或其他特定类型,如日期或二进制)将在稍后介绍mORMot的MongoDB支持以及BSON类型内容时说明。BSON/MongoDB在SynMongoDB.pas单元中实现。

  我们现在重点讨论TDocVariant本身,它是JSON类型的对象或数组的通用容器。这种自定义变体类型在SynCommons.pas单元中实现,不需要链接到mORMot ORM内核或MongoDB,可直接在代码的任何地方使用。

4.1.1. TDocVariant文档

  TDocVariant实现了一种自定义的变体类型,可用于存储任何基于文档的JSON/BSON内容,即:

  • 用于面向对象文档(内部标识为dvObject子类型)的名值对;
  • 一个数组值(包括嵌套文档),用于面向数组的文档(内部标识为dvArray子类型);
  • 上述两者通过嵌套TDocVariant实例的任意组合。

  以下是这种自定义变体类型的主要特征:

  • 任何对象或数组文档的DOM方法;
  • 使用无模式的方法完美存储动态内容对象值(在Python或JavaScript等脚本语言中您可能已经习惯了这种方法);
  • 允许嵌套文档,没有深度限制,只受限于可用内存;
  • 赋值可以是按值赋值(提供默认值可确保安全,但在包含大量嵌套数据时比较慢),也可以是按引用赋值(直接引用计数赋值);
  • 非常快速的JSON序列化、反序列化,支持类似MongoDB的扩展语法;
  • 通过后期绑定访问代码中的属性(正如SDD # DI-2.2.3中详细介绍的那样,几乎不会因为VCL大量提交而影响速度);
  • 通过将类型转换为TDocVariantData record,直接从代码访问内部名值变体数组;
  • 由编译器管理实例生命周期(与其他各种变体类型一样),不需要使用interfaces 或显式使用try..finally语句块;
  • 优化以尽可能少地使用内存和CPU资源(与其他大多数库不同,它不会为每个节点逐一分配一个类实例,而是为数组预先分配好内存);
  • 可以扩展任何存储内容,如与BSON序列化和MongoDB自定义类型(ObjectID、Decimal128、RegEx…)完美集成,以便与MongoDB服务器一起使用;
  • 与我们的TDynArray动态数组封装器及其JSON序列化完美集成,类似于record序列化;
  • 设计用于我们的mORMot ORM,ORM核心将识别任何包含发布属性为自定义变体类型的TSQLRecord实例,并同步处理各种数据库后端(将内容作为JSON存储在文本列中);
  • 设计用于我们的mORMot SOA,任何基于接口的服务都能够使用或发布此类内容,作为变体类型参数;
  • 与Delphi IDE完全集成:任何变体实例都将在IDE调试器中显示为JSON,这使得它的使用非常方便。

  要创建这种变体实例,可以使用如下一些易于记住的函数:

  • _Obj() _ObjFast()全局函数来创建变体对象文档;
  • _Arr() _ArrFast()全局函数来创建变体数组文档;
  • _Json() _JsonFast() _JsonFmt() _JsonFastFmt()全局函数,用于从JSON创建任何变体对象或数组文档,这些函数使用标准或MongoDB扩展语法。

  您有两种非排他的使用TDocVariant存储的方法:

  • 对普通变体变量,使用后期绑定或_Safe()快速访问其数据;
  • 直接作为TDocVariantData变量,然后使用variant(aDocVariantData)重新生成一个变体实例。

  注意,您不需要使用try..finally语句保护堆栈分配的TDocVariantData实例,因为编译器会为您完成它。这种记录类型有很多强大的方法,例如在内容上应用map/reduce,或者处理高级搜索或编码。

4.4.1.1. 变体对象文档

  最直接的方法是使用后期绑定来设置一个新的TDocVariant实例属性:

var V: variant;
 ...
  TDocVariant.New(V); // or slightly slower V := TDocVariant.New;
  V.name := 'John';
  V.year := 1972;
  // now V contains {"name":"john","year":1982}

  通过_Obj(),可以使用名值对数据初始化变体对象实例。

var V1,V2: variant; // stored as any variant
 ...
  V1 := _Obj(['name','John','year',1972]);
  V2 := _Obj(['name','John','doc',_Obj(['one',1,'two',2.5])]); // with nested objects

  然后您可以通过以下两种方法将这些对象转换为JSON:

  • 使用VariantSaveJson()函数,该函数直接返回一个UTF-8内容;
  • 或者将变体实例转换为字符串(这样会慢一些,但也是可以的)。
 writeln(VariantSaveJson(V1)); // explicit conversion into RawUTF8
 writeln(V1);                  // implicit conversion from variant into string
 // both commands will write '{"name":"john","year":1982}'
 writeln(VariantSaveJson(V2)); // explicit conversion into RawUTF8
 writeln(V2);                  // implicit conversion from variant into string
 // both commands will write '{"name":"john","doc":{"one":1,"two":2.5}}'

  因此,Delphi IDE调试器能够以JSON形式显示变体值。也就是说,V1将以{“name”:“john”,“year”:1982}的形式显示在IDE调试器观察列表窗中,或者显示在Evaluate/Modify (F7)表达式工具中,这非常方便,比任何基于类的解决方案(需要在IDE中安装特定的设计时包)更友好。

  您可以通过后期绑定访问对象属性,并在您的代码中使用任意深度的嵌套对象:

writeln('name=',V1.name,' year=',V1.year);
 // will write 'name=John year=1972'
 writeln('name=',V2.name,' doc.one=',V2.doc.one,' doc.two=',doc.two);
 // will write 'name=John doc.one=1 doc.two=2.5
 V1.name := 'Mark';       // overwrite a property value
 writeln(V1.name);        // will write 'Mark'
 V1.age := 12;            // add a property to the object
 writeln(V1.age);         // will write '12'

  注意,属性名称只在运行时计算,而不是在编译时。例如,如果你写成V1.nome而不是V1.name,在编译时不会出错,但是在执行时将引发EDocVariant异常(除非您为_Obj/_Arr/_Json/_JsonFmt设置了dvoReturnNullForUnknownProperty选项,该选项将为未定义的属性返回null变体值)。

  除了属性名之外,对于这些变体对象实例,还可以使用一些伪方法:

  writeln(V1._Count);  // will write 3 i.e. the number of name/value pairs in the object document
  writeln(V1._Kind);   // will write 1 i.e. ord(dvObject)
  for i := 0 to V2._Count-1 do
    writeln(V2.Name(i),'=',V2.Value(i));
  // will write to the console:
  //  name=John
  //  doc={"one":1,"two":2.5}
  //  age=12
  if V1.Exists('year') then
    writeln(V1.year);
  V1.Add('key','value');  // add one property to the object

  后期绑定返回varByRef类型的变体值,它有两个优点:

  • 更好的性能,即使嵌套对象是按值创建的;
  • 允许伪方法的嵌套调用,如:
var V: variant;
 ...
  V := _Json('{arr:[1,2]}');
  V.arr.Add(3);     // will work, since V.arr is returned by reference (varByRef)
  writeln(V);       // will write '{"arr":[1,2,3]}'
  V.arr.Delete(1);
  writeln(V);       // will write '{"arr":[1,3]}'

  您还可以将变体实例转换为TDocVariantData record,并直接访问其内部结构,例如:

 TDocVariantData(V1).AddValue('comment','Nice guy');
 with TDocVariantData(V1) do             // direct transtyping
   if Kind=dvObject then                 // direct access to the TDocVariantKind field
   for i := 0 to Count-1 do              // direct access to the Count: integer field
     writeln(Names[i],'=',Values[i]);    // direct access to the internal storage arrays

  根据定义,通过TDocVariantData记录进行类型转换比使用后期绑定稍微快一些。

  你必须确保类型转换前变体实例确实是一个TDocVariant类型的数据,通过调用_Safe(aVariant)^函数(或DocVariantType.IsOfType(aVariant)DocVariantData(aVariant)^),甚至通过返回varByRef的后期成员绑定(如V2.doc):

 with _Safe(V1)^ do                        // note ^ to de-reference into TDocVariantData
   for ndx := 0 to Count-1 do              // direct access to the Count: integer field
     writeln(Names[ndx],'=',Values[ndx]);  // direct access to the internal storage arrays

 writeln(V2.doc); // will write  '{"name":"john","doc":{"one":1,"two":2.5}}'
 if DocVariantType.IsOfType(V2.Doc) then // will be false, since V2.Doc is a varByRef variant
   writeln('never run');                 // .. so TDocVariantData(V2.doc) will fail
 with DocVariantData(V2.Doc)^ do         // note ^ to de-reference into TDocVariantData
   for ndx := 0 to Count-1 do            // direct access the TDocVariantData methods
     writeln(Names[ndx],'=',Values[ndx]);
  // will write to the console:
  //  one=1
  //  two=2.5

  在实践中,首选_Safe(aVariant)^,因为如果aVariant不是TDocVariant类型,DocVariantData(aVariant)^将触发EDocVariant异常,但_Safe(aVariant)^将返回一个DocVariant空值实例,该实例的Count=0Kind=dbUndefined

  TDocVariantData类型有一些附加的U[] I[] B[] D[] O[] O_[] A[] A_[] _[]属性,这些属性可直接用于获取数据类型,如RawUTF8Int64/integerDouble,或检查嵌套文档是O[]对象还是A[]数组。

  如果您不需要任何面向变体的对象访问,而只需要一些本地存储,那么您还可以直接在堆栈上分配TDocVariantData实例:

var Doc1,Doc2: TDocVariantData;
 ...
  Doc1.Init; // needed for proper initialization
  assert(Doc1.Kind=dvUndefined);
  Doc1.AddValue('name','John');        // add some properties
  Doc1.AddValue('birthyear',1972);
  assert(Doc1.Kind=dvObject);          // is now identified as an object
  assert(Doc1.Value['name']='John');    // read access to the properties (also as varByRef)
  assert(Doc1.Value['birthyear']=1972);
  assert(Doc1.U['name']='John');        // slightly faster read access
  assert(Doc1.I['birthyear']=1972);
  writeln(Doc1.ToJSON); // will write '{"name":"John","birthyear":1972}'
  Doc1.Value['name'] := 'Jonas';      // update one property
  writeln(Doc1.ToJSON); // will write '{"name":"Jonas","birthyear":1972}'
  Doc2.InitObject(['name','John','birthyear',1972],
    aOptions+[dvoReturnNullForUnknownProperty]); // initialization from name/value pairs
  assert(Doc2.Kind=dvObject);
  assert(Doc2.Count=2);
  assert(Doc2.Names[0]='name');
  assert(Doc2.Values[0]='John');
  writeln(Doc2.ToJSON);         // will write '{"name":"John","birthyear":1972}'
  Doc2.Delete('name');
  writeln(Doc2.ToJSON);         // will write '{"birthyear":1972}'
  assert(Doc2.U['name']='');
  assert(Doc2.I['birthyear']=1972);
  Doc2.U['name'] := 'Paul';
  Doc2.I['birthyear'] := 1982;
  writeln(Doc2.ToJSON);         // will write '{"name":"Paul","birthyear":1982}'

  您不需要使用try..finally保护堆栈分配的TDocVariantData实例,因为编译器会为您做这些。查看TDocVariantData获取其所有方法和属性。

4.4.1.2. 变体数组文档

  通过_Arr(),将使用提供的Value1、Value2、…列表数据初始化变体数组实例。

var V1,V2: variant; // stored as any variant
 ...
  V1 := _Arr(['John','Mark','Luke']);
  V2 := _Obj(['name','John','array',_Arr(['one','two',2.5])]); // as nested array

  然后您可以通过以下两种方法将这些对象转换为JSON:

  • 使用VariantSaveJson()函数,该函数直接返回一个UTF-8内容;
  • 或者将变体实例转换为字符串(这样会慢一些,但也是可以的)。
 writeln(VariantSaveJson(V1));
 writeln(V1);  // implicit conversion from variant into string
 // both commands will write '["John","Mark","Luke"]'
 writeln(VariantSaveJson(V2));
 writeln(V2);  // implicit conversion from variant into string
 // both commands will write '{"name":"john","array":["one","two",2.5]}'

  与对象文档一样,Delphi IDE调试器能够以JSON格式显示变体数组值。

  也可使用后期绑定,拥有一组特殊的伪方法:

  writeln(V1._Count); // will write 3 i.e. the number of items in the array document
  writeln(V1._Kind);  // will write 2 i.e. ord(dvArray)
  for i := 0 to V1._Count-1 do
    writeln(V1.Value(i),':',V2._(i));    // Value() or _() pseudo-methods
  // will write in the console:
  //  John John
  //  Mark Mark
  //  Luke Luke
  if V1.Exists('John') then             // Exists() pseudo-method
    writeln('John found in array');
  V1.Add('new item');                   // add "new item" to the array
  V1._ := 'another new item';           // add "another new item" to the array
  writeln(V1);          // will write '["John","Mark","Luke","new item","another new item"]'
  V1.Delete(2);
  V1.Delete(1);
  writeln(V1);          // will write '["John","Luke","another new item"]'

  在使用后期绑定时,对象属性或数组项被检索为varByRef,因此您可以在任何嵌套成员上运行伪方法:

  V := _Json('["root",{"name":"Jim","year":1972}]');
  V.Add(3.1415);
  assert(V='["root",{"name":"Jim","year":1972},3.1415]');
  V._(1).Delete('year');          // delete a property of the nested object
  assert(V='["root",{"name":"Jim"},3.1415]');
  V.Delete(1);                    // delete an item in the main array
  assert(V='["root",3.1415]');

  当然,可以将数据转换成TDocVariantData记录,并且比使用后期绑定稍微快一些。与通常一样,使用_Safe(aVariant)^函数是安全的,尤其是与返回的varByRef的后期成员绑定一起工作时。

  与对象文档一样,如果不需要进行任何面向变体的数组访问,还可以直接在堆栈上分配TDocVariantData实例:

var Doc: TDocVariantData;
 ...
  Doc.Init; // needed for proper initialization  - see also Doc.InitArray()
  assert(Doc.Kind=dvUndefined);      // this instance has no defined sub-type
  Doc.AddItem('one');                // add some items to the array
  Doc.AddItem(2);
  assert(Doc.Kind=dvArray);          // is now identified as an array
  assert(Doc.Value[0]='one');         // direct read access to the items
  assert(Doc.Values[0]='one');        // with index check
  assert(Doc.Count=2);
  writeln(Doc.ToJSON); // will write '["one",2]'
  Doc.Delete(0);
  assert(Doc.Count=1);
  writeln(Doc.ToJSON); // will write '[2]'

  您可以使用A[]属性从TDocVariant数组检索对象属性,或者使用A_[]属性向数组添加缺少的对象属性,例如:

  Doc.Clear;  // reset the previous Doc content
  writeln(Doc.A['test']); // will write 'null'
  Doc.A_['test']^.AddItems([1,2]);
  writeln(Doc.ToJSON);    // will write '{"test":[1,2]}'
  writeln(Doc.A['test']); // will write '[1,2]'
  Doc.A_['test']^.AddItems([3,4]);
  writeln(Doc.ToJSON);    // will write '{"test":[1,2,3,4]}'

4.4.1.3. 通过JSON创建变体对象或数组文档

  通过_Json()_JsonFmt(),可使用JSON形式的数据初始化文档或数组变体实例,例如:

var V1,V2,V3,V4: variant; // stored as any variant
 ...
  V1 := _Json('{"name":"john","year":1982}'); // strict JSON syntax
  V2 := _Json('{name:"john",year:1982}');     // with MongoDB extended syntax for names
  V3 := _Json('{"name":?,"year":?}',[],['john',1982]);
  V4 := _JsonFmt('{%:?,%:?}',['name','year'],['john',1982]);
  writeln(VariantSaveJSON(V1));
  writeln(VariantSaveJSON(V2));
  writeln(VariantSaveJSON(V3));
  // all commands will write '{"name":"john","year":1982}'

  当然,可以将对象或数组作为参数嵌套到_JsonFmt()函数中。

  JSON提供的既可以是严谨的JSON语法,也可以是MongoDB扩展语法,即不带引号的属性名。在输入Delphi代码时有可能会忘记JSON属性名的引号,这样更方便,而且不易出错。

  注意,TDocVariant实现了一个开放接口,用于向JSON添加自定义扩展。例如,如果您的应用程序中定义了SynMongoDB.pas单元,您将能够在JSON中创建任何MongoDB特定的类型,如ObjectID()NumberDecimal(“…”)new Date()甚至/regex/option

  与任何对象或数组文档一样,Delphi IDE调试器能够显示JSON形式的变体值。

4.4.1.4. 按值或按引用

  默认情况下,_Obj() _Arr() _Json() _JsonFmt()创建的变体实例将使用逐值复制模式。这意味着当一个实例被另一个变量赋值时,将创建一个新的变体文档,并复制所有内部值,就像record类型一样。

  这意味着,如果你修改复制变量的任何一项,它不会改变原来的变量:

var V1,V2: variant;
 ...
 V1 := _Obj(['name','John','year',1972]);
 V2 := V1;                // create a new variant, and copy all values
 V2.name := 'James';      // modifies V2.name, but not V1.name
 writeln(V1.name,' and ',V2.name);
 // will write 'John and James'

  因此,您的代码将非常安全,因为V1V2被解耦。

  但缺点就是传递这样的值可能非常慢,如当您嵌套对象时:

var V1,V2: variant;
 ...
 V1 := _Obj(['name','John','year',1972]);
 V2 := _Arr(['John','Mark','Luke']);
 V1.names := V2; // here the whole V2 array will be re-allocated into V1.names

  对于大型文档,这样的行为可能会耗费大量时间和资源。

  所有 _Obj() _Arr() _Json() _JsonFmt()函数都有一个可选的TDocVariantOptions参数,该参数可以更改创建TDocVariant实例的行为,特别是dvoValueCopiedByReference参数。

  这个特定选项将设置为引用复制模式:

var V1,V2: variant;
 ...
 V1 := _Obj(['name','John','year',1972],[dvoValueCopiedByReference]);
 V2 := V1;             // creates a reference to the V1 instance
 V2.name := 'James';   // modifies V2.name, but also V1.name
 writeln(V1.name,' and ',V2.name);
 // will write 'James and James'

  您可能认为这种行为对于变体类型有些奇怪。但是,如果您忘记了值对象,并将这些TDocVariant类型视为Delphi类实例(它是引用类型),不需要固定的模式,也不需要手动处理内存,那么它就开始有意义了。

  注意,已经定义了一组全局函数,它允许直接创建拥有每个引用实例生命周期的文档,名为_ObjFast() _ArrFast() _JsonFast() _JsonFmtFast()。这些只是对应的_Obj() _Arr() _Json() _JsonFmt()函数的封装器,并使用如下JSON_OPTIONS[true]可选常量参数:

const
  /// some convenient TDocVariant options
  // - JSON_OPTIONS[false] is _Json() and _JsonFmt() functions default
  // - JSON_OPTIONS[true] are used by _JsonFast() and _JsonFastFmt() functions
  JSON_OPTIONS: array[Boolean] of TDocVariantOptions = (
    [dvoReturnNullForUnknownProperty],
    [dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]);

  在处理复杂文档时,如BSON/MongoDB文档,几乎所有内容都将按“快速”的引用模式创建。

4.4.2. TDocVariant高级处理

4.4.2.1. 数值选项

  默认情况下,TDocVariantData将只识别integerInt64currency (请参阅货币处理)数值。任何不能安全地与JSON文本相互转换的浮点值都将存储为JSON字符串,也就是说,如果它匹配一个整数或最多4个固定小数,则其精度为64位。

  您可以为TDocVariantData设置dvoAllowDoubleValue 选项,以识别并将其存储为double,在这种情况下,变体值只能使用varDouble存储,如32位IEEE存储、处理5.0 x 10 ^ -324 . .1.7 x 10 ^ 308范围的数据。使用这种浮点值,在JSON序列化过程中可能会损失精度和数字,这就是为什么默认情况下不启用它的原因。

  还要注意,一些JSON引擎不支持64位整数,如JavaScript引擎最多只能存储53位的信息才不会损失精度,因为它们的内部存储是8字节的IEEE 754容器。在某些情况下,使用这些数字的JSON字符串表示才是最安全的,就像使用TTextWriterWriteObjectOptionwoIDAsIDstr值来安全序列化TSQLRecord.ID ORM值一样。

  如果您希望使用高精度浮点数,可以考虑使用TDecimal128值,就像在SynMongoDB.pas中实现的那样,支持128位高精度十进制,由IEEE 754-2008 128位十进制浮点标准定义,在MongoDB 3.4+中使用。

4.4.2.2. 创建对象或数组文档选项

  如上所述,TDocVariantOptions参数允许为给定TDocVariant自定义类型实例定义行为,请参阅这些选项的相关文档,了解可用的设置,一些与内存模型有关,另一些与属性名的大小写敏感有关,还有一些与不存在属性时的预期行为有关,等等……

  请注意,此设置是给定变体实例的局部设置。

  实际上,TDocVariant并不强制您只能使用一个内存模型或一组全局选项,所以您可以根据您的具体进程使用最佳模式。您甚至可以混合使用这些选项,如将一些对象作为属性包含在与其他选项一起创建的对象中,在这种情况下,嵌套对象的初始选项将保留。因此,您应该谨慎地使用这个特性。

  您可以使用_Unique()全局函数强制一个变体实例只有唯一的一组选项,所有嵌套文档都按值表示,或者使用_UniqueFast()强制所有嵌套文档都按引用表示。

  // assuming V1='{"name":"James","year":1972}' created by-reference
  _Unique(V1);             // change options of V1 to be by-value
  V2 := V1;                // creates a full copy of the V1 instance
  V2.name := 'John';       // modifies V2.name, but not V1.name
  writeln(V1.name);        // write 'James'
  writeln(V2.name);        // write 'John'
  V1 := _Arr(['root',V2]); // created as by-value by default, as V2 was
  writeln(V1._Count);      // write 2
  _UniqueFast(V1);         // change options of V1 to be by-reference
  V2 := V1;
  V1._(1).name := 'Jim';
  writeln(V1);
  writeln(V2);
  // both commands will write '["root",{"name":"Jim","year":1972}]'

  最简单的是在你的代码中只使用一组选项,即:

  • 如果您的业务代码确实将一些TDocVariant实例发送到其他业务逻辑部分进行进一步存储,那么可以使用_*()全局函数:在这种情况下,按值模式处理更有意义;
  • 如果TDocVariant实例是一小部分代码的局部实例,则使用_*Fast()全局函数,如用作动态无模式数据传输对象(DTO)。

  在所有情况下,请注意,与任何class 类型一样,方法的constvarout 参数描述符不是针对TDocVariant值,而是针对其引用。

4.4.2.3. 与其它mORMot单元集成

  事实上,当需要动态无模式存储结构时,您可以使用TDocVariant实例,而不是classrecord 强类型:

  • 客户端-服务端ORM通过在TSQLRecord中发布variant属性以支持TDocVariant(并作为JSON存储在文本列中);
  • 基于接口的服务支持TDocVariant作为任何方法的变体参数,使其成为完美的DTO;
  • 由于所有TDocVariant值的JSON支持是从底层实现的,因此它非常适合AJAX客户端以类似脚本方式进行处理。
  • 如果您使用我们的SynMongoDB.pasmORMotMongoDB.pas单元访问MongoDB服务器,TDocVariant将作为原生存储来创建或访问嵌套的BSON数组或对象文档,也就是说,它支持完整的ODM存储;
  • 基础特性(如日志或record /动态数组增强)也将受益于此TDocVariant自定义类型。

  我们非常确信,您一旦使用了TDocVariant,您将再也离不开它,它将强大的后期绑定和动态无模式功能引入到应用程序代码中,这对于原型设计或敏捷开发非常有用。您不需要使用Python或JavaScript之类的脚本引擎,Delphi就完全能够处理动态编码。

4.5. 基础功能

4.5.1. Iso8601时间和日期

  对于日期/时间的文本存储,框架将使用ISO 8601编码。日期可以编码为YYYY-MM-DDYYYYMMDD,时间可以编码为hh:mm:sshhmmss,日期和时间的组合表示形式为<date>T<time>,即YYYY-MM-DDThh:mm:ssYYYYMMDDThhmmss

  因此,除了涉及负年的日期表示外,该表示的字典顺序与时间顺序相对应,这使得日期可以自然地按照文件系统、表格列表等进行排序。

4.5.1.1. TDateTime和TDateTimeMS

  除了默认的TDateTime类型(序列化时精确到秒)之外,您还可以使用包含毫秒的TDateTimeMS,即YYYY-MM-DDThh:mm:ss.sssYYYYMMDDThhmmss.sss

type
  TDateTimeMS = type TDateTime;

  这个TDateTimeMS类型由框架的 ORM在record 、动态数组、JSON序列化过程中处理。

4.5.1.2. TTimeLog

  SynCommons.pas单元还定义了TTimeLog类型,以及一些与TDateTime值相互转换的函数:

type
  TTimeLog = type Int64;

  这个整数存储被编码为一系列位,这些位将映射SynCommons.pas单元中定义的TTimeLogBits记录类型。

  这些值的分辨率是秒。实际上,它内部用于计算一个抽象的“年”,即16个月、32天、32小时、64分钟、64秒。

  因此,各种日期/时间信息都可以从其内部位中获取:

  • 0..5位将映射秒,
  • 6..11位将映射分钟,
  • 12..16位会映射小时,
  • 17..21位将映射天(减1),
  • 22..25位会映射月(减1),
  • 26..38位将映射年。

  ISO 8601标准允许毫秒分辨率,编码为 hh:mm:ss.ssshhmmss.sss。我们的TTimeLog/TTimeLogBits整数编码使用了秒级分辨率和64位整数存储,因此不能处理这样的精度。如果需要毫秒,可以使用TDateTimeMS值。

  注意,由于TTimeLog类型是面向位的,所以在执行这样的日期/时间计算时,不能对两个TTimeLog值简单的进行加减,在这种情况下,请使用TDateTime进行临时转换。如下是实例的TSQLRest.ServerTimestamp属性是如何进行计算的。

function TSQLRest.GetServerTimestamp: TTimeLog;
begin
  PTimeLogBits(@result)^.From(Now+fServerTimestampOffset);
end;

procedure TSQLRest.SetServerTimestamp(const Value: TTimeLog);
begin
  fServerTimestampOffset := PTimeLogBits(@Value)^.ToDateTime-Now;
end;

  如果您只是想比较TTimeLog类型的日期/时间,那么直接比较它们的Int64值是安全的,因为时间戳将以递增的顺序存储,分辨率为秒。

  由于旧版本Delphi的编译器限制,直接将TTimeLogInt64变量转换为TTimeLogBits记录(如TTimeLogBits(aTimeLog).ToDateTime)可能会导致内部编译器错误。为了规避这个错误,你将不得不使用指针类型的转换,如上面的TimeLogBits(@Value)^.ToDateTime

  但在大多数情况下,您最好使用以下函数来管理这些时间戳:

 function TimeLogNow: TTimeLog;
 function TimeLogNowUTC: TTimeLog;
 function TimeLogFromDateTime(DateTime: TDateTime): TTimeLog;
 function TimeLogToDateTime(const Timestamp: TTimeLog): TDateTime; overload;
 function Iso8601ToTimeLog(const S: RawByteString): TTimeLog;

  有关此TTimeLog的更多信息,以及框架ORM如何通过TModTimeTCreateTime类型处理时间日期,参见日期时间字段

4.5.1.3. TUnixTime

  另一种选择是,您可以使用TUnixTime时间类型,它是自Unix纪元以来的64位编码的秒数,即1970-01-01 00:00:00 UTC以来:

type
  TUnixTime = type Int64;

  你可以转换这些值:

  • 使用TTimeLogBits.ToUnixTimeTTimeLogBits.FromUnixTime方法实现与TTimeLog类型的相互转换;
  • 使用UnixTimeToDateTime/DateTimeToUnixTime函数实现与TDateTime类型的相互转换;
  • 使用UnixTimeUTC调用快速的OS API返回当前时间戳。

  您可以考虑使用TUnixTime时间,特别是如果第三方客户端遵循此编码的话。在Delphi世界中,首选TDateTimeTTimeLog类型。

4.5.2. 时区

  在处理日期和时间时,一个常见的问题是,时间通常按当地时间显示和输入,而计算机确应该使用非地理信息,特别是在客户端-服务端体系结构中,两端可能不在同一物理区域。

  时区是为法律、商业和社会目的而需遵守的统一标准时间的区域。时区往往遵循国家的边界及其划分,因为在商业或其他交流密切的地区保持相同的时间是很方便的。陆地上的大多数时区与世界标准时间(UTC)相差几个小时或几十分钟。更糟糕的是,一些国家在一年中有一段时间使用夏令时,通常是将时钟修改一小时,一年两次。

  主要规则是,任何日期和时间都应该存储为UTC格式,或者具有显式的时区标识(即对UTC值的显式偏移量)。我们的框架期望这种行为:ORM、SOA或其任何其他部分存储和处理的每个日期/时间值都是UTC编码的。在表示层(如用户界面),应转换为当地时间,为最终用户提供友好的时间格式。

  正如您可能猜到的,处理时区是一项复杂的任务,应该由操作系统本身来管理,作为操作系统的一部分而更新。

  在实践中,可以从UTC时间转换为各个时区的本地时间。安装操作系统时需要设置的仅有几个参数是选择键盘布局……以及当前的时区。但是在客户端-服务端环境中,您可能必须在服务端管理多个时区,因此不能依赖这个全局设置。

  一个悲哀但可以想到的问题是,没有通用的时区信息编码方法。在Windows中,注册表包含时区列表和相关的时间偏差数据。大多数POSIX系统(包括Linux和Mac OSX)确实依赖于IANA数据库,也称为tzdata,您可能已经注意到,这个特定的包经常随系统一起更新。这两个区域标识互不映射,因此我们的框架需要在所有系统上共享一些东西。

  SynCommons.pas单元拥有TSynTimeZone类,它能够通过 TSynTimeZone.LoadFromRegistry将信息从Windows注册表检索到内存,或通过TSynTimeZone.SaveToFile将其存储为压缩文件。稍后,可以通过TSynTimeZone.LoadFromFile将该文件重新加载到各个系统中,包括各种Linux版本,结果是一样的。压缩文件很小,由于其优化方案,并使用了我们的SynLZ压缩算法:完整的信息存储在一个7 KB的文件,未压缩的JSON信息是130 KB左右,而http://www.iana.org官方提供的内容大小为280KBtar.gz...当然,tzdata存储的信息可能比我们需要的多。

  实际上,您可以使用TSynTimeZone.Default,读取Windows注册表,并尝试在其他操作系统上加载信息。

  因此,你可以这样写:

 aLocalTime := TSynTimeZone.Default.NowToLocal(aTimeZoneID);

  类似地,您可以使用TSynTimeZone.UtcToLocalTSynTimeZone.LocalToUtc方法,并使用正确的TZ标识符。

  您必须在Windows机器下创建所需的.tz压缩文件,然后将该文件与任何Linux服务器可执行文件一起提供在相同的文件夹中。在一个类似云的系统中,您可以将这些信息存储在一个集中服务器中,如通过TSynTimeZone.SaveToBuffer从单个作为参考的Windows系统生成的专用服务来存储这些信息,保存到缓冲区,然后使用TSynTimeZone.LoadFromBuffer从所有云节点解码它。主要的好处是,时间信息将保持一致,无论它运行在什么系统上,正如您所希望的那样。

  用户界面可以检索ID并使用TSynTimeZone.IdsTSynTimeZone.Displays属性作为简单TStrings实例显示为文本,索引将遵循TSynTimeZone.Zone[]内部信息。

  作为一个不错的副作用,TSynTimeZone二进制内存被发现非常有效,而且比手动读取Windows注册表快得多。复杂的本地时间计算可以在服务端完成,不必担心会破坏您的处理性能。

4.5.3. 用于多线程应用的安全锁

4.5.3.1. 保护你的资源

  多线程应用需要做好数据的并发访问保护,否则,可能会出现“竞争”问题:如两个线程同时修改同一个变量(如计数器减—),值可能会变得不一致和不安全。最常见的问题是“死锁”,死锁状态,整个应用程序被阻塞且没有响应。对于服务器系统,需要24/7运行,没有维护,这样的问题是必须避免的。

  在Delphi中,资源(可能是对象或各种变量)的保护通常通过临界区来完成。临界区是一个对象,用于确保代码的某些部分一次只由一个线程执行。在使用临界区之前需要创建/初始化临界区,在不再需要临界区时需要释放临界区。所以,使用Enter/Leave方法保护一些代码,这些方法将锁定其执行:在实践中,只有一个线程拥有临界区,因此只有一个线程能够执行该代码部分,其他线程需等待保护锁释放。为了获得最佳性能,受保护部分代码应该尽可能少,否则,使用线程的好处可能会被抵消,因为任何其他线程都将等待拥有临界部分的线程释放安全锁。

4.5.3.2. 修复TRTLCriticalSection

  在实践中,您可以使用TCriticalSection类,或首选更底层的TRTLCriticalSection记录,因为它使用更少的内存,并可以轻松地将其作为(protected)字段包含到各种类定义中。

  假设我们想要对变量a和b的访问进行保护。

var CS: TRTLCriticalSection;
    a, b: integer;
// set before the threads start
InitializeCriticalSection(CS);
// in each TThread.Execute:
EnterCriticalSection(CS);
try // protect the lock via a try ... finally block
  // from now on, you can safely make changes to the variables
  inc(a);
  inc(b);
finally
  // end of safe block
  LeaveCriticalSection(CS);
end;
// when the threads stop
DeleteCriticalSection(CS);

  在Delphi最新版本中,您可以使用TMonitor类,该类将使安全锁归属于DelphiTObject。在XE5之前,存在一些性能问题,即使是现在,这个受java启发的特性也可能不是最好的方法,因为它绑定在一个对象上,并且与旧版本的Delphi(或FPC)不兼容。

  Eric Grange几年前曾报告,参见https://www.delphitools.info/2011/11/30/fixing-tcriticalsection,TRTLCriticalSection(连同TMonitor)存在严重的设计缺陷,在这个缺陷中,进入/离开不同的临界区可能会导致线程队列终止,并且整个临界区甚至会比线程队列排队的性能更差。这是因为它是一个很小的动态分配的对象,所以几个TRTLCriticalSection内存可能最终位于同一个CPU缓存队列中,当发生这种情况时,在运行线程的内核之间会有大量缓存冲突。

  Eric提出的解决办法非常简单:

type
   TFixedCriticalSection = class(TCriticalSection)
   private
     FDummy: array [0..95] of Byte;
   end;

4.5.3.3. 引入TSynLocker

  因为我们想使用TRTLCriticalSection记录而不是TCriticalSection类实例,所以我们在syncommon.pas中定义了TSynLocker记录:

  TSynLocker = record
  private
    fSection: TRTLCriticalSection;
  public
    Padding: array[0..6] of TVarData;
    procedure Init;
    procedure Done;
    procedure Lock;
    procedure UnLock;
  end;

  如您所见,Padding[]数组将确保CPU缓存队列问题不会影响我们的对象。

  TSynLocker的使用类似于TRTLCriticalSection,拥有一些面向方法的行为:

var safe: TSynLocker;
    a, b: integer;
// set before the threads start
safe.Init;
// in each TThread.Execute:
safe.Lock
try // protect the lock via a try ... finally block
  // from now on, you can safely make changes to the variables
  inc(a);
  inc(b);
finally
  // end of safe block
  safe.Unlock;
end;
// when the threads stop
safe.Done;

  如果您的目的是保护方法的执行,那么您可以使用TSynLocker.ProtectMethod功能显式锁定/解锁,如:

type
  TMyClass = class
  protected
    fSafe: TSynLocker;
    fField: integer;
  public
    constructor Create;
    destructor Destroy; override;
    procedure UseLockUnlock;
    procedure UseProtectMethod;
  end;

{ TMyClass }

constructor TMyClass.Create;
begin
  fSafe.Init; // we need to initialize the lock
end;

destructor TMyClass.Destroy;
begin
  fSafe.Done; // finalize the lock
  inherited;
end;

procedure TMyClass.UseLockUnlock;
begin
  fSafe.Lock;
  try
    // now we can safely access any protected field from multiple threads
    inc(fField);
  finally
    fSafe.UnLock;
  end;
end;

procedure TMyClass.UseProtectMethod;
begin
  fSafe.ProtectMethod; // calls fSafe.Lock and return IUnknown local instance
  // now we can safely access any protected field from multiple threads
  inc(fField);
  // here fSafe.UnLock will be called when IUnknown is released
end;

4.5.3.4. 从T*Locked继承

  对于您自己定义的类,您可以在派生类中使用TSynLocker实例,如syncommon.pas中定义的:

  TSynPersistentLocked = class(TSynPersistent)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TInterfacedObjectLocked = class(TInterfacedObjectWithCustomCreate)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TObjectListLocked = class(TObjectList)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TRawUTF8ListHashedLocked = class(TRawUTF8ListHashed)
  ...
    property Safe: TSynLocker read fSafe;
  end;

  这些类需要在它们的constructor/destructor中初始化并释放它们的安全实例。

  所以,我们可以这样写我们的类:

type
  TMyClass = class(TSynPersistentLocked)
  protected
    fField: integer;
  public
    procedure UseLockUnlock;
    procedure UseProtectMethod;
  end;

{ TMyClass }

procedure TMyClass.UseLockUnlock;
begin
  fSafe.Lock;
  try
    // now we can safely access any protected field from multiple threads
    inc(fField);
  finally
    fSafe.UnLock;
  end;
end;

procedure TMyClass.UseProtectMethod;
begin
  fSafe.ProtectMethod; // calls fSafe.Lock and return IUnknown local instance
  // now we can safely access any protected field from multiple threads
  inc(fField);
  // here fSafe.UnLock will be called when IUnknown is released
end;

  如上,Safe: TSynLocker实例将由TSynPersistentLocked父类定义和处理。

4.5.3.5. TAutoLocker实例注入

  TSynPersistentLocked派生类(或它的一个分支类)只能为每个实例提供单个的TSynLocker访问。如果您的类继承自TSynAutoCreateFields,您可以创建一个或多个TAutoLocker发布属性,这些属性将由实例自动创建:

type
  TMyClass = class(TSynAutoCreateFields)
  protected
    fLock: TAutoLocker;
    fField: integer;
  public
    function FieldValue: integer;
  published
    property Lock: TAutoLocker read fLock;
  end;

{ TMyClass }

function TMyClass.FieldValue: integer;
begin
  fLock.ProtectMethod;
  result := fField;
  inc(fField);
end;

var c: TMyClass;
begin
  c := TMyClass.Create;
  Assert(c.FieldValue=0);
  Assert(c.FieldValue=1);
  c.Free;
end.

  实际上,TSynAutoCreateFields是一种非常强大的定义值对象的方法,如包含嵌套对象甚至对象数组的对象。您可以使用它的功能以自动化的方式创建所需的TAutoLocker实例。但是请注意,如果将此类实例序列化为JSON,则其嵌套的TAutoLocker属性将序列化为void属性,这可能不是希望的结果。

4.5.3.6. IAutoLocker实例注入

  如果你的类继承自TInjectableObject,你可以定义:

type
  TMyClass = class(TInjectableObject)
  private
    fLock: IAutoLocker;
    fField: integer;
  public
    function FieldValue: integer;
  published
    property Lock: IAutoLocker read fLock write fLock;
  end;

{ TMyClass }

function TMyClass.FieldValue: integer;
begin
  Lock.ProtectMethod;
  result := fField;
  inc(fField);
end;

var c: TMyClass;
begin
  c := TMyClass.CreateInjected([],[],[]);
  Assert(c.FieldValue=0);
  Assert(c.FieldValue=1);
  c.Free;
end;

  在这里,我们使用依赖性解析让TMyClass.CreateInjected构造函数扫描其发布属性,搜索IAutoLocker的提供者。由于IAutoLocker是全局注册的,需要用TAutoLocker解析,所以我们的类将使用一个新实例初始化fLock字段,这样我们就可以用Lock.ProtectMethod像往常一样使用关联的TAutoLockerTSynLocker临界区。

  当然,这听起来可能比手动TSynLocker处理更复杂,但是如果您正在编写基于接口的服务,您的类可能已经从TInjectableObject继承了它自己的依赖性解析,所以这个技巧可能非常方便。

4.5.3.7. TSynLocker的安全锁缓存

  当我们修复潜在的CPU缓存队列问题时,您还记得我们向TSynLocker定义中添加了一个padding二进制缓冲区吗?我们不想浪费该资源,TSynLocker提供了对其内部数据的简单访问,并允许直接处理这些值。由于它存储为7个不同值槽,因此可以存储任何类型的数据,包括复杂的TDocVariant文档或数组。

  我们的类可以使用这个特性,将它的整型字段值存储在内部槽0中:

type
  TMyClass = class(TSynPersistentLocked)
  public
    procedure UseInternalIncrement;
    function FieldValue: integer;
  end;

{ TMyClass }

function TMyClass.FieldValue: integer;
begin // value read will also be protected by the mutex
  result := fSafe.LockedInt64[0];
end;

procedure TMyClass.UseInternalIncrement;
begin // this dedicated method will ensure an atomic increase
  fSafe.LockedInt64Increment(0,1);
end;

  请注意,我们使用了TSynLocker.LockedInt64Increment()方法,因为下面的方法是不安全的:

procedure TMyClass.UseInternalIncrement;
begin
  fSafe.LockedInt64[0] := fSafe.LockedInt64[0]+1;
end;

  在上面的行中,获取了两个锁(每个LockedInt64属性调用一个锁),因此另一个线程可能会修改其中的值,增加操作可能没有预期的那么精确。

  TSynLocker提供了一些专用属性和方法来安全地处理这种存储,索引值范围为0..6:

    property Locked[Index: integer]: Variant read GetVariant write SetVariant;
    property LockedInt64[Index: integer]: Int64 read GetInt64 write SetInt64;
    property LockedPointer[Index: integer]: Pointer read GetPointer write SetPointer;
    property LockedUTF8[Index: integer]: RawUTF8 read GetUTF8 write SetUTF8;
    function LockedInt64Increment(Index: integer; const Increment: Int64): Int64;
    function LockedExchange(Index: integer; const Value: variant): variant;
    function LockedPointerExchange(Index: integer; Value: pointer): pointer;

  如果需要,可以存储TObject实例的指针或引用。

  在提供多线程服务功能的框架中,拥有这样一组线程安全的方法是有意义的。