Думки вголос. Незворотне перекриття ідентифікаторів.
Явище перекриття1 (англ. overlapping) ідентифікаторів відоме ще з часів перших мов програмування. Мене турбує той факт що подібна проблема дожила до наших днів і зустрічається майже в кожній сучасній мові. Я вирішив присвятити цій темі окрему статтю, в якій і спробую викласти свої погляди на ситуацію.
Проблематика
Приклад коду №1
Змінна перекриває доступ до аргумента
function f(x = 1) {
var x = 2;
print(x); // 2
print(???); // як нам отримати х = 1?
}
В коді вище (див. Приклад коду №1) змінна х перекриває аргумент х. Після її оголошення попередній об'єкт втрачається назавжди2. І це проблема, адже однакові назви можуть бути зовсім не випадковими. Якщо подібне іменування буде нести деяке корисне навантаження, то механізм перекриття стане перешкодою.
Але чому не передбачено механізму для звернення до перекритої змінної? Чому взагалі перекриття дозволене? В такому вигляді від нього немає користі, адже змінні все одно доведеться перейменовувати якщо знадобиться одночасний доступ до обох.
Життя без перекриття
Існують мови позбавлені подібної вади. Використана в них стратегія не розвиває цей механізм, а позбувається від нього. Наприклад, в мові програмування C# перекриття заборонене3. Змінні повинні оголошуватись так, щоб до них можна було звернутись з будь-якого місця в програмі (див. Приклад коду №2). При цьому зберігається можливість перекривати поля класу при наслідуванні, а також аргументами функцій. Це не суперечить правилам, неоднозначності в цих випадках завжди можна вирішити (див. Приклад коду №3). Це дуже важливий момент, адже таке зворотне перекриття не несе в собі явних недоліків, лише переваги4.
Приклад коду №2
В C# спроба перекрити змінну приведе до помилки
int x = 5;
...
{
int x = 10; // помилка
}
Приклад коду №3
Зворотне перекриття поля класу аргументом в C#
class X {
int x;
public void F(int x)
{
print(x); // аргумент
print(this.x); // поле класу
}
}
Часткові рішення
В JavaScript, С++ та Python проблему перекриття вирішили частково. Перекриття локальних змінних залишається незворотнім процесом. А от звернення до глобальних об'єктів можна відрізнити за допомогою спеціальних операторів. В С++ для цього використовується оператор :: (див. Приклад коду №4). Щоб виконати таке в Python змінна повинна бути оголошена через ключове слово global (див. Приклад коду №5). А в JavaScript global виступає спеціальним об'єктом, який містить в собі усі глобальні об'єкти в якості полів (див. Приклад коду №6).
Приклад коду №4
Звернення до глобального об'єкта в С++ за допомогою оператора ::
int x = 5;

void f() {
int x = 10;
print(::x); // 5
}
Приклад коду №5
Робота з глобальною змінною в Python
x = 5

def f():
global x
x = 10

f()

print(x) # 10
Приклад коду №6
Звернення через глобальний об'єкт в JavaScript
var x = 5;

function f() {
var x = 10;
console.log(x); // 10
console.log(global.x); // 5
}

f();
Перелічені приклади ілюструють намагання боротись з цією проблемою. Але задіяні в них механізми розраховані лише на один рівень вкладення і перестають працювати для вкладень більших порядків (див. Приклад коду №7). Можливо саме через невелику актуальність подібних проблем цей підхід не був розвинутий5. Але ніколи не пізно це виправити!
Приклад коду №7
Оператор :: не спрацює для змінної х зі значенням 2
int x = 1;

void f()
{
int x = 2;
{
int x = 3
print(x); // 3
print(::x); // 1
print(???); // 2?
}
}
Звернення в порядку оголошення
Одна зі стратегій - це звернення до перекритих змінних через спеціальний префікс або ключове слово. Причому зі збільшенням порядку перекриття буде збільшуватись і число таких префіксів (див. Приклад коду №8). Такий підхід цілком природний і добре відомий з роботи з файловою системою. Цікаво те, що з такими можливостями взагалі немає сенсу вводити обмеження на унікальність ідентифікатора в межах області6 (див. Приклад коду №9).
Приклад коду №8
Звернення в порядку оголошення
int x = 1;

{
int x = 2;
{
int x = 3
print(x); // 3
print(../x); // 2
print(../../x); // 1
}
}
Приклад коду №9
Звернення до послідовно оголошених об'єктів в межах однієї області
int x = 1;
int x = 2;
int x = 3;

print(x); // 3
print(../x); // 2
print(../../x); // 1
Звернення через зовнішню область
Існує цікавий механізм, який дозволяє боротись з перекриттям полів при наслідуванні. Ідея в тому, що звернення відбувається не напряму, а через ім'я відповідного класу (див. Приклад коду №10). Такий підхід досить розповсюджений і його можна було б розвинути. Наприклад, аргументи функції можуть бути отримані через однойменний префікс (див. Приклад коду №11).
Приклад коду №10
Звернення до перекритих полів в С++
struct A
{
int x = 1;
};

struct B: public A
{
int x = 2;
void F()
{
print(A::x); // 1
print(x); // 2
}
};
Приклад коду №11
Звернення до перекритих аргументів функції
void f(int x = 1)
{
int x = 2;
print(x); // 2
print(f.x); // 1
}
Те саме можна застосувати і до циклів, якщо дозволити їх іменування (див. Приклад коду №12). Окремо варто зазначити, що це також вирішило б проблему виходу з декількох циклів (див. Приклад коду №13), оскільки команда break теж виступає заручником механізму перекриття.
Приклад коду №12
Звернення до перекритої змінної циклу
for row (int index = 0; index < 10; ++index)
{
for column (int index = 0; index < 10; ++index)
{
print( matrix[row.index][column.index] );
}
}
Приклад коду №13
Звернення до інструкції break зовнішнього циклу
for outer (int i = 0; i < 10; ++i)
{
for inner (int j = 0; j < 10; ++j)
{
outer.break; // вихід з зовнішнього циклу
}
}
Висновки
Існуюча реалізація перекриття обмежена і позбавлена переваг. Але її все ж можна розвинути, особливо приймаючи до уваги існуючі рішення схожих проблем. Запропоновані у цій статті підходи інтуїтивно зрозумілі та мають відносно просту реалізацію. В залежності від дизайну мови їх можна використовувати не лише окремо, але й комбіновано.
1
У цій статті я буду вживати термін перекриття замість звичного приховування.
2
Саме тому я називаю це перекриття незворотнім.
3
Характерно те, що судячи зі всього розробники мови явно знали що роблять. При спробі перекрити існуючий об'єкт виводиться попередження, що це недопустимо адже може спричинити неоднозначність.
4
Принаймні це можна назвати додатковими можливостями.
5
Варто зауважити і відсутність єдиного підходу до вирішення проблеми. На відміну від часто вживаних засобів, типу звернення до полів об'єкта через крапку або виклик функції через круглі дужки, тут немає єдиного загальноприйнятого стандарту.
6
Це працює лише для впорядкованих областей, по типу блоків чи тіла функції. Для просторів імен або класів цей підхід незастосовний.