- 1 A First C# Program
- 2 Namespaces
- 3 Alternative Forms of the Main() Function
- 4 Making a Statement
- 5 Opening a Text File for Reading and Writing
- 6 Formatting Output
- 7 The string Type
- 8 Local Objects
- 9 Value and Reference Types
- 10 The C# Array
- 11 The new Expression
- 12 Garbage Collection
- 13 Dynamic Arrays: The ArrayList Collection Class
- 14 The Unified Type System
- 15 Jagged Arrays
- 16 The Hashtable Container
- 17 Exception Handling
- 18 A Basic Language Handbook for C#
1.8 Local Objects
A data object must be defined within either a function or a class; it cannot exist as an independent object either in a namespace or within the global declaration space. Objects that are defined within a function are called local objects. A local object comes into existence when its enclosing function begins execution. It ceases to exist when the function terminates. A local object is not provided with a default initial value.
Before a local object can be read or written to, the compiler must feel sure that the object has been assigned to. The simplest way to reassure the compiler is to initialize the local object when we define itfor example,
int ival = 1024;
This statement defines an integer object ival and initializes it with a value of 1024.
iSometimes it doesn't make sense to initialize an object because we don't use it until after it has the target of an assignment. For example, consider user_name in the following program fragment:
static int Main() { string user_name; int num_tries = 0; const int max_tries = 4; while ( num_tries < max_tries ) { // generate user message ... ++num_tries; user_name = Console.ReadLine(); // test whether entry is valid } // compiler error here! // use of unassigned local variable user_name Console.WriteLine( "Hello, {0}", user_name ); return 0; }
By inspection, we see that user_name must always be assigned to within the while loop. We know this because num_tries is initialized to 0. The while loop is always evaluated at least once. The compiler, however, flags the use of user_name in the WriteLine() statement as the illegal use of an unassigned local object. What do we know that it doesn't?
Each time we access a local object, the compiler checks that the object has been definitely assigned to. It determines this through static flow analysisthat is, an analysis of what it can know at compile time. The compiler cannot know the value of a nonconstant object, such as num_tries, even if its value is painfully obvious to us. The static flow analysis carried out by the compiler assumes that a nonconstant object can potentially hold any value. Under that assumption, the while loop is not guaranteed to execute. Therefore, user_name is not guaranteed to be assigned to, and the compiler thus issues the error message.
The compiler can fully evaluate only constant expressions, such as the literal values 7 or 'c', and nonwritable constant objects, such as max_tries. Nonconstant objects and expressions can be definitely known only during runtime. This is why the compile treats all nonconstants as potentially holding any value. It's the most conservative and therefore safest approach.
One fix to our program, of course, is to provide a throwaway initial value:
string user_name = null;
An alternative solution is to use the fourth of our available loop statements in C#, the do-while loop statement. The do-while loop always executes its loop body at least once before evaluating a condition. If we rewrite our program to use the do-while loop, even the compiler can recognize that user_name is guaranteed to be assigned because its assignment is independent of the value of the nonconstant num_tries:
do { // generate user message ... ++num_tries; user_name = Console.ReadLine(); // test whether entry is valid } while ( num_tries < max_tries );
Local objects are treated differently from other objects in that their use is order dependent. A local object cannot be used until it has been declared. There is also a subtle extension to the rule: Once a name has been used within a local scope, it is an error to change the meaning of that use by introducing a new declaration of that name. Let's look at an example.
public class EntryPoint { private string str = "hello, field"; public void local_member() { // OK: refers to the private member /* 1 */ str = "set locally"; // error: This declaration changes the // meaning of the previous statement /* 2 */ string str = "hello, local"; } }
At 1, the assignment of str is resolved to the private member of the class EntryPoint. At 2, however, the meaning of str changes with the declaration of a local str string object. C# does not allow this sort of change in the meaning of a local identifier. The occurrence of the local definition of str triggers a compile-time error.
What if we move the declaration of the private str data member so that it occurs after the definition of the method? That doesn't change the behavior. The entire class definition is inspected before the body of each member function is evaluated. The name and type of each class member are recorded within the class declaration space for subsequent lookup. The order of member declarations is not significantfor example,
public class EntryPoint { // OK: let's place this first public void local_member() { // still refers to the private class member /* 1 */ str = "set locally"; // still the same error /* 2 */ string str = "hello, local"; } // position of member does not change its visibility private string str = "hello, field"; }
Each local block maintains a declaration space. Names declared within the local block are not visible outside of it. The names are visible, however, within any blocks nested with the containing blockfor example,
public void example() { // top-level local declaration space int ival = 1024; { // ival is still in scope here double ival = 3.14; // error: reuse of name string str = "hello"; } { // ival still in scope, str is not! double str = 3.14159; // OK } // what would happen if we defined a str object here? }
If we added a local declaration of str at the end of the function, what would happen? Because this declaration occurs at the top-level local declaration space, the two previous legal uses of the identifier str within the local nested blocks would be invalidated, and a compiler error would be generated.
Why is there such strict enforcement against multiple uses of a name within the local declaration spaces? In part because local declaration spaces are considered under the ownership of the programmer. That is, the enforcement of a strict policy is not considered onerous for the programmer. She can quickly go in and locally modify one or another of the identifiers. And by doing so, the thinking goes, she is improving the clarity of her program.