Компания 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}
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 }
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
Form1: TForm1;
List: IXList;
Item: IXListItem;
И методы для создания и удаления объектов.
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] ссылки:
Пример работы без объявлением [weak] ссылки:
Как видим без использования [weak] ссылки, коллекция и элементы циклически ссылаются друг на друга и у них не вызывается деструкторы, и соответственно мы получаем утечку памяти о чем нам и сообщает FastMM.
Также в новой версии появился атрибут [unsafe] который является аналогом [weak], но при удалении объекта на который она ссылается, ссылка автоматически не “заниливается”.
С ним кстати мы уже нашли один баг, который и отправили в qc.
Благодарю за пример и помощь в написании коллегу h_xandr.
upd. Ссылка на реппозиторий
Комментарии (7)
instigator21
20.04.2016 15:06+1Всё верно, но поддержка weak ссылок была доступна только в мобильном компиляторе, я проверял этот же код в XE 10 Seatle, ловим такую же утечку.
h_xandr
20.04.2016 15:18+4Очень нужная фича, Delphi потихоньку приближается к согласованному состоянию. Интерфейсные типы появились очень давно, а слабые ссылки только сейчас. У нас очень крупный проект написан на интерфейсах и слабых ссылок очень не хватало!
MrShoor
23.04.2016 03:36Слабые ссылки достаточно легко реализуются и без этой фичи, но согласен, с синтаксическим сахаром намнооого вкуснее.
zedxxx
20.04.2016 16:01Радует, что Delphi развивается. А по поводу weak ссылок — это вообще нормальная практика их использовать? Я имею в виду, не является ли это антипаттерном, использовать объекты, ссылающиеся друг на друга?
Error1024
20.04.2016 17:32Иногда просто нельзя по другому реализовать, сама VCL/FMX использует перекрестные ссылки
MrShoor
23.04.2016 03:39Классический паттерн — издатель/подписчик без слабых ссылок в большинстве случаев не сделать. Издатель должен держать ссылку на подписчика, чтобы рассылать ему уведомления, а подписчик должен держать ссылку на издателя, чтобы он мог отписаться от рассылки.
Merlok
Если я правильно помню weak ссылки работали и раньше) начиная примерно с xe4 или xe5