Pull to refresh

Возможное решение проблемы ссылок в языках программирования

Level of difficultyMedium
Reading time5 min
Views4.8K


Любому программисту знакомо понятие "ссылка". Под этим термином обычно понимают небольшой объект, главная задача которого обеспечить доступ к другому объекту, физически расположенному в другом месте. Из-за этого ссылки удобно использовать, они легко копируются, и с их помощью очень просто получить доступ к объекту, на который эта ссылка ссылается и можно получить доступ к одним и тем же данным из разных мест программы.


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


Тем не менее, ссылки в той или иной форме поддерживаются во всех языках программирования, хотя под этим термином часто подразумеваются не полностью эквивалентные термины. Например, под словом "ссылка" можно понимать ссылку как адрес в памяти (как в С++) и ссылку, как указатель на объект (как в Python или Java).


Хотя встречаются языки программирования, которые пытаются решать данные проблемы за счет концепции "владения" (Rust, Аргентум или NewLang). О возможном решении этих и других имеющихся проблем со ссылками далее и пойдет речь.


Какие ссылки бывают?


Например, в языке C есть указатели, но работать с ними не очень удобно и одновременно очень опасно из-за наличия адресной арифметики (возможности напрямую изменять адрес указателя на данные в памяти компьютера). В C++ появилась отдельная сущность — reference, а в C++11 ссылки получили дальнейшее развитие, появились rvalue-ссылки.


Тогда как в С++ существует сразу несколько видов ссылок, то разработчики Python наверно специально попробовали "упростить" работу со ссылками и вообще отказались от них. Хотя де факто в Python каждый объект является ссылкой, но некоторые типы (простые значения) автоматически передаются "по значению", тогда как сложные типы (объекты) всегда "по ссылке".


Циклические ссылки


Еще существует глобальная проблема циклический (круговых) ссылок, которая затрагивает практически все языки программирования (когда объект прямо или через несколько других объектов указывает на самого себя). Часто разработчикам языка (в первую языков со сборщиками мусора) приходится идти на различные алгоритмические ухищрения, чтобы почистить пул созданных объектов от подобных "зависших" и циклических ссылок, хотя обычно эту проблему отдают на откуп разработчикам, например, в С++ есть сильные (std::shared_ptr) и слабые (std::weak_ptr) указатели.


Неоднозначная семантика ссылок


Еще не менее важной, но часто игнорируемой проблемой ссылок является семантика языка для работы с ними. Например в С/С++ для обращения к данным по ссылке и по значению используются отдельные операторы звездочка "*", стрелочка "->" и точка ".". Но работа с reference переменными в С++ происходит как с обычными переменными "по значению", хотя по факту это не так. Конечно при явной типизации С++, компилятор не даст ошибиться, но при обычном чтении кода, у вас не получится отличить reference переменную от обычной "по значению".


А вот в Python можно очень легко попутать как будет происходить передача переменой в качестве аргумента функции, по значению или по ссылке. Так как это зависит от самих данных, которые содержатся в переменной. Особой пикантности тут добавляет тот факт, что Python является языком программирования с динамической неявной типизацией, и в общем случае заранее не известно, какое значение хранится в переменной.


Кто виноват и что делать?


Мне кажется, что основная причина, по крайней мере неоднозначной семантики, это постоянный рост сложности инструментов разработки, и как следствие — усложнение и доработка синтаксиса языков программирования под новые концепции и возможности с сохранением обратной совместимости со старым legacy кодом.


А что если начать с чистого листа? Вот например, универсальная концепция управления объектами и ссылками на объекты, которая не требует от пользователя (программиста) ручного управления памятью, для которой не нужен сборщик мусора, а ошибки при работе с памятью и ссылками на объекты становятся невозможны за счет полного контроля управления памятью еще на этапе компиляции исходного кода приложения!


Термины:


  • Объект — данные в памяти компьютера в машинном (двоичном) представлении.
  • Переменная — человекочитаемый идентификатор в теле программы, который однозначно определяется по своему имени и идентифицирует объект (непосредственное значение объекта или ссылку на него). Переменные могут быть:
    • Переменная владелец — единственная постоянная ссылка на объект со счетчиком использования объекта (shared_ptr).
    • Переменная ссылка — временная ссылка на объект, которая не изменяет счетчик использования объекта (weak_ptr).

Возможные операции:


  • создание новой переменной-владельца и начальная инициализация значения объекта.
  • создание переменной-ссылки на существующую переменную-владельца.
  • присваивание нового значения объекту по имении переменой-владельца.
  • присваивание нового значения объекту на которую указывает переменная-ссылка.
  • присваивание переменной-ссылке, новой ссылки другую переменную-владельца.

Пример исходного кода и условные сокращения:


  • "&" — создание ссылки на переменную (создание слабого указателя)
  • "*" — обращение к данным объекта (захват ссылки — преобразование слабого указателя в сильный с увеличением счетчика ссылок)

# переменные - владелец
val1 := 1; 
val2 := 2;
# val1 = 1, а val2 = 2

val1 = val2; # Ошибка - владелец только один!
*val1 = *val2; # ОК - присваивается значение
# val1 = 2, а val2 = 2

# переменные - ссылки
ref1 := &val1;
ref2 := &val2;
# ref1 -> val1, а ref2 -> val2

ref1 = 3; # Ошибка - переменная является ссылкой!
*ref1 = 3; # ОК - значение присваивается "по ссылке"
# val1 = 3, а val2 = 2
# *ref1 = 3,  *ref2 = 2

*ref2 = *ref1;  # ОК - одно значение присваивается другому значению "по ссылке"
# val1 = 3, а val2 = 3
# *ref1 = 3,  *ref2 = 3

ref1 = ref2; # ОК - одна ссылка присваивается другой ссылке
# ref1 -> val2, а ref2 -> val2
# *ref1 = 3,  *ref2 = 3

*val2 = 5;
# *ref1 -> 5,  *ref2 -> 5

При таком синтаксисе все становится просто, наглядно и понятно. Но если я что-то где-то упустил, напишите пожалуйста в комментарии или в личном сообщении.


З.Ы.


Как в комментариях написал unreal_undead2 "в результате в коде будут сплошные звёздочки".


Поэтому как компромисс, звездочку при обращении к данным объекта можно не указывать, если переменная — владелец используются в выражении как rvalue, т.е.:


   *val1 = val2; # Так тоже можно

З.З.Ы.


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


class A {
  A ref;
};
A owner := A();
owner.ref := &owner;  # Циклическая ссылка :-)
# owner.*ref -> owner

A link := &owner;
# *link.ref  - reference field
# *link.*ref -> owner
Tags:
Hubs:
Total votes 5: ↑4 and ↓1+7
Comments103

Articles