Добавление одного и того же объекта дважды в TObjectDictionary освобождает объект

Посмотрите на этот код:

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

Код

  • Создает словарь, которому принадлежат содержащиеся значения
  • Добавляет значение
  • Добавляет то же значение снова, используя тот же ключ

Удивительно, что объект освобождается, когда вы добавляете его во второй раз.

Является ли это предполагаемым поведением? Или ошибка в библиотеках Delphi?

В документации просто говорится: "Если объект принадлежит, когда запись удаляется из словаря, ключ и/или значение освобождаются". Так что кажется немного странным, чтобы освободить объект, который я только что попросил добавить!

Есть ли способ сказать TObjectDictionary, чтобы не делать этого? В настоящее время, каждый раз, когда я добавляю значение, я должен сначала проверить, если эта комбинация Key-Value уже находится в словаре.

Delphi 2010

[EDIT: После прочтения всех комментариев:

Мои выводы (для чего они стоят)]

  • Это, по-видимому, предполагаемое поведение
  • Нет способа изменить это поведение.
  • Не используйте TObjectDictionary (или любой другой аналогичный класс) для чего-либо другого, кроме обычного "Добавить эти объекты в контейнер. Оставьте их там. Сделайте некоторые вещи. Освободите контейнер и все добавленные вами объекты", Если вы делаете что-то более сложное, лучше управлять объектами самостоятельно.
  • Поведение плохо документировано, и вы должны прочитать источник, если хотите действительно знать, что происходит.

[/EDIT]

Ответы

Ответ 1

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> для выполнения основной работы, но сам справляется с собственными проблемами. Думаю, я сделаю последнее.

Ответ 2

Это потому, что при повторном использовании ключа вы заменяете объект, и поскольку словарь владеет объектом, он освобождает старый. Словарь не сравнивает значение, только ключ, поэтому он не обнаруживает, что значение (объект) одинаково. Не ошибка, как запроектирована (ошибка пользователя IOW).

Во второй раз - возможно, разработчик диктата должен был заботиться о том, чтобы иметь как doOwnsValues, так и AddOrSetValue()... можно спорить в обоих направлениях... Я предлагаю вам записать его в QC, но я бы "Я задерживаю дыхание - это было так сейчас, по крайней мере, в двух выпусках, поэтому вряд ли это изменится.

Ответ 3

Это поведение по дизайну и дизайн звучат.

Если бы класс взял на себя ответственность за освобождение дубликатов, он должен был бы перебирать весь контейнер каждый раз, когда была сделана модификация, как добавление, так и удаление. Итерация будет проверять любые повторяющиеся значения и соответственно проверять.

Было бы катастрофой навязать эту дьявольскую производительность для всех пользователей класса. Если вы хотите разместить дубликаты в списке, вам придется придумать политику управления жизненным циклом на заказ, которая соответствует вашим конкретным потребностям. В этом случае необоснованно ожидать, что контейнер общего назначения будет поддерживать ваш конкретный шаблон использования.


В комментариях к этому ответу и многим другим было высказано предположение о том, что лучший дизайн должен был тестировать в AddOrSetValue, было ли задано уже заданное значение указанному ключу. Если это так, то AddOrSetValue может сразу вернуться.

Я считаю, что всем ясно, что проверка дубликатов в полной общности слишком дорого для размышлений. Тем не менее, я утверждаю, что есть хорошие проектные причины, по которым проверка дублирования K и V в AddOrSetValue также была бы плохим дизайном.

Помните, что 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 в изоляции.

Ответ 4

Поскольку реализация Delphi TDictionary не позволяет использовать более одного ключа, вы можете проверить отличную коллекцию общих коллекций от Alex Ciobanu, Он поставляется с TMultiMap или для вашего случая TObjectMultiMap, который позволяет использовать несколько значений для каждого ключа.

Изменить: Если вы не хотите использовать несколько значений для каждого ключа, а хотите не добавлять дубликаты в словарь, вы можете попробовать TDistinctMultiMap или TObjectDistinctMultiMap из той же библиотеки коллекций.

Ответ 5

Так что кажется немного странным, чтобы освободить объект, который я только что попросил добавить!

Вы не просили словарь добавить - вы назвали 'AddorSet', и поскольку ключ уже был найден, ваш вызов был 'set', а не 'add'. Несмотря на это, я не вижу здесь ничего странного в том, что касается поведения Delphi: в Delphi объекты - это только ссылки на объекты, и нет подсчета ссылок или собственности для простых объектов.

Поскольку в этом случае словарь владеет объектами, он делает именно то, что он должен делать: "Если объект принадлежит, когда запись удаляется из словаря, ключ и/или значение освобождаются". Вы удалили значение при перезаписывании записи [1] - поэтому объект, указанный в "testObject", немедленно удаляется, а ваша ссылка на "testObject" недействительна.

В настоящее время каждый раз, когда я добавляю значение, я должен сначала проверить, если он уже находится в словаре.

Почему? Поведение, которое вы описали, должно происходить только в том случае, если вы перезаписываете ранее использованный ключ со ссылкой на тот же объект.


Edit:

Возможно, что-то "нечетное" - попробуйте этот тестовый код:

        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 управляет памятью его объектов, освобождая объект, когда его индекс переназначается"

Ответ 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 ;

После нескольких обновлений я столкнулся с некоторыми неприятными нарушениями доступа и со словарем, полным освобожденных объектов.

Это очень плохо спроектированный контейнер.

При обновлении необходимо проверить, совпадает ли новый объект с старым, а затем он НЕ должен освобождать объект.