obo.dev

Области видимости (Scope)

05 Dec 2022

Области видимости (Scope) в Swift и контекст определения переменных

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

Но не все переменные в нашем коде одинаково доступны. Доступна ли переменная и как получить к ней доступ, определяют области видимости.

Что такое область видимости

Область видимости (англ. scope) — это часть программы, в пределах которой некая сущность (обычно — переменная, тип данных или функция) связана с её идентификатором (её именем), то есть область видимости позволяет обратиться по идентификатору (имени) к этой сущности. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной или функцией, либо быть свободным (не связанным ни с какой из них).

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

Ещё одно определение.

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

И параметры функции, и переменные, которые объявлены внутри функции, имеют локальную область видимости. Другими словами, эти параметры и переменные используются только внутри функции, в которой они объявлены. Локальные переменные создаются в точке объявления и уничтожаются, когда выходят из области видимости. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной, либо быть свободным (вообще не связанным ни с какой из переменных).

Пример из жизни

Представьте улицу, на которой есть дома, в домах есть квартиры. Из одного дома (области видимости дома) можно увидеть улицу (область видимости улицы) и все что в ней содержится - другие дома, администрацию и т.д.. Но увидеть квартиры в других домах (в области видимости других домов) - вы не можете. Так же, внутри своего дома можно увидеть, что есть квартиры, но нельзя увидеть содержимое квартир - у них своя область видимости. Номера квартир в пределах одного дома не должны повторятся, но аналогичные номера квартир могут быть в других домах. А могут и не быть.

Пример в виде псевдокода:

...
Street-1 {

	Administration {}

	House-1 {
		Apartment-1 {}
		Apartment-2 {}
		...
	}

	House-2 {

		You are here

		Apartment-1 {}
		Apartment-2 {}
		...
	}
	
	...
	
	House-N {
		Apartment-1 {}
		Apartment-2 {}
		...
	}
}
...

Зачем нужна область видимости?

Область видимости определяет доступность объектов, определяет границы действия объектов. Объекты (переменные, константы, функции, пользовательские типы данных), которые объявлены в текущей области видимости, доступны только внутри нее.

Области видимости могут быть вложенными. Объекты, объявленные во внешней области видимости, доступны во внутренней области.

Создаются области видимости во время выполнения программы. Самая первая область, которая создаётся и которая включает в себя все остальные называется глобальной. Остальные области видимости называются локальными.

Глобальная область видимости никак не обозначается. Она содержит в себе весь остальной код и локальные области видимости.

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

let num = 5

Объекты объявленные в глобальной области видимости могут быть доступны в любой точке программы.

Локальная область видимости определяется через блоки кода.

Блок кода используется различными объявлениями и контролирующими структурами для объединения объявлений. Выглядит следующим образом:

{
	// instruction
}

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

Блоки используются в конструкциях if, for, while и т.д. Даже тело функции является блоком, т.к. находится между фигурными скобками.

Объекты объявленные в локальной области видимости могут быть доступны в текущей области видимости или во вложенных локальных областях видимости.

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

Пример определения локальных областей видимости для некоторых объектов в Swift:

// If Statement
if (true) {
	...
} else {
	...
}

// Switch Statement
switch foo {...}

// For Loop
for _ in Array {...}

// Computed Property
var someVariable: Int {
    get {
        return ...
    }
	set {
		...
	}
}

// Function
func funcName() {...}

// Closure
var someClosure = {...}

// Structure
struct StructureName {...}

// Class
class ClassName {...}

// Enum
enum EnumName {...}

// Protocol
protocol ProtocolName {...}

Пример. Посмотрите на следующий код:

func getAge() -> Int {
    var age = 42
    age += 1
    return age
}
 
var age = 99
var anotherAge = getAge()
anotherAge += 1
 
print(age)

// Output
// 99

print(anotherAge)

// Output
// 44

В приведенном выше коде работаем с двумя типами областей: глобальной и локальной.

В функции присутствует локальная область видимости getAge(), поэтому ее часто называют областью действия функции. Переменные, подобные age, объявленные внутри функции, не могут быть доступны вне ее.

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

Переменная age определена внутри функции getAge(). Нельзя получить доступ к той же самой переменной за пределами функции.

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

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

Локальная область — это область, в которой находитесь в данный момент, то есть это блок кода между фигурными скобками { }. Всегда следите за тем, какова текущая локальная область видимости, и к каким переменным, типам и т.д. есть доступ.

Как происходит поиск объектов в области видимости. Вложенность областей видимости

При обращении к объекту в одной области видимости программа попытается его найти в этой области видимости.

Пример:

func foo() {
    var bar = 10
    print(bar)
}
foo()

// Output
// 10

Если объект не будет найден, тогда программа поднимется на один уровень выше и будет искать объект выше. Так программа доходит до глобальной области видимости:

var bar = 10
func foo() {
    print(bar)
}
foo()

// Output
// 10

Если объекта нет и в глобальной области, тогда будет ошибка: Cannot find in scope.

Пример:

func foo() {
    print(bar) // Error: Cannot find 'bar' in scope
}
foo()

Область видимости и конфликт имен объектов

В одной области видимости не должно быть объектов с одинаковыми именами. Это приведет к ошибке, так как программа будет воспринимать такой код, как повторное объявление одного и того же объекта.

Пример:

var foo = 0
var foo = 10 // Error: invalid redeclaration on 'foo'

При этом можно объявлять объекты с одинаковыми именами в разных областях видимости. Рассмотрим следующий код:

let bar = 10

func foo() {
    let bar = 5
    for _ in 1...1 {
        let bar = 0
        print(bar)
    }
    print(bar)
}

foo()

// Output
// 0
// 5

print(bar)

// Output
// 10

Здесь была созданы три константы с одним именем - bar, но в разных областях видимости:

  • первая константа bar - в глобальной области видимости;
  • вторая константа bar - в локальной области видимости функции foo();
  • третья константа bar - в локальной области видимости цикла for-in.

Если обратиться в глобальной области видимости к константе bar, то значение будет взято у константы bar, которая объявлена в этой области видимости. При этом, остальные две константы bar были объявлены в локальных областях (внутри функции и внутри цикла) и поэтому они не доступны.

Внутри цикла, при обращении к константе bar, происходит её поиск в данной области и берется её значение 0.

Так же и при обращении к константе bar внутри области видимости функции: происходит поиск в текущей области видимости, а область видимости цикла не видна.

Теперь удалим константу bar из функции foo():

let bar = 10

func foo() {
    for _ in 1...1 {
        let bar = 0
        print(bar)
    }
    print(bar)
}

foo()

// Output
// 0
// 10

print(bar)

// Output
// 10

В этом случае, при обращении к константе bar внутри области видимости функции: происходит поиск в текущей области видимости, а область видимости цикла не видна. Но так как в локальной области видимости константы bar нет, то происходит переход на область видимости выше. Там и находится константа bar со значением 10, что и видно в выводе.

Если же удалить константу bar и из глобальной области, то будет выше названная ошибка: Cannot find ‘bar’ in scope. И это несмотря на то, что константа bar есть в цикле. Как указано выше, она будет не видима для функции.

Такое поведение позволяет использовать одинаковые название в разных областях видимости и не придумывать большое количество разнообразных имен объектов (например, в классах, структурах и т.д.)

Пример:

class Administration {
    var name: String = "Adam"
}

class Worker {
    var name: String = "Bob"
}

Здесь конфликта одинаковых имен не будет, так как свойство name указано в разных классах со своими областями видимости.

Глобальные и локальные функции, область видимости класса

До сих пор рассматривали только глобальные и локальные области видимости на примере области видимости функций, но классы также имеют область видимости. Фактически, фреймворки, модули и сами Swift файлы имеют область видимости.

Давайте начнем с простого класса:

class Product {
 
}

Данный класс определен в глобальной области видимости. Теперь добавим перечисление в этот класс как вложенный тип:

class Product {
    var kind: Kind = .thing
 
    enum Kind {
        case food
        case thing
    }
}

В приведенном выше коде определили перечисление под названием Kind. Также добавили свойство экземпляра kind типа Kind, которое по умолчанию инициализируется со значением .thing.

Область действия класса Product является глобальной. Он определен глобально, поэтому можно создавать объекты Product в любом месте кода.

Область действия перечисления Kind ограничена классом Product. Можно использовать тип Kind только внутри класса.

Область действия свойства kind также ограничена классом Product. Можно использовать это свойство только внутри класса.

Можно использовать класс Product в глобальной области видимости, и, поскольку перечисление Kind имеет уровень доступа по умолчанию internal, можно использовать его как тип в любом месте кода — Product.Kind:

let banana = Product()
 
if banana.kind == Product.Kind.food {
}

Аналогично свойство kind определяется в области видимости класса, но поскольку оно также общедоступно, можно получить доступ к этому свойству для любого объекта типа Product.

Добавим функцию canEat() в класс Product:

class Product {
    var kind: Kind = .thing
 
    enum Kind {
        case food
        case thing
    }
 
    func canEat() -> Bool {
        return kind == .food
    }
}

Здесь имеем дело с 3 уровнями области видимости:

  • Глобальный масштаб, в котором определен класс Product.
  • Область видимости класса, в которой определено свойство kind, перечисление Kind и функция canEat().
  • Область видимости функции внутри canEat().

Класс Product определен в глобальном масштабе, так что можно использовать его в любом месте нашего приложения. Свойство kind определено в рамках класса, так что можно использовать его только в классе Product. То же самое касается перечисления Kind и функции canEat().

Свойство kind используется внутри функции canEat(). Это означает, что у областей видимости есть своя иерархия, потому что можно получить доступ к свойству из области видимости класса в пределах функции. Однако, если определили локальную переменную внутри canEat(), то нельзя использовать эту переменную в другой функции того же класса, потому что они имеют разные области видимости.

func canEat() -> Bool {
    let hungry = ...
}
 
func isThing() -> Bool {
    // Нельзя использовать здесь hungry
}

В этом примере показано, что константа hungry, которая определена в функции canEat(), не может быть использована в функции isThing(), так как для функции isThing() область видимости функции canEat() недоступна.

Область видимости для замыканий

Область видимости используются повсюду в разработке для iOS. Всегда полезно знать, к какой переменной можно получить доступ из текущей области видимости.

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

Замыкания часто используются в качестве так называемых обработчиков завершения (completion handlers). Например, загружаем изображение асинхронно из интернета. Когда загрузка завершится, необходимо выполнить некоторый код, чтобы показать изображение. Определяем этот код в замыкании, передаем функции, которая загружает изображение. Затем эта функция выполняет код замыкания после завершения загрузки.

class DetailViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView?
 
    func viewDidLoad() {
        network.downloadImage(url, completionHandler: { image in
            imageView?.image = image
        })
    }
}

В приведенном выше коде создали класс DetailViewController со свойством imageView. Внутри функции вызываем функцию downloadImage(_:completionHandler:). Второй параметр, обработчик завершения, заключенный между внутренними фигурными скобками, является замыканием. Когда загрузка изображения завершается, загруженное значение присваивается свойству image для imageView, которое выводит загруженное изображение.

Замыкания называется замыканием, потому что они фиксируют («замыкают») значения внешних параметров.

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

Захват значений работает, только если замыкание имеет доступ к той же области видимости, что и функция, в которой оно определено. Даже если замыкание является отдельной сущностью, оно может получить доступ к свойству imageView, поскольку функция viewDidLoad() имеет доступ к этой области. Замыкания имеют ту же область действия, что и объект, в котором они определены, и в данном случае это область действия функции.


Еще полезные ссылки