Переменное затенение

затенение переменной происходит, когда переменная, объявленная в определенной области... имеет то же имя, что и переменная, объявленная во внешней области.

– Из «Переменное затенение» в Википедии

Рассмотрим этот код Эликсира:

defmodule Shadowing do
  x = 5
  def x, do: x
  def x(x = 0), do: x
  def x(x), do: x(x - 1)
end

Не запуская код, скажите мне, каковы возвращаемые значения этих трех вызовов функций:

  1. Shadow.x()
  2. Shadow.x(0)
  3. Shadow.x(2)

Нет, правда. Подумайте о коде на минуту.

Теперь… вы уверены, что ваши ответы верны?

Иногда x относится к функции. Иногда это относится к параметру функции. Когда-то это временная переменная, которая оценивается в контексте модуля во время компиляции. Значение x меняется в зависимости от контекста.

Перепривязка переменных

В Эликсире перепривязка происходит, когда вы присваиваете новое значение переменной, которую уже используете. Например:

def calculate_name do
  name = "Chloe"
  # Perform some calculations...
  name = "Charlotte"
  # Perform some calculations...
  name
end

Что в этом плохого?

Затенение переменных и повторное связывание переменных неоптимальны по нескольким причинам:

  1. Это требует, чтобы вы знали о правилах области видимости языка.
  2. Он связывает разные понятия с одним и тем же именем.

Правила определения области видимости

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

Должны ли поэтому разработчики ограничиваться хорошо известными функциями языка? Не обязательно. Однако часто легко распознать незнакомые языковые конструкции. Например, когда я увидел Array#abbrev в Ruby, я сразу определил его как метод, которого не знал. В этот момент достаточно просто прочитать документацию, чтобы узнать, что он делает.

Правила масштабирования не такие. Они скрыты. Когда вы читаете код, основанный на этих правилах, вы можете даже не осознавать, что ваши предположения о коде неверны. Ни один из кодов, на которых вы сосредоточены, не может сказать вам: «Вам нужно изучить это. Происходит нечто большее, чем вы думаете, и вы собираетесь ввести ошибку в систему».

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

Путаница с идентификатором

Вы когда-нибудь слышали, как кто-то произносит ваше имя только для того, чтобы понять, что говорящий обращается к кому-то другому? Тот факт, что у кого-то еще было то же имя, что и у вас, привел к запутанной ситуации. Это требует от вас больших усилий, когда два разных объекта используют один и тот же идентификатор.

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

Когда можно перепривязывать переменные

Можно повторно привязать значение к переменной, когда единственной целью переменной является накопление значения.¹ Например:

items = ["shoe"]
items = ["shirt" | items]
items = ["hat" | items]

Мы создаем список с одним элементом, а затем начинаем добавлять элементы в начало списка. Пример надуманный, и нам было бы проще создать список со всеми тремя элементами за один шаг. Однако бывают ситуации, когда вы будете создавать коллекцию с течением времени, и может иметь смысл присваивать значения одной и той же переменной несколько раз.

Вы можете сделать это и с другими типами аккумуляторов. Рассмотрим некоторый код, который подсчитывает игральные карты французской масти в колоде по масти:

card_count = length(hearts)
card_count = card_count + length(spades)
card_count = card_count + length(diamonds)
card_count = card_count + length(clubs)

Значение card_count меняется, но переменная всегда ссылается на одно и то же.

Если вы знакомы с Elixir, структура Plug.Conn часто действует как аккумулятор для информации о соединении.

Замечу, что в функциональной парадигме вы можете найти больше идиоматических способов накопления значений, чем в двух приведенных выше примерах.

Предложения

  1. Не давайте параметрам функций или переменным те же имена, что и другим функциям в вашем модуле.²
  2. Используйте () при вызове функции. Это дает понять, что вы не работаете с локальной переменной.
  3. Не привязывайте значения к переменной, если вы не используете аккумулятор.
  4. Если вы используете аккумулятор, не распространяйте код перепривязки по всей функции. Это затрудняет определение места изменения значения.

¹ Refactoring: Ruby Edition, ссылаясь на Передовые практики Smalltalk Кента Бека, называет это «сбором временных переменных».

  • Джей Филдс, Шейн Харви и Мартин Фаулер с Кентом Беком. Рефакторинг: Ruby Edition. Река Аппер-Сэдл, Нью-Джерси: Addison-Wesley, 2010. стр. 122.
    Кент Бек.
  • Рекомендации по использованию Smalltalk. Река Аппер-Сэдл, Нью-Джерси: Prentice Hall, 1997.

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