在通用集合中记录相等性

8
假设您有一个具有重载等号运算符的记录。
TSomeRecord = record
  Value : String;
  class operator Equal(Left, Right : TSomeRecord) : Boolean;
end;

(实现比较字符串值)。如果将两条记录添加到列表中,这些记录根据重载运算符相等,我希望Contains方法在两种情况下都返回true。但事实上,泛型列表似乎只比较记录的内存内容,而不是应用重载的等号运算符。

var
  List : TList <TSomeRecord>;
  Record1,
  Record2 : TSomeRecord;

begin
Record1.Value := 'ABC';
Record2.Value := 'ABC';
List.Add(Record1);

Assert(List.Contains(Record1));
Assert(List.Contains(Record2));    //  <--- this is not true
end;

这是否是预期的行为?有什么解释吗?

等于运算符的实现是什么样子的?可能与https://dev59.com/i1_Va4cB1Zd3GeqPR0dU有关。 - Jack G.
通常情况下,记录不支持 = 运算符,而且在代码中无法检测到任何特定类型是否支持它,因此默认实现必须对所有没有 a priori 知识的类型使用简单的内存比较。 - Rob Kennedy
感谢@RobKennedy,那么在泛型类型上有一个等式约束将确保存在等式运算符,这将是很好的。 - jpfollenius
@Smasher,有很多限制条件都是很好的。例如,如果我们可以约束算术运算符,那么我们就可以编写通用数学算法。 - David Heffernan
2
是的,那只是一个白日梦想而已 :) - jpfollenius
2个回答

8
假设您在创建TList.Create时没有指定比较器,那么您将获得TComparer<TSomeRecord>.Default作为您的比较器。这是一个使用CompareMem执行简单二进制比较的比较器。
对于一个由值类型组成、没有填充的记录而言,这是可以接受的。但是,如果记录中有填充或者更多属性,您需要在实例化列表时提供自己的比较函数。
如果您想查看详细信息,请参考Generics.Defaults中实现的记录默认比较器。对于更大的记录,等式比较器是这个函数:
function Equals_Binary(Inst: PSimpleInstance; const Left, Right): Boolean;
begin
  Result := CompareMem(@Left, @Right, Inst^.Size);
end;

对于较小的记录,有一种优化方法,您的比较器将是4字节比较器。其看起来像这样:

function Equals_I4(Inst: Pointer; const Left, Right: Integer): Boolean;
begin
  Result := Left = Right;
end;

这有点奇怪,但它将您记录的4个字节解释为4个字节的整数,并执行整数相等比较。换句话说,与CompareMem相同,但更有效。

您想要使用的比较器可能如下所示:

TComparer<TSomeRecord>.Construct(
  function const Left, Right: TSomeRecord): Integer
  begin
    Result := CompareStr(Left.Value, Right.Value);
  end;
)

如果您想要不区分大小写等等,则可以使用 CompareText。我使用了有序比较函数,因为这是 TList<T> 所需的。默认记录比较是相等比较的事实告诉您,如果尝试对记录列表进行排序而没有指定自己的比较器,将会出现意外结果。由于默认比较器使用相等比较,所以使用此类比较器并不完全不合理:
TComparer<TSomeRecord>.Construct(
  function const Left, Right: TSomeRecord): Integer
  begin
    Result := ord(not (Left = Right));
  end;
)

对于像 IndexOfContains 这样的无序操作来说,这样做是可以的,但显然对于排序、二分查找等操作没有任何用处。


嗨,David,感谢您的解释。这是否意味着通常不可能编写一个仅使用等式运算符的通用类型,以便使用类型内置的相等性而无需指定自定义比较器? - jpfollenius
@Smasher 没错。泛型框架只是不会寻找你的等式运算符。无论如何,正如我在我的更新中解释的那样,列表类需要的不仅仅是一个相等比较。它希望能够对元素进行排序。这样它就可以进行排序了。 - David Heffernan
那么,在不考虑顺序的情况下,没有办法正确地定义相等性 - 这在我的情况下是没有意义的? - jpfollenius
你可以更改匿名比较函数为 Result := ord(Left=Right),这样对于 IndexOfContains 等方法来说就有了正确的含义。但如果你尝试排序,结果会很混乱。由于 Generics.Default 中的默认比较器使用 CompareMem,所以你所犯的错误并不比它更严重。因此,如果你不使用依赖于顺序的操作,可以通过比较器基于你的相等运算符进行比较。 - David Heffernan
Result := ord(Left=Right); 这不是笔误吗?如果 Left=Right 那么 Result 将等于 ord(True),也就是 1,而不是 0Result := ord(not (Left=Right)); 可能是你想要的。 - Andreas Rejbrand

3
为了获得预期的行为,您需要使用比较器创建列表。
在这种情况下,您应该使用:
List := TList<TSomeRecord>.Create( TComparer<TSomeRecord>.Construct(
  function ( const L, R : TSomeRecord ) : Integer
  begin
    Result := CompareStr( L.Value, R.Value );
  end ) );

是的,我觉得很奇怪,因为我必须两次指定相等逻辑。请注意,只执行“Result:= L = R”操作的比较器同样有效。这就是为什么我感到惊讶的原因——泛型集合不能在没有比较器的情况下处理它。 - jpfollenius
啊...对了。那我得考虑排序(其实我不需要排序)来获得正确的相等比较吗?感觉不太对劲啊... - jpfollenius
1
@Smasher:你需要考虑排序,因为TList<T>在排序和检查相等性时都使用了TComparer - Mason Wheeler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接