Core Types
BCL provides several generic core types that are used throughout the TMS BIZ suite and are available for use in your own applications. These types complement the Delphi RTL with patterns for nullable values, assignment tracking, multicast events, and ordered dictionaries.
Nullable<T>
Nullable is a generic record that wraps a value of type Tand adds the ability to represent a null (missing) state. It is declared in the Bcl.Types.Nullable unit.
Value types in Delphi always have a value — an Integer is always some number, a TDateTime is always some date. Nullable solves this by wrapping the value and tracking whether it has been set:
procedure NullableBasicUsage;
var
Age: Nullable<Integer>;
V: Integer;
B: Boolean;
begin
B := Age.IsNull; // True - no value assigned yet
Age := 42;
V := Age.Value; // 42
B := Age.HasValue; // True
Age := SNull; // clear the value
B := Age.IsNull; // True again
end;
Creating and Clearing
You can create a nullable by assigning a value directly (implicit conversion), by calling the constructor, or by using the Nullable.Empty class property. To clear a nullable back to the null state, assign the global SNull variable:
procedure NullableCreateAndClear;
var
Name: Nullable<string>;
begin
Name := 'Alice'; // implicit conversion from T
Name := Nullable<string>.Create('Bob'); // explicit constructor
Name := SNull; // set to null
Name := Nullable<string>.Empty; // also null
end;
Reading Values Safely
Accessing Nullable.Value on a null instance raises ENullConvertException. Use Nullable.HasValue or Nullable.IsNull to check first, or use Nullable.ValueOrDefault to get a fallback:
procedure NullableReadSafely;
var
Score: Nullable<Double>;
V: Double;
begin
Score := SNull;
V := Score.ValueOrDefault; // 0.0 (default for Double)
if not Score.IsNull then
V := Score.Value;
end;
Comparisons
Nullable supports equality and relational operators. Two null values are considered equal. Ordering comparisons (>, >=, <, <=) on null operands raise ENullValueException:
procedure NullableComparisons;
var
A, B: Nullable<Integer>;
Equal, LessThan: Boolean;
begin
A := SNull;
B := SNull;
Equal := (A = B); // True - both null
A := 10;
B := 20;
LessThan := (A < B); // True
end;
Type Aliases
For common types, BCL provides predefined aliases so you do not need to write the generic syntax:
| Alias | Expands to |
|---|---|
NullableString |
Nullable<string> |
NullableInteger |
Nullable<Integer> |
NullableBoolean |
Nullable<Boolean> |
NullableDouble |
Nullable<Double> |
NullableDateTime |
Nullable<TDateTime> |
NullableDate |
Nullable<TDate> |
NullableTime |
Nullable<TTime> |
NullableInt64 |
Nullable<Int64> |
NullableCurrency |
Nullable<Currency> |
NullableGuid |
Nullable<TGUID> |
Assignable<T>
Assignable is a generic record declared in Bcl.Types.Assignable that wraps a value and tracks whether it has been explicitly assigned. It is similar to Nullable but is designed for a different purpose: distinguishing between "not provided" and "explicitly set" rather than "null" and "not null".
A typical use case is partial updates in ORM operations or REST API PATCH requests, where you need to know which fields the client actually sent versus which were simply absent from the request.
procedure AssignableBasicUsage;
var
Name: Assignable<string>;
B: Boolean;
S: string;
begin
B := Name.IsAssigned; // False - nothing assigned yet
Name := 'Alice';
B := Name.IsAssigned; // True
S := Name.Value; // 'Alice'
end;
Reading Values
Accessing Assignable.Value when the instance is unassigned raises EUnassignedConvertException. Use Assignable.IsAssigned to check first, or use Assignable.ValueOrDefault for a safe read that returns Default(T) when unassigned:
procedure AssignableReadValues;
var
Count: Assignable<Integer>;
V: Integer;
begin
Count := Assignable<Integer>.Empty; // unassigned
V := Count.ValueOrDefault; // 0
if Count.IsAssigned then
V := Count.Value;
end;
Type Aliases
Predefined aliases are available for common types:
| Alias | Expands to |
|---|---|
AssignableString |
Assignable<string> |
AssignableInteger |
Assignable<Integer> |
AssignableBoolean |
Assignable<Boolean> |
AssignableDouble |
Assignable<Double> |
AssignableDateTime |
Assignable<TDateTime> |
AssignableDate |
Assignable<TDate> |
AssignableTime |
Assignable<TTime> |
AssignableInt64 |
Assignable<Int64> |
AssignableCurrency |
Assignable<Currency> |
AssignableGuid |
Assignable<TGUID> |
TEvent<T>
TEvent is a generic multicast event record declared in Bcl.Types that implements the observer pattern. It allows multiple listeners to subscribe to an event and be notified when it fires.
Unlike Delphi's built-in event properties, which can hold only a single handler, TEvent supports multiple subscribers:
procedure EventBasicUsage;
var
OnMessage: TEvent<TNotifyProc>;
Listener: TNotifyProc;
begin
OnMessage.Subscribe(
procedure(const Msg: string)
begin
WriteLn('Listener 1: ' + Msg);
end
);
OnMessage.Subscribe(
procedure(const Msg: string)
begin
WriteLn('Listener 2: ' + Msg);
end
);
// Fire the event by iterating listeners
for Listener in OnMessage.Listeners do
Listener('Hello');
end;
Subscribing and Unsubscribing
Use TEvent.Subscribe to add a listener and TEvent.Unsubscribe to remove one. If the listener is not found, Unsubscribe is silently ignored. If the same listener was added multiple times, only the first occurrence is removed.
Checking for Listeners
Use TEvent.HasListeners to check if any listeners are subscribed before raising the event. The TEvent.Listeners property returns the current array of subscribers for iteration.
TOrderedDictionary<K, V>
TOrderedDictionary is a generic dictionary declared in Bcl.Collections that preserves the insertion order of key-value pairs. UnlikeTDictionary, it maintains items in the order they were added and supports indexed access by position.
procedure OrderedDictBasicUsage;
var
Dict: TOrderedDictionary<string, Integer>;
Pair: TPair<string, Integer>;
S: string;
V: Integer;
begin
Dict := TOrderedDictionary<string, Integer>.Create;
try
Dict.Add('banana', 2);
Dict.Add('apple', 1);
Dict.Add('cherry', 3);
S := Dict.Keys[0]; // 'banana' - insertion order preserved
V := Dict.Values[1]; // 1
V := Dict['apple']; // 1 - access by key
for Pair in Dict do
WriteLn(Pair.Key, ': ', Pair.Value);
// Output: banana: 2, apple: 1, cherry: 3
finally
Dict.Free;
end;
end;
Key Operations
The dictionary provides the standard dictionary operations such as TOrderedDictionary.Add, TOrderedDictionary.Remove, TOrderedDictionary.ContainsKey, and TOrderedDictionary.TryGetValue. It also supports indexed access through the TOrderedDictionary.Keysconst-index and TOrderedDictionary.Valuesconst-index properties, and removing by position with TOrderedDictionary.Delete.
Sorting
Entries can be sorted by key using the default comparer, or with a custom comparer:
procedure OrderedDictSort;
var
Dict: TOrderedDictionary<string, Integer>;
begin
Dict := TOrderedDictionary<string, Integer>.Create;
try
Dict.Add('banana', 2);
Dict.Add('apple', 1);
Dict.Add('cherry', 3);
Dict.Sort; // sort by key using default comparer
finally
Dict.Free;
end;
end;
Object Ownership
TOrderedObjectDictionary extends the ordered dictionary with automatic lifetime management of keys and/or values. By default it owns its values, freeing them when they are removed or when the dictionary is destroyed:
procedure OrderedObjectDictUsage;
var
Dict: TOrderedObjectDictionary<string, TObject>;
begin
Dict := TOrderedObjectDictionary<string, TObject>.Create;
try
Dict.Add('item1', TObject.Create);
Dict.Add('item2', TObject.Create);
// Objects are automatically freed when removed or when Dict is destroyed
finally
Dict.Free;
end;
end;
You can control ownership through the Ownerships parameter in the constructor, using the standard TDictionaryOwnerships flags (doOwnsKeys, doOwnsValues).
Note
TOrderedDictionary uses a linear scan for key lookups, so it is best suited for smaller collections where insertion order matters. For large collections where lookup performance is critical, use TDictionary from the Delphi RTL.