Здравствуйте.

Компания Embarcadero вчера объявила о выходе новой версии Delphi RAD studio XE 10.1,
Весь список изменений можно посмотреть тут, я же хочу рассказать о наиболее ценном(для нашей компании) улучшении, а именно о внедрение слабых [weak] ссылок в классический компилятор (Win32/Win64).

Выше в статье даны подробности проблемы, так что тем кто желает посмотреть что сделали в делфи прошу под кат.

Итак, рассмотрим в качестве примера использования слабых ссылок, интерфейсный контейнер. Конечно, слабые ссылки необходимы и в других ситуациях, но мы рассмотрим именно этот случай. В такой коллекции у нас есть следующая проблема: элементы контейнера должны иметь ссылку на коллекцию-владельца (например чтобы имея ссылку на элемент, его можно было удалить из коллекции или просто получить доступ к ней). В свою очередь сама коллекция также должна хранить ссылки на элементы. Так как в предыдущих версиях Делфи, у нас была возможность объявлять только обычные ссылки (strong references), то в результате мы получали циклические ссылки в данной коллекции, что приводило к невозможности корректно освободить память. Из данной ситуации выходили обычно двумя способами:
1. В классе элемента использовали не типизированный указатель для поля FOwner. Присвоение интерфейсной ссылки к такому указателю не приводило к увеличению счетчика ссылок, что решало проблему циклической ссылки, однако этот код не безопасен — в случае если коллекция умрет раньше элемента (ссылка на который где-то оказалась присвоена), то нет безопасного способа узнать об удалении коллекции — владельца. Указатель FOwner — продолжал указывать на уже удаленную память.
2. Чтобы решить проблему предыдущего способа можно внедрить оповещение элементов об удалении коллекции. При удалении коллекция пробегает по всем своим элементам и вызывает у них некий метод, который в свою очередь “заниливает” поле FOwner. Минус этого подхода — это дополнительный код и время необходимое на его выполнение. В случае с нашим примером коллекции это еще не так страшно, но в других не тривиальных примерах этот код может очень сильно запутать логику и добавить ошибок.
И вот в последней версии XE10.1 Berlin появилась по человечески нормальная возможность решать эту проблему. Все что нужно, это объявить поле FOwner c атрибутом [weak]. Такая ссылка по прежнему будет строго типизированной, но присвоение в которую не будет увеличивать счетчик ссылок на исходный объект. В случае же когда объект (в нашем случае коллекция-владелец) удаляется из памяти, такая ссылка автоматически “занилится”, что будет индикатором что объект удален.

Напишем два интерфейса, и 2 класса реализующих эти интерфейсы (данный пример упрощен до предела, а вопросы оптимизации скорости вообще не стояли)
Описание интерфейсов и классов
type
  {интерфейс элемента}
  IXListItem = interface
    ['{02E680D6-9F86-4303-85B5-256ACD89AD46}']
    function GetName: string;
    procedure SetName(const Name: string);
    property Name: string read GetName write SetName;
  end;

  {интерфейс коллекции}
  IXList = interface
    ['{922BDB26-4728-46DA-8632-C4F331C5A013}']
    function Add: IXListItem;
    function Count: Integer;
    function GetItem(Index: Integer): IXListItem;
    property Items[Index: Integer]: IXListItem read GetItem;
    procedure Clear;
  end;

  {класс имплементатор элемента}
  TXListItem = class(TInterfacedObject, IXListItem)
  private
    [weak] FOwner: IXList;  // слабая ссылка на коллекцию
    FName: string;
  public
    constructor Create(const Owner: IXList);
    destructor Destroy; override;
    procedure SetName(const Name: string);
    function GetName: string;
  end;
  
  {класс имплементатор коллекции}
  TXList = class(TInterfacedObject, IXList)
  private
    FItems: array of IXListItem;
  public
    function Add: IXListItem;
    function Count: Integer;
    function GetItem(Index: Integer): IXListItem;
    procedure Clear;
    destructor Destroy; override;
  end;


Как видим в объекте TXListItem есть слабая ссылка на объект владелец.

Реализация класса TXListItem, в методе GetName мы пользуемся нашей слабой ссылкой:
TXListItem
{TXListItem}
constructor TXListItem.Create(const Owner: IXList);
begin
  FOwner := Owner;  // получение слабой ссылки
end;

destructor TXListItem.Destroy;
begin
  ShowMessage('Destroy ' + GetName);
  inherited;
end;

function TXListItem.GetName: string;
var
  List: IXList;
begin
  List := FOwner; 
  if Assigned(List) then
    Exit(FName + '; owner assined!')
  else
    Exit(FName + '; owner NOT assined!');
end;

procedure TXListItem.SetName(const Name: string);
begin
  FName := Name;
end;


Класс TXList банален:
TXList
{ TXList }

function TXList.Add: IXListItem;
var
  c: Integer;
begin
  c := Length(FItems);
  SetLength(FItems, c + 1);
  Result := TXListItem.Create(Self);
  FItems[c] := Result;
end;

procedure TXList.Clear;
var
  i: Integer;
begin
  for i := 0 to Length(FItems) - 1 do
    FItems[i] := nil;
end;

function TXList.Count: Integer;
begin
  Result := Length(FItems);
end;

destructor TXList.Destroy;
begin
  Clear;
  ShowMessage('Destroy list');
  inherited;
end;

function TXList.GetItem(Index: Integer): IXListItem;
begin
  Result := FItems[Index];
end;


Объявляем наши переменные:
var
var
  Form1: TForm1;
  List: IXList;
  Item: IXListItem;


И методы для создания и удаления объектов.
methods
procedure TForm1.btnListCreateAndFillClick(Sender: TObject);
begin
  List := TXList.Create;         // создаем коллекцию
  List.Add.Name := 'item1';  // заполняем тремя элементами
  List.Add.Name := 'item2';
  Item := List.Add;  // последний элемент запоминаем в глобальную переменную
  Item.Name := 'item3';
end;

procedure TForm1.btnListClearClick(Sender: TObject);
begin
  List := nil; // освобождаем коллекцию
end;

procedure TForm1.btnLastItemFreeClick(Sender: TObject);
begin
  Item := nil;  // освобождаем последний элемент, который отдельно запомнили 
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  ReportMemoryLeaksOnShutdown := true; // Включаем проверку утечек памяти
end;


Пример работы с объявлением [weak] ссылки:
image

Пример работы без объявлением [weak] ссылки:


Как видим без использования [weak] ссылки, коллекция и элементы циклически ссылаются друг на друга и у них не вызывается деструкторы, и соответственно мы получаем утечку памяти о чем нам и сообщает FastMM.

Также в новой версии появился атрибут [unsafe] который является аналогом [weak], но при удалении объекта на который она ссылается, ссылка автоматически не “заниливается”.
С ним кстати мы уже нашли один баг, который и отправили в qc.

Благодарю за пример и помощь в написании коллегу h_xandr.

upd. Ссылка на реппозиторий

Комментарии (7)


  1. Merlok
    20.04.2016 15:04
    +1

    Если я правильно помню weak ссылки работали и раньше) начиная примерно с xe4 или xe5


  1. instigator21
    20.04.2016 15:06
    +1

    Всё верно, но поддержка weak ссылок была доступна только в мобильном компиляторе, я проверял этот же код в XE 10 Seatle, ловим такую же утечку.


  1. h_xandr
    20.04.2016 15:18
    +4

    Очень нужная фича, Delphi потихоньку приближается к согласованному состоянию. Интерфейсные типы появились очень давно, а слабые ссылки только сейчас. У нас очень крупный проект написан на интерфейсах и слабых ссылок очень не хватало!


    1. MrShoor
      23.04.2016 03:36

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


  1. zedxxx
    20.04.2016 16:01

    Радует, что Delphi развивается. А по поводу weak ссылок — это вообще нормальная практика их использовать? Я имею в виду, не является ли это антипаттерном, использовать объекты, ссылающиеся друг на друга?


    1. Error1024
      20.04.2016 17:32

      Иногда просто нельзя по другому реализовать, сама VCL/FMX использует перекрестные ссылки


    1. MrShoor
      23.04.2016 03:39

      Классический паттерн — издатель/подписчик без слабых ссылок в большинстве случаев не сделать. Издатель должен держать ссылку на подписчика, чтобы рассылать ему уведомления, а подписчик должен держать ссылку на издателя, чтобы он мог отписаться от рассылки.