Речь в этом выпуске пойдет о разных языках программирования, так как правила по большей части одни и те же. Но начнём с примера на Python.
Я сейчас на живом примере могу наблюдать миф о том, какой это "лёгкий язык для начинающих".
Нет, это не плохой язык. Просто ВСЕ языки программирования трудные для начинающих. Можно сколько угодно говорить о простом синтаксисе и как быстро можно написать первую программу, но начинающий столкнётся с проблемами буквально сразу же.
Вот условный код:
Тут всё правильно, но редактор, в котором новичок пишет программу, чересчур умный и начинает делать замечания про "теневые имена". Что здесь не так?
Есть внешняя (глобальная) переменная с именем x, и есть параметр функции с именем x. У них одинаковые имена, но это разные сущности. Редактор предупреждает, что используя в качестве параметра функции имя "x", вы "затеняете" (делаете недоступным) другое имя "x" глобальной переменной.
Это абсолютно стандартная, не страшная ситуация, но что делает новичок? Он видит какое-то непонятное предупреждение, он сразу начинает бояться, и лучший выход, который он находит – изменить имя параметра функции:
Редактор перестаёт ругаться, новичок успокаивается, но программа теперь работает неправильно. Потому что новичок изменил имя параметра в скобках, но не изменил его в самой функции.
Он не забыл это сделать. Он просто не видит связи между всеми этими именами. Это всё какие-то буквы, в которых он плавает, где-то угадывая их значение, а где-то нет. А возникающие ошибки пытается решить методом тыка.
Чтобы такого не происходило, надо раз и навсегда, отложив все другие вопросы обучения, уяснить, как и что работает.
Что такое имя переменной?
Имя для нас выглядит как самостоятельная сущность, в виде буквы "x", допустим. У этого имени может быть значение, например, 5.
На самом деле никаких имён в программе нет. Есть память. У памяти есть адреса, куда мы кладём данные и откуда мы читаем данные. Большая часть программы это просто инструкции "возьми данные отсюда и положи сюда".
Вот чтобы ориентироваться, где "отсюда", и где "сюда", мы даём им (этим адресам в памяти) условные имена. Чтобы вы понимали, что адрес и имя не одно и то же:
- одним именем в течение жизни программы могут называться разные адреса памяти
- один адрес в памяти может иметь несколько разных имён одновременно
То, чем манипулирует программа – всегда адреса в памяти. Каждый адрес – конкретен и уникален. Имена же, в зависимости от контекста, могут означать совершенно разные вещи. То есть, когда мы видим имя, нужно понимать, с каким адресом оно связано в данный конкретный момент в этой строчке кода. Потому что в другой строчке оно может указывать уже на что-то другое.
Например, у вас дома кого-то могут звать Андрей. И на работе у вас тоже есть кто-то по имени Андрей. Это одно и то же имя. Но когда вы находитесь дома и говорите "Андрей", то вы имеете в виду именно того Андрея, который у вас дома. А если нет, то вам придётся уточнить – "Андрей с работы". На работе же будет всё наоборот. Ваш дом – это контекст. В этом контексте имя Андрей указывает на одну сущность. А в контексте работы оно указывает уже на другую.
Это пример, когда одно имя указывает на две разные сущности.
Далее, когда человек дома, его зовут по имени, а на работе могут позвать по фамилии. Имя и фамилия – разные, но человек – один и тот же. Это пример, когда на одну сущность указывают разные имена, и опять же они зависят от контекста.
Наконец, если группа преступников идёт грабить банк, они договориваются звать друг друга кличками, придуманными специально для этого ограбления. В таком случае кличка "мистер Розовый" будет указывать на конкретную сущность только в контексте ограбления банка. После того, как ограбление завершилось, кличка перестаёт существовать. Покинув этот контекст, преступники снова зовут друг друга обычными именами.
Поэтому вся работа с именами сводится к пониманию того, на какую сущность они указывают в конкретный момент.
Контексты имён
Где в программе мы можем создавать переменные и давать им имена?
- В основном коде, то есть вне функций, циклов, условных блоков, объектов. Это глобальный контекст. Имена, которые созданы там, существуют, пока живёт программа, и (как правило) видны из любого локального контекста. Есть исключения. К примеру, в языке Java вся программа уже сразу находится внутри функции, поэтому создать такие глобальные переменные просто невозможно.
- Внутри блока. Блоком в программе считается последовательность инструкций, отделённая скобками {}, или снабженных отступом, как в Python. Имена, объявленные внутри блока, в некоторых языках являются локальными, то есть существуют только внутри блока.
- Внутри функции или в методе объекта. Это то же самое, что внутри блока, но на этот раз нет никаких сомнений, что эти имена исключительно локальные.
- Свойство объекта. Имя свойства объекта привязано к самом объекту, поэтому его нельзя спутать ни с чем.
- Параметры функции или метода. Это в чистом виде клички грабителей банка. Они существуют только в контексте функции.
Правила поиска имён
Общий принцип – матрёшка. Контексты вложены друг в друга. Самый большой контекст это основная программа. Внутри него вложены функции или методы. Внутри них могут быть вложены ещё функции, а внутри них блоки – условия, циклы и т.д. Транслятор языка, встретив имя переменной, начинает поиск – что это за имя и откуда оно взялось? И поиск всегда начинается от локального контекста, то есть от той матрёшки, где мы находимся в данный момент.
Если имя переменной нашлось в этой матрёшке, значит переменная была создана локально в этой матрёшке. Она и будет использована. Вне этой матрёшки может существовать переменная с таким же именем, но мы до неё просто не дошли. Если же локально ничего не найдено, тогда поиск ведётся в матрёшке выше уровнем, и так до тех пор, пока мы не попадём в самый внешний контекст.
Имя переменной, созданной во внутреннем контексте, перекрывает собой такое же имя переменной, созданной во внешнем.
Рассмотрим пример на языке C:
Во-первых, вся программа выполняется в функции 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:
Тут в общем-то всё абсолютно то же самое, но есть нюанс:
Переменная, объявленная с ключевым словом var, всегда создаётся на уровне функции или на глобальном уровне, даже если объявлена внутри блока. То есть после выхода из блока она продолжает существовать. А вот переменная с ключевым словом let создаётся и существует только внутри блока.
Теперь на PHP:
Здесь всё несколько не так. Чтобы получить доступ к переменной $a снаружи, необходимо внутри функции написать global $a. Иначе транслятор попросту не будет ничего искать снаружи функции. А так как внутри функции он тоже не найдёт ничего с именем $a, то случится ошибка. Значит, неопределённость имени уже как бы заранее исключена. Либо мы вообще не смотрим наружу, либо смотрим туда явно.
Далее, присваивание внутри блока $a = 10 изменит внешнюю переменную $a. Потому что, как и было определено, это имя показывает на внешнюю переменную. А переменная $b = 1 создастся внутри блока, но будет существовать и вне его, на уровне функции test() (аналог var в JavaScript). Но – существовать вне блока она будет только в том случае, если выполнилось условие if () и следовательно выполнился сам блок, где создаётся $b. Иначе – получим ошибку, что переменной $b не существует.
Теперь Python:
Ситуация здесь полностью аналогична PHP, и даже ключевое слово global то же.
Как видим, в языках, где синтаксис не отличает создание и присваивание переменной (без ключевых слов int, var, let и др.), контроль над контекстом существования переменной не такой тонкий. Но тем не менее принцип матрёшки поддерживается везде.
Но мы так и не обсудили заглавный вопрос, а именно использование имён переменных в аргументах функции. Всё, что было написано, является предварительной подготовкой, а аргументы функции мы обсудим в следующем выпуске.
Читайте дальше: Окончание
Читайте также:
- Типы данных и указатели
- Глобальные и локальные переменные