Scope and Declaration Space
We briefly mentioned scope and declaration space in Hour 1, saying that scope defines where you can use a name, whereas declaration space focuses on where that name is unique. Scope and declaration space are closely related, but there are a few subtle differences.
A more formal definition is that scope is an enclosing context or region that defines where a name can be used without qualification.
In C#, both scope and declaration space is defined by a statement block enclosed by braces. That means namespaces, classes, methods, and properties all define both a scope and a declaration space. As a result, scopes can be nested and overlap each other.
If scope defines the visibility of a name and scopes are allowed to overlap, any name defined in an outer scope is visible to an inner scope, but not the other way around.
In the code shown in Listing 3.1, the field age is in scope throughout the entire body of Contact, including within the body of F and G. In F, the use of age refers to the field named age.
Listing 3.1. Scope and Declaration Space
class Contact { public int age; public void F() { age = 18; } public void G() { int age; age = 21; } }
However, in G, the scopes overlap because there is also a local variable named age that is in scope throughout the body of G. Within the scope of G, when you refer to age, you are actually referring to the locally scoped entity named age and not the one in the outer scope. When this happens, the name declared in the outer scope is hidden by the inner scope.
Figure 3.1 shows the same code with the scope boundaries indicated by the dotted and dashed rectangles.
Figure 3.1 Nested scopes and hiding
Declaration space, on the other hand, is an enclosing context or region in which no two entities are allowed to have the same name. In the Contact class, for example, you are not allowed to have anything else named age in the body of the class, excluding the bodies of F and G. Likewise, inside the body of G, when you redeclare age, you aren't allowed to have anything else named age inside the declaration space of G.
You learn about method overloading a bit later this hour, but methods are treated a little differently when it comes to declaration spaces. If you consider the set of all overloaded methods with the same name as a single entity, the rule of having a unique name inside a declaration space is still satisfied.
Accessibility
Accessibility enables you to control the visibility, or accessibility, of an entity outside of its containing scope. C# provides this through access modifiers, which specify constraints on how members can be accessed outside the boundary of the class and, in some cases, even constrain inheritance. A particular class member is accessible when access to that member has been allowed; conversely, the member is inaccessible when access has been disallowed.
These access modifiers follow a simple set of contextual rules that determine when certain types of accessibility are permitted:
- Namespaces are not allowed to have any access modifiers and are always public.
- Classes default to internal accessibility but are allowed to have either public or internal declared accessibility. A nested class, which is a class defined inside of another class, defaults to private accessibility but can have any of the five kinds of declared accessibility.
- Class members default to private accessibility but can have any of the five kinds of declared accessibility.
These rules also define the default accessibility, which occurs when a member does not include any access modifiers.
The access modifiers supported by C# are shown in Table 3.1.
Table 3.1. Access Modifiers
Modifier |
Description |
public |
Access is not limited. |
protected |
Access is limited to the containing class or types derived from the containing class. |
internal |
Access is limited to the containing assembly. |
protected internal |
Access is limited to the containing assembly or types derived from the containing class. |
private |
Access is limited to the containing class only. |
Fields and Constants
Fields are variables that represent data associated with a class. In other words, a field is simply a variable defined in the outermost scope of a class. If you recall from Hour 1, a field can be either an instance field or a static field, and for both types of field, you can specify any of the five access modifiers. Typically, fields are private, which is the default.
If a field, no matter whether it is an instance or static field, is not given an initial value when it is declared, it is assigned the default value appropriate for its type.
Similar to fields, constants can be declared with the same access modifiers. Because a constant must have a value that can be computed at compile time, it must be assigned a value as part of its declaration. One benefit of requiring a value that can be computed at compile time is that a constant can depend on other constants. A constant is usually a value type or a string literal because the only way to create a non-null value of a reference type other than string is to use the new operator, which is not permitted.
If you need to create a field that has constant-like behavior but uses a type not allowed in a constant declaration, you can use a static read-only field instead by specifying both the static and readonly modifiers. A read-only field can be initialized only as part of its declaration or in a constructor.
Properties
If fields represent state and data but are typically private, there must be a mechanism that enables the class to provide that information publicly. Knowing the different accessibility options allowed it would be tempting to simply declare the class fields to have public accessibility.
This would allow us to satisfy the rules of abstraction, but this would then violate the rules of encapsulation because the fields could be directly manipulated. How, then, is it possible to satisfy both the rules of encapsulation and abstraction? What is needed is something accessed using the same syntax as a field but that can define different accessibility than the field itself. Properties enable us to do exactly that. A property provides a simple way to access a field, called the backing field, which can be publicly available while still allowing the internal details of that field to be hidden. Just as fields can be static, properties can also be static and are not associated with an instance of the class.
Although fields declare variables, which require storage in memory, properties do not. Instead, properties are declared with accessors that enable you to control whether a value can be read or written and what should occur when doing so. The get accessor enables the property value to be read, whereas the set accessor enables the value to be written.
Listing 3.2 shows the simplest way to declare a property. When using this syntax, known as automatic properties, you omit the backing field declaration and must always include both the get and set accessor without a declared implementation, which the compiler provides.
Listing 3.2. Declaring an Automatic Property
class Contact { public string FirstName { get; set; } }
In fact, the compiler transforms the code shown in Listing 3.2 into code that looks roughly like that shown in Listing 3.3.
Listing 3.3. Declaring a Property
class Contact { private string firstName; public string FirstName { get { return this.firstName; } set { this.firstName = value; } } }
The get accessor uses a return statement, which simply instructs the accessor to return the value indicated. In the set accessor of the code in Listing 3.3, the class field firstName is set equal to value, but where does value come from? From Table 1.6 in Chapter 1, you know that value is a contextual keyword. When used in a property set accessor, the value keyword always means "the value that was provided by the caller" and is always typed to be the same as the property type.
By default, the property accessors inherit the accessibility declared on the property definition itself. You can, however, declare a more restrictive accessibility for either the get or the set accessor.
You can also create calculated properties that are read-only and do not have a backing field. These calculated properties are excellent ways to provide data derived from other information.
Listing 3.4 shows a calculated FullName property that combines the firstName and lastName fields.
Listing 3.4. Declaring a Calculated Property
class Contact { private string firstName; private string lastName; public string FullName { get { return this.firstName + " " + this.lastName; } } }
Because properties are accessed as if they were fields, the operations performed in the accessors should be as simple as possible. If you need to perform more complex operations or perform an operation that could be time-consuming or expensive (resource consuming), it might be better to use a method rather than a property.
Methods
If fields and properties define and implement data, methods, which are also called functions, define and implement a behavior or action that can be performed. The WriteLine action of the Console class you have been using in the examples and exercises so far is an example of a method.
Listing 3.5 shows how to add a method to the Contact class that verifies an email address. In this case, the VerifyEmailAddress method specifies void as the return type, meaning that it does not return a value.
Listing 3.5. Declaring a Method
class Contact { public void VerifyEmailAddress(string emailAddress) { } }
Listing 3.6 shows the same method declared to have a bool as the return type.
Listing 3.6. Declaring a Method That Returns a Value
class Contact { public bool VerifyEmailAddress(string emailAddress) { return true; } }
A method declaration can specify any of the five access modifiers. In addition to the access modifiers, a method can also include the static modifier. Just as static properties and fields are not associated with an instance of the class, neither are static methods. The WriteLine method is actually a static method on the Console class.
Methods can accept zero or more parameters, or input, declared by the formal parameter list, which consists of one or more comma-separated parameters. Each parameter must include both its type and an identifier. If a method accepts no parameters, an empty parameter list must be specified.
Parameters are divided into three categories:
- Value parameters—The most common. When a method is called, a local variable is implicitly created for each value parameter and assigned the value of the corresponding argument in the argument list.
- Reference parameters—Do not create a new storage location but represent the same storage location as the corresponding argument in the argument list. Reference parameters are declared using the ref keyword, which must be present both in the parameter list and the argument list.
- Output parameters—Similar to reference parameters but require the out keyword to be present in both the parameter and invocation lists. Unlike reference parameters, they must be given a definite value before the method returns.
For a method to actually perform its desired action on the object, it must be invoked, or called. If the method requires input parameters, those values must be provided in an argument list, and if the method provides an output value, that value can also be stored in a variable.
The argument list is normally a one-to-one relationship with the parameter list, meaning that for each parameter, you must provide a value of the appropriate type in the same order when you call the method.
Looking at the VerifyEmailAddress method that has a void return type from the earlier examples, you would call the method like this:
Contact c = new Contact(); c.VerifyEmailAddress("joe@example.com");
However, for the VerifyEmailAddress method defined to return a bool, you would call the method like this:
Contact c = new Contact(); bool result = c.VerifyEmailAddress("joe@example.com");
Just as you do with the parameter list, if a method invocation requires no arguments, you must still specify an empty list.
Method Overloading
Ordinarily, two entities cannot have the same name within a declaration space, except for overloaded methods. When two or more methods have the same name in a declaration space but have different method signatures, they are overloaded.
The method signature is made up of the method name and the number, types, and modifiers of the formal parameters and must be different from all other method signatures declared in the same class; the method name must be different from all other non-methods declared in the class.
Overloaded methods can vary only by signature. More appropriately, they can vary only by the number and types of parameters. Consider the Console.WriteLine method you have already used; there are 19 different overloads from which you can choose.
Overloading methods is common in the .NET Framework and enables you to give the users of your class a single method with which they interact and provide different input. Based on that input, the compiler figures out which method should actually be used.
Method overloading is useful when you want to provide several different possibilities for initiating an action, but method overloading can become unwieldy when there are many options. An example of method overloading is shown in Listing 3.7.
Listing 3.7. Method Overloading
public void Search(float latitude, float longitude) { Search(latitude, longitude, 10, "en-US"); } public void Search(float latitude, float longitude, int distance) { Search(latitude, longitude, distance, "en-US"); } public void Search(float latitude, float longitude, int distance, string culture) { }
Optional Parameters and Named Arguments
Optional parameters enable you to omit that argument in the invocation list when calling a method. Only value parameters can be optional, and all optional parameters must appear after required parameters, but before a parameter array.
To declare a parameter as optional, you simply provide a default value for it. The modified Search method using optional parameters is shown here:
public void Search(float latitude, float longitude, int distance = 10, string culture = "en-US");
The latitude and longitude parameters are required, whereas distance and culture are both optional. The default values used are the same values provided by the first overloaded Search method.
Looking at the Search method overloads from the previous section, it should become clear that the more parameters you have the more overloads you need to provide. In this case, there are only a few overloads, but that is still more than providing a single method with optional parameters. Although overloads are the only option in some cases, particularly those that don't imply a reasonable default for a parameter, often you can achieve the same result using optional parameters.
Optional parameters are also particularly useful when integrating with unmanaged programming interfaces, such as the Office automation APIs, which were written specifically with optional parameters in mind. In these cases, the original API call might require a large number of arguments (sometimes as many as 30), most of which have reasonable default values.
A method that contains optional parameters can be invoked without explicitly passing arguments for those parameters, allowing the default arguments to be used instead. If, however, the method is invoked and provides an argument for an optional parameter, that argument is used instead of the default.
Listing 3.8 shows an example of calling the Search method, allowing the default values to be used.
Listing 3.8. Using Optional Parameters
Search(27.966667f, 82.533333f, 3); Search(27.966667f, 82.533333f, 3, "en-GB"); Search(27.966667f, 82.533333f);
The drawback to optional parameters is that you cannot omit arguments between the commas, meaning you could not call the Search method like this:
Search(27.966667f, 82.533333f, , "en-GB");
To resolve this situation, C# enables any argument to be passed by name, whereby you are explicitly indicating the relationship between the argument and its corresponding parameter. Using named arguments, the different method calls in Listing 3.8 and the illegal call just shown could be written as shown in Listing 3.9.
Listing 3.9. Using Named Arguments
Search(latitude: 27.966667f, longitude: 82.533333f, distance: 3); Search(latitude: 27.966667f, longitude: 82.533333f, distance: 3, culture: "en-GB"); Search(latitude: 27.966667f, longitude: 82.533333f); Search(27.966667f, 82.533333f, culture: "en-GB"); Search(latitude: 27.966667f, longitude: 82.533333f, culture: "en-GB");
All these calls are equivalent. The first three calls are the same as the calls in Listing 3.8 except that each parameter is explicitly named. The last two calls show how we can omit an argument in the middle of the parameter list and are also the same, although one uses a mixture of named and positional arguments.
Named arguments are most often used with optional parameters, but they can be used without them as well. Unlike optional parameters, named arguments can be used with value, reference, and output parameters. You can also use named arguments with parameter arrays, but you must explicitly declare a new array to contain the values, as shown here:
Console.WriteLine(String.Concat(values: new string[] { "a", "b", "c" }));
As you can see from the Search method, by enabling you to explicitly indicate the name of an argument, C# provides an additional (and powerful) way to help write fully describing and self-documenting code.
Instantiating a Class
Unlike the predefined value types in which you could simply declare a variable and assign it a value, to use a class in your own programs, you must create an instance of that class.
Remember, even though you create new objects directly using the new keyword, the virtual execution system is responsible for actually allocating the memory required, and the garbage collector is responsible for deallocating that memory.
Instantiating a class is accomplished using the new keyword, like this:
Contact c = new Contact();
A newly created object must be given an initial state, which means any fields declared must be given an initial value either by explicitly providing one or accepting the default values (see Table 2.13 in Chapter 2).
Sometimes this level of initialization is sufficient, but often it won't be. To provide additional actions that occur during initialization, C# provides an instance constructor (sometimes just called a constructor), which is a special method executed automatically when you create the instance.
A constructor has the same name of the class but it cannot return a value, which is different from a method that returns void. If the constructor has no parameters, it is the default constructor.
Listing 3.10 shows the default constructor for the Contact class.
Listing 3.10. Declaring a Default Constructor
public class Contact { public Contact() { } }
Just as it is possible to overload regular methods, it is also possible to overload constructors. The signature for a constructor is the same as it is for a regular method, so the set of overloaded constructors must also vary by signature.
Some reasons for providing specialized constructors follow:
- There is no reasonable initial state without parameters.
- Providing an initial state is convenient and reasonable for the type.
- Constructing the object can be expensive, so you want to ensure that the object has the correct initial state when it is created.
- A non-public constructor restricts who can create objects using it.
Looking at the Contact class you have been using, it would certainly be useful if you provided values for the firstName, lastName, and dateOfBirth fields when creating a new instance. To do that, you would declare an overloaded constructor like the one shown in Listing 3.11.
Listing 3.11. Declaring a Constructor Overload
public class Contact { public Contact(string firstName, string lastName, DateTime dateOfBirth) { this.firstName = firstName; this.lastName = lastName; this.dateOfBirth = dateOfBirth; } }
In the constructor overload from Listing 3.11, you assigned the value of the parameter to its corresponding private field.
Typically, although not always, when a class contains multiple constructors, those constructors are chained together. To chain constructors together, you use a special syntax that uses the this keyword.
Listing 3.12 shows the Contact class with both constructors from Listing 3.10 and Listing 3.11 using constructor chaining.
Listing 3.12. Constructor Chaining
public class Contact { public Contact() { } public Contact(string firstName, string lastName, DateTime dateOfBirth) : this() { this.firstName = firstName; this.lastName = lastName; this.dateOfBirth = dateOfBirth; } }
One benefit of constructor chaining is that you can chain in any constructor provided by the class, not just the default constructor. When you use constructor chaining, it is important to understand the order in which the constructors execute. The constructor chain is followed until it reaches the last chained constructor, and then constructors will be executed in order going back out of the chain. Listing 3.13 shows a class, C, with three constructors, each chained through to the default constructor.
Listing 3.13. Chained Constructor Order of Execution
public class C { string c1; string c2; int c3; public C() { Console.WriteLine("Default constructor"); } public C(int i, string p1) : this(p1) { Console.WriteLine(i); } public C(string p1) : this() { Console.WriteLine(p1); } }
Figure 3.7 shows the sequence in which each constructor would execute when instantiated using the second constructor (the one that takes an int and a string as input).
Figure 3.7 Constructor chaining sequence
Static Construction
Instance constructors, like you have just seen, implement the actions required to initialize instances of the class. In some cases, a class might require specific initialization actions to occur at most once and before any instance members are accessed.
To accomplish this, C# provides a static constructor, which has the same form as the default constructor with the addition of the static modifier instead of one of the access modifiers. Because static constructors initialize the class, you cannot directly call a static constructor.
A static constructor executes at most once and will be executed the first time an instance is created or the first time any of the static class members are referenced.