Страницы

Поиск по вопросам

четверг, 27 февраля 2020 г.

Создание хранилища объектов

#c_sharp #память


Товарищи, встал перед такой проблемой:

Мне необходимо реализовать нечто вроде хранилища объектов, которое бы выдавало по
одному экземпляру указанного объекта на пользователя

Попробую пояснить с помощью псевдокода:

// Добавим в хранилище правило создания объекта 
storage.Add("tmp", () => new Foo()); 

{
    // В хранилище пока нет сгенерированных объектов
    // Так что создаётся новый, сохраняется и возвращается 
    Foo tmp0 = storage["tmp"];
}

// Здесь ссылка на tmp0 уже недействительна 
// Так что единственная ссылка на тот созданный объект лежит внутри хранилища 
// Возвращаем тот же объект, что был в tmp0
Foo tmp1 = storage["tmp"];

// Тот объект, что был создан для tmp0, а теперь хранится в tmp1, уже занят 
// Так что создаём новый объект, сохраняем его в хранилище и его же и возвращаем 
Foo tmp2 = storage["tmp"];


То есть при завершении области видимости для tmp0 хранилище, содержащее в себе ссылку
на тот же объект, его не удаляет, а сохраняет до следующего запроса подобного объекта. 

То есть логика такая:


Идёт запрос объекта по ключу 
Если в хранилище нет уже созданных объектов - сгенерировать его по заданному правилу,
сохранить и после этого вернуть . Иначе:
Проверить для каждого созданного объекта, есть ли на него ссылка в пользовательском
коде. Если нет (то есть единственная ссылка лежит в самом хранилище) - вернуть найдённый
объект 


Такое поведение нужно мне по следующей причине: создание Foo - трудоёмкий процесс.
Помимо этого Foo не является потокобезопасным, так что на руках у каждого пользователя
Foo должен быть свой уникальный экземпляр, который, возможно, уже был создан и использован
ранее. 

Вроде как нигде нет «счётчика» ссылок на объекты, так как сборщику мусора плевать,
сколько там раз была объявлена ссылка на определённый объект. Важно лишь то, есть ли
таковая ссылка вообще

Так что даже не знаю, возможно ли вообще реализовать подобное средствами C#...



UPD:

Я никак не могу выбрать, какой ответ пометить галочкой, ибо ответы от iluxa1810 и
default locale  являются более правильными для общего случая. Однако в рамках моей
весьма специфичной задачи более подходит метод, описанный в ответе от John... 
    


Ответы

Ответ 1



Можно вывернуться, создав класс-обертку. Я постараюсь вкратце сейчас, потому что с телефона. Суть в том, что мы используем финализатор обертки, чтобы отловить событие удаления обертки и вернуть наш экземпляр в строй. Создаём наш класс-обертку: class FooWrapper { public event Action Final; private Foo foo; private int id; public FooWrapper(Foo foo, int id) { this.foo = foo; this.id = id; } // Вот и наш финализатор ~FooWrapper() { if (Final != null) Final(id) } } В самой программе создаём два словаря. Один - для неиспользуемых экземпляров foo, а второй - для используемых. Думаю, логика более или менее понятна? 1) по требованию пользователя ищется в словарях экземпляр Foo. В данном случае по Id. 2) если нет, то создаётся Foo и помещается во второй словарь. Его мы передаём в FooWrapper и обязательно подписываемся на событие Final. Полученный FooWrapper уже отдаем пользователю. 3) после того, как у пользователя пропадают все ссылки на FooWrapper срабатывает финализатор, в котором срабатывает событие Final, передающее ID нашего Foo. 4) В основной программе мы получаем этот Id и переносим из второго словаря в первый. Главный недостаток, что в данном случае мы полностью исключает возможность взаимодействия напрямую с foo, только через методы, который в fooWrapper пропишешь.

Ответ 2



Вот тут похожий вопрос и там предоставляется решение в виде использования WeakReference -это такая ссылка на объект, которая не препятствует сборки мусора. У него есть свойство IsAlive, которое говорит жив ли объект или уже был собран сборщиком мусора. Мне видится, что это вы можете задействовать в своей задаче. В вашем случае, объект будет жив до тех пор, пока кто-то на него ссылается из кода. Проверить, есть ли ссылка на объект в коде нельзя. .NET ушли от подсчета кол-ва ссылок на объект в пользу построения графа доступности объектов. Как альтернативный вариант- это ввести внутри вашего хранилища подсчет, заставляя пользователя вызывать спец. метод. Но тут все строится на доверии... Например, вдруг забудут что-либо вызвать => объект будет зарегистрирован за кем-то.

Ответ 3



Возможно, в Net найдется механизм, который решит задачу, в том виде, в каком Вы ее написали. Но, если честно, мне это решение кажется неочевидным и хрупким: неочевидным, т.к. конец блока кода легко пропустить при чтении, и, поэтому, редко используется для критичных операций: } //за одним символом здесь кроется важная операция по освобождению объекта. Как разработчик, я ожидаю, что блоки кода можно свободно переставлять при рефакторинге. В данном случае при этом нужно будет очень внимательно отследить срок жизни всех переменных. Ситуация усложнится если ссылки на Foo будут сохраняться в полях других классов (объекты которых сами будут храниться в коллекциях и захватываться в лямбдах). хрупким, т.к. в данном случае логика будет сильно зависеть от работы сборщика кода, которую сложно контролировать. Альтернатива: объектный пул Для решения Вашей проблемы (дорогостоящие объекты, которые желательно переиспользовать) подойдет шаблон проектирования «объектный пул». Пулы широко используются для схожих задач (хранения соединений к БД, например). В простейшем варианте Вам достаточно: Реализовать для Foo интерфейс IDisposable. В storage отслеживать вызов метода Dispose для созданных Foo и освобождать объекты. В вызывающем коде всегда оборачивать полученные из storage объекты в блок using Для чистоты эксперимента можно вынести методы/свойства Foo в отдельный интерфейс, который и возвращать из storage. Реализацию Foo оставить доступной только для storage, чтобы никто не мог переопределить методы. Решение скучное и предполагает изменение вызывающего кода. Зато в данном варианте процесс освобождения объектов становится очевидным. Ну и код будет выглядеть примерно так: using(var tmp0 = fooPool.GetFoo("tmp")) { //работаем с одним объектом } using(var tmp1 = fooPool.GetFoo("tmp")) { using(var tmp2 = fooPool.GetFoo("tmp")) { //работаем с двумя объектами } }

Комментариев нет:

Отправить комментарий