看看这段代码:
dic:=TObjectDictionary<Integer, TObject>.Create([doOwnsValues]);
testObject:=TObject.Create;
dic.AddOrSetValue(1,testObject);
dic.AddOrSetValue(1,testObject);
代码
令人惊讶的是,当您第二次添加对象时,该对象将被释放。
这是预期的行为吗?或者是Delphi库中的错误?
文档简单地说“如果对象是拥有的,当从字典中删除条目时,键和/或值被释放”。因此,释放一个我刚刚要求添加的对象似乎有点奇怪!
有没有办法告诉TObjectDictionary不这样做?目前,每次添加一个值时,我必须首先检查Key-Value组合是否已经在Dictionary中。
Delphi 2010
[编辑: 阅读完所有评论后:
我的结论(他们的价值)]
[/编辑]
答案 0 :(得分:9)
TObjectDictionary<TKey,TValue>
实际上只是一个TDictionary<TKey,TValue>
,在KeyNotify
和ValueNotify
方法中有一些额外的代码:
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用户错误)。
第二个想法 - 也许这个词典的设计者应该更加谨慎地同时拥有doOwnsValues
和AddOrSetValue()
......人们可以双向争论......我建议你在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 ;
经过多次更新后,我最终遇到了一些令人讨厌的访问冲突和一个充满释放对象的字典。
这是一个非常糟糕的设计容器。
在更新时,需要检查新对象是否与旧对象相同,然后它不能释放对象。