Lexical Scope
Scope delineates the reach of an inferred namespace where you can directly access named identifiers. Scope, in Dart, is delineated by each new set of curly braces. Each set of curly braces acquires its own new scope while inheriting from the scope in which it was declared.
Dart is a lexically scoped language. With lexical scoping, descendant scopes will access the most recently declared variable of the same name. The innermost scope is searched first, followed by a search outward through other enclosing scopes.
{
//search outermost last
String name = "Jack Murphy"
{
//search innermost first
print(name)
}
}
Let’s define a nested function inside main(). Inside the inner() function, declare two variables named level and example. These variables will be available only inside the wrapping scope (Example 4.1).
EXAMPLE 4.1
main() {
void inner ()
{
int level = 1; //not visible in main()
String example = "scope"; //not visible in main()
print('example: $example, level: $level');
}
inner(); //calls the function which prints - example: scope, level: 1
}
Let’s try to access the variables of example and level outside the inner() function.
EXAMPLE 4.2
main() {
void inner()
{
int level = 1;
String example = "scope";
print('example: $example, level: $level');
}
inner();
print('level: $level and example: $example'); //results in an Error
}
In the Dart Analyzer, you’ll see that the print() line in Example 4.2 results in an error: undefined name 'level'. Let’s take a look at how a scope inherits from the scope in which it’s declared and how it searches for named identifiers from inside out.
EXAMPLE 4.3
main() { //a new scope
String language = "Dart";
void outer() {
//curly bracket opens a child scope with inherited variables
String level = 'one';
String example = "scope";
void inner() { //another child scope with inherited variables
//the next 'level' variable has priority over previous
//named variable in the outer scope with the same named identifier
Map level = {'count': "Two"};
//prints example: scope, level:two
print('example: $example, level: $level');
//inherited from the outermost scope: main
print('What Language: $language');
} //end inner scope
inner();
//prints example: scope, level:one
print('example: $example, level: $level');
} //end outer scope
outer();
} //end main scope
In Example 4.3, inner() inherits scope from outer(), which inherits scope from main(). This gives inner() access to the outermost scope where the variable language is accessible. Conversely, main() has no idea of the existence of function inner().
To further illustrate lexical scope, let’s take a look at the hashcode property of each variable from within its respective scope.
A hashcode is a value generated by converting each property of an object to a numeric value and then joining those values together to create a single numeric representation of the entire object. Hashcodes are not guaranteed to be the same between runs or on different machines. Objects of differing values or differing types cannot share the same hashcode. Hashcodes give us a uniform approach to compare variables, as shown in Example 4.4.
EXAMPLE 4.4
main() {
String language = "Dart";
void outer() {
String level = 'one';
String example = "scope";
void inner() {
//declare a new variable named level in memory on the 'inner' scope
//even though the named identifier is the same as the variable in outer()
Map level = {'count': "Two"};
print('-----');
print('inner::outer.hashcode ' + outer.hashCode.toString());
print('inner::inner.hashcode ' + outer.hashCode.toString());
print('inner::language.hashcode ' + language.hashCode.toString());
print('inner::example.hashcode ' + example.hashCode.toString());
print('inner::level.hashcode ' + level.hashCode.toString());
}
//has access to only outer scope variables
print('-----');
print('outer::outer.hashcode ' + outer.hashCode.toString());
print('outer::inner.hashcode ' + outer.hashCode.toString());
print('outer::language.hashcode ' + language.hashCode.toString());
print('outer::example.hashcode ' + example.hashCode.toString());
print('outer::level.hashcode ' + level.hashCode.toString());
inner();
}
print('-----');
print('main::language.hashcode ' + language.hashCode.toString());
print('main::outer.hashcode ' + outer.hashCode.toString());
print('main::inner.hashcode N/A');
outer();
}
//Output:
//main::language.hashcode 482586172
//main::outer.hashcode 380883474
//main::inner.hashcode N/A
//-----
//outer::outer.hashcode 380883474
//outer::inner.hashcode 380883474
//outer::language.hashcode 482586172
//outer::example.hashcode 857747343
//outer::level.hashcode 1058535322
//-----
//inner::outer.hashcode 380883474
//inner::inner.hashcode 380883474
//inner::language.hashcode 482586172
//inner::example.hashcode 857747343
//inner::level.hashcode 802594681
Table 4.1 is a matrix of hashcodes for each object from within its respective scopes.
The empty cells in Table 4.1 illustrate that objects that are declared only inside a child scope, such as outer(), are not accessible from parent scopes, such as main().
As you can see in Table 4.1, inner() inherits scope from outer() but also declares its own named identifier of the variable level.
The second declaration of level as a Map does not modify the variable in scope outer(), but instead creates a new variable with its own object reference that’s only accessible within the scope for inner(). After the new level variable is initialized within scope inner(), the hashcode for the variable level inside the scope of inner() reports as 802594681 instead of 1058535322.
Table 4.1 Example Object Hashcode Matrix
OBJECT |
SCOPE: main{ } |
SCOPE: outer{ } |
SCOPE: inner{ } |
outer |
380883474 |
380883474 |
380883474 |
language |
482586172 |
482586172 |
482586172 |
inner |
380883474 |
380883474 |
|
level |
1058535322 |
802594681 |
|
example |
857747343 |
857747343 |