Search code examples
delphinullabledelphi-xe5

Problem assigning null to simulated generic nullable data types


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?


Solution

  • 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;