openlang

Ссылка на интерфейс и ссылка на объект для одного экземпляра

Ошибка времени жизни, которую я чаще всего встречаю в старом коде на Delphi, возникает, когда на одном экземпляре смешивают два правила владения. Класс, унаследованный от TInterfacedObject, освобождается, когда счётчик ссылок на интерфейс доходит до нуля. Переменная классового типа не считается вовсе. Если держать один экземпляр через оба вида ссылок, правила расходятся в том, когда он умирает.

program Greeter;
{$APPTYPE CONSOLE}
type
  IGreeter = interface
    procedure Hello;
  end;

  TGreeter = class(TInterfacedObject, IGreeter)
    procedure Hello;
  end;

procedure TGreeter.Hello;
begin
  WriteLn('hello');
end;

procedure Use(G: IGreeter);
begin
  G.Hello;
end;

var
  Obj: TGreeter;
begin
  Obj := TGreeter.Create; // count 0, a class reference is not counted
  Use(Obj);               // 1 inside Use, back to 0 on the way out
  Obj.Free;               // the object is already gone
end.

Экземпляр начинает со счётчиком ноль, потому что ссылка на класс его не трогала. При передаче в Use он преобразуется в IGreeter, на время вызова счётчик становится единицей, на выходе возвращается к нулю, и на нуле объект уничтожает себя сам. Следующий за этим Obj.Free - повторное уничтожение уже освобождённой памяти. Компилятор всё это принимает.

Моё правило таково. Если класс реализует интерфейсы через TInterfacedObject, то каждая долгоживущая ссылка на экземпляр - интерфейсная, а классовый тип появляется только в строке с вызовом конструктора. Любопытно, проводят ли другие черту там же.

Разговор продолжается 5

Следуя доводам
  1. Ссылка на ответ

    Хочу убедиться, что правильно понял порядок, потому что весь вопрос во времени. Первое, Create возвращает объект со счётчиком ноль. Второе, вход в Use поднимает его до единицы. Третье, выход из Use опускает его до нуля, и деструктор выполняется прямо там, в завершающем коде Use, ещё до возврата управления вызывающему. Значит, к моменту Obj.Free уничтожение уже произошло, а не просто запланировано.

    Если так, то объявление параметра как const G: IGreeter убирает второй и третий шаги целиком? Я читал, что да, и что при этом появляется другая проблема, когда аргумент - только что созданный объект.

  2. Ссылка на ответ

    Я бы смотрел на строку объявления, Obj: TGreeter. Поменяйте на Obj: IGreeter, и пример перестанет компилироваться на Obj.Free, потому что Free принадлежит TObject, а интерфейс его не объявляет. Это приятное свойство вашего правила. Второе уничтожение тогда отвергает компилятор, а не пропускает.

    Правда, это работает, только пока интерфейс действительно уже класса. Если кто-нибудь ради удобства добавит в IGreeter метод Dispose, тип переменной перестанет что-либо говорить о том, кто что освобождает.

  3. Ссылка на ответ

    С объявлением соглашусь, но есть ещё одна несчитаемая ссылка, которую тип переменной не покажет. Присвойте где-нибудь Obj.Hello полю типа procedure of object, и этот указатель на метод тоже держит экземпляр, без всякого счётчика. Интерфейсные ссылки уходят в ноль, объект исчезает, а событие потом всё равно вызывается в него. В редакторе всё выглядит как код на интерфейсах.

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

  4. Ссылка на ответ

    Правило заодно даёт дешёвую проверку. Если классовый тип появляется только рядом с Create, текстовый поиск по : TGreeter должен ничего не найти, а всё, что он найдёт, и есть список, который надо пройти.

  5. Ссылка на ответ

    Ваши три шага верны, и уничтожение действительно происходит внутри Use, последним действием перед возвратом. С const G: IGreeter вызываемая процедура вообще не считает ссылки, так что второй и третий шаги исчезают, и пример выше перестаёт уничтожать объект раньше времени.

    Другая проблема, о которой вы читали, касается Use(TGreeter.Create). С параметром const в этом вызове новым экземпляром никто не владеет, потому что на стороне вызывающего нет переменной, которая держала бы счётчик. Я встречал описание этого как утечки и не уверен, что все версии компилятора ведут себя одинаково, поэтому такую форму просто не пишу. Экземпляр сначала попадает в интерфейсную переменную и передаётся оттуда.

    К собственному правилу надо добавить одно исключение. TComponent реализует _AddRef и _Release без подсчёта, так что компонентом, полученным через интерфейс, по-прежнему владеет его Owner. Правило относится к потомкам TInterfacedObject, а не к любому классу с интерфейсом.

Другие стороны того же вопроса

  1. Предупреждение про висячий указатель пропало, а указатель остался

    Передаю имя в библиотеку на C++ через обёртку, и сначала написал всё в одну строку, CString::new(name).unwrap().as_ptr(), и r…

    Ответы 1rustcpp
  2. getline оставляет буфер вызывающему, даже когда возвращает -1

    POSIX getline выделяет или увеличивает буфер через переданный указатель, и этот буфер принадлежит вызывающему с первого же вы…

    Ответы 4c
  3. Один и тот же вызов с sink, сначала копия, потом перенос

    Nim 2 решает, копировать или переносить, в каждой точке вызова. Не на уровне proc. Тот же аргумент, та же proc. Первый вызов…

    Ответы 2nim