【问题标题】:Record equality in generic collections在泛型集合中记录相等性
【发布时间】:2013-05-31 00:09:28
【问题描述】:

假设您有一个带有重载相等运算符的记录

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;

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

【问题讨论】:

  • Equal 运算符 impl 是什么样子的?可能与stackoverflow.com/questions/8862807/list-and-contains-method有关
  • 一般情况下,记录不支持=操作符,并且无法在代码中检测到是否有特定类型支持它,因此默认实现必须对它不支持的所有类型使用简单的内存比较'没有先验知识。
  • 感谢@RobKennedy,那么最好对泛型类型进行等式约束,以确保等式运算符的存在。
  • @Smasher 有很多约束条件会很好。例如,如果我们可以约束算术运算符,那么我们可以编写通用数学算法
  • 是的,那只是一个白日梦:)

标签: delphi generics operator-overloading delphi-xe2 record


【解决方案1】:

假设您没有在构造函数中指定TList.Create 的比较器,您将获得TComparer&lt;TSomeRecord&gt;.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&lt;T&gt; 想要的。

默认记录比较是相等比较这一事实告诉您,在不指定自己的比较器的情况下尝试对记录列表进行排序会产生意想不到的结果。

鉴于默认比较器使用相等比较告诉您使用这样的比较器并非完全不合理:

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

这对于像IndexOfContains 这样的无序操作会很好,但对于排序、二分搜索等显然没有用处。

【讨论】:

  • 嗨,大卫,感谢您的解释。这是否意味着通常不可能编写仅使用相等运算符的泛型类型,以便在不指定自定义比较器的情况下使用类型中内置的相等?
  • @Smasher 没错。泛型框架不会寻找您的相等运算符。无论如何,正如我在更新中解释的那样,列表类需要的不仅仅是相等比较。它希望能够对元素进行排序。这样它就可以排序了。
  • 所以如果不考虑排序就无法正确定义相等 - 这在我的场景中没有意义?
  • 您可以将匿名比较函数更改为Result := ord(Left=Right),这对于IndexOfContains 等具有正确的含义。但是如果您尝试排序,则会导致混乱。由于Generics.Default 中的默认比较器使用CompareMem,因此您不会犯下比这更糟糕的罪行。因此,如果您不使用依赖于顺序的操作,请随时通过比较器将其基于您的相等运算符。
  • Result := ord(Left=Right); 不是错字吗?如果Left=RightResult 将等于ord(True),即1,而不是0Result := ord(not (Left=Right)); 可能就是你想要的。
【解决方案2】:

要获得预期的行为,您必须使用比较器创建列表。

在这种情况下你应该使用

List := TList<TSomeRecord>.Create( TComparer<TSomeRecord>.Construct(
  function ( const L, R : TSomeRecord ) : Integer
  begin
    Result := CompareStr( L.Value, R.Value );
  end ) );

【讨论】:

  • 是的,我必须两次指定相等逻辑对我来说很奇怪。请注意,只执行Result := L = R 的比较器同样有效。这就是为什么我很惊讶通用集合在没有比较器的情况下无法处理它。
  • 啊..正确。所以我必须考虑排序(我不需要)以获得正确的相等比较?就是感觉不对……
  • @Smasher:您需要考虑排序,因为TList&lt;T&gt; 使用TComparer 进行排序以及检查相等性。
猜你喜欢
  • 2013-03-01
  • 2019-03-17
  • 1970-01-01
  • 2013-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-16
  • 1970-01-01
相关资源
最近更新 更多