6 minutes read

It's hard to imagine a program where you don't need to use variables. But what parts of the code do you think have access to your variables? Of course, they are not seen by another program or another computer. In fact, your code may not see them either! How come? Let's find out.

Scope

Actually, each variable has a special scope in which it can be used. The scope of a variable is the syntactically-delimited area of code in which this variable or identifier, once declared, can be used in one way or another. That is, it is visible. For example, the scope can be an entire program, or a function, or a file.

In the simplest cases, the scope is determined by where the identifier is declared. Out of this scope, the same identifier can be associated with another variable or not be associated with any of the variables at all. Let's look at this in more detail below.

Branching

Let's start by considering scopes in a branch. Look at the following example:

val outer = 10
if (outer > 0) {
    val inner = 10
    print(inner)
} else {
    // beyond the if block 
    print(outer)
    print(inner)  // Unresolved reference: inner
}
// beyond the if-else expression
print(outer)
print(inner)  // Unresolved reference: inner

Here we have declared the variable inner inside the body of if, so the scope of this variable is the if block, from one curly brace to the other. If we try to access this variable in the else block or outside the if-else expression, we will get an error. The same will happen if we declare some variable in the body of the else block and try to reach it outside the if-else expression.

There are no such problems with the variable outer: it is declared outside the if-else expression, so the if-else expression is included in outer's scope. We can refer to this variable anywhere we want without any problem.

Variables in loops

Let's take a look at scopes in loops. Here's an example with a while loop:

var outer = 5
while (outer < 10) {
    var inner = 10
    inner *= 2
    print(inner)
    outer++
}  // prints 2020202020

// outside the while loop
print(inner)  // Unresolved reference: inner

You can see a similar situation here: the inner variable, which we initialized inside the body of the loop, is available only there: its scope is strictly the body of the loop. As you can see, with each new iteration of the cycle, inner initializes again, so as a result, the printed output will be "2020202020".

Meanwhile, the outer variable declared outside the cycle is easily available anywhere in this code: the while cycle is just included in its scope.

Variables in functions

Finally, let's deal with the variables that we declare inside a function. Take a look at the example:

fun localScope() {
    val identifier = "Variable in the function localScope()"
    println(identifier)  // prints "Variable in the function localScope()"
}

fun main() {
    val identifier = "Variable in fun main()"
    localScope()
    println(identifier)  // prints "Variable in fun main()"
}

We have the localScope() function here, in which we have declared the variable identifier. We have also declared a variable with the same name in the main() function. How will it all work?

To begin with: the scope of a variable declared within a function is the function itself. In fact, you can think of this program as just two different functions. So when you declare an identifier variable inside a function, it will exist exactly within that function.

Actually, we've just created two different variables with the same name. So the output of this program will be:

Variable in the function localScope()
Variable in fun main()

Out of scope, the same identifier can be associated with another variable or not be associated with any of the variables at all.

Interaction of scopes

In general, program scopes are often layered. Some scopes are included in others, and all this forms a hierarchy. Let's consider the following example:

var id = 5
if (true) {
    id = 6
    var id = 10
    println(id)  // prints 10
}
println(id)  // prints 6

As you can see, println(id) prints different results. Why? Well, here we need to look at the identifier id. Let's see what's going on line by line:

  1. On the first line, we declare a variable with the name id and assign the value 5 to it.

  2. Then the if block is declared, and it has its own scope.

  3. Inside the if block, we assign the variable named id to 6. In the if scope, such variable is not declared, but it was declared earlier in the outer scope that includes the if scope. Therefore, this id variable is assigned the value 6.

  4. Next, we declare a new variable id within the scope of the if block and assign it the value 10. This is another variable that exists in the current scope.

  5. When we call the println(id) command inside if, the program first looks for a variable with this name in the scope of if. The program is not interested whether a variable with the same name has been declared in other scopes; it has already found it in the current one. It is located, and it is equal to 10. So 10 is printed.

  6. When we go outside if and call println(id), the program finds a variable named id again. In the current scope, it is equal to 6, since we changed the value of this particular variable in step 3. So 6 is printed.

Look at the same code with explanations:

var id = 5  // id (outer scope) = 5
if (true) {
    id = 6  // id (outer scope) = 6
    var id = 10  // id (inner scope) = 10
    println(id)  // prints inner id
}
println(id)  // prints outer id

The previous examples help us formulate the way scopes interact:

  • If scope B is nested in scope A, then identifiers from scope A can be used in scope B without additional declarations.

  • If any identifiers are defined in such nested scope B, they are not visible in scope A.

  • The called identifier is first searched in the current scope and then in scopes up the hierarchy.

Top-level variables

The top level of a Kotlin file is also a scope – it contains all the scopes within a file. And broadly speaking, the variables in the program, depending on the scope, can be divided into top-level and local ones. Top-level variables are declared outside the body of any class or function, and their scope is the whole file, while local variables are created when you define them, for example, in the body of a function.

In the following example, a top-level variable top is declared:

val top = "Top-level variable"
fun localScope() {
    println(top) 
}

fun main() {
    println(top)  // prints "Top-level variable"
    localScope()  // prints "Top-level variable"
}

As you can easily see, it is accessible from anywhere in the program. This is the way to define constants:

const val CONSTANT = 0 // constant value definition

fun main() {
    // ...
}

In all other examples, we've used local variables. Here’s a common courtesy rule: it is better, if possible, to use local variables. It is not very good to use top-level ones: it impairs the readability of the code and leads to errors, which are sometimes difficult to find and correct.

Conclusion

Let's summarize it all! Each variable has a scope – a program area where this variable is visible and can be used. In general, the scope of a variable is a block of code where this variable has been declared:

  • if a variable is declared in one of the blocks of an if-else expression, then the scope of this variable is the body of this branch block;

  • if a variable is declared inside a loop, then its scope is this loop;

  • if a variable is declared inside a function, then its scope is this function.

Some scopes are included in others, and all this forms a hierarchy. If we refer to some identifier, an entity with that name is first searched in the current scope and then in scopes up the hierarchy.

291 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo