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:
On the first line, we declare a variable with the name
idand assign the value 5 to it.Then the
ifblock is declared, and it has its own scope.Inside the
ifblock, we assign the variable namedidto 6. In theifscope, such variable is not declared, but it was declared earlier in the outer scope that includes theifscope. Therefore, thisidvariable is assigned the value 6.Next, we declare a new variable
idwithin the scope of theifblock and assign it the value 10. This is another variable that exists in the current scope.When we call the
println(id)command insideif, the program first looks for a variable with this name in the scope ofif. 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.When we go outside
ifand callprintln(id), the program finds a variable namedidagain. 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-elseexpression, 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.