Должен ли я не передавать интерфейс как const?

Недавно я встретил (снова) ошибку кода-компилятора Delphi при передаче интерфейса как const, утечка ссылки.

Это происходит, если ваш метод объявлен для передачи интерфейсной переменной как const, например:

procedure Frob(const Grob: IGrobber);

и исправить это просто удалить const:

procedure Frob(Grob: IGrobber);

Я понимаю, что constvar и out) позволяют передавать элементы по ссылке. В случае структуры это сохраняет копию аргумента; позволяя вместо этого просто передать указатель на элемент.

В случае Object/Pointer/Interface нет необходимости передавать по ссылке, так как является ссылкой; он уже войдет в регистр.

Чтобы снова не иметь эту проблему, я отправился в крестовый поход. Я искал все мои исходные деревья для:

const [A-Za-z]+\: I[A-Z]

И я удалил около 150 экземпляров, где я передаю интерфейс как const.

Но есть те, которые я не могу изменить. Событие обратного вызова TWebBrowser объявляется как:

OnDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant);
                                    \___/
                                      |
                                      ?

Неужели я зашел слишком далеко? Я сделал что-то плохое?

Изменить. Или, если фраза это в менее "основанном на мнении" вопросе стиля: есть ли серьезные нисходящие стороны, чтобы не передавать интерфейс как const?

Бонус: когда Delphi не (всегда) увеличивает счетчик ссылок интерфейса, они нарушают Правила COM:

Правила подсчета ссылок

Правило 1: AddRef должно быть вызвано для каждой новой копии указателя интерфейса, а Release - для каждого уничтожения указателя интерфейса, за исключением случаев, когда в последующих правилах явно разрешено иное.

Правило 2. Особые знания со стороны фрагмента кода отношений начала и окончания времен жизни двух или более копий указателя интерфейса могут позволить AddRef/Release.

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

Ответ 1

Это происходит, если ваш метод объявлен для передачи интерфейсной переменной как const, например:

procedure Frob(const Grob: IGrobber);

Это не совсем правильно. Чтобы произошла утечка, вам нужно, чтобы в коде не было ничего, что когда-либо ссылалось на вновь созданный объект. Поэтому, если вы пишете:

Frob(grob);

Там нет проблем, потому что интерфейс grob уже имеет хотя бы одну ссылку.

Проблема возникает при написании:

Frob(TGrobberImplementer.Create);

В этом сценарии ничего не делается для ссылки на интерфейс, и поэтому он просочился. Ну, он будет просочиться, если ничего в реализации Frob не ссылается на него.

Неужели я сделал что-то плохое?

Ну, это зависит. Я не думаю, что что-то особенно плохое из того, что вы сделали. Существует недостаток в плане производительности, потому что теперь все ваши функции, которые принимают параметры интерфейса, должны будут добавлять и освобождать ссылки, используя неявные блоки try/finally. Только вы можете судить об этом.

Более значимая проблема связана с кодом, который находится вне вашего контроля. Вы даете

procedure OnDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant);

в качестве примера. Там нет проблемы, потому что вы никогда не называете этот метод. Это обработчик событий, который вы реализуете. Структура вызывает его, и он передает интерфейс, на который уже ссылаются.

Реальная проблема возникает из методов, объявленных в RTL или любого другого кода третьей стороны, который вы вызываете. Если вы вызываете методы, и если они используют аргументы интерфейса const, вы можете попасть в ловушку.

Это достаточно легко обойти, хотя и утомительно.

grob := TGrobberImplementer.Create;
Frob(grob);

Мое рассуждение о решении проблемы выглядит следующим образом:

  • Существует стоимость производительности при передаче параметров интерфейса по значению.
  • Я не могу гарантировать, что каждый метод, который я вызываю, будет принимать параметры интерфейса по значению.
  • Поэтому я принимаю тот факт, что мне нужно иметь дело с вызовом параметров интерфейса const, по крайней мере, некоторое время.
  • Поскольку мне приходится иметь дело с ним некоторое время, и, поскольку я ненавижу несогласованность, я предпочитаю заниматься этим все время.
  • Поэтому я решил сделать все параметры интерфейса в методах, которые я пишу, const.
  • И поэтому я уверен, что никогда не передаю интерфейс в качестве аргумента, если он уже не ссылается на переменную.