将两个相同的Object添加到TObjectDictionary会释放该对象

时间:2011-08-02 01:33:53

标签: delphi delphi-2010

看看这段代码:

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

代码

  1. 创建拥有所包含值的词典
  2. 添加值
  3. 使用相同的密钥
  4. 再次添加相同的值

    令人惊讶的是,当您第二次添加对象时,该对象将被释放。

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

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

    有没有办法告诉TObjectDictionary不这样做?目前,每次添加一个值时,我必须首先检查Key-Value组合是否已经在Dictionary中。

    Delphi 2010

    [编辑: 阅读完所有评论后:

    我的结论(他们的价值)]

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

    [/编辑]

6 个答案:

答案 0 :(得分:9)

TObjectDictionary<TKey,TValue>实际上只是一个TDictionary<TKey,TValue>,在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<K,V>,或者根本不使用doOwnsValues。你也可以写一个简单的包装器,例如TValueOwningDictionary<K,V> TDictionary<K,V>使用{{1}}来完成工作,但处理所有权问题本身。我想我会做后者。

答案 1 :(得分:7)

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

第二个想法 - 也许这个词典的设计者应该更加谨慎地同时拥有doOwnsValuesAddOrSetValue() ......人们可以双向争论......我建议你在QC中提交它,但我不会屏住呼吸 - 至少在两个版本中已经如此,所以它不太可能改变。

答案 2 :(得分:5)

此行为是设计使然,设计合理。

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

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


在对这个答案的评论以及其他许多评论中,有人建议更好的设计是在AddOrSetValue中测试所设置的值是否已经分配给指定的密钥。如果是这样,那么AddOrSetValue可以立即返回。

我认为任何人都很清楚,完全通用性地检查重复项是太昂贵而无法考虑的。但是,我认为有很好的设计理由,为什么在AddOrSetValue中检查重复的K和V也会是糟糕的设计。

请记住,TObjectDictionary<K,V>源自TDictionary<K,V>。对于更通用的类,比较V的相等性可能是一项昂贵的操作,因为我们对V没有约束,它是通用的。因此,对于TDictionary<K,V>,我们不应该包含假定的AddOrSetValue测试,这是性能原因。

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

这些通用容器类非常重要,设计决策必须考虑到大量的用例,一致性考虑因素等。在我看来,单独考虑TObjectDictionary<K,V>.AddOrSetValue是不合理的。

答案 3 :(得分:2)

由于Delphi TDictionary实现不允许使用多个相同的键,因此您可以查看Alex Ciobanu的优秀Generic collections library。它带有一个TMultiMap或者你的TObjectMultiMap案例,它允许每个键有多个值。

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

答案 4 :(得分:0)

  

因此,释放一个我刚刚要求添加的对象似乎有点奇怪!

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

因为在这种情况下字典拥有对象,所以它正在做它应该做的事情:“如果对象是拥有的,当从字典中删除条目时,键和/或值被释放”。您在覆盖条目[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控制其对象的内存,在重新分配索引时释放对象”

答案 5 :(得分:0)

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

我使用TObjectDictionary存储一些实时的telemetria数据,这些数据经常用新数据更新。

例如:

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 ;

经过多次更新后,我最终遇到了一些令人讨厌的访问冲突和一个充满释放对象的字典。

这是一个非常糟糕的设计容器。

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