Problem assigning null to simulated generic nullable data types

347 Views Asked by At

I am trying to create a nullable data type in Delphi:

type
  TNullable<T> = record
  public
    Value: T;
    IsNull: Boolean;
    class operator Implicit(const AValue: T): TNullable<T>;
    class operator Implicit(const AValue: TNullable<T>): T;
    class operator Implicit(const AValue: Variant): TNullable<T>;
    class operator Explicit(const AValue: T): TNullable<T>;
  end;

So far so good, but what to assign as a null literal so that the nullable data type remains of its basic type? For example:

var
  v: TNullable<Integer>;
begin
  //What type is this "null"? A Variant null?
  //How TNullable<Integer> could remain of Integer after the assignment?
  v := null;

  //How to compare this "null"? Compare to what type?
  if v = null then begin
  end;
end;

Let us assume that null is the variant null:

class operator TNullable<T>.Implicit(const AValue: Variant): TNullable<T>;
begin
  if VarIsNull(AValue) or VarIsClear(AValue) then begin
    Result.IsNull := True;
    Result.Value := Default(T);
  end
  else begin
    Result.IsNull := False;
    Result.Value := AValue; //Version 1: Incompatible types: 'T' and 'Variant'!!!
    Result.Value := T(AValue); //Version 2: Invalid typecast!!!
    //Should I write a big "case" block here in order to handle each data type?!
  end;
end;

Do you have ideas?

1

There are 1 best solutions below

5
Remy Lebeau On BEST ANSWER

Null in this case is indeed a Variant, see System.Variants.Null. Using a Variant in this situation is not a good idea, in part because of the assignment troubles you are seeing with it.

A better option is to define a distinct type to represent your null values (similar to nullptr_t in C++11 and later), eg:

type
  TNullValue = record
  end;

  TNullable<T{: record}> = record
  public
    Value: T;
    HasValue: Boolean;

    class operator Implicit(const AValue: T): TNullable<T>;
    class operator Implicit(const AValue: TNullable<T>): T;
    class operator Implicit(const AValue: TNullValue): TNullable<T>;
    class operator Explicit(const AValue: T): TNullable<T>;

    // add these...
    class operator Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
    class operator NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
    ...
  end;

const
  NullValue: TNullValue;

...

class operator TNullable<T>.Implicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Implicit(const AValue: TNullable<T>): T;
begin
  if AValue.HasValue then
    Result := AValue.Value
  else
    Result := Default(T); // or raise an exception
end;

class operator TNullable<T>.Implicit(const AValue: TNullValue): TNullable<T>;
begin
  Result.Value := Default(T);
  Result.HasValue := False;
end;

class operator TNullable<T>.Explicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := not A.HasValue;
end;

class operator TNullable<T>.NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := A.HasValue;
end;
var
  v: TNullable<Integer>;
begin
  v := NullValue;

  if v = NullValue then begin
    ...
  end;

  if v <> NullValue then begin
    ...
  end;
end;