Целью ручного подсчета ссылок для компилятора ARC в приведенном выше коде является моделирование словаря со слабыми ссылками.Универсальные коллекции Delphi поддерживаются универсальными массивами, которые будут содержать строгую ссылку на любой объект, добавленный в коллекцию на компиляторе ARC.
Существует несколько способов получения слабых ссылок - с помощью указателей, с помощью оберток вокруг объекта, где объявлен объекткак слабый и ручной подсчет ссылок в соответствующих местах.
С указателями вы теряете безопасность типов, оболочкам требуется значительно больше кода, поэтому я предполагаю, что автор приведенного выше кода выбрал ручной подсчет ссылок.Ничего плохого в этой части.
Однако, как вы заметили, в этом коде есть что-то подозрительное - хотя подпрограмма SetExt
написана правильно, RemoveExt
имеет ошибку, которая впоследствии приводит к сбою.
Давайте рассмотрим код в контексте на компиляторе ARC (для краткости я опущу директивы компилятора и несвязанный код):
Поскольку добавление объекта в коллекцию (массив) увеличивает количество ссылок, чтобы добиться слабогоссылка, мы должны уменьшить количество ссылок на добавленный экземпляр объекта - таким образом, счетчик ссылок экземпляра останется неизменным после того, как он будет сохранен в коллекции.Затем, когда мы удаляем объект из такой коллекции, мы должны восстановить баланс счетчика ссылок и увеличить счетчик ссылок.Также мы должны убедиться, что объект будет удален из такой коллекции, прежде чем он будет уничтожен - хорошее место для этого - деструктор.
Добавление в коллекцию:
LRelease := not FHTTPClientList.ContainsKey(Self);
FHTTPClientList.AddOrSetValue(Self, LExt);
if LRelease then __ObjRelease;
Мы добавляем объект в коллекцию, а затем после того, как коллекция содержит сильную ссылку на наш объект, мы можем освободить его счетчик ссылок.Если объект уже находится внутри коллекции, это означает, что его счетчик ссылок уже был уменьшен, и мы не должны уменьшать его снова - это цель LRelease
flag.
Удаление из коллекции:
if FHTTPClientList.ContainsKey(Self) then
begin
__ObjAddRef;
FHTTPClientList.Remove(Self);
end;
Если объект находится в коллекции, мы должны восстановить баланс и увеличить количество ссылок перед удалением объекта из коллекции.Это та часть, которая отсутствует в методе RemoveExt
.
Убедиться в том, что при уничтожении объекта нет в списке:
destructor THTTPClient.Destroy;
begin
RemoveExt;
inherited;
end;
Примечание: Для того, чтобы такая фальшивая слабая коллекция работала должным образом, элементы должны быть добавлены и удалены только с помощью вышеуказанных методов, которые заботятся о балансировке количества ссылок.Использование любых других оригинальных методов сбора, таких как Clear
, приведет к нарушению подсчета ссылок.
Ошибка или нет?
В System.Net.HttpClient
код нарушен RemoveExt
метод вызывается только в деструкторе, также FHTTPClientList
является частной переменной и не изменяется никаким другим способом.На первый взгляд, этот код работает правильно, но на самом деле содержит довольно тонкую ошибку.
Чтобы распутать реальную ошибку, нам нужно рассмотреть возможные сценарии использования, начиная с нескольких установленных фактов:
- Только методы, которые изменяют содержимое и по этому количеству ссылок в словаре
FHTTPClientList
, являются SetExt
и RemoveExt
методами SetExt
метод является правильным - Сломанный
RemoveExt
методкоторый не вызывает __ObjAddRef
, вызывается только в THTTPClient
деструкторе, и именно здесь возникает эта незначительная ошибка.
Когда деструктор вызывается для любого конкретного экземпляра объекта, что означаетЭкземпляр объекта достиг своего времени жизни, и любые последующие триггеры подсчета ссылок (во время выполнения деструктора) не влияют на правильность кода.
Это обеспечивается применением переменной objDestroyingFlag
к FRefCount
, изменяющей ее значение и любое дальнейшее увеличение / уменьшение счетчика больше не может привести к специальному значению 0
, которое запускает процесс уничтожения - таким образом, объект безопасен и будетне быть уничтоженным дважды.
В приведенном выше коде, когда вызывается деструктор THTTPClient
, это означает, что последняя сильная ссылка на экземпляр объекта вышла из области видимости или была установлена на nil
, и в этот момент единственной оставшейся действующей ссылкой, которая может вызвать механизм подсчета ссылок, является один в FHTTPClientList
. Эта ссылка была очищена методом RemoveExt
(сломан или нет) в тот момент, как было сказано ранее, это не имеет значения. И все отлично работает.
Но автор кода забыл одну крошечную причудливую вещь - DisposeOf
метод, который вызывает деструктор, но в тот момент экземпляр объекта не достиг своего времени подсчета ссылок. Другими словами - если деструктор вызывается с помощью DisposeOf
, любые последующие триггеры подсчета ссылок должны быть сбалансированы , поскольку все еще существуют действующие ссылки на объект, которые будут запускать механизм подсчета ссылок после завершения цепочек вызовов деструктора. Если мы прервем подсчет в этот момент, результат будет катастрофическим.
Поскольку THTTPClient
не является TComponent
потомком, для которого требуется DisposeOf
, можно легко упустить возможность и забыть, что кто-то где-то может вызвать DipsoseOf
для такой переменной в любом случае - например, если Вы создаете собственный список THTTPClient
экземпляров, и очистка такого списка вызовет для них DisposeOf
и с радостью нарушит их счетчик ссылок, потому что метод RemoveExt
в конечном счете сломан.
Вывод: да, это БАГ.