Table of Contents

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 ENullConvert​Exception. Use Nullable.HasValue or Nullable.IsNull to check first, or use Nullable.​Value​OrDefault 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 EUnassignedConvert​Exception. Use Assignable.​IsAssigned to check first, or use Assignable.​Value​OrDefault 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.​Try​GetValue. 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

TOrderedObject​Dictionary 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.