Слабые[weak] ссылки в новой версии Delphi

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

    Компания 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. Ссылка на реппозиторий
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 7
    • +1
      Если я правильно помню weak ссылки работали и раньше) начиная примерно с xe4 или xe5
      • +1
        Всё верно, но поддержка weak ссылок была доступна только в мобильном компиляторе, я проверял этот же код в XE 10 Seatle, ловим такую же утечку.
        • +4
          Очень нужная фича, Delphi потихоньку приближается к согласованному состоянию. Интерфейсные типы появились очень давно, а слабые ссылки только сейчас. У нас очень крупный проект написан на интерфейсах и слабых ссылок очень не хватало!
          • 0
            Слабые ссылки достаточно легко реализуются и без этой фичи, но согласен, с синтаксическим сахаром намнооого вкуснее.
          • 0
            Радует, что Delphi развивается. А по поводу weak ссылок — это вообще нормальная практика их использовать? Я имею в виду, не является ли это антипаттерном, использовать объекты, ссылающиеся друг на друга?
            • 0
              Иногда просто нельзя по другому реализовать, сама VCL/FMX использует перекрестные ссылки
              • 0
                Классический паттерн — издатель/подписчик без слабых ссылок в большинстве случаев не сделать. Издатель должен держать ссылку на подписчика, чтобы рассылать ему уведомления, а подписчик должен держать ссылку на издателя, чтобы он мог отписаться от рассылки.

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