Найти в Дзене
ZDG

Как новичку-программисту не запутаться в именах переменных

Речь в этом выпуске пойдет о разных языках программирования, так как правила по большей части одни и те же. Но начнём с примера на Python.

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

Нет, это не плохой язык. Просто ВСЕ языки программирования трудные для начинающих. Можно сколько угодно говорить о простом синтаксисе и как быстро можно написать первую программу, но начинающий столкнётся с проблемами буквально сразу же.

Вот условный код:

Тут всё правильно, но редактор, в котором новичок пишет программу, чересчур умный и начинает делать замечания про "теневые имена". Что здесь не так?

Есть внешняя (глобальная) переменная с именем x, и есть параметр функции с именем x. У них одинаковые имена, но это разные сущности. Редактор предупреждает, что используя в качестве параметра функции имя "x", вы "затеняете" (делаете недоступным) другое имя "x" глобальной переменной.

Это абсолютно стандартная, не страшная ситуация, но что делает новичок? Он видит какое-то непонятное предупреждение, он сразу начинает бояться, и лучший выход, который он находит – изменить имя параметра функции:

-2

Редактор перестаёт ругаться, новичок успокаивается, но программа теперь работает неправильно. Потому что новичок изменил имя параметра в скобках, но не изменил его в самой функции.

Он не забыл это сделать. Он просто не видит связи между всеми этими именами. Это всё какие-то буквы, в которых он плавает, где-то угадывая их значение, а где-то нет. А возникающие ошибки пытается решить методом тыка.

Чтобы такого не происходило, надо раз и навсегда, отложив все другие вопросы обучения, уяснить, как и что работает.

Что такое имя переменной?

Имя для нас выглядит как самостоятельная сущность, в виде буквы "x", допустим. У этого имени может быть значение, например, 5.

На самом деле никаких имён в программе нет. Есть память. У памяти есть адреса, куда мы кладём данные и откуда мы читаем данные. Большая часть программы это просто инструкции "возьми данные отсюда и положи сюда".

Вот чтобы ориентироваться, где "отсюда", и где "сюда", мы даём им (этим адресам в памяти) условные имена. Чтобы вы понимали, что адрес и имя не одно и то же:

  • одним именем в течение жизни программы могут называться разные адреса памяти
  • один адрес в памяти может иметь несколько разных имён одновременно

То, чем манипулирует программа – всегда адреса в памяти. Каждый адрес – конкретен и уникален. Имена же, в зависимости от контекста, могут означать совершенно разные вещи. То есть, когда мы видим имя, нужно понимать, с каким адресом оно связано в данный конкретный момент в этой строчке кода. Потому что в другой строчке оно может указывать уже на что-то другое.

Например, у вас дома кого-то могут звать Андрей. И на работе у вас тоже есть кто-то по имени Андрей. Это одно и то же имя. Но когда вы находитесь дома и говорите "Андрей", то вы имеете в виду именно того Андрея, который у вас дома. А если нет, то вам придётся уточнить – "Андрей с работы". На работе же будет всё наоборот. Ваш дом – это контекст. В этом контексте имя Андрей указывает на одну сущность. А в контексте работы оно указывает уже на другую.

Это пример, когда одно имя указывает на две разные сущности.

Далее, когда человек дома, его зовут по имени, а на работе могут позвать по фамилии. Имя и фамилия – разные, но человек – один и тот же. Это пример, когда на одну сущность указывают разные имена, и опять же они зависят от контекста.

Наконец, если группа преступников идёт грабить банк, они договориваются звать друг друга кличками, придуманными специально для этого ограбления. В таком случае кличка "мистер Розовый" будет указывать на конкретную сущность только в контексте ограбления банка. После того, как ограбление завершилось, кличка перестаёт существовать. Покинув этот контекст, преступники снова зовут друг друга обычными именами.

Reservoir Dogs
Reservoir Dogs

Поэтому вся работа с именами сводится к пониманию того, на какую сущность они указывают в конкретный момент.

Контексты имён

Где в программе мы можем создавать переменные и давать им имена?

  1. В основном коде, то есть вне функций, циклов, условных блоков, объектов. Это глобальный контекст. Имена, которые созданы там, существуют, пока живёт программа, и (как правило) видны из любого локального контекста. Есть исключения. К примеру, в языке Java вся программа уже сразу находится внутри функции, поэтому создать такие глобальные переменные просто невозможно.
  2. Внутри блока. Блоком в программе считается последовательность инструкций, отделённая скобками {}, или снабженных отступом, как в Python. Имена, объявленные внутри блока, в некоторых языках являются локальными, то есть существуют только внутри блока.
  3. Внутри функции или в методе объекта. Это то же самое, что внутри блока, но на этот раз нет никаких сомнений, что эти имена исключительно локальные.
  4. Свойство объекта. Имя свойства объекта привязано к самом объекту, поэтому его нельзя спутать ни с чем.
  5. Параметры функции или метода. Это в чистом виде клички грабителей банка. Они существуют только в контексте функции.

Правила поиска имён

Общий принцип – матрёшка. Контексты вложены друг в друга. Самый большой контекст это основная программа. Внутри него вложены функции или методы. Внутри них могут быть вложены ещё функции, а внутри них блоки – условия, циклы и т.д. Транслятор языка, встретив имя переменной, начинает поиск – что это за имя и откуда оно взялось? И поиск всегда начинается от локального контекста, то есть от той матрёшки, где мы находимся в данный момент.

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

Имя переменной, созданной во внутреннем контексте, перекрывает собой такое же имя переменной, созданной во внешнем.

Рассмотрим пример на языке C:

-4

Во-первых, вся программа выполняется в функции main() – это уже локальный контекст, но вне этой функции мы можем завести глобальную переменную int a = 5;

Теперь внутри функции main() мы проверяем условие: if (a == 5)

Встретив имя "а", компилятор начинает его искать. Оно есть в функции? Нет. Оно есть снаружи функции? Да. Значит, имя "a" сейчас указывает на адрес памяти, зарезервированный инструкцией int a = 5, и в этом адресе сейчас хранится 5.

Значит условие if (a == 5) даёт нам результат "истина", и мы начинаем новый блок. Это ещё один уровень вложенности. В этом блоке мы заводим новую переменную: int a = 10, и еще одну новую переменную int b = 1. Мы печатаем значения переменных, и компилятор начинает искать, где объявлены имена "a" и "b". Они объявлены прямо в этом блоке, поэтому компилятор сразу их находит и понимает, что это адреса памяти, в которых хранится 10 и 1.

Затем блок заканчивается, и адреса, где хранились числа 10 и 1, освобождаются. Также "забываются" локальные имена "a" и "b".

Затем мы опять печатаем значения переменных "a" и "b". На этот раз "a" находится снаружи и печатается как 5, потому что других "a" здесь уже нет. А вот строчка, где печатается "b", закомментирована, потому что иначе при компиляции случится ошибка: переменной b в этом месте не существует. Она была объявлена только внутри блока if ().

Теперь пример на языке JavaScript:

-5

Тут в общем-то всё абсолютно то же самое, но есть нюанс:

Переменная, объявленная с ключевым словом var, всегда создаётся на уровне функции или на глобальном уровне, даже если объявлена внутри блока. То есть после выхода из блока она продолжает существовать. А вот переменная с ключевым словом let создаётся и существует только внутри блока.

Теперь на PHP:

-6

Здесь всё несколько не так. Чтобы получить доступ к переменной $a снаружи, необходимо внутри функции написать global $a. Иначе транслятор попросту не будет ничего искать снаружи функции. А так как внутри функции он тоже не найдёт ничего с именем $a, то случится ошибка. Значит, неопределённость имени уже как бы заранее исключена. Либо мы вообще не смотрим наружу, либо смотрим туда явно.

Далее, присваивание внутри блока $a = 10 изменит внешнюю переменную $a. Потому что, как и было определено, это имя показывает на внешнюю переменную. А переменная $b = 1 создастся внутри блока, но будет существовать и вне его, на уровне функции test() (аналог var в JavaScript). Но – существовать вне блока она будет только в том случае, если выполнилось условие if () и следовательно выполнился сам блок, где создаётся $b. Иначе – получим ошибку, что переменной $b не существует.

Теперь Python:

-7

Ситуация здесь полностью аналогична PHP, и даже ключевое слово global то же.

Как видим, в языках, где синтаксис не отличает создание и присваивание переменной (без ключевых слов int, var, let и др.), контроль над контекстом существования переменной не такой тонкий. Но тем не менее принцип матрёшки поддерживается везде.

Но мы так и не обсудили заглавный вопрос, а именно использование имён переменных в аргументах функции. Всё, что было написано, является предварительной подготовкой, а аргументы функции мы обсудим в следующем выпуске.

Читайте дальше: Окончание

Читайте также:

  • Типы данных и указатели
  • Глобальные и локальные переменные
Наука
7 млн интересуются