-
-
Notifications
You must be signed in to change notification settings - Fork 141
/
Copy pathmormot.core.collections.pas
2535 lines (2336 loc) · 89.8 KB
/
mormot.core.collections.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// Framework Core Low-Level Generics Collection Process
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.core.collections;
{
*****************************************************************************
Generics Collections as used by all framework units
- JSON-aware IList<> List Storage
- JSON-aware IKeyValue<> Dictionary Storage
- Collections Factory for IList<> and IKeyValue<> Instances
In respect to generics.collections, this unit uses interfaces as variable
holders, and leverage them to reduce the generated code as much as possible,
as the Spring4D 2.0 framework does, but for both Delphi and FPC.
It publishes TDynArray and TSynDictionary high-level features like indexing,
sorting, JSON/binary serialization or thread safety as Generics strong typing.
Use Collections.NewList<T> and Collections.NewKeyValue<TKey, TValue> factories
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
// current Delphi compiler support: since Delphi XE, but disabled for XE6/XE7
// which trigger internal errors; specialization only since Delphi XE8
{$ifdef HASGENERICS} // do-nothing unit on oldest compilers (e.g. Delphi 7/2009)
// FPC 3.2+ and Delphi XE8+ allow to gather most common specializations in this
// unit and not in the end-user units to reduce executable code size
// - NOSPECIALIZE disable ahead-of-time compilation and make naive bloated generics
// - you may try this conditional to circumvent some Delphi internal errors
// - see also SPECIALIZE_HASH, SPECIALIZE_SMALL SPECIALIZE_WSTRING conditionals
// - on XE8 Win32 we can observe
// mormot.core.collections.dcu: default=496KB NOSPECIALIZE=75KB
// test.core.collections.dcu: default=181KB NOSPECIALIZE=263KB
// -> so the main size reduction of those collections is that they are based on
// TDynArray and TSynDictionary, then specialization helps a little more
{.$define NOSPECIALIZE}
// you could try to define this conditional to generate even less code, which
// may be slightly slower - perhaps not really noticeable on production
{.$define SMALLGENERICS}
uses
classes,
contnrs,
sysutils,
mormot.core.base,
mormot.core.os,
mormot.core.unicode,
mormot.core.text,
mormot.core.buffers,
mormot.core.data,
mormot.core.rtti,
mormot.core.json;
// note: we defined "var value" instead of "out value" to avoid finalizer calls
{ ************** JSON-aware IList<> List Storage }
type
TIListParent = class;
/// abstract execution context for the TIListEnumerator<T> record
// - as filled from shared TIListParent.NewEnumerator overloaded methods
TIListEnumeratorState = record
Current, After: PtrUInt; // 2 pointers on stack
end;
/// efficient mean to iterate over a generic collection of a specific type
// - as used by IList<T>.GetEnumerator/Range methods
// - we redefined our own record type for better performance: it properly
// inlines, and allocates as 2 pointers on stack with no try..finally
TIListEnumerator<T> = record
private
fState: TIListEnumeratorState;
// some property accessor
function DoGetCurrent: T; inline;
public
type
PT = ^T;
/// this property is needed for any enumerator
property Current: T
read DoGetCurrent;
/// go to the next item iterated in this collection
function MoveNext: boolean; inline;
/// self-reference is needed for IList<T>.Range custom enumerator
function GetEnumerator: TIListEnumerator<T>; inline;
end;
/// exception class raised by IList<T>
EIList = class(ESynException);
/// how Collections.NewList<T> will handle its IList<T> storage
// - by default, string values would be searched following exact case,
// unless the loCaseInsensitive option is set
// - by default, managed values and T*ObjArray will delete their content
// unless the loNoFinalize option is set (handle with care to avoid mem leaks)
// - loCreateUniqueIndex will maintain a hash table over the items so that
// Add() would avoid any duplicate and Find() perform in O(1) fast lookup -
// note that aSortAs could be set in Collections.NewPlainList<> to index the
// first field of a record instead of the whole collection item
TListOptions = set of (
loCaseInsensitive,
loNoFinalize,
loCreateUniqueIndex);
/// customize IList<T>.Pop() behaviour
// - popPeek won't remove the item from the list, just copy the value
// - Add+Pop implement a LIFO (Last-In-First-Out) stack by default, but a
// FIFO (First-In-First-Out) if popFromHead is defined in this options set
TListPop = set of (
popPeek,
popFromHead);
/// gives access to a generics-based collection of items
// - as generated by Collections.NewList<T> main factory
// - defined as an interface for automatic memory management, and class
// prototypes reuse between units, to reduce the executable size
// - methods are not thread-safe, but an associated TRWLock is available
// - storage is implemented via a TDynArray wrapper, optionally with a hash
// table for fast Find() lookup if loCreateUniqueIndex option is set
IList<T> = interface
// some property accessors
function GetItem(ndx: PtrInt): T;
procedure SetItem(ndx: PtrInt; const value: T);
function GetCount: PtrInt;
procedure SetCount(value: PtrInt);
function GetCapacity: PtrInt;
procedure SetCapacity(value: PtrInt);
function GetComparer: TDynArraySortCompare;
procedure SetComparer(customcompare: TDynArraySortCompare);
/// append a new value to the collection
// - returns the index of the newly added item
// - always append the new item at the end, unless loCreateUniqueIndex was
// defined and then any duplicate is ignored and existing index is returned
// - you may pre-allocate the array with a previous set of Capacity property
// - a faster alternative is to set the Count then assign values with Items[]
function Add(const value: T; wasadded: PBoolean = nil): PtrInt;
/// insert a new value to the collection
// - raise EIList if loCreateUniqueIndex is set: use Remove() then Add()
procedure Insert(ndx: PtrInt; const value: T);
/// delete one item inside the collection from its index
// - the deleted item is finalized unless loNoFinalize was defined
// - raise EIList if loCreateUniqueIndex is defined: use Remove()
function Delete(ndx: PtrInt): boolean;
/// delete one item inside the collection from its value
// - the deleted item is finalized unless loNoFinalize was defined
// - is the proper way of deleting an item if loCreateUniqueIndex is defined
function Remove(const value: T): boolean;
/// get and remove the last item stored in the collection
// - set popPeek in opt if you don't want to remove the item, just copy its value
// - Add+Pop implement a LIFO (Last-In-First-Out) stack by default
// - Add+Pop implement a FIFO (First-In-First-Out) stack if popFromHead is
// set - but slower, since all existing data is moved in memory by Pop()
// - returns true if the item was successfully copied and removed from the list
// - the existing dest is finalized/release before copying the poped value,
// unless loNoFinalize was defined
function Pop(var dest: T; opt: TListPop = []): boolean;
/// delete all stored items
// - the items are released/cleared unless loNoFinalize was defined
procedure Clear;
/// will reverse all collection items, in place
procedure Reverse;
/// sort the collection items
// - use the main Comparer function from RTTI, unless customcompare is set
procedure Sort(customcompare: TDynArraySortCompare = nil); overload;
/// sort a collection range
// - use the main Comparer function from RTTI, unless customcompare is set
// - this method allows to sort only some part of the items
procedure Sort(start, stop: integer;
customcompare: TDynArraySortCompare = nil); overload;
/// sort the collection items using an external lookup array of indexes
// - use the main Comparer function from RTTI, unless customcompare is set
// - in comparison to the Sort method, this overload won't change the
// collection content, but only create (or update) the supplied indexes[]
// - if the indexes lookup table has less items than the collection,
// its content will be recreated
procedure Sort(var indexes: TIntegerDynArray;
customcompare: TDynArraySortCompare = nil); overload;
/// sort the collection, using a comparison property method (not function)
// - you could optionally sort in reverse order
procedure Sort(const customcompare: TOnDynArraySortCompare;
descending: boolean = false); overload;
/// search and add an item inside a sorted collection
// - a sorted collection will use O(log(n)) binary search
// - this method will use the main Comparer function for the search
// - returns the index of the existing Item if wasadded^=false
// - returns the sorted index of the inserted Item if wasadded^=true
// - if the collection is not sorted, returns -1 and wasadded^=false
// - raise EIList if loCreateUniqueIndex is set: use plain Add()
function AddSorted(const value: T; wasadded: PBoolean = nil): integer;
/// will check all items against customcompare, calling Sort() if needed
// - faster than plain Sort() if the array is likely to be already sorted
// - won't check for the Sorted property flag, so will always compare all
procedure EnsureSorted(customcompare: TDynArraySortCompare = nil);
/// is true if Sort() has just been called, or AddSorted() used
function Sorted: boolean;
/// search for a value inside this collection using Comparer function
// - if the collection was created with loCreateUniqueIndex, will use
// an internal hash table for O(1) efficient lookup - aSortAs could be set
// in Collections.NewPlainList<> to hash the first field of a record instead
// of the whole collection item
// - if the collection is sorted (i.e. AddSorted was used, or Sort was
// called after Add) will perform fast O(log(n)) binary search
// - on a non-sorted collection, will make O(n) comparisons with the value
// - if customcompare is set, a O(n) comparison lookup will be done
function IndexOf(const value: T; customcompare: TDynArraySortCompare = nil): PtrInt;
/// allow to iterate over a generic collection of a specific type
// - this enumerator is faster than for i := 0 to Count - 1 do ... list[i]
// - we redefined our own TIListEnumerator<T> record type which is much faster
// than using classes or interfaces, and provide very readable code:
// ! var i: integer;
// ! list: IList<integer>;
// ! begin
// ! list := Collections.NewList<integer>;
// ! for i := 1 to 20 do // populate with some data
// ! list.Add(i);
// ! for i in list do // use an enumerator - fast, safe and clean
// ! writeln(i);
function GetEnumerator: TIListEnumerator<T>;
/// allow to iterate over a range of the collection
// - returned iterator will efficiently browse the items data in-place:
// ! for i in list.Range do // = for i in list do (all data)
// ! for i in list.Range(10) do // items 10..Count-1
// ! for i in list.Range(0, 10) do // first 0..9 items
// ! for i in list.Range(10, 20) do // items 10..29 - truncated if Count<30
// ! for i in list.Range(-10) do // last Count-10..Count-1 items
function Range(Offset: PtrInt = 0; Limit: PtrInt = 0): TIListEnumerator<T>;
/// low-level pointer over the first item of the collection
// - can be nil if there is no item stored yet
// - could be used to quickly lookup all items of the array, using Count:
// ! var pi: PInteger; ...
// ! pi := list.First; // fastest method
// ! for i := 1 to list.Count do
// ! begin
// ! writeln(pi^);
// ! inc(pi);
// ! end;
// - could be used to set all items of the array, with a previous Count set
// (faster than Add or even Count+SetItems)
function First: pointer;
/// returns a dynamic array containing data of this collection
// - is a convenient way to consume such a list as regular SOA parameters
// - Offset/Limit could be used to create a new dynamic array with some part
// of the existing content (Offset<0 meaning from the end):
// ! a := list.AsArray; // whole data assigned with refcount
// ! a := list.AsArray(10); // items 10..Count-1
// ! a := list.AsArray(0, 10); // first 0..9 items
// ! a := list.AsArray(10, 20); // items 10..29 - truncated if Count<30
// ! a := list.AsArray(-10); // last Count-10..Count-1 items
function AsArray(Offset: PtrInt = 0; Limit: PtrInt = 0): TArray<T>;
/// add some items from another IList<T> instance
procedure AddFrom(const Another: IList<T>; Offset: PtrInt = 0;
Limit: PtrInt = -1);
/// high-level access to the stored values from their associated indexes
// - raise EIList if the supplied index is out of range
// - SetItem() will raise EIList if loCreateUniqueIndex is defined
// - is the default propery so that IList<T> could be used as an array:
// ! for i := 0 to list.Count - 1 do // regular Items[] access
// ! writeln(list[i]);
// - note that using an enumerator is faster than using this property within
// a loop, since TIListEnumerator<T> is a record which can be inlined
property Items[ndx: PtrInt]: T
read GetItem write SetItem; default;
/// returns the number of items actually stored
// - you can also set the Count value then fill it with Items[] or even
// Data.First with pointers for best performance
property Count: PtrInt
read GetCount write SetCount;
/// returns the internal array capacity
property Capacity: PtrInt
read GetCapacity write SetCapacity;
/// the current comparison function, used e.g. for Sort() or Find()
// - will be assigned by default from RTTI and the loCaseInsensitive option
property Comparer: TDynArraySortCompare
read GetComparer write SetComparer;
/// the associated lightweight multiple Reads / exclusive Write lock
// - TRWLock is spinning on wait, so locks are expected to be released ASAP
function Safe: PRWLock;
/// low-level access to the internal TDynArray wrapper
// - you can use e.g. Data.SaveToJson/SaveTo and
// Data.LoadFromJson/LoadFromBinary
function Data: PDynArray;
end;
/// abstract parent of TIList<T> to reduce code size
// - contains all fields and methods not explicitly related to type T
TIListParent = class(TInterfacedObject)
protected
fSafe: TRWLock;
fCount: integer; // external fDynArray count
fOptions: TListOptions;
fValue: pointer; // holds the actual dynamic array of <T>
fDynArray: TDynArray;
fHasher: PDynArrayHasher;
function DoPop(var dest; opt: TListPop): boolean;
function DoRemove(const value): boolean;
function DoAdd(const value; var added: boolean): PtrInt;
function DoAddSorted(const value; wasadded: PBoolean): integer;
procedure DoAddFrom(Another: PDynArray; Offset, Limit: PtrInt);
procedure DoInsert(ndx: PtrInt; const value);
function DoFind(const value; customcompare: TDynArraySortCompare): PtrInt;
procedure RaiseGetItem(ndx: PtrInt);
procedure RaiseSetItem(ndx: PtrInt);
procedure NewEnumerator(var state: TIListEnumeratorState); overload;
procedure NewEnumerator(var state: TIListEnumeratorState;
Offset, Limit: PtrInt); overload;
// some property accessors
function GetCount: PtrInt;
procedure SetCount(value: PtrInt);
function GetCapacity: PtrInt;
procedure SetCapacity(value: PtrInt);
function GetComparer: TDynArraySortCompare;
procedure SetComparer(customcompare: TDynArraySortCompare);
public
/// internal constructor to create an IList<T> instance from RTTI
// - main factories are Collections.NewList<T> or NewPlainList<T> class
// functions, which returns a IList<> interface for reusing most class
// specializations: you should NOT call any TIListParent<>.Create
// - constructor is at TIListParent level to reduce the executable size
// - if aSortAs is ptNone, will guess the comparison/sort function from RTTI
// - used only to circumvent FPC internal error 2010021502 on x86_64/aarch64
// (root cause seems comes from T through another generic method), i.e.
// direct specialization like Collections.NewList<integer> works fine,
// but cascaded generics like TTestCoreCollections.TestOne<T> need this:
// ! {$ifdef FPC_64}
// ! li := TIList<T>.Create(TypeInfo(TArray<T>), TypeInfo(T));
// ! {$else}
// ! li := Collections.NewList<T>;
// ! {$endif FPC_64}
constructor Create(aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo;
aOptions: TListOptions = []; aSortAs: TRttiParserType = ptNone);
/// internal constructor to create an IList<T> instance from our RTTI
constructor CreateRtti(aDynArray: TRttiCustom; aItemTypeInfo: PRttiInfo;
aOptions: TListOptions = []; aSortAs: TRttiParserType = ptNone);
/// finalize the array storage, mainly the internal TDynArray
destructor Destroy; override;
/// IList<> method to delete one item inside the collection from its index
function Delete(ndx: PtrInt): boolean;
/// IList<> method to delete all stored items
procedure Clear;
/// IList<> method to reverse all collection items, in place
procedure Reverse;
/// IList<> method to sort the collection items
procedure Sort(customcompare: TDynArraySortCompare = nil); overload;
/// IList<> method to sort a collection range
procedure Sort(start, stop: integer; customcompare: TDynArraySortCompare = nil); overload;
/// IList<> method to sort the collection items using an external lookup array
procedure Sort(var indexes: TIntegerDynArray;
customcompare: TDynArraySortCompare = nil); overload;
/// IList<> method to sort the collection, using a comparison method
procedure Sort(const customcompare: TOnDynArraySortCompare;
descending: boolean = false); overload;
/// IList<> method to ensure collection is sorted, using a comparison method
procedure EnsureSorted(customcompare: TDynArraySortCompare);
/// IList<> method returning true if Sort() or AddSorted() have been used
function Sorted: boolean;
/// low-level IList<> method to access the first item of the collection
function First: pointer; inline;
/// IList<> method to return the number of items actually stored
property Count: PtrInt
read GetCount write SetCount;
/// IList<> method to return the internal array capacity
property Capacity: PtrInt
read GetCapacity write SetCapacity;
/// IList<> method to access an associated lightweight read/write lock
function Safe: PRWLock; inline;
/// low-level IList<> method to access to the internal TDynArray wrapper
function Data: PDynArray; inline;
end;
/// meta-class of TIListParent types
TIListParentClass = class of TIListParent;
/// generics-based collection storage
// - high level wrapper around our regular TDynArray implementing IList<T>
// - main factory is Collections.NewList<T> class function, which returns a
// IList<T> interface for reusing most class specializations: you should
// NOT have to define a TIList<T> instance anywhere
TIList<T> = class(TIListParent, IList<T>)
protected
// some property accessors
function GetItem(ndx: PtrInt): T;
procedure SetItem(ndx: PtrInt; const value: T);
public
/// IList<T> method to append a new value to the collection
function Add(const value: T; wasadded: PBoolean = nil): PtrInt;
/// IList<T> method to insert a new value to the collection
procedure Insert(ndx: PtrInt; const value: T);
/// IList<T> method to get and remove the last item stored in the collection
function Pop(var dest: T; opt: TListPop): boolean;
/// IList<T> method for (sorted) search using a comparison function
function IndexOf(const value: T; customcompare: TDynArraySortCompare = nil): PtrInt;
/// IList<> method to delete one item inside the collection from its value
function Remove(const value: T): boolean;
/// IList<T> method to search and add an item inside a sorted collection
function AddSorted(const value: T; wasadded: PBoolean = nil): integer;
/// IList<T> method to return a dynamic array of this collection items
function AsArray(Offset: PtrInt = 0; Limit: PtrInt = 0): TArray<T>;
/// IList<T> method to iterate over a generic collection
function GetEnumerator: TIListEnumerator<T>;
/// IList<T> method to iterate over some range of the generic collection
function Range(Offset: PtrInt = 0; Limit: PtrInt = 0): TIListEnumerator<T>;
/// IList<T> method to add items from another IList<T> method
procedure AddFrom(const Another: IList<T>; Offset: PtrInt = 0;
Limit: PtrInt = -1);
end;
{ ************** JSON-aware IKeyValue<> Dictionary Storage }
type
/// exception class raised by TIKeyValue<TKey, TValue>
EIKeyValue = class(ESynException);
/// pair result as returned by TIKeyValueEnumerator<TKey, TValue>
TPair<TKey, TValue> = record
public
/// the current Key content
Key: TKey;
/// the current Value content
Value: TValue;
end;
/// efficient mean to iterate over a generic collection of key/value pairs
// - as used by IKeyValue<>.GetEnumerator
TIKeyValueEnumerator<TKey, TValue> = record
public
type
PKey = ^TKey;
PValue = ^TValue;
private
fKey: PKey;
fValue: PValue;
fCount: integer;
function DoGetCurrent: TPair<TKey, TValue>; inline;
public
/// this property is needed for any enumerator
property Current: TPair<TKey, TValue>
read DoGetCurrent;
/// go to the next key/value pair iterated in this dictionary
function MoveNext: boolean; inline;
/// self-reference
function GetEnumerator: TIKeyValueEnumerator<TKey, TValue>; inline;
end;
/// gives access to a generics-based dictionary holding key/value pairs
// - as generated by Collections.NewKeyValue<TKey, TValue> main factory
// - defined as an interface for automatic memory management, and class
// prototypes reuse between units, to reduce the executable size
// - optionally thread-safe when created with the kvoThreadSafe option
// - all process is done by an internal TSynDictionary with extended features
// like binary or JSON serialization, thread-safety or deprecation/timeout
IKeyValue<TKey, TValue> = interface
// some property accessors
function GetItem(const key: TKey): TValue;
procedure SetItem(const key: TKey; const value: TValue);
function GetKey(ndx: PtrInt): TKey;
function GetValue(ndx: PtrInt): TValue;
function GetCapacity: integer;
procedure SetCapacity(value: integer);
function GetTimeOutSeconds: cardinal;
procedure SetTimeOutSeconds(value: cardinal);
/// add a key/value pair to be unique
// - raise an EIKeyValue if key was already set
// - use default Items[] property to add or replace a key/value pair
procedure Add(const key: TKey; const value: TValue);
/// add a key/value pair if key is not existing
// - returns true if was added, false if key was already set
// - use default Items[] property to add or replace a key/value pair
function TryAdd(const key: TKey; const value: TValue): boolean;
/// search a key and return the associated value
// - returns true if the key was found, false otherwise
function TryGetValue(const key: TKey; var value: TValue): boolean;
/// search a key and return the associated value or a supplied default
function GetValueOrDefault(const key: TKey; const defaultValue: TValue): TValue;
/// remove a key/value pair
// - returns true if the entry was deleted, false if key was not found
function Remove(const key: TKey): boolean;
/// search a key, get the associated value, then delete the key/value pair
function Extract(const key: TKey; var value: TValue): boolean;
/// search for a key/value pair from a key
// - returns true if the key was found, false otherwise
function ContainsKey(const key: TKey): boolean;
/// search for a key/value pair from a value
// - returns true if the value was found, false otherwise
function ContainsValue(const value: TValue): boolean;
/// search and delete all deprecated items according to TimeoutSeconds
// - returns how many items have been deleted
// - you can call this method very often: it will ensure that the
// search process will take place at most once every second
function DeleteDeprecated: integer;
/// delete all stored key/value pairs
procedure Clear; overload;
/// thread-safety protection when accessing Count/Key[]/Value[] members
procedure ReadLock;
/// thread-safety protection when accessing Count/Key[]/Value[] members
procedure ReadUnLock;
/// allows to iterate over all key/value pairs in this collection
// - this is not thread-safe so to be protected by ReadLock/ReadUnLock
// - code is cleaner and safer than using Key[] Value[] and Count:
// ! var
// ! kv: IKeyValue<RawUtf8, double>;
// ! e: TPair<RawUtf8, double>;
// ! i: integer;
// ! begin
// ! kv := Collections.NewKeyValue<RawUtf8, double>;
// ! for i := 1 to 20 do
// ! kv.Add(UInt32ToUtf8(i), i); // populate with some data
// ! for e in kv do
// ! writeln(e.Key, ' = ', e.Value);
function GetEnumerator: TIKeyValueEnumerator<TKey, TValue>;
/// search the index of given key
// - the index could then be used with Key[] and Value[] properties
// - this is not thread-safe so to be protected by ReadLock/ReadUnLock
// - consider using the safer TryGetValue() or Items[] instead
function FindKeyIndex(const key: TKey): PtrInt;
/// returns the number of key/value pairs actually stored
// - this is not thread-safe so to be protected by ReadLock/ReadUnLock if
// you want to use the Key[] Value[] indexed properties
function Count: integer;
/// high-level access to the stored values from their associated keys
// - GetItem() raise an EIKeyValue if the key is not available, unless
// kvoDefaultIfNotFound option was set - use TryGetValue() if you want to
// detect (without any exception) any non available key
// - SetItem() will add or replace the value associated with the key
property Items[const key: TKey]: TValue
read GetItem write SetItem; default;
/// low-level access to the stored keys, in their 0..Count-1 internal order
// - indexes are not thread-safe so to be protected by ReadLock/ReadUnLock
// - warning: won't raise any exception if ndx is out-of-range
// - consider using the safer TPair<TKey, TValue> enumerator instead
property Key[ndx: PtrInt]: TKey
read GetKey;
/// low-level access to the stored values, in their 0..Count-1 internal order
// - indexes are not thread-safe so to be protected by ReadLock/ReadUnLock
// - warning: won't raise any exception if ndx is out-of-range
// - consider using the safer TPair<TKey, TValue> enumerator instead
property Value[ndx: PtrInt]: TValue
read GetValue;
/// returns the internal TSynDictionary capacity
property Capacity: integer
read GetCapacity write SetCapacity;
/// returns the TimeOutSeconds parameter, as specified to NewKeyValue<>
// - warning: setting a new timeout will clear all previous content
property TimeOutSeconds: cardinal
read GetTimeOutSeconds write SetTimeOutSeconds;
/// low-level access to the internal TSynDictionary storage
// - which handles a lot of other useful methods not included as generics
// to reduce the executable code size
// - you can use e.g. Data.Keys/Data.Values or Data.SaveToJson/SaveToBinary
// and Data.LoadFromJson/LoadFromBinary
function Data: TSynDictionary;
end;
/// how TIKeyValue<TKey, TValue>.Create() will handle its storage
// - kvoKeyCaseInsensitive will let TKey values lookup ignore the case
// - kvoThreadSafe will force the instance to be thread-safe via a TRWLock
// - kvoThreadCriticalSection + kvoThreadSafe will force to use a regular
// TCriticalSection for the thread safety
// - kvoDefaultIfNotFound will let IKeyValue<TKey, TValue>.Items[] return the
// default TValue (e.g. 0 or '') and raise no exception if TKey is not found
// - by default, managed values and T*ObjArray will delete their content unless
// kvoKeyNoFinalize/kvoValueNoFinalize options are set (handle with care)
TKeyValueOptions = set of (
kvoKeyCaseInsensitive,
kvoThreadSafe,
kvoThreadCriticalSection,
kvoDefaultIfNotFound,
kvoKeyNoFinalize,
kvoValueNoFinalize);
/// stack parameters to ease TIKeyValue<TKey, TValue> creation
TNewKeyValueContext = record
Options: TKeyValueOptions;
KeySpecific: TRttiParserType;
Timeout: cardinal;
KeyArrayTypeInfo,
KeyItemTypeInfo: PRttiInfo;
ValueArrayTypeInfo,
ValueItemTypeInfo: PRttiInfo;
Compress: TAlgoCompress;
Hasher: THasher;
end;
/// abstract parent of TIKeyValue<TKey, TValue> to reduce code size
// - contains all fields and methods not explicitly related to TKey/TValue
TIKeyValueParent = class(TInterfacedObject)
protected
fData: TSynDictionary;
fOptions: TKeyValueOptions;
fHasTimeout, fHasLock: boolean; // internal flags
function GetKeyTypeInfo: PRttiInfo;
function GetValueTypeInfo: PRttiInfo;
procedure AddOne(key, value: pointer);
procedure GetDefaultOrRaise(value: pointer);
procedure GetDefaultOrUnlockAndRaise(value: pointer);
function GetCapacity: integer;
procedure SetCapacity(value: integer);
function GetTimeOutSeconds: cardinal;
procedure SetTimeOutSeconds(value: cardinal);
procedure ReadLock;
procedure ReadUnLock;
public
/// initialize the dictionary storage, specifying dynamic array keys/values
// - main factory is Collections.NewKeyValue<TKey, TValue> class function,
// which returns a IKeyValue<> interface for reusing most class
// specializations: you should NOT call any TIKeyValue<> constructor anywhere
constructor Create(const aContext: TNewKeyValueContext); reintroduce; virtual;
/// finalize the dictionary storage
destructor Destroy; override;
/// IKeyValue<> method to search and delete all deprecated items
function DeleteDeprecated: integer;
/// IKeyValue<> method to delete all stored key/value pairs
procedure Clear;
/// IKeyValue<> method to get the number of key/value pairs actually stored
function Count: integer;
/// IKeyValue<> method to get the internal TSynDictionary capacity
property Capacity: integer
read GetCapacity write SetCapacity;
/// IKeyValue<> method to get the TimeOutSeconds param of NewKeyValue<>
// - warning: setting a new timeout will clear all previous content
property TimeOutSeconds: cardinal
read GetTimeOutSeconds write SetTimeOutSeconds;
/// low-level IKeyValue<> method to get the internal TSynDictionary storage
function Data: TSynDictionary;
/// low-level IKeyValue<> method to get the NewKeyValue<> TKeyValueOptions
property Options: TKeyValueOptions
read fOptions;
/// low-level TypeInfo(TKey) access
property KeyTypeInfo: PRttiInfo
read GetKeyTypeInfo;
/// low-level TypeInfo(TValue) access
property ValueTypeInfo: PRttiInfo
read GetValueTypeInfo;
end;
/// meta-class of TIKeyValueParent type definitions
TIKeyValueParentClass = class of TIKeyValueParent;
/// thread-safe generics-based dictionary holding key/value pairs
// - is a high level wrapper around our regular TSynDictionary
// - main factory is Collections.NewKeyValue<TKey, TValue> class function,
// which returns a IKeyValue<> interface for reusing most class
// specializations: you should NOT directly use a TIKeyValue<> anywhere
TIKeyValue<TKey, TValue> = class(
TIKeyValueParent, IKeyValue<TKey, TValue>)
protected
// some property accessors
function GetItem(const key: TKey): TValue;
procedure SetItem(const key: TKey; const value: TValue);
function GetKey(ndx: PtrInt): TKey;
function GetValue(ndx: PtrInt): TValue;
public
/// IKeyValue<> method to add an unique key/value pair
procedure Add(const key: TKey; const value: TValue);
/// IKeyValue<> method to add a key/value pair if key is not existing
function TryAdd(const key: TKey; const value: TValue): boolean;
/// IKeyValue<> method to search a key and return its associated value
function TryGetValue(const key: TKey; var value: TValue): boolean;
{$ifndef SMALLGENERICS} inline; {$endif}
/// IKeyValue<> method to search a key or a supplied default
function GetValueOrDefault(const key: TKey;
const defaultValue: TValue): TValue;
/// IKeyValue<> method to remove a key/value pair
function Remove(const key: TKey): boolean;
/// IKeyValue<> method to search a key/value, then delete the pair
function Extract(const key: TKey; var value: TValue): boolean;
/// IKeyValue<> method to search for a key/value pair from a key
function ContainsKey(const key: TKey): boolean;
/// IKeyValue<> method to search for a key/value pair from a value
function ContainsValue(const value: TValue): boolean;
/// IKeyValue<> method to iterate over all key/value pairs
function GetEnumerator: TIKeyValueEnumerator<TKey, TValue>;
/// IKeyValue<> method to search the index of given key
function FindKeyIndex(const key: TKey): PtrInt;
/// high-level IKeyValue<> method to get the stored values from their keys
property Items[const key: TKey]: TValue
read GetItem write SetItem; default;
end;
{ ************ Collections Factory for IList<> and IKeyValue<> Instances }
{$ifdef HASGETTYPEKIND} // our specialization rely on new compiler intrinsics
{$ifndef NOSPECIALIZE} // if not disabled for the project
// enable generics cold compilation in mormot.core.collections unit
{$define SPECIALIZE_ENABLED}
// small byte/word are not useful in dictionaries (use integer instead)
// so are not pre-compiled by default - this conditional generates them
// - this affects only IKeyValue<> not IList<> which specializes byte/word
{.$define SPECIALIZE_SMALL}
// enable cold compilation of THash128/TGuid and THash256/THash612
// - those types are hardly used, so not cold compiled by default
{.$define SPECIALIZE_HASH}
// WideString are slow - RawUtf8 or UnicodeString are to be used instead -
// so are not pre-compiled by default - this conditional generates them
{.$define SPECIALIZE_WSTRING}
{$endif NOSPECIALIZE}
{$else}
// disable ahead-of-time specialized factories on unsupported compilers
{$undef SPECIALIZE_ENABLED}
{$endif HASGETTYPEKIND}
type
/// various factories to create instances of our generic collections
// - this is main entry point of mormot.core.collections unit
// - you should never call TIList<T>.Create nor
// TIKeyValue<TKey, TValue>.Create constructors, but the static
// Collections.NewList<T> and Collections.NewKeyValue<TKey, TValue> methods
// - NewList/NewKeyValue will raise an exception if the types are too complex
// (e.g. with a record): redirecting to NewPlainList/NewPlainKeyValue would
// generate the whole class anyway (even if not used), so would bloat the exe
Collections = class
protected
{$ifdef SPECIALIZE_ENABLED}
{$ifdef FPC}
const
tkLString = tkAString; // circumvent FPC RTTI incompatibility
{$endif FPC}
// dedicated factories for most common TIList<T> types
class procedure NewOrdinal(aSize: integer; aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
class procedure NewFloat(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
class procedure NewLString(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
{$ifdef SPECIALIZE_WSTRING}
class procedure NewWString(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
{$endif SPECIALIZE_WSTRING}
class procedure NewUString(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
class procedure NewInterface(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
class procedure NewVariant(aOptions: TListOptions;
aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo; var result); static;
// dedicated factories for most common TIKeyValue<> types
class procedure NewOrdinalOrdinal(const aContext: TNewKeyValueContext;
aSizeKey, aSizeValue: integer; var result); static;
class procedure NewOrdinalFloat(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
class procedure NewOrdinalLString(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
class procedure NewOrdinalUString(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
class procedure NewOrdinalInterface(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
class procedure NewOrdinalVariant(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
{$ifdef SPECIALIZE_WSTRING}
class procedure NewOrdinalWString(const aContext: TNewKeyValueContext;
aSizeKey: integer; var result); static;
class procedure NewWStringOrdinal(const aContext: TNewKeyValueContext;
aSizeValue: integer; var result); static;
class procedure NewWStringManaged(const aContext: TNewKeyValueContext;
aValue: TTypeKind; var result); static;
{$endif SPECIALIZE_WSTRING}
class procedure NewLStringOrdinal(const aContext: TNewKeyValueContext;
aSizeValue: integer; var result); static;
class procedure NewLStringManaged(const aContext: TNewKeyValueContext;
aValue: TTypeKind; var result); static;
class procedure NewUStringOrdinal(const aContext: TNewKeyValueContext;
aSizeValue: integer; var result); static;
class procedure NewUStringManaged(const aContext: TNewKeyValueContext;
aValue: TTypeKind; var result); static;
class procedure NewInterfaceOrdinal(const aContext: TNewKeyValueContext;
aSizeValue: integer; var result); static;
class procedure NewInterfaceManaged(const aContext: TNewKeyValueContext;
aValue: TTypeKind; var result); static;
class procedure NewVariantOrdinal(const aContext: TNewKeyValueContext;
aSizeValue: integer; var result); static;
class procedure NewVariantManaged(const aContext: TNewKeyValueContext;
aValue: TTypeKind; var result); static;
// the RTTI is too complex -> should call NewPlain*<>() methods instead
class function RaiseUseNewPlainList(aItemTypeInfo: PRttiInfo): pointer; static;
class function RaiseUseNewPlainKeyValue(
const aContext: TNewKeyValueContext): pointer; static;
{$endif SPECIALIZE_ENABLED}
public
/// generate a new IList<T> instance for most simple types
// - use this factory method instead of plain TIList<T>.Create
// so that the types will be specialized and compiled once in this unit
// - by default, string values would be searched following exact case,
// unless the loCaseInsensitive option is set
// - will associate a TArray<T> storage, unless aDynArrayTypeInfo is set
// - raise EIKeyValue if T type is too complex (e.g. record, array or
// hash): use NewPlainList<T>() instead
class function NewList<T>(aOptions: TListOptions = [];
aDynArrayTypeInfo: PRttiInfo = nil): IList<T>; static;
/// generate a new IList<T> instance with exact TIList<T>
// - to be called for complex types (e.g. record, array or hash) when
// NewList<T> fails with "too complex" error and triggers EIList
// - by default, string values would be searched following exact case,
// unless the loCaseInsensitive option is set
// - will associate a TArray<T> storage, unless aDynArrayTypeInfo is set
// - if aSortAs is ptNone, will guess the comparison/sort function from RTTI
// but you can force one e.g. to sort/compare/hash using a record first field
class function NewPlainList<T>(aOptions: TListOptions = [];
aDynArrayTypeInfo: PRttiInfo = nil; aSortAs: TRttiParserType = ptNone): IList<T>;
static; {$ifdef FPC} inline; {$endif}
/// generate a new IKeyValue<TKey, TValue> instance
// - use this factory method instead of NewPlainKeyValue<TKey, TValue>
// so that simple types will be specialized and compiled once in this unit
// - you can set an optional timeout period, in seconds - you should call
// DeleteDeprecated periodically to search and delete for deprecated items
// - you can provide specific TypeInfo() if TArray<TKey/TValue> is not enough
// - by default, this instance won't be thread-safe unless the kvoThreadSafe
// option is forced, so that process is protected with a TSynLocker mutex
// - by default, string keys would be searched following exact case, unless
// the kvoKeyCaseInsensitive option is set
// - raise EIKeyValue if T type is too complex (e.g. record, array or
// hash): use NewPlainKeyValue<TKey, TValue>() instead
class function NewKeyValue<TKey, TValue>(aOptions: TKeyValueOptions = [];
aTimeoutSeconds: cardinal = 0; aCompressAlgo: TAlgoCompress = nil;
aKeyDynArrayTypeInfo: PRttiInfo = nil; aValueDynArrayTypeInfo: PRttiInfo = nil;
aHasher: THasher = nil; aKeySpecific: TRttiParserType = ptNone): IKeyValue<TKey, TValue>;
static; {$ifdef FPC} inline; {$endif}
/// generate a new IKeyValue<TKey, TValue> instance with exact
// TIKeyValue<TKey, TValue>
// - to be called for complex types (e.g. record, array or hash) when
// NewKeyValue<TKey, TValue> fails and triggers EIKeyValue
// - won't be able to reuse specialized IKeyValue<> between types and type
// definitions, so resulting executable size may be slightly bigger
class function NewPlainKeyValue<TKey, TValue>(aOptions: TKeyValueOptions = [];
aTimeoutSeconds: cardinal = 0; aCompressAlgo: TAlgoCompress = nil;
aKeyDynArrayTypeInfo: PRttiInfo = nil; aValueDynArrayTypeInfo: PRttiInfo = nil;
aHasher: THasher = nil; aKeySpecific: TRttiParserType = ptNone): IKeyValue<TKey, TValue>;
static; {$ifdef FPC} inline; {$endif}
end;
implementation
{ ************** JSON-aware IList<> List Storage }
{ TIListEnumerator }
function TIListEnumerator<T>.MoveNext: boolean;
var
c: PtrUInt; // to enhance code generation
begin
c := fState.Current;
inc(PT(c));
fState.Current := c;
result := c < fState.After; // false if fCurrent=fAfter=0
end;
function TIListEnumerator<T>.GetEnumerator: TIListEnumerator<T>;
begin
result := self; // just a copy of 2 PtrInt
end;
function TIListEnumerator<T>.DoGetCurrent: T;
begin
result := {%H-}PT(fState.Current)^;
// faster than fDynArray^.ItemCopy() - at least for simple types
end;
{ TIListParent }
constructor TIListParent.Create(aDynArrayTypeInfo, aItemTypeInfo: PRttiInfo;
aOptions: TListOptions; aSortAs: TRttiParserType);
begin
fOptions := aOptions;
if (aDynArrayTypeInfo = nil) or
(aDynArrayTypeInfo^.Kind <> rkDynArray) then
EIList.RaiseUtf8('%.Create: % should be a dynamic array of T',
[self, aDynArrayTypeInfo^.Name^]);
CreateRtti(Rtti.RegisterType(aDynArrayTypeInfo), aItemTypeInfo, aOptions, aSortAs);
end;
constructor TIListParent.CreateRtti(aDynArray: TRttiCustom;
aItemTypeInfo: PRttiInfo; aOptions: TListOptions; aSortAs: TRttiParserType);
begin
fDynArray.InitRtti(aDynArray, fValue, @fCount);
aSortAs := fDynArray.SetParserType(aSortAs, // aSortAs=ptNone->RTTI
loCaseInsensitive in fOptions);
if (fDynArray.Info.ArrayRtti = nil) or
(fDynArray.Info.ArrayRtti.Kind <> aItemTypeInfo^.Kind) then
EIList.RaiseUtf8('%.Create<%> (%) does not match % (%)',
[self, aItemTypeInfo^.RawName, ToText(aItemTypeInfo^.Kind)^,
aDynArray.Info^.RawName, ToText(fDynArray.Info.ArrayRtti.Kind)^]);
if loNoFinalize in fOptions then
fDynArray.NoFinalize := true; // force weak references
if loCreateUniqueIndex in fOptions then
begin
fHasher := AllocMem(SizeOf(fHasher^));
fHasher^.InitSpecific(@fDynArray, aSortAs, loCaseInsensitive in fOptions, nil);
end;
end;
destructor TIListParent.Destroy;
begin
inherited Destroy;
fDynArray.Clear;
if fHasher <> nil then
Dispose(fHasher);
end;
function TIListParent.Delete(ndx: PtrInt): boolean;
begin
if fHasher <> nil then
EIList.RaiseUtf8('%.Delete(%) is not allowed with ' +
'loCreateUniqueIndex: use Remove()', [self, ndx]);
result := fDynArray.Delete(ndx);
end;
function TIListParent.DoPop(var dest; opt: TListPop): boolean;
begin
if fHasher <> nil then
EIList.RaiseUtf8(
'%.Pop() is not compatible with loCreateUniqueIndex', [self]);
if popFromHead in opt then
if popPeek in opt then
result := fDynArray.PeekHead(dest)
else
result := fDynArray.PopHead(dest)
else if popPeek in opt then
result := fDynArray.Peek(dest)
else
result := fDynArray.Pop(dest);
end;
function TIListParent.DoRemove(const value): boolean;
var
ndx: PtrInt;
h: PDynArrayHasher;
begin
h := fHasher;
if h <> nil then
ndx := h^.FindBeforeDelete(@value)
else
ndx := fDynArray.Find(value);
result := (ndx >= 0) and
fDynArray.Delete(ndx);
end;
function TIListParent.DoAdd(const value; var added: boolean): PtrInt;
var
n: PtrInt;
h: PDynArrayHasher;
begin
h := fHasher;
if h <> nil then
begin
result := h^.FindBeforeAdd(@value, added, h^.HashOne(@value));
if not added then
exit; // already existing -> just return previous value index
end
else
added := true;
n := fCount;
if n = length(TByteDynArray(fValue)) then // all dyn array share same length()
fDynArray.Capacity := NextGrow(n);
inc(fCount);
result := n;
end;
function TIListParent.DoAddSorted(const value; wasadded: PBoolean): integer;
begin
if fHasher <> nil then
EIList.RaiseUtf8('%.AddSorted() is not allowed with ' +
'loCreateUniqueIndex: use Add()', [self]);
result := fDynArray.FastLocateOrAddSorted(value, wasadded);
end;
procedure TIListParent.DoAddFrom(Another: PDynArray; Offset, Limit: PtrInt);
var
max, i: PtrInt;
p: PByte;
added: boolean;
begin
if fHasher = nil then
// efficient adding of whole bunch
fDynArray.AddDynArray(Another, Offset, Limit)
else