Nullable<T> does not support injecting a custom IEqualityComparer<T>

Issue #299 resolved
Jens Mertelmeyer created an issue

I would suggest changing the visibility of class var fComparer: IEqualityComparer<T>; of Nullable<T> from private to public (and perhaps rename it to Comparer).

Nullable<T> already seems well prepared for it:

class function Nullable<T>.EqualsComparer(const left, right: T): Boolean;
begin
  if not Assigned(fComparer) then
    fComparer := TEqualityComparer<T>.Default;
  Result := fComparer.Equals(left, right);
end;

Reason: I would like to provide my own comparer as this would allow me to continue using the Equals(..) method (or operator overload) of TNullable<T> for types of T that cannot be handled by the logic already implemented in Base/Spring.pas.

Example:

program Spring4D_Issue299;

{$APPTYPE CONSOLE}

uses System.Generics.Defaults, Spring;

type
    TStruct = record
        values: TBytes;
        class operator Equal(a, b: TStruct): Boolean;
        class function EqualityComparer(): IEqualityComparer<TStruct>; static;
    end;

class operator TStruct.Equal(a, b: TStruct): Boolean;
begin
    // left out for brevity
end;

class function TStruct.EqualityComparer: IEqualityComparer<TStruct>;
begin
    // left out for brevity
end;

procedure p();
var
    a, b: TStruct;
    x, y: Nullable<TStruct>;
begin
    SetLength(a.values, 1);
    a.values[0] := 99;
    SetLength(b.values, 1);
    b.values[0] := 99;

    Assert(a = b);

    x := a;
    y := b;
    // If I could do this:
    // Nullable<TStruct>.Comparer := TStruct.EqualityComparer()
    // Then this would not fail:
    Assert(x = y);
end;

begin
    p();
end.

Many thanks for your time.

Comments (10)

  1. Stefan Glienke repo owner

    Wouldn't picking an equalitycomparer that knows how to handle the equals operator be enough? Or even better try to handle that directly in EqualsInternal for tkRecord

    Sure, that leaves out all records that don't have equals operator but one could argue that then also a nullable of such type should not be comparable.

  2. Jens Mertelmeyer reporter

    I am probably missing something - I don't see where I could pass my equality comparer. For I both example variables x, y: Nullable<TStruct>, I must not use x.Equals(y) or x = y and will instead have to go with a rather lengthy if (x.HasValue and y.HasValue) and (x.Value = y.Value) then …

    PS: I have no idea how EqualsInternal could be extended to detect whether a record has an operator overload for equality. Is that even possible?

    (I probably completely misunderstood)

  3. Stefan Glienke repo owner

    Yes, RTTI contains information of operator overloads, they are just static methods on the type so they can be put into a function(const left, right: T): Boolean pointer and be called. No overhead. The Nullable type can read them in its class constructor - I just need to check since which version that is available since I know that XE did not have RTTI for class operators.

    This is how the code looks like to get an equals operator if available. I would use that to detect if there is any and use that in the EqualsInternal call in case of tkRecord.

    function GetEqualsOperator(const typeInfo: PTypeInfo): Pointer;
    var
      parameters: TArray<TRttiParameter>;
      method: TRttiMethod;
    begin
      for method in TType.GetType(typeInfo).GetMethods('&op_Equality') do
      begin
        if method.MethodKind <> mkOperatorOverload then
          Continue;
        parameters := method.GetParameters;
        if (Length(parameters) = 2)
          and (parameters[0].ParamType.Handle = typeInfo)
          and (parameters[1].ParamType.Handle = typeInfo) then
          Exit(method.CodeAddress);
      end;
      Result := nil;
    end;
    

    Need to do some testing on various Delphi versions to figure out the best solution - but I am currently in the middle of some rather big refactoring so will take a few days - I will probably add it to 1.2.2

  4. Jens Mertelmeyer reporter

    This sounds extremely exciting, I never knew it was possible. Just determining it once in the class constructor also sounds pretty clever.

    There is one minor thing: I think you are not correct about CodeAddress pointing to a function(const left, right: T): Boolean, I suppose the parameters are not const which is a shame because it would have been matching with TEqualityComparison<T>. You can check parameters[0] and [1] for their Flags which are empty and do not include TParamFlag.pfConst. For value-types like records, this is extremely important.

  5. Stefan Glienke repo owner

    You can declare the parameters as you want in some operators, however I usually make them const.

  6. Jens Mertelmeyer reporter

    That is entirely new to me. I was sticking to the official documentation which clearly did not have the parameters const but as it turns out, you really can make them const. Neat.

    I was just pointing out that I would strongly recommend checking for the const flags as calling the &op_Equality method from my code above by passing the records by reference would be disastrous 😎

  7. Stefan Glienke repo owner

    It would not because the parameter passing is exactly the same. See https://www.guidogybels.eu/asmtable3.html

    Only difference which is not contained in that table because it is rather new is when you add the [ref] attribute to a const parameter which forces to pass as pointer even if it would fit into a register.

  8. Log in to comment