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;
Keynotes:
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.
http://www.spring4d.org
Copyright 2012 (c) Baoquan Zuo
Why didn’t you use the interface style that Allen Bauer demonstrated? It should be faster and take up less memory than the string style.
An interface alone doesn’t have an advantage over string, only if you implement it using that “fake instance” as Allen described it will be faster – but also a bit more complicated to understand – so maybe they didn’t want to scare all Spring users ;-)
Olaf
Yes, I just think the string approach is very simple and efficient.