Pull to refresh

Слабые ссылки в различных языках программирования

Reading time 6 min
Views 38K
В языках с автоматическим управлением памятью сборщик мусора удаляет объекты, когда они перестают быть доступными по ссылкам. Обычно это именно то, что нужно: объект существует, пока есть возможность к нему обратиться.
Иногда такое поведение не подходит. Например, программе понадобилось хранить некоторую вспомогательную информацию об экземплярах некоторого класса, но у вас нет возможности добавить в этот класс своё поле. В этом случае можно создать отображение, в котором ключом будет объект, а значением — вспомогательная информация.
Вот тут-то и начинаются проблемы. Поскольку отображение хранит ссылки на ключи, те объекты, к которым была привязана вспомогательная информация, перестают освобождаться из памяти. Если программа в процессе своей работы создаёт много объектов, память рано или поздно заканчивается.
Избежать подобных проблем помогают слабые ссылки. Слабые ссылки отличаются от обычных тем, что не препятствуют удалению объекта из памяти. Когда память, занимаемая объектом, освобождается, все указывающие на него слабые ссылки обнуляются. В некоторых реализациях в этом случае также вызывается установленный пользователем обработчик — финализатор.
В приведённом выше примере нужно заменить обычное отображение на отображение, которое хранит слабые ссылки на ключи. Когда ключ исчезает, вместе с ним автоматически исчезает и запись в отображении, после чего значение тоже может быть освобождено.
Другой пример — функция путём сложных вычислений получает большое значение по набору параметров. Чтобы не вычислять одно и то же несколько раз, можно сделать кеш вычисленных значений. Если значение является объектом, то нет никакого смысла удалять его из кеша, пока на него есть ссылки. Здесь можно в качестве кеша использовать отображение со слабыми ссылками на значения, а не ключи.
Вернёмся к примеру со вспомогательной информацией. Иногда бывает, что вспомогательная информация — это объект, и он содержит ссылку на ключ. Отображение со слабыми ссылками на ключи перестаёт работать: поскольку ссылка на значение не слабая, а у значения есть ссылка на ключ, ключ получается всегда достижимым по обычным ссылкам, и память не освобождается. Такую проблему можно решить, сделав ссылку из значения тоже слабой.
Но в некоторых языках такую проблему можно решить с помощью особого вида слабых ссылок: эфемерона. Эфемерон поддерживает ссылки на два объекта: ключ и значение. Пока ключ достижим по обычным ссылкам, ни ключ, ни значение не удаляются. Если ключ перестаёт быть достижим, то обе ссылки обнуляются, и ключ освобождается. Вся соль в том, что ссылки на ключ из значения не учитываются при определении достижимости, поэтому если в примере выше заменить отображение со слабыми ссылками на ключи на отображение, использующее эфемероны, то всё опять заработает.
С точки зрения сборщика мусора присутствие эфемерона создаёт слабую ссылку из эфемерона в ключ и сильную ссылку из ключа в значение. В некоторых языках, таких как Haskell, также возникает сильная ссылка из ключа в сам эфемерон, благодаря чему эфемерон продолжает существовать, пока существует ключ, что позволяет не сохранять ссылку на эфемерон.

C#


В C# слабые ссылки реализованы двумя классами: WeakReference и WeakReference<T> в пространстве имён System. Эти классы отличаются тем, что второй из них является generic-типом, то есть позволяет указать тип объекта, на который указывает ссылка, кроме того, у них разные интерфейсы для обращения к объекту, на который указывает ссылка: у первого из них для этого используются свойства IsAlive и Target, а у второго — методы TryGetTarget и SetTarget.
В отличие от других языков, C# позволяет изменять то, на какой объект указывает слабая ссылка, после её создания. Также у слабых ссылок есть конструктор, который принимает дополнительный параметр типа bool: если его значение равно false, то создаётся обычная слабая ссылка, а если true, то ссылка будет вести себя, как PhantomReference в Java. В C# у слабых ссылок нет финализаторов, как и других способов быстро обнаружить, что одна из множества слабых ссылок обнулилась.

C++


В C++ за слабые ссылки отвечает класс std::weak_ptr, который рассчитан на использование совместно с классом std::shared_ptr. Чтобы работать с объектом, на который указывает слабая ссылка, нужно вначале получить сильную ссылку с помощью метода lock.

Пример кода:
#include <iostream>
#include <memory>

using namespace std;

void print_weak(weak_ptr<int> w) {
	if (auto s = w.lock()) {
		cout << *s << endl;
	} else {
		cout << "Объект больше не достижим" << endl;
	}
}

int main(int argc, char *argv[]) {
	shared_ptr<int> s(new int(123));
	weak_ptr<int> w(s);
	print_weak(w);
	s = NULL;
	print_weak(w);
}


Haskell


В отличие от многих других языков, в Haskell поддерживаются эфемероны. Все слабые ссылки имеют тип Weak, который определён в модуле System.Mem.Weak. В Haskell слабая ссылка будет существовать как минимум до тех пор, пока ключ достижим по ссылкам, поэтому можно создать слабую ссылку с финализатором, «забыть» о ней, и финализатор всё равно будет вызван. Функция addFinalizer именно это и делает. Чтобы создать слабую ссылку, есть функции mkWeak и mkWeakPtr.

Пример кода:
import Control.Exception
import System.Mem.Weak

main = do
	l <- getLine
	mkWeakPtr l (Just $ putStrLn "Объект больше не достижим")
	evaluate $ sum [1..100000]


Java


В Java слабая ссылка — это отдельный объект, и для того, чтобы работать с объектом, на который указывает слабая ссылка, нужно вначале получить обычную ссылку с помощью метода get. Существует целых 3 вида слабых ссылок: «мягкие» ссылки (класс SoftReference в пакете java.lang.ref), обычные слабые ссылки (WeakReference) и «фантомные» ссылки (PhantomReference). Отличаются они тем, когда сборщик мусора будет освобождать объекты, достижимые по таким ссылкам. Если объект достижим по «мягким» ссылкам, то он будет освобождён, только если в куче мало свободного места. Такие ссылки хорошо подходят для всякого рода кешей. Обычные слабые ссылки отличаются от «фантомных» тем, когда ссылка обнуляется: первые обнуляются до вызова финализатора, а вторые — после, непосредственно перед освобождением памяти. Чтобы предотвратить доступ к финализованным объектам, функция чтения для PhantomReference всегда возвращает null. Единственный способ получить них пользу — использовать очереди ссылок.
Если в программе используется много слабых ссылок, то проверять каждую из них, не обнулилась ли она, может быть накладно. Java не позволяет задавать финализаторы для слабых ссылок, а вместо этого можно создать специальный объект — очередь ссылок (ReferenceQueue) — и настроить слабые ссылки так, чтобы они сами себя туда добавляли при обнулении. Теперь можно проверять не все ссылки, а только одну очередь.
Также в Java есть класс WeakHashMap — аналог HashMap со слабыми ссылками на ключи.

Пример кода:
import java.lang.ref.*;

class WeakRefTest implements Runnable {

	// Выводит сообщение, когда сборщик мусора
	// узнаёт, что объект перестал быть достижимым.
	public static void printWhenUnreachable(Object obj) {
		ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
		WeakReference<Object> ref = new WeakReference<Object>(obj, queue);
		new Thread(new WeakRefTest(ref, queue)).start();
	}

	private final WeakReference<Object> ref;
	private final ReferenceQueue<Object> queue;

	private WeakRefTest(WeakReference<Object> ref, ReferenceQueue<Object> queue) {
		this.ref = ref;
		this.queue = queue;
	}

	public void run() {
		try {
			queue.remove();
			System.out.println("Объект больше не достижим");
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
}

Заметьте, что у класса WeakRefTest есть поле ref, которое не используется. Оно нужно для того, чтобы сама слабая ссылка не была освобождена раньше времени, иначе она не сработает.

Lua


Lua поддерживает слабые ссылки посредством «слабых таблиц»: в соответствующей метатаблице ключ __mode можно установить в строковое значение, и тогда, если оно содержит символ k, то ссылки на ключи будут слабыми, а если оно содержит символ v, то слабыми будут ссылки на значения. Тип таблицы можно менять после её создания, однако изменения вступят в силу только при следующей сборке мусора. Как и в других реализациях таблиц со слабыми ссылками, при обнулении ключа или значения вся запись удаляется.
Если ссылки на ключи слабые, а на значения сильные, то записи в таблице будут вести себя, как эфемероны: ссылка из значения на ключ не будет препятствовать удалению записи.

Perl


В Perl любую ссылку можно сделать слабой с помощью метода weaken из пакета Scalar::Util. В отличие от других языков, если скопировать слабую ссылку, копия будет обычной ссылкой.

Python


Python поддерживает слабые ссылки, но их можно создавать только на экземпляры тех классов, которые поддерживают это. К ним относятся большинство пользовательских классов и некоторые встроенные. Кроме того, в Python есть 2 класса для слабых ссылок: из экземпляров класса ref (пакет weakref) обычную ссылку на объект нужно получать явно, а с экземплярами класса proxy можно работать напрямую.
При создании слабой ссылки можно указать финализатор. Также можно для любого объекта получить список слабых ссылок на него (обычных, кстати, тоже).
В отличие от Java, в Python есть целых 3 типа коллекций со слабыми ссылками: WeakSet, WeakKeyDictionary и WeakValueDictionary.

Пример кода:
# -*- coding: utf8 -*-

from weakref import *

def finalizer(ref):
	print("Объект больше не достижим")

obj = set()
reference = ref(obj, finalizer)
del obj

Как и в Java, если ссылка удаляется раньше объекта, финализатор не вызывается.

Ruby


В Ruby слабые ссылки реализованы в единственном классе WeakRef, который ведёт себя по аналогии с классом proxy в Python, то есть со слабой ссылкой можно работать так же, как и с самим объектом. В дополнение к методам самого объекта у слабых ссылок есть метод weakref_alive?, который возвращает true, если объект, на который указывает ссылка, всё ещё существует, иначе false.

UPD: отсортировал языки по алфавиту, добавил C# и Lua, более подробно описал поведение эфемеронов.
Tags:
Hubs:
+42
Comments 8
Comments Comments 8

Articles