【问题标题】:Adding the same Object twice to a TObjectDictionary frees the object将相同的对象两次添加到 TObjectDictionary 会释放该对象
【发布时间】:2011-08-02 01:33:53
【问题描述】:

看这段代码:

dic:=TObjectDictionary<Integer, TObject>.Create([doOwnsValues]);
testObject:=TObject.Create;
dic.AddOrSetValue(1,testObject);
dic.AddOrSetValue(1,testObject);

代码

  1. 创建一个拥有所包含值的字典
  2. 添加值
  3. 再次添加相同的值,使用相同的键

令人惊讶的是,当你第二次添加对象时,它被释放了。

这是预期的行为吗?还是 Delphi 库中的错误?

文档只是说“如果对象被拥有,当条目从字典中删除时,键和/或值被释放”。所以释放一个我刚刚要求它添加的对象似乎有点奇怪!

有没有办法告诉 TObjectDictionary 不要这样做?目前,每次我添加一个值时,我都必须先检查该键值组合是否已经在字典中。

德尔福 2010

[编辑: 看完所有的cmets:

我的结论(对于它们的价值)]

  • 这似乎是预期的行为
  • 无法修改此行为
  • 不要将 TObjectDictionary(或任何其他类似的类)用于常见的“将这些对象添加到容器中。将它们留在那里。做一些事情。释放容器和您添加的所有对象”用法.如果您正在做更复杂的事情,最好自己管理对象。
  • 该行为的文档记录不充分,如果您想真正了解发生了什么,您应该阅读源代码

[/编辑]

【问题讨论】:

  • @everyone 对不起,我错过了所有的论点。我在另一个时区睡着了 ;-)
  • 对你的态度多么不体谅。 ;-)
  • 为什么不一次又一次地停止尝试添加相同的项目?
  • 我就是这么做的。 “每次我添加一个值时,我都必须先检查它是否已经在字典中”
  • "这就是我所做的。"每次我添加一个值时,我都必须先检查它是否已经在字典中":不确定我是否理解 - 你描述的行为只有在你覆盖具有相同值的相同键 - 因此您不必每次都检查值,如果您必须将相同的对象作为值添加到不同的键中,只需维护唯一的键。

标签: delphi delphi-2010


【解决方案1】:

TObjectDictionary&lt;TKey,TValue&gt; 实际上只是一个TDictionary&lt;TKey,TValue&gt;,它在KeyNotifyValueNotify 方法中有一些额外的代码:

procedure TObjectDictionary<TKey,TValue>.ValueNotify(const Value: TValue; 
  Action: TCollectionNotification);
begin
  inherited;
  if (Action = cnRemoved) and (doOwnsValues in FOwnerships) then
    PObject(@Value)^.Free;
end;

这是,IMO,一个相当简单的方法,但在 ValueNotify 方法中,无法分辨这是哪个键,所以它只是释放“旧”值(没有办法检查是否此值是为同一个键设置的)。

您可以编写自己的类(这不是微不足道的),派生自TDictionary&lt;K,V&gt;,或者干脆不使用doOwnsValues。您还可以编写一个简单的包装器,例如TValueOwningDictionary&lt;K,V&gt; 使用 TDictionary&lt;K,V&gt; 首当其冲,但自己处理所有权问题。我想我会做后者。

【讨论】:

  • FWIW 在 embarcadero.public.delphi.rtl 中提出了这个问题,大家一致认为这确实是一个错误。
【解决方案2】:

那是因为通过重用密钥,您正在替换对象,并且由于字典拥有该对象,因此它释放了旧对象。字典不比较值,只比较键,所以它不会检测到值(对象)是否相同。不是设计的错误(IOW 用户错误)。

再想一想-也许 dict 的设计者应该更加小心同时拥有doOwnsValuesAddOrSetValue()... 可以双向争论...我建议您将其归档在 QC 中,但我不会不要屏住呼吸 - 现在至少在两个版本中都是如此,因此不太可能改变。

【讨论】:

  • 但那是糟糕的编码。它应该像:if doOwnsvalues in WhateverItIsCalled and (oldObject &lt;&gt; newObject) then ...,即它应该防止释放相同的旧值。
  • 我不确定我是否同意。 dict 数据结构用于将键映射到值 - dict 只关心键,值是用户的责任。相反,我认为将值的所有权交给 dict 是糟糕的设计。
  • @ain:如果只是为了映射,应该没有doOwnsValues能力。 TDictionary 仅用于映射。如果一个 TObjectDictionary (或任何其他类)拥有它包含的项目,它应该检查它是否没有释放它正在分配的项目。
  • @awmross 就是这样。只是习惯于阅读源代码。这将带来许多附带好处,因为您会发现许多您不知道存在的功能。好吧,当我阅读资料时我会这样做!
  • @David 好的,但是如果没有明确的规范,没有人可以判断来源是否是已定义行为的正确实现。就是这样。
【解决方案3】:

这种行为是设计使然,设计合理。

如果该类负责不释放重复项,那么每次进行修改(添加和删除)时,它都必须遍历整个容器。迭代将检查任何重复值并进行相应检查。

将这种恶魔般的性能消耗强加给班级的所有用户将是灾难性的。如果您希望将重复项放在列表中,那么您必须制定适合您特定需求的定制生命周期管理策略。在这种情况下,期望通用容器支持您的特定使用模式是不合理的。


在此答案的 cmets 和许多其他答案中,有人建议更好的设计是在 AddOrSetValue 中测试所设置的值是否已分配给指定的键。如果是这样,那么AddOrSetValue 可以立即返回。

我认为任何人都清楚,全面检查重复项的成本太高,无法考虑。但是,我认为在 AddOrSetValue 中检查重复的 K 和 V 也是糟糕的设计,这是有充分的设计理由的。

请记住,TObjectDictionary&lt;K,V&gt; 派生自 TDictionary&lt;K,V&gt;。对于更一般的类,比较 V 的相等性可能是一个昂贵的操作,因为我们对 V 是什么没有限制,它是通用的。所以对于TDictionary&lt;K,V&gt;,我们不应该包含假定的AddOrSetValue 测试是出于性能原因。

可以说我们为TObjectDictionary&lt;K,V&gt; 做了一个特殊的例外。那当然是可能的。这需要对两个类之间的耦合进行一些重新设计,但这是非常可行的。但是现在您遇到了TDictionary&lt;K,V&gt;TObjectDictionary&lt;K,V&gt; 具有不同语义的情况。这是一个明显的缺点,必须与AddOrSetValue 测试的潜在好处进行权衡。

这些通用容器类非常基础,以至于设计决策必须考虑到广泛的用例、一致性考虑等。在我看来,孤立地考虑 TObjectDictionary&lt;K,V&gt;.AddOrSetValue 是不合理的。

【讨论】:

  • IMO 的设计非常不合理。正确的设计只需要检查对于它无论如何都必须搜索的给定键,新值是否与旧值不同。无论如何它都必须检索它,所以这是不费吹灰之力的,不需要任何额外的迭代。具有相同值的两个键也没有错,例如age=30, weight=110, numberofCats=30 等。但是释放您要添加的项目时出现问题。
  • @rudy 为什么只检查正在添加的密钥?如果对象已经由不同的键拥有怎么办? OP 的非常具体的用例使您的想法蒙上了阴影。设计师必须考虑通用性。
  • 如果对象已经被一个不同的键拥有,那么这是一个用户错误,IMO。 TObjectDictionary&lt;K,V&gt; 用于将成对的项目链接在一起。更改键的值并非不寻常,但如果只是再次添加值,则释放该值。我不希望字典检查所有条目。但是,它应该在释放“旧”项之前检查新添加的项。 FWIW,如果用户必须检查重复,字典有什么用?
  • 我不希望 dict 检查用户错误,但是两次将相同的项目添加到同一个键是 不是用户错误,并且 dict 应该处理它优雅地。应该可以根据需要经常这样做。
  • 语义已经非常不同了:TObjectXXX拥有他们的项目,如果他们释放它们,它们就会失效。 TDictionary&lt;K,V&gt; 只是在KV 之间创建一个映射,所以那里不存在风险,它不会使其项目无效。我认为 owning 类应该注意不要释放它正在添加的项目。甚至TObjectList 也应该这样做(但是,AFAICT 也不这样做)。
【解决方案4】:

由于 Delphi TDictionary 实现不允许使用多个相同的键,您可以查看 Alex Ciobanu 的出色 Generic collections library。它带有一个 TMultiMap 或您的情况下允许每个键有多个值的 TObjectMultiMap。

编辑: 如果您不希望每个键有多个值,而是希望避免向 Dictionary 添加重复项,那么您可以尝试 TDistinctMultiMap 或同一 Collections 库中的 TObjectDistinctMultiMap。

【讨论】:

  • OP 不希望每个键有多个值
  • @David,我不同意,OP 希望每个键有多个值,尽管值可能相同。
  • 如果 OP 确实希望每个键有多个值,那么您的答案是正确的。我认为 OP 在TStringList 中想要类似dupIgnore 的东西。
  • @David,再次重读了这个问题,你可能是对的,实际上 OP 想要类似于 dupIgnore 的东西。也许OP可以澄清
  • @David:我经常同意你的观点。猜猜谁给了你所有这些赞成票?
【解决方案5】:

所以释放一个我刚刚要求它添加的对象似乎有点奇怪!

您没有要求字典添加 - 您调用了“AddorSet”,并且由于已找到密钥,因此您的调用是“set”,而不是“add”。无论如何,就 Delphi 的行为而言,我认为这里没有什么奇怪的地方:在 Delphi 中,对象只是对象引用,简单对象没有引用计数或所有权。

由于在这种情况下字典拥有对象,它正在做它应该做的事情:“如果对象被拥有,当条目从字典中删除时,键和/或值被释放”。您在覆盖 entry[1] 时删除了该值 - 因此“testObject”中引用的对象会立即被删除,并且您对“testObject”的引用无效。

目前,每次我添加一个值时,我都必须先检查它是否已经在字典中。

这是为什么呢?您描述的行为只有在您使用对同一对象的引用覆盖以前使用的键时才会发生。


编辑:

也许毕竟有些“奇怪” - 试试这个测试代码:

        procedure testObjectList;
        var ol:TObjectList;
            o,o1:TObject;
        begin

          ol:=TObjectList.create;
          ol.OwnsObjects:=true;//default behavior, not really necessary
          try
            o:=TObject.create;      
            ol.add(o);
            ol[0]:=o;
            showmessage(o.ClassName);//no av-although ol[0] is overwritten with o again, o is not deleted
            o1:=TObject.create;
            ol[0]:=o1;
            showmessage(o.ClassName);//av - when o is overwritten with o1, o is deleted
          finally
            ol.free
          end;

        end;

尽管它在 (Delphi 7) 帮助中说:“TObjectList 控制其对象的内存,当它的索引被重新分配时释放一个对象”

【讨论】:

  • 我在这里看到了一些很奇怪的东西。如果它被添加两次,可能会发生很多有效的事情(沉默拒绝、异常等),但简单地释放它不应该是其中之一。没有什么可以辩护的,这显然是一个错误。
  • David 已经回答了您的争论 Rudy - 我不认为开发人员要求太多不要用相同的值覆盖相同的键。
  • 另外,即使同意这是一个糟糕的(我不知道)设计,考虑到 Delphi 处理对象引用的方式,这当然不是“奇怪” - 不乏射击自己的方法如果你不注意的话,会在脚下。
  • 释放一个刚刚添加的项目肯定是糟糕的设计,糟糕的设计应该是奇怪的。我理解它为什么会发生,改变它并不容易,但它不应该发生。我确实认为这是在向用户询问班级可以而且应该做的事情。如果您将重复项添加到任何其他容器类,则会收到错误(异常),或者它被静默拒绝,但该项目不会以任何方式失效。这是没有预期或应该预期的行为。
  • @Rudy - “如果您将重复项添加到任何其他容器类” - 尝试使用默认拥有对象的 TOBjectList。我现在还没有尝试过(这里已经很晚了......)但我很确定你会看到同样的行为。 IMO 跟踪对象生命周期不是这种容器类的工作,而是开发人员的工作,至少在 Delphi 世界中(我只是在重复 David 现在所说的话)此外,这并不是真正的添加,它覆盖了以前的密钥 (addOrSet) 所以我想我们只需要同意我们不同意......干杯。
【解决方案6】:

我认为这是一个错误。 我一周前遇到了它。

我使用 TObjectDictionary 来存储一些实时遥测数据,这些数据经常更新为新数据。

例如:

Type TTag = class
               updatetime : TDateTime;
               Value      : string ;
            end ;

TTagDictionary:= TObjectDictionary<string,TTag>.Create([doOwnsValues]);


procedure UpdateTags(key: string; newValue: String) ;
var 
   tag : TTag ;
begin
     if TTagDictionary.TryGetValue(key,tag) then begin  // update the stored tag
        tag.Value = newValue ;
        tag.updatetime := now ;
        TTagDictionary.AddorSetValue(key,tag) ;
     else begin
        tag := TTag.Create ;
        tag.updatetime := now ;
        tag.Vluae := newValue ;
        TTagDictionary.AddorSetValue(key,tag) ;
     end ;
end ;

经过几次更新后,我发现了一些令人讨厌的访问冲突,并且字典中充满了释放的对象。

这是一个设计很差的容器。

在更新时它需要检查新对象是否与旧对象相同,然后它不能释放该对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-11
    • 1970-01-01
    • 2012-07-02
    • 2019-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多