Search code examples
delphi

How do I tell if a generic type is nil without using constraints in Delphi?


I want to check to see if a generic type is "valid" without using constraints.

In other words, I want to write the following code:

class function TMaybe<T>.FromValue(aValue: T): TMaybe<T>;
begin
  if T <> nil then
  begin
    Result := TMaybe<T>.Some(aValue);
  end else
  begin
    Result := TMaybe<T>.None;
  end;
end;

However, this doesn't compile with the error:

E2571 Type parameter 'T' doesn't have class or interface constraint

Obviously for a class like this, I'd like to be able to have any type be a TMaybe.

Is there a way to check to see if contraint-less type is "valid", that is, not null? (I don't care about empty strings, etc.)

Should I write a TypeIsEmpty<T>(aValue: T): Boolean that uses TypInfo to figure it out? I'd like to avoid that.


Solution

  • Try something like this:

    type
      PMethod = ^TMethod;
    
    class function TMaybe<T>.FromValue(aValue: T): TMaybe<T>;
    begin
      {
      the *undocumented* IsManagedType() intrinsic function returns True if T
      is an interface, string, or dynamic array, but it also returns True if T
      is a record containing such a field. Since a record can't be compared to
      nil, IsManagedType(T) is not useful here.
    
      Using the *undocumented* GetTypeKind() intrinsic function can be used
      instead to handle ONLY nil-able types...
      }
      // if IsManagedType(T) then...
      case GetTypeKind(T) of
        tkString, tkClass, tkLString, tkWString, tkInterface, tkDynArray, tkUString:
        begin
          if PPointer(@aValue)^ = nil then
            Exit(TMaybe<T>.None);
        end;
        tkMethod:
        begin
          if (PMethod(@aValue)^.Data = nil) or (PMethod(@aValue)^.Code = nil) then
            Exit(TMaybe<T>.None);
        end;
      end;
      Exit(TMaybe<T>.Some(aValue));
    end;
    

    Intrinsic functions like GetTypeKind() (and IsManagedType()) are evaluated at compile-time, and as such code branches that evaluate to False are optimized out of the final executable. However, as @DavidHeffernan mentions in comments, the compiler checks the code syntax before it instantiates the generic, hence the PPointer typecast to get around that.

    So, if you set T to a nil-able type, like a String, the compiler will be able to optimize the code down to this:

    class function TMaybe<String>.FromValue(aValue: String): TMaybe<String>;
    begin
      if Pointer(aValue) = nil then
        Exit(TMaybe<String>.None);
      Exit(TMaybe<String>.Some(aValue));
    end;
    

    And if you set T to a non nil-able type, like an Integer, the compiler will be able to optimize the code down to this instead:

    class function TMaybe<Integer>.FromValue(aValue: Integer): TMaybe<Integer>;
    begin
      Exit(TMaybe<Integer>.Some(aValue));
    end;