В комментариях к статье в очередной раз услышал мнение, что поскольку невозможно в принципе гарантировать отсутствие циклических ссылок при статическом анализе кода, то в языке Rust такие утечки памяти считаются безопасными, так как это, не влияет на безопасное управление памятью.
При работе с памятью существует много типов ошибок, но единственные ошибки, которые до сих пор не имеют нормального способа решения, это утечки памяти из-за циклических ссылок, когда два или более объектов напрямую или косвенно ссылаются друг на друга, в результате чего доступная приложению оперативная память постепенно уменьшается, так как ее невозможно освободить автоматически.
Утечки памяти из-за циклических ссылок являются наиболее сложными для анализа. В то время как для всех остальных типов ошибок при работе с памятью уже найдены и используются различные решения, например на уровне языка программирования, с помощью сборщиков мусора, проверки заимствований или шаблонов библиотек, то проблема утечек памяти из-за циклических ссылок остается нерешенной и по сей день.
Но мне кажется, что есть очень простой способ устранить циклические ссылки в программе, который можно реализовать практически в любом типизированном языке программирования, конечно, если при этом не использовать все разрешающее ключевое слово unsafe для Rust или оператор reinterpret_cast в случае С++.
В чем изначальная проблема?
Чтобы понять, почему проблема циклических ссылок до сих пор не была решена, следует пояснить, откуда эта проблема вообще взялась.
Если говорить про серьезные языки программирования, которые предназначенные для разработки коммерческих приложений, то обязательным и безусловным требованием для них будет возможность повторения любой из уже существующих структур данных и алгоритмов, которые были придуманы и реализованы до этого ранее. И одной из таких базовых структур данных является связный список.
И так как каждый элемент связного списка должен хранить как минимум одну ссылку на точно такой же тип данных, вот тут и возникает необходимость наличия рекурсивных (циклических) ссылок, а вместе с ними и потенциальные утечки памяти.
А так как связные списки могут создаваться динамически (во время выполнения приложения), то в общем случае невозможно проанализировать логику возникновения зависимостей и доказать отсутствие циклических ссылок с помощью статического анализатора кода во время компиляции.
Как статически доказать отсутствие циклических ссылок в программе?
Если вспомнить теорию алгоритмов и структуры данных, то концепция рекурсивных ссылок у элемента на точно такой же тип данных используется очень-очень давно. Однако с той поры теория и практика языков программирования продвинулась очень далеко. Разработано множество концепций, с помощью которых удается решать многие проблемы автоматически, которые изначально приходилось программировать полностью вручную (из-за чего и происходит много ошибок).
Например, для автоматического освобождения ресурсов была придумана концепция RAII, для решения проблемы циклических ссылок были придуманы и успешно используются сильные и слабые ссылки.
Постойте, но раз проблема циклических ссылок решается с помощью сильных и слабых указателей, то почему эта проблема до сих пор существует в языках программирования??
Ведь проблему утечки памяти из-за циклических ссылок очень просто решить путем запрета на определение сильных рекурсивных ссылок в компиляторе на уровне типов (структур) данных.
Да, в этом случае классический связной список сделать не получится. Для связного списка в новой парадигме потребуется отдельный контейнер для хранения элементов списка с использованием сильных ссылок, так как самим элементам списка будет запрещено иметь сильные ссылки на свой собственный тип. Конечно, такая реализация связного списка потребуется немного больше памяти, но зато она в принципе исключит возможность создания циклических зависимостей!
Но самое главное заключается в том, что анализ типов на возможные циклические зависимости легко сделать статически во время компиляции исходного кода приложения (например, вот так). В этом случае становится ненужным анализ графа выполнения кода на предмет возникновения циклических ссылок, так как они будут запрещены компилятором на уровне типов(структур) данных!
При реализации такого подхода скорее всего будет нарушена обратная совместимость с некоторыми структурами данных, но зато при использовании сильных и слабых указателей в комбинация с концепцией RAII, сборщики мусора становятся вообще ненужными! А это может оказать очень полезным для таких языков, как Java, Go или типизированного Python, (а скорее всего можно будет отказать и от проверки заимствований).