Back to 2008, Allen Bauer wrote A “Nullable” Post. Although this interesting stuff has not been introduced in Delphi RTL yet, it has been in the Spring4D family for a while.
So, what are nullable types and when we should use them? Let’s get the answer from a simple example:
// uses Spring; TPerson = class private fBirthDate: Nullable<TDateTime>; public property BirthDate: Nullable<TDateTime> read fBirthDate write fBirthDate; property Age: Nullable<Integer> read GetAge; // Calculated Property end;
The BirthDate of a person is optional (not required) therefore we may not be able to calculate his(her) age. Nullable<T> type perfectly fit this kind of cases. Otherwise, we have to use some special values (magic number) to represent this nullabe state which is really a bad smell. It also makes it easier to map database data types to Delphi as most data type may be null.
OK, now let’s put the mini Nullable<T> on table:
Nullable<T> = packed record private fValue: T; fHasValue: string; public constructor Create(const value: T); overload; constructor Create(const value: Variant); overload; function GetValueOrDefault: T; overload; function GetValueOrDefault(const defaultValue: T): T; overload; function Equals(const other: Nullable<T>): Boolean; property HasValue: Boolean read GetHasValue; property Value: T read GetValue; end;
Nullable<T> is a record type wrapper which allows you use it without any initialization. T is the underlying type of the nullable type. T can be any value type, e.g. Integer, TDateTime, Boolean, String or even another record type. The following example demonstrates how to use a nullable integer:
procedure TestNullableUsage; var age: Nullable<Integer>; value: Integer; begin Assert(not age.HasValue); // An EInvalidOperationException exception will be raised // when accessing the Value property of a null variable try age.Value; except on e: Exception do Assert(e is EInvalidOperationException); end; Assert(age.GetValueOrDefault = Default(Integer)); Assert(age.GetValueOrDefault(30) = 30); // Implicit Conversions between Nullable<T> and T age := 30; value := age; // Equality Operators Assert(age = value); Assert(age <> 20); // Assign nil to the nullable value to make it null age := nil; Assert(not age.HasValue); end;
1. Nullable<T> is designed as immutable.
Nullable<T> itself doesn’t provide any member to change its internal state or value. When you need a writable property, you must declare a setter, just like TPerson.BirthDate, otherwise, the property is read-only.
2. Compatible with Variant
When you pass a Variant value, whose type is either varNull or varEmpty, to a nullable variable, the latter will be marked as null.
3. Equality Operator
function Nullable<T>.Equals(const other: Nullable<T>): Boolean; begin if HasValue and other.HasValue then Result := TEqualityComparer<T>.Default.Equals(Value, other.Value) else Result := HasValue = other.HasValue; end;
4. Tricky implementation of HasValue
Since we cannot define the default constructor for record types, how to ensure that the HasValue proerty always returns false when the value has not been initialized? It is really not a good idea to use a Boolean flag in a record type.
procedure TestNullableHasValue; var age: Nullable<Integer>; // Lives in stack begin Assert(not age.HasValue); end;
As Hallvard Vassbotn had reminded us in the comments of the post, the compiler will automatically initialize a record once when it contains a managed type field (string, interface, etc.). We use the string flag in Spring4D.
constructor Nullable<T>.Create(const value: T); begin fValue := value; fHasValue := CHasValueFlag; // '@' end; function Nullable<T>.GetHasValue: Boolean; begin Result := fHasValue <> ''; end;
Note: This post is a part of Spring4D in Action Series.
Copyright 2012 (c) Baoquan Zuo