首先,我们将介绍一些在Synopse源代码中无处不在的基础功能,即使您不需要深入了解实现细节,它也可以帮助您理解在框架源代码及其文档中遇到的一些类和类型。
使用一些从底层构建的自定义类型、类和函数而不是调用Delphi官方RTL是一种设计选择。
这样做的好处可能是:
为了使用Synopse mORMot框架,您最好能熟悉其中一些定义。
首先是提供的**Synopse.inc**包含文件,它出现在框架的很多单元中:
{$I Synopse.inc} // define HASINLINE USETYPEINFO CPU32 CPU64
它定义了一些条件,以帮助编写高效可移植的代码。
接下来,我们将说明框架的底层部分的一些主要功能,这些功能主要位于**syncommon.pas**中:
Currency货币类型;TDynArray和TDynArrayHash);TDocVariant自定义变体类型。 SynTests.pas和SynLog.pas中的其他共享特功能在稍后详细介绍,详见测试和日志记录部分。
我们的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);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);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类型,它与我们的框架集成得更好。
比较两个货币值的更快、更安全的方法是将变量映射为内部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数据库。
1.13版本的SynCommons.pas单元引入了两种封装函数:
RecordEquals、RecordSave、RecordSaveLength、RecordLoad;TDynArray和TDynArrayHashed对象,它们是所有动态数组的封装器。 通过TDynArray您可以访问各种类似于TList动态数组的属性和方法(如TIntegerDynArray = array of integer),这些属性和方法包括Count、Add、Insert、Delete、Clear、IndexOf、Find、Sort和一些新方法,如LoadFromStream、SaveToStream、LoadFrom、SaveTo、Slice、Reverse和AddArray。它包括字符串、记录等各种动态数组的快速二进制序列化,还可以使用CreateOrderedIndex方法根据动态数组内容创建单独的索引,如果需要,还可以将数组内容序列化为JSON。
动态数组的一个优点是它们是引用计数的,所以不需要使用Create/try..finally…Free代码块,Delphi编译器就可以很好地处理它们(进行了访问优化,一次分配所有数组内容,因此减少了因内存碎片和CPU缓存的性能下降)。
没有将TDynArray替换为TCollection、TList(这是标准和有效的存储类实例的方式,自框架的1.13版本它们也支持发布属性),是因为他们非常方便的内容列表或数据字典方式,而不是使用先前的类及属性定义。
您可以以纯Delphi的方式查看类似Python的列表、元组(通过记录处理)和字典(通过Find方法,特别是使用专用的TDynArrayHashed封装器),我们的新方法(搜索和序列化)允许在Delphi代码中使用这些脚本类型的结构。
为了在ORM中处理动态数组,设计了一些基于RTTI的动态数组结构。由于需要使用动态记录数组,因此使用RTTI实现了对记录内容的一些底层快速访问(比Delphi 2010之后版本增强的新RTTI快得多)。
下面是如何通过方法驱动来访问动态数组:
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 string或array of record…
记录、字符串、变体不需要定义引用计数字段(byte、integer、double,目前还不支持Interface嵌套),TDynArray能够处理record中的record,或者record中的动态数组。
是的,您的理解是对的,它能处理record动态数组,其中包含字符串或您需要的各种数据。
IndexOf()方法将搜索指定内容,也就是说,对于array of record,必须匹配所有记录字段内容(包括string属性)。
注意,TDynArray只是一个现有动态数组变量的包装。在上面的代码中,Add和Delete方法是修改Group变量的内容。因此,您可以根据需要初始化TDynArray封装器,以便更有效地访问各种Delphi原生动态数组。TDynArray不包含任何数据,元素存储在动态数组变量中,而不是存储在TDynArray实例中。
TDynArray封装器中还定义了一些TList没有的方法,有了这些方法,我们更接近于一些原生泛型实现:
LoadFromStream/SaveToStream或LoadFrom/ 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的方法,如Slice、Reverse或AddArray。
这些方法更接近于通用泛型,支持Delphi 6到Delphi 10.2 Tokyo,不需要使用低速的增强版RTTI,也没有大的可执行文件开销和泛型的编译问题……
使用TDynArray的一个常见的速度问题是,当您改变动态数组长度时,就会重新分配内存缓冲区,就像普通的Delphi动态数组一样。
也就是说,无论何时调用Add或Delete方法,都会执行对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));
TDynArray封装器具有一些JSON序列化原生功能,TTextWriter.AddDynArrayJSON和TDynArray.LoadFromJSON方法可用于动态数组的UTF-8 JSON序列化。
有关此独特功能的所有详细信息,请参见动态数组序列化部分。
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;
生成的代码执行起来非常快,并且易于阅读、维护。
我们在框架的1.18修订版引入了两种新的自定义变体类型:
TDocVariant变体类型;TBSONVariant变体类型。 第二个自定义类型(它处理针对MongoDB的扩展,如ObjectID或其他特定类型,如日期或二进制)将在稍后介绍mORMot的MongoDB支持以及BSON类型内容时说明。BSON/MongoDB在SynMongoDB.pas单元中实现。
我们现在重点讨论TDocVariant本身,它是JSON类型的对象或数组的通用容器。这种自定义变体类型在SynCommons.pas单元中实现,不需要链接到mORMot ORM内核或MongoDB,可直接在代码的任何地方使用。
TDocVariant实现了一种自定义的变体类型,可用于存储任何基于文档的JSON/BSON内容,即:
dvObject子类型)的名值对;dvArray子类型);TDocVariant实例的任意组合。以下是这种自定义变体类型的主要特征:
TDocVariantData record,直接从代码访问内部名值变体数组;interfaces 或显式使用try..finally语句块;TDynArray动态数组封装器及其JSON序列化完美集成,类似于record序列化;TSQLRecord实例,并同步处理各种数据库后端(将内容作为JSON存储在文本列中);要创建这种变体实例,可以使用如下一些易于记住的函数:
_Obj() _ObjFast()全局函数来创建变体对象文档;_Arr() _ArrFast()全局函数来创建变体数组文档;_Json() _JsonFast() _JsonFmt() _JsonFastFmt()全局函数,用于从JSON创建任何变体对象或数组文档,这些函数使用标准或MongoDB扩展语法。 您有两种非排他的使用TDocVariant存储的方法:
_Safe()快速访问其数据;TDocVariantData变量,然后使用variant(aDocVariantData)重新生成一个变体实例。 注意,您不需要使用try..finally语句保护堆栈分配的TDocVariantData实例,因为编译器会为您完成它。这种记录类型有很多强大的方法,例如在内容上应用map/reduce,或者处理高级搜索或编码。
最直接的方法是使用后期绑定来设置一个新的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=0 、Kind=dbUndefined。
TDocVariantData类型有一些附加的U[] I[] B[] D[] O[] O_[] A[] A_[] _[]属性,这些属性可直接用于获取数据类型,如RawUTF8、Int64/integer、Double,或检查嵌套文档是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获取其所有方法和属性。
通过_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]}'
通过_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形式的变体值。
默认情况下,_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'
因此,您的代码将非常安全,因为V1和V2被解耦。
但缺点就是传递这样的值可能非常慢,如当您嵌套对象时:
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文档,几乎所有内容都将按“快速”的引用模式创建。
默认情况下,TDocVariantData将只识别integer、Int64 和currency (请参阅货币处理)数值。任何不能安全地与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字符串表示才是最安全的,就像使用TTextWriterWriteObjectOption的woIDAsIDstr值来安全序列化TSQLRecord.ID ORM值一样。
如果您希望使用高精度浮点数,可以考虑使用TDecimal128值,就像在SynMongoDB.pas中实现的那样,支持128位高精度十进制,由IEEE 754-2008 128位十进制浮点标准定义,在MongoDB 3.4+中使用。
如上所述,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 类型一样,方法的const、var 和out 参数描述符不是针对TDocVariant值,而是针对其引用。
事实上,当需要动态无模式存储结构时,您可以使用TDocVariant实例,而不是class 或record 强类型:
TSQLRecord中发布variant属性以支持TDocVariant(并作为JSON存储在文本列中);TDocVariant作为任何方法的变体参数,使其成为完美的DTO;TDocVariant值的JSON支持是从底层实现的,因此它非常适合AJAX客户端以类似脚本方式进行处理。SynMongoDB.pas、mORMotMongoDB.pas单元访问MongoDB服务器,TDocVariant将作为原生存储来创建或访问嵌套的BSON数组或对象文档,也就是说,它支持完整的ODM存储;record /动态数组增强)也将受益于此TDocVariant自定义类型。 我们非常确信,您一旦使用了TDocVariant,您将再也离不开它,它将强大的后期绑定和动态无模式功能引入到应用程序代码中,这对于原型设计或敏捷开发非常有用。您不需要使用Python或JavaScript之类的脚本引擎,Delphi就完全能够处理动态编码。
对于日期/时间的文本存储,框架将使用ISO 8601编码。日期可以编码为YYYY-MM-DD或YYYYMMDD,时间可以编码为hh:mm:ss或hhmmss,日期和时间的组合表示形式为<date>T<time>,即YYYY-MM-DDThh:mm:ss或YYYYMMDDThhmmss。
因此,除了涉及负年的日期表示外,该表示的字典顺序与时间顺序相对应,这使得日期可以自然地按照文件系统、表格列表等进行排序。
除了默认的TDateTime类型(序列化时精确到秒)之外,您还可以使用包含毫秒的TDateTimeMS,即YYYY-MM-DDThh:mm:ss.sss或YYYYMMDDThhmmss.sss:
type
TDateTimeMS = type TDateTime;
这个TDateTimeMS类型由框架的 ORM在record 、动态数组、JSON序列化过程中处理。
SynCommons.pas单元还定义了TTimeLog类型,以及一些与TDateTime值相互转换的函数:
type
TTimeLog = type Int64;
这个整数存储被编码为一系列位,这些位将映射SynCommons.pas单元中定义的TTimeLogBits记录类型。
这些值的分辨率是秒。实际上,它内部用于计算一个抽象的“年”,即16个月、32天、32小时、64分钟、64秒。
因此,各种日期/时间信息都可以从其内部位中获取:
ISO 8601标准允许毫秒分辨率,编码为 hh:mm:ss.sss或hhmmss.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的编译器限制,直接将TTimeLog或Int64变量转换为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如何通过TModTime和TCreateTime类型处理时间日期,参见日期时间字段。
另一种选择是,您可以使用TUnixTime时间类型,它是自Unix纪元以来的64位编码的秒数,即1970-01-01 00:00:00 UTC以来:
type
TUnixTime = type Int64;
你可以转换这些值:
TTimeLogBits.ToUnixTime和TTimeLogBits.FromUnixTime方法实现与TTimeLog类型的相互转换;UnixTimeToDateTime/DateTimeToUnixTime函数实现与TDateTime类型的相互转换;UnixTimeUTC调用快速的OS API返回当前时间戳。 您可以考虑使用TUnixTime时间,特别是如果第三方客户端遵循此编码的话。在Delphi世界中,首选TDateTime或TTimeLog类型。
在处理日期和时间时,一个常见的问题是,时间通常按当地时间显示和输入,而计算机确应该使用非地理信息,特别是在客户端-服务端体系结构中,两端可能不在同一物理区域。
时区是为法律、商业和社会目的而需遵守的统一标准时间的区域。时区往往遵循国家的边界及其划分,因为在商业或其他交流密切的地区保持相同的时间是很方便的。陆地上的大多数时区与世界标准时间(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.UtcToLocal 或TSynTimeZone.LocalToUtc方法,并使用正确的TZ标识符。
您必须在Windows机器下创建所需的.tz压缩文件,然后将该文件与任何Linux服务器可执行文件一起提供在相同的文件夹中。在一个类似云的系统中,您可以将这些信息存储在一个集中服务器中,如通过TSynTimeZone.SaveToBuffer从单个作为参考的Windows系统生成的专用服务来存储这些信息,保存到缓冲区,然后使用TSynTimeZone.LoadFromBuffer从所有云节点解码它。主要的好处是,时间信息将保持一致,无论它运行在什么系统上,正如您所希望的那样。
用户界面可以检索ID并使用TSynTimeZone.Ids 和TSynTimeZone.Displays属性作为简单TStrings实例显示为文本,索引将遵循TSynTimeZone.Zone[]内部信息。
作为一个不错的副作用,TSynTimeZone二进制内存被发现非常有效,而且比手动读取Windows注册表快得多。复杂的本地时间计算可以在服务端完成,不必担心会破坏您的处理性能。
多线程应用需要做好数据的并发访问保护,否则,可能会出现“竞争”问题:如两个线程同时修改同一个变量(如计数器减—),值可能会变得不一致和不安全。最常见的问题是“死锁”,死锁状态,整个应用程序被阻塞且没有响应。对于服务器系统,需要24/7运行,没有维护,这样的问题是必须避免的。
在Delphi中,资源(可能是对象或各种变量)的保护通常通过临界区来完成。临界区是一个对象,用于确保代码的某些部分一次只由一个线程执行。在使用临界区之前需要创建/初始化临界区,在不再需要临界区时需要释放临界区。所以,使用Enter/Leave方法保护一些代码,这些方法将锁定其执行:在实践中,只有一个线程拥有临界区,因此只有一个线程能够执行该代码部分,其他线程需等待保护锁释放。为了获得最佳性能,受保护部分代码应该尽可能少,否则,使用线程的好处可能会被抵消,因为任何其他线程都将等待拥有临界部分的线程释放安全锁。
在实践中,您可以使用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;
因为我们想使用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;
对于您自己定义的类,您可以在派生类中使用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父类定义和处理。
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属性,这可能不是希望的结果。
如果你的类继承自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像往常一样使用关联的TAutoLocker的TSynLocker临界区。
当然,这听起来可能比手动TSynLocker处理更复杂,但是如果您正在编写基于接口的服务,您的类可能已经从TInjectableObject继承了它自己的依赖性解析,所以这个技巧可能非常方便。
当我们修复潜在的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实例的指针或引用。
在提供多线程服务功能的框架中,拥有这样一组线程安全的方法是有意义的。