You briefly saw in Chapter 1 how to declare a new class called HelloWorld
. In Chapters 2 and 3, you learned about the built-in primitive types included with C#. Since you have now also learned about control flow and how to declare methods, it is time to discuss defining your own types. Type definition is a core construct of any C# program; this support for classes and the objects created from them is what makes C# an object-oriented language.
This chapter introduces the basics of object-oriented programming using C#. A key focus is on how to define classes, which are the templates for objects themselves.
All the constructs of structured, control-flow-based programming from the previous chapters still apply within object-oriented programming. However, by wrapping those constructs within classes, you can create much larger, more organized programs, significantly increasing maintainability.
One of the key advantages of object-oriented programming is that instead of creating new programs entirely from scratch, you can assemble a collection of existing objects from prior work, extending the classes with new features, adding more classes, and thereby providing new functionality.
Readers unfamiliar with object-oriented programming should read the Beginner Topic blocks for an introduction. The general text outside the Beginner Topics focuses on using C# for object-oriented programming, under the assumption that readers are already familiar with object-oriented concepts.
This chapter delves into how C# supports encapsulation through constructs such as classes, properties, and access modifiers; we covered methods in Chapter 5. Chapter 7 builds on this foundation by introducing inheritance and the polymorphism that object-oriented programming enables.
Defining a class involves first specifying the keyword class
, followed by an identifier, as shown in Listing 6.1.
public class Employee
{
}
All code that belongs to the class will appear between the curly braces following the class declaration. Although not a requirement, generally you place each class into its own file. This makes it easier to find the code that defines a particular class, because the convention is to name the file using the class name.
DO NOT place more than one class in a single source file.
DO name the source file with the name of the public type it contains.
Once you have defined a new class, you can use that class as though it were built into the framework. In other words, you can declare a variable of that type or define a method that takes a parameter of the new class type. Listing 6.2 demonstrates such declarations.
class Program { static void Main() { Employee employee1, employee2; // ... } static void IncreaseSalary(Employee employee) { // ... } }
In casual conversation, the terms class and object appear interchangeably. However, they actually have distinct meanings. A class is a template for what an object will look like at instantiation time. An object, therefore, is an instance of a class. Classes are like the mold for what a widget will look like; objects correspond to widgets created by the mold. The process of creating an object from a class is called instantiation because an object is an instance of a class.
Now that you have defined a new class type, it is time to instantiate an object of that type. Mimicking its predecessors, C# uses the new
keyword to instantiate an object (see Listing 6.3).
class Program { static void Main() { Employee employee1 = new Employee(); Employee employee2; employee2 = new Employee(); IncreaseSalary(employee1); IncreaseSalary(employee2); } }
Not surprisingly, the assignment can occur either in the same statement as the declaration or in a separate statement.
Unlike the primitive types you have worked with so far, there is no literal way to specify an Employee
. Instead, the new
operator provides an instruction to the runtime to allocate memory for an Employee
object, initialize the object, and return a reference to the instance.
Although an explicit operator for allocating memory exists, there is no such operator for de-allocating the memory. Instead, the runtime automatically reclaims the memory sometime after the object becomes inaccessible. The garbage collector is responsible for the automatic de-allocation. It determines which objects are no longer referenced by other active objects and then de-allocates the memory for those objects. As a result, there is no compile-time–determined program location where the memory will be collected and restored to the system.
In this trivial example, no explicit data or methods are associated with an Employee
, which renders the object essentially useless. The next section focuses on adding data to an object.
If you received a stack of index cards with employees’ first names, a stack of index cards with their last names, and a stack of index cards with their salaries, the cards would be of little value unless you knew that the cards were in the same order in each stack. Even so, the data would be difficult to work with because determining a person’s full name would require searching through two stacks. Worse, if you dropped one of the stacks, there would be no way to reassociate the first name with the last name and the salary. Instead, you would need one stack of employee cards in which the data for each employee is grouped on one card. With this approach, first names, last names, and salaries will be encapsulated together.
Outside the object-oriented programming context, to encapsulate a set of items is to enclose those items within a capsule. Similarly, object-oriented programming encapsulates methods and data together into an object. This provides a grouping of all of the class members (the data and methods within a class) so that they no longer need to be handled individually. Instead of passing a first name, a last name, and a salary as three separate parameters to a method, objects enable a call to pass a reference to an employee object. Once the called method receives the object reference, it can send a message (e.g., it can call a method such as AdjustSalary()
) to the object to perform a particular operation.
delete
OperatorC# programmers should view the new
operator as a call to instantiate an object, not as a call to allocate memory. Both objects allocated on the heap and objects allocated on the stack support the new
operator, emphasizing the point that new
is not about how memory allocation should take place and whether de-allocation is necessary.
Thus C# does not need the delete
operator found in C++. Memory allocation and de-allocation are details that the runtime manages, allowing the developer to focus more on domain logic. However, although memory is managed by the runtime, the runtime does not manage other resources such as database connections, network ports, and so on. Unlike C++, C# does not support implicit deterministic resource cleanup (the occurrence of implicit object destruction at a compile-time–defined location in the code). Fortunately, C# does support explicit deterministic resource cleanup via a using
statement and implicit nondeterministic resource cleanup using finalizers.
One of the key aspects of object-oriented design is the grouping of data to provide structure. This section discusses how to add data to the Employee
class. The general object-oriented term for a variable that stores data within a class is member variable. This term is well understood in C#, but the more standard term—and the one used in the specification—is field, which is a named unit of storage associated with the containing type. Instance fields are variables declared at the class level to store data associated with an object. Hence, association is the relationship between the field data type and the containing field.
In Listing 6.4, the class Employee
has been modified to include three fields: FirstName
, LastName
, and Salary
(For elucidation, this listing and the related listings throughout the example are not marked as C# 8.0, even though Salary
is declared as a C# 8.0 nullable reference type.).
public class Employee { public string FirstName; public string LastName; public string? Salary; }
With these fields added, it is possible to store some fundamental data with every Employee
instance. In this case, you prefix the fields with an access modifier of public
. The use of public
on a field indicates that the data within the field is accessible from classes other than Employee
(see the section “Access Modifiers” later in this chapter).
Like a local variable declaration, a field declaration includes the data type to which the field refers. Furthermore, it is possible to assign fields an initial value at declaration time, as demonstrated with the Salary
field in Listing 6.5.
// Non-nullable field uninitialized warning disabled while code is incomplete #pragma warning disable CS8618 public class Employee { public string FirstName; public string LastName; public string? Salary = "Not enough"; }
We delay the guidelines of naming and coding fields until later in the chapter, after C# properties and constructors have been introduced. Suffice it to say, until then the listings frequently do not follow the standard coding guidelines. In fact, there is a preponderance of the following warnings:
“CS0649: Field is never assigned to, and will always have its default value null.”
“CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.”
In this case, since FirstName
and LastName
are not initialized, they trigger the CS8618 warning.
For purposes of elucidation, these warnings are ignored and, in fact, disabled with #pragma
directives in the accompanying source code until the concepts are fully developed later in the chapter.
You can set and retrieve the data within fields. However, the fact that a field does not include a static
modifier indicates that it is an instance field. You can access an instance field only from an instance of the containing class (an object). You cannot access it from the class directly (without first creating an instance, in other words).
Listing 6.6 shows an updated look at the Program
class and its utilization of the Employee
class, and Output 6.1 shows the results.
class Program { static void Main() { Employee employee1 = new Employee(); Employee employee2; employee2 = new Employee(); employee1.FirstName = "Inigo"; employee1.LastName = "Montoya"; employee1.Salary = "Too Little"; IncreaseSalary(employee1); Console.WriteLine( "{0} {1}: {2}", employee1.FirstName, employee1.LastName, employee1.Salary); // ... } static void IncreaseSalary(Employee employee) { employee.Salary = "Enough to survive on"; } }
Output 6.1
Inigo Montoya: Enough to survive on
Listing 6.6 instantiates two Employee
objects, as you saw earlier. It then sets each field, calls IncreaseSalary()
to change the salary, and displays each field associated with the object referenced by employee1
.
Notice that you first have to specify which Employee
instance you are working with. Therefore, the employee1
variable appears as a prefix to the field name when assigning and accessing the field.
One alternative to formatting the names in the WriteLine()
method call within Main()
is to provide a method in the Employee
class that takes care of the formatting. Changing the functionality to be within the Employee
class rather than a member of Program
is consistent with the encapsulation of a class. Why not group the methods relating to the employee’s full name with the class that contains the data that forms the name? Listing 6.7 demonstrates the creation of such a method.
public class Employee { public string FirstName; public string LastName; public string? Salary; public string GetName() { return $"{ FirstName } { LastName }"; } }
There is nothing particularly special about this method compared to what you learned in Chapter 5, except that now the GetName()
method accesses fields on the object instead of just local variables. In addition, the method declaration is not marked with static
. As you will see later in this chapter, static methods cannot directly access instance fields within a class. Instead, it is necessary to obtain an instance of the class to call any instance member, whether a method or a field.
Given the addition of the GetName()
method, you can update Program.Main()
to use the method, as shown in Listing 6.8 and Output 6.2.
class Program { static void Main() { Employee employee1 = new Employee(); Employee employee2; employee2 = new Employee(); employee1.FirstName = "Inigo"; employee1.LastName = "Montoya"; employee1.Salary = "Too Little"; IncreaseSalary(employee1); Console.WriteLine( $"{ employee1.GetName() }: { employee1.Salary }"); // ... } // ... }
Output 6.2
Inigo Montoya: Enough to survive on
this
KeywordYou can obtain the reference to a class from within instance members that belong to the class. To indicate explicitly that the field or method accessed is an instance member of the containing class in C#, you use the keyword this
. Use of this
is implicit when calling any instance member, and it returns an instance of the object itself.
For example, consider the SetName()
method shown in Listing 6.9.
public class Employee { public string FirstName; public string LastName; public string? Salary; public string GetName() { return $"{ FirstName } { LastName }"; } public void SetName( string newFirstName, string newLastName) { this.FirstName = newFirstName; this.LastName = newLastName; } }
This example uses the keyword this
to indicate that the fields FirstName
and LastName
are instance members of the class.
Although the this
keyword can prefix any and all references to local class members, the general guideline is not to clutter code when there is no additional value. Therefore, you should avoid using the this
keyword unless it is required. Listing 6.12 (later in this chapter) is an example of one of the few circumstances when such a requirement exists. Listings 6.9 and 6.10, however, are not good examples. In Listing 6.9, this
can be dropped entirely without changing the meaning of the code. And in Listing 6.10 (presented next), by changing the naming convention for fields and following the naming convention for parameters, we can avoid any ambiguity between local variables and fields.
In the SetName()
method, you did not have to use the this
keyword because FirstName
is obviously different from newFirstName
. But suppose that, instead of calling the parameter “newFirstName,” you called it “FirstName” (using PascalCase), as shown in Listing 6.10.
public class Employee { public string FirstName; public string LastName; public string? Salary; public string GetName() { return $"{ FirstName } { LastName }"; } // Caution: Parameter names use PascalCase public void SetName(string FirstName, string LastName) { this.FirstName = FirstName; this.LastName = LastName; } }
In this example, it is not possible to refer to the FirstName
field without explicitly indicating that the Employee
object owns the variable. this
acts just like the employee1
variable prefix used in the Program.Main()
method (see Listing 6.8); it identifies the reference as the one on which SetName()
was called.
Listing 6.10 does not follow the C# naming convention in which parameters are declared like local variables, using camelCase. This can lead to subtle bugs, because assigning FirstName
(intending to refer to the field) to FirstName
(the parameter) will lead to code that still compiles and even runs. To avoid this problem, it is a good practice to have a different naming convention for parameters and local variables than the naming convention for fields and properties. We demonstrate one such convention later in this chapter.
Me
The C# keyword this
is identical to the Visual Basic keyword Me
.
In Listings 6.9 and 6.10, the this
keyword is not used in the GetName()
method—it is optional. However, if local variables or parameters exist with the same name as the field (see the SetName()
method in Listing 6.10), omitting this
would result in accessing the local variable/parameter when the intention was to access the field; given this scenario, use of this
is required.
You also can use the keyword this
to access a class’s methods explicitly. For example, this.GetName()
is allowed within the SetName()
method, permitting you to print out the newly assigned name (see Listing 6.11 and Output 6.3).
public class Employee { // ... public string GetName() { return $"{ FirstName } { LastName }"; } public void SetName(string newFirstName, string newLastName) { this.FirstName = newFirstName; this.LastName = newLastName; Console.WriteLine( $"Name changed to '{ this.GetName() }'"); } }
class Program { static void Main() { Employee employee = new Employee(); employee.SetName("Inigo", "Montoya"); // ... } // ... }
Output 6.3
Name changed to 'Inigo Montoya'
Sometimes it may be necessary to use this
to pass a reference to the currently executing object. Consider the Save()
method in Listing 6.12.
public class Employee { public string FirstName; public string LastName; public string? Salary; public void Save() { DataStorage.Store(this); } }
class DataStorage { // Save an employee object to a file // named with the Employee name public static void Store(Employee employee) { // ... } }
The Save()
method in Listing 6.12 calls a method on the DataStorage
class, called Store()
. The Store()
method, however, needs to be passed the Employee
object, which needs to be persisted. This is done using the keyword this
, which passes the instance of the Employee
object on which Save()
was called.
The actual implementation of the Store()
method inside DataStorage
involves classes within the System.IO
namespace, as shown in Listing 6.13. Inside Store()
, you begin by instantiating a FileStream
object that you associate with a file corresponding to the employee’s full name. The FileMode.Create
parameter indicates that you want a new file to be created if there isn’t already one with the <firstname><lastname>.dat
name; if the file exists already, it will be overwritten. Next, you create a StreamWriter
class, which is responsible for writing text into the FileStream
. You write the data using WriteLine()
methods, just as though writing to the console.
using System; // IO namespace using System.IO; class DataStorage { // Save an employee object to a file // named with the Employee name. // Error handling not shown. public static void Store(Employee employee) { // Instantiate a FileStream using FirstNameLastName.dat // for the filename. FileMode.Create will force // a new file to be created or override an // existing file. FileStream stream = new FileStream( employee.FirstName + employee.LastName + ".dat", FileMode.Create);1 // Create a StreamWriter object for writing text // into the FileStream StreamWriter writer = new StreamWriter(stream); // Write all the data associated with the employee writer.WriteLine(employee.FirstName); writer.WriteLine(employee.LastName); writer.WriteLine(employee.Salary); // Close the StreamWriter and its stream writer.Dispose();// Automatically closes the stream } // ... }
1. This code could be improved with a using
statement—a construct that we have avoided because it has not yet been introduced.
Once the write operations are completed, both the FileStream
and the StreamWriter
need to be closed so that they are not left open indefinitely while waiting for the garbage collector to run. Listing 6.13 does not include any error handling, so if an exception is thrown, neither Close()
method will be called.
The load process is similar (see Listing 6.14).
public class Employee { // ... }
// IO namespace using System; using System.IO; class DataStorage { // ... public static Employee Load(string firstName, string lastName) { Employee employee = new Employee(); // Instantiate a FileStream using FirstNameLastName.dat // for the filename. FileMode.Open will open // an existing file or else report an error. FileStream stream = new FileStream( firstName + lastName + ".dat", FileMode.Open);2 // Create a StreamReader for reading text from the file StreamReader reader = new StreamReader(stream); // Read each line from the file and place it into // the associated property employee.FirstName = reader.ReadLine()?? throw new InvalidOperationException("FirstName cannot be null"); employee.LastName = reader.ReadLine()?? throw new InvalidOperationException("LastName cannot be null"); employee.Salary = reader.ReadLine(); // Close the StreamReader and its stream reader.Dispose(); // Automatically closes the stream return employee; } }
class Program { static void Main() { Employee employee1; Employee employee2 = new Employee(); employee2.SetName("Inigo", "Montoya"); employee2.Save(); // Modify employee2 after saving IncreaseSalary(employee2); // Load employee1 from the saved version of employee2 employee1 = DataStorage.Load("Inigo", "Montoya"); Console.WriteLine( $"{ employee1.GetName() }: { employee1.Salary }"); // ... } // ... }
2. This code could be improved with a using
statement—again, a construct that we have avoided because it has not yet been introduced.
Output 6.4 shows the results.
Output 6.4
Name changed to 'Inigo Montoya' Inigo Montoya:
The reverse of the save process appears in Listing 6.14, which uses a StreamReader
rather than a StreamWriter
. Again, Close()
needs to be called on both FileStream
and StreamReader
once the data has been read.
Output 6.4 does not show any salary after Inigo Montoya:
because Salary
was not set to Enough to survive on
by a call to IncreaseSalary()
until after the call to Save()
.
Notice in Main()
that we can call Save()
from an instance of an employee, but to load a new employee we call DataStorage.Load()
. To load an employee, we generally don’t already have an employee instance to load into, so an instance method on Employee
would be less than ideal. An alternative to calling Load
on DataStorage
would be to add a static Load()
method (see the section “Static Members” later in this chapter) to Employee
so that it would be possible to call Employee.Load()
(using the Employee
class, not an instance of Employee
).
Notice the inclusion of the using System.IO
directive at the top of the listing. This directive makes each IO
class accessible without prefixing it with the full namespace.
When declaring a field earlier in the chapter, you prefixed the field declaration with the keyword public
. public
is an access modifier that identifies the level of encapsulation associated with the member it decorates. Six access modifiers are available: public
, private
, protected
, internal
, protected internal
, and private protected
. This section considers the first two.
Besides wrapping data and methods together into a single unit, encapsulation deals with hiding the internal details of an object’s data and behavior. To some degree, methods do this; from outside a method, all that is visible to a caller is the method declaration. None of the internal implementation is visible. Object-oriented programming enables this further, however, by providing facilities for controlling the extent to which members are visible from outside the class. Members that are not visible outside the class are private members.
In object-oriented programming, encapsulation is a term meaning not only grouping data and behavior, but also hiding data and behavior implementation details within a class (the capsule) so that the inner workings of a class are not exposed. This reduces the chance that callers will modify the data inappropriately or program according to the implementation, only to have it change in the future.
The purpose of an access modifier is to provide encapsulation. By using public
, you explicitly indicate that it is acceptable that the modified fields are accessible from outside the Employee
class—that they are accessible from the Program
class, for example.
But consider an Employee
class that includes a Password
field. It should be possible to call an Employee
object and verify the password using a Logon()
method. Conversely, it should not be possible to access the Password
field on an Employee
object from outside the class.
To define a Password
field as hidden and inaccessible from outside the containing class, you use the keyword private
for the access modifier, in place of public
(see Listing 6.15). As a result, the Password
field is only accessible from inside the Employee
class, for example.
public class Employee { public string FirstName; public string LastName; public string? Salary; // Working with decrypted passwords for elucidation // only – this is not recommended. private string Password; private bool IsAuthenticated; public bool Logon(string password) { if(Password == password) { IsAuthenticated = true; } return IsAuthenticated; } public bool GetIsAuthenticated() { return IsAuthenticated; } // ... }
class Program { static void Main() { Employee employee = new Employee(); employee.FirstName = "Inigo"; employee.LastName = "Montoya"; // ... // Password is private, so it cannot be // accessed from outside the class // Console.WriteLine( // $"Password = { employee.Password}"); } // ... }
Although this option is not shown in Listing 6.15, it is possible to decorate a method with an access modifier of private
as well.
If no access modifier is placed on a class member, the declaration defaults to private
. In other words, members are private by default and programmers need to specify explicitly that a member is to be public.
The preceding section, “Access Modifiers,” demonstrated how you can use the private
keyword to encapsulate a password, preventing access from outside the class. This type of encapsulation is often too strict, however. For example, sometimes you might need to define fields that external classes can only read but whose values you can change internally. Alternatively, perhaps you want to allow access to write some data in a class, but you need to be able to validate changes made to the data. In yet another scenario, perhaps you need to construct the data on the fly. Traditionally, languages enabled the features found in these examples by marking fields as private and then providing getter and setter methods for accessing and modifying the data. The code in Listing 6.16 changes both FirstName
and LastName
to private fields. Public getter and setter methods for each field allow their values to be accessed and changed.
public class Employee { private string FirstName; // FirstName getter public string GetFirstName() { return FirstName; } // FirstName setter public void SetFirstName(string newFirstName) { if(newFirstName != null && newFirstName != "") { FirstName = newFirstName; } } private string LastName; // LastName getter public string GetLastName() { return LastName; } // LastName setter public void SetLastName(string newLastName) { if(newLastName != null && newLastName != "") { LastName = newLastName; } } // ... }
Unfortunately, this change affects the programmability of the Employee
class. You can no longer use the assignment operator to set data within the class, nor can you access the data without calling a method.
Recognizing the frequency of this type of pattern, the C# designers provided explicit syntax for it. This syntax is called a property (see Listing 6.17 and Output 6.5).
class Program { static void Main() { Employee employee = new Employee(); // Call the FirstName property's setter employee.FirstName = "Inigo"; // Call the FirstName property's getter System.Console.WriteLine(employee.FirstName); } }
public class Employee { // FirstName property public string FirstName { get { return _FirstName; } set { _FirstName = value; } } private string _FirstName; // ... }
Output 6.5
Inigo
The first thing to notice in Listing 6.17 is not the property code itself, but rather the code within the Program
class. Although you no longer have the fields with the FirstName
and LastName
identifiers, you cannot see this by looking at the Program
class. The syntax for accessing an employee’s first and last names has not changed at all. It is still possible to assign the parts of the name using a simple assignment operator—for example, employee.FirstName = "Inigo"
.
The key feature is that properties provide a syntax that looks programmatically like a field. In actuality, no such fields exist. A property declaration looks exactly like a field declaration, but following it are curly braces in which to place the property implementation. Two optional parts make up the property implementation. The get
part defines the getter portion of the property. It corresponds directly to the GetFirstName()
and GetLastName()
functions defined in Listing 6.16. To access the FirstName
property, you call employee.FirstName
. Similarly, setters (the set
portion of the implementation) enable the calling syntax of the field assignment:
employee.FirstName = "Inigo";
Property definition syntax uses three contextual keywords. You use the get
and set
keywords to identify either the retrieval or the assignment portion of the property, respectively. In addition, the setter uses the value
keyword to refer to the right side of the assignment operation. When Program.Main()
calls employee.FirstName = "Inigo"
, therefore, value
is set to "Inigo"
inside the setter and can be used to assign _FirstName
. Listing 6.17’s property implementations are the most commonly used. When the getter is called (such as in Console.WriteLine(employee.FirstName)
), the value from the field (_FirstName
) is obtained and written to the console.
Begin 7.0
Starting with C# 7.0, it is also possible to declare property getters and setters using expression bodied members, as shown in Listing 6.18.
public class Employee { // FirstName property public string FirstName { get { return _FirstName; } set { _FirstName = value; } } private string _FirstName; // LastName property public string LastName { get => _LastName; set => _LastName = value; } private string _LastName; // ... }
End 7.0
Listing 6.18 uses two different syntaxes for an identical property implementation. In real-world code, you should try to be consistent in your choice of syntax.
Begin 3.0
In C# 3.0, property syntax included another syntax shortcut. Since a property with a single backing field that is assigned and retrieved by the get and set accessors is so trivial and common (see the implementations of FirstName
and LastName
), the C# 3.0 (and higher) compiler allows the declaration of a property without any accessor implementation or backing field declaration. Listing 6.19 demonstrates the syntax with the Title
and Manager
properties, and Output 6.6 shows the results.
class Program { static void Main() { Employee employee1 = new Employee(); Employee employee2 = new Employee(); // Call the FirstName property's setter employee1.FirstName = "Inigo"; // Call the FirstName property's getter System.Console.WriteLine(employee1.FirstName); // Assign an auto-implemented property employee2.Title = "Computer Nerd"; employee1.Manager = employee2; // Print employee1's manager's title System.Console.WriteLine(employee1.Manager.Title); } }
public class Employee { // FirstName property public string FirstName { get { return _FirstName; } set { _FirstName = value; } } private string _FirstName; // LastName property public string LastName { get => _LastName; set => _LastName = value; } private string _LastName; public string? Title { get; set; } public Employee? Manager { get; set; } public string? Salary { get; set; } = "Not Enough"; // ... }
Output 6.6
Inigo Computer Nerd
Auto-implemented properties provide for a simpler way of writing properties in addition to reading them. Furthermore, when it comes time to add something such as validation to the setter, any existing code that calls the property will not have to change, even though the property declaration will have changed to include an implementation.
End 3.0
Begin 6.0
Throughout the remainder of the book, we frequently use this C# 3.0 or later syntax without indicating that it is a feature introduced in C# 3.0.
One final thing to note about automatically declared properties is that in C# 6.0, it is possible to initialize them as Listing 6.19 does for Salary
:
public string? Salary { get; set; } = "Not Enough";
Prior to C# 6.0, property initialization was possible only via a method (including the constructor, as we discuss later in the chapter). However, with C# 6.0, you can initialize automatically implemented properties at declaration time using a syntax much like that used for field initialization.
End 6.0
Given that it is possible to write explicit setter and getter methods rather than properties, on occasion a question may arise as to whether it is better to use a property or a method. The general guideline is that methods should represent actions and properties should represent data. Properties are intended to provide simple access to simple data with a simple computation. The expectation is that invoking a property will not be significantly more expensive than accessing a field.
With regard to naming, notice that in Listing 6.19 the property name is FirstName
, and the field name changed from earlier listings to _FirstName
—that is, PascalCase with an underscore prefix. Other common naming conventions for the private field that backs a property are _firstName
and, on occasion, the camelCase convention, just like with local variables.3 The camelCase convention should be avoided, however. The camelCase used for property names is the same as the naming convention used for local variables and parameters, meaning that overlaps in names become highly probable. Also, to respect the principles of encapsulation, fields should not be declared as public or protected.
3. I prefer _FirstName
because the m in front of the name is unnecessary when compared with an underscore (_). Also, by using the same casing as the property, it is possible to have only one string within the Visual Studio code template expansion tools instead of having one for both the property name and the field name.
DO use properties for simple access to simple data with simple computations.
AVOID throwing exceptions from property getters.
DO preserve the original property value if the property throws an exception.
DO favor automatically implemented properties over properties with simple backing fields when no additional logic is required.
Regardless of which naming pattern you use for private fields, the coding standard for properties is PascalCase. Therefore, properties should use the LastName
and FirstName
pattern with names that represent nouns, noun phrases, or adjectives. It is not uncommon, in fact, for the property name to be the same as the type name. Consider an Address
property of type Address
on a Person
object, for example.
CONSIDER using the same casing on a property’s backing field as that used in the property, distinguishing the backing field with an “_” prefix.
DO name properties using a noun, noun phrase, or adjective.
CONSIDER giving a property the same name as its type.
AVOID naming fields with camelCase.
DO favor prefixing Boolean properties with “Is,” “Can,” or “Has,” when that practice adds value.
DO declare all instance fields as private (and expose them via a property).
DO name properties with PascalCase.
DO favor automatically implemented properties over fields.
DO favor automatically implemented properties over using fully expanded ones if there is no additional implementation logic.
Notice in Listing 6.20 that the Initialize()
method of Employee
uses the property rather than the field for assignment as well. Although this is not required, the result is that any validation within the property setter will be invoked both inside and outside the class. Consider, for example, what would happen if you changed the LastName
property so that it checked value
for null
or an empty string before assigning it to _LastName
. (Recall that this step is necessary because even though the data type is a non-nullable string, the caller may have nullable reference types disabled, or the method may be invoked from C# 7.0 or earlier—before nullable reference types existed.)
public class Employee { // ... public void Initialize( string newFirstName, string newLastName) { // Use property inside the Employee // class as well FirstName = newFirstName; LastName = newLastName; } // LastName property public string LastName { get => _LastName; set { // Validate LastName assignment if(value is null) { // Report error // In C# 6.0 replace "value" with nameof(value) throw new ArgumentNullException("value"); } else { // Remove any whitespace around // the new last name value = value.Trim(); if(value == "") { // Report error throw new ArgumentException( // Use "value" rather than nameof(value) // prior to C# 6.0. "LastName cannot be blank.", nameof(value)); } else _LastName = value; } } } private string _LastName; // ... }
With this new implementation, the code throws an exception if LastName
is assigned an invalid value, either from another member of the same class or via a direct assignment to LastName
from inside Program.Main()
. The ability to intercept an assignment and validate the parameters by providing a field-like API is one of the advantages of properties.
It is a good practice to access a property-backing field only from inside the property implementation. In other words, you should always use the property rather than calling the field directly. In many cases, this principle holds even from code within the same class as the property. If you follow this practice, when you add code such as validation code, the entire class immediately takes advantage of it.4
4. As described later in the chapter, one exception to this occurs when the field is marked as read-only, because then the value can be set only in the constructor. In C# 6.0, you can directly assign the value of a read-only property, completely eliminating the need for the read-only field.
Although rare, it is possible to assign value
inside the setter, as Listing 6.20 does. In this case, the call to value.Trim()
removes any whitespace surrounding the new last name value.
Begin 6.0
Prior to C# 6.0, you would specify "value"
for the paramName
value of the exception. However, in C# 6.0, you can instead use nameof(value)
—see Advanced Topic: nameof
Operator for more information. I use nameof(value)
for the remainder of the chapter, so you will need to switch the code back to "value"
when compiling for C# 5.0 or earlier.
nameof
OperatorIf during property validation you determine that the new value assignment is invalid, it is necessary to throw an exception—generally of type ArgumentException()
or ArgumentNullException()
. Both exceptions take an argument of type string
called paramName
that identifies the name of the parameter that is invalid. In Listing 6.20, we first pass "value"
as the argument for this parameter, but C# 6.0 provides an improvement with the nameof
operator. The nameof
operator takes an identifier, like the value
variable, and returns a string representation of that name—in this case, "value"
. Listing 6.20 takes this approach when reporting the second error.
The advantage of using the nameof
operator is that if the identifier name changes, then refactoring tools will automatically change the argument to nameof
as well. If no refactoring tool is used, the code will no longer compile, forcing the developer to change the argument manually. The result is that nameof
will even check for spelling errors. The resulting guideline is: DO use nameof
for the paramName
argument passed into exceptions like ArgumentNullException
and ArgumentNullException
that take such a parameter. For more information, see Chapter 18.
AVOID accessing the backing field of a property outside the property, even from within the containing class.
DO use nameof(value)
(which resolves to “value”
) for the paramName
argument when creating ArgumentException()
or ArgumentNullException()
type exceptions. ("value"
is the implicit name of the parameter on property setters.)
By removing either the getter or the setter portion of a property, you can change a property’s accessibility. Properties with only a setter are write-only, which is a relatively rare occurrence. Similarly, providing only a getter will cause the property to be read-only; any attempts to assign a value will cause a compile error. To make Id
read-only, for example, you would use the code shown in Listing 6.21.
class Program { static void Main() { Employee employee1 = new Employee(); employee1.Initialize(42); // ERROR: Property or indexer 'Employee.Id' // cannot be assigned to; it is read-only // employee1.Id = "490"; } } public class Employee { public void Initialize(int id) { // Use field because Id property has no setter; // it is read-only _Id = id.ToString(); } // ... // Id property declaration public string Id { get => _Id; // No setter provided } private string _Id; }
Listing 6.21 assigns the field from within the Employee Initialize()
method rather than the property (_Id = id
). Assigning via the property causes a compile error, as it does in Program.Main()
.
Begin 6.0
Starting in C# 6.0, there is also support for read-only, automatically implemented properties as follows:
public bool[,,] Cells { get; } = new bool[2, 3, 3];
This is clearly a significant improvement over the pre–C# 6.0 approach, especially given the commonality of read-only properties for something like an array of items or the Id
in Listing 6.21.
One important note about a read-only automatically implemented property is that, like read-only fields, the compiler requires that such a property be initialized via an initializer (or in the constructor). In the preceding snippet we use an initializer, but the assignment of Cells
from within the constructor is also permitted, as we shall see shortly.
Given the guideline that fields should not be accessed from outside their wrapping property, those programming in a C# 6.0 world will discover that there is almost never a need to use pre–C# 6.0 syntax for read-only properties; instead, the programmer can almost always use a read-only, automatically implemented property. The only exception might be when the data type of the read-only modified field does not match the data type of the property—for example, if the field was of type int
and the read-only property was of type double
.
DO create read-only properties if the property value should not be changed.
DO create read-only automatically implemented properties in C# 6.0 (or later), rather than read-only properties with a backing field if the property value should not be changed.
End 6.0
In some instances, you do not need a backing field at all. Instead, the property getter returns a calculated value, while the setter parses the value and persists it to some other member fields (if it even exists). Consider, for example, the Name
property implementation shown in Listing 6.22. Output 6.7 shows the results.
class Program { static void Main() { Employee employee1 = new Employee(); employee1.Name = "Inigo Montoya"; System.Console.WriteLine(employee1.Name); // ... } }
public class Employee { // ... // FirstName property public string FirstName { get { return _FirstName; } set { _FirstName = value; } } private string _FirstName; // LastName property public string LastName { get => _LastName; set => _LastName = value; } private string _LastName; // ... // Name property public string Name { get { return $"{ FirstName } { LastName }"; } set { // Split the assigned value into // first and last names string[] names; names = value.Split(new char[]{' '}); if(names.Length == 2) { FirstName = names[0]; LastName = names[1]; } else { // Throw an exception if the full // name was not assigned throw new System. ArgumentException ( $"Assigned value '{ value }' is invalid", nameof(value));5 } } } public string Initials => $"{ FirstName[0] } { LastName[0] }"; // ... }
5. See Advanced Block: nameof Operator earlier in the chapter or the full explanation in Chapter 18.
Output 6.7
Inigo Montoya
The getter for the Name
property concatenates the values returned from the FirstName
and LastName
properties. In fact, the name value assigned is not actually stored. When the Name
property is assigned, the value on the right side is parsed into its first and last name parts.
Begin 2.0
As previously mentioned, it is a good practice not to access fields from outside their properties because doing so circumvents any validation or additional logic that may be inserted.
An access modifier can appear on either the get or the set portion of the property implementation6 (not on both), thereby overriding the access modifier specified on the property declaration. Listing 6.23 demonstrates how to do this.
6. Introduced in C# 2.0. C# 1.0 did not allow different levels of encapsulation between the getter and setter portions of a property. It was not possible, therefore, to create a public getter and a private setter so that external classes would have read-only access to the property while code within the class could write to the property.
class Program { static void Main() { Employee employee1 = new Employee(); employee1.Initialize(42); // ERROR: The property or indexer 'Employee.Id' // cannot be used in this context because the set // accessor is inaccessible // employee1.Id = "490"; } }
public class Employee { public void Initialize(int id) { // Set Id property Id = id.ToString(); } // ... // Id property declaration public string Id { get => _Id; // Providing an access modifier is possible in C# 2.0 // and higher only private set => _Id = value; } private string _Id; }
By using private
on the setter, the property appears as read-only to classes other than Employee
. From within Employee
, the property appears as read/write, so you can assign the property within the class itself. When specifying an access modifier on the getter or setter, take care that the access modifier is more restrictive than the access modifier on the property as a whole. It is a compile error, for example, to declare the property as private
and the setter as public
.
DO apply appropriate accessibility modifiers on implementations of getters and setters on all properties.
DO NOT provide set-only properties or properties with the setter having broader accessibility than the getter.
End 2.0
ref
or out
Parameter ValuesC# allows properties to be used identically to fields, except when they are passed as ref
or out
parameter values. ref
and out
parameter values are internally implemented by passing the memory address to the target method. However, because properties can be virtual fields that have no backing field or can be read-only or write-only, it is not possible to pass the address for the underlying storage. As a result, you cannot pass properties as ref
or out
parameter values. The same is true for method calls. Instead, when code needs to pass a property or method call as a ref
or out
parameter value, the code must first copy the value into a variable and then pass the variable. Once the method call has completed, the code must assign the variable back into the property.
Listing 6.24 shows that getters and setters are exposed as get_FirstName()
and set_FirstName()
in the Common Intermediate Language (CIL).
// ... .field private string _FirstName .method public hidebysig specialname instance string get_FirstName() cil managed { // Code size 12 (0xc) .maxstack 1 .locals init (string V_0) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld string Employee::_FirstName IL_0007: stloc.0 IL_0008: br.s IL_000a IL_000a: ldloc.0 IL_000b: ret } // End of method Employee::get_FirstName .method public hidebysig specialname instance void set_FirstName(string 'value') cil managed { // Code size 9 (0x9) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld string Employee::_FirstName IL_0008: ret } // End of method Employee::set_FirstName .property instance string FirstName() { .get instance string Employee::get_FirstName() .set instance void Employee::set_FirstName(string) } // End of property Employee::FirstName // ...
Just as important to their appearance as regular methods is the fact that properties are an explicit construct within the CIL, too. As Listing 6.25 shows, the getters and setters are called by CIL properties, which are an explicit construct within the CIL code. Because of this, languages and compilers are not restricted to always interpreting properties based on a naming convention. Instead, CIL properties provide a means for compilers and code editors to provide special syntax.
.property instance string FirstName()
{
.get instance string Program::get_FirstName()
.set instance void Program::set_FirstName(string)
} // End of property Program::FirstName
Notice in Listing 6.24 that the getters and setters that are part of the property include the specialname
metadata. This modifier is what IDEs, such as Visual Studio, use as a flag to hide the members from IntelliSense.
Begin 3.0
An automatically implemented property is almost identical to one for which you define the backing field explicitly. In place of the manually defined backing field, the C# compiler generates a field with the name <PropertyName>k_BackingField
in CIL. This generated field includes an attribute (see Chapter 18) called System.Runtime.CompilerServices.CompilerGeneratedAttribute
. Both the getters and the setters are decorated with the same attribute because they, too, are generated—with the same implementation as in Listings 5.23 and 5.24.
End 3.0
Begin 8.0
Now that you have added fields to a class and can store data, you need to consider the validity of that data. As you saw in Listing 6.3, it is possible to instantiate an object using the new
operator. The result, however, is the ability to create an employee with invalid data. Immediately following the assignment of employee
, you have an Employee
object whose name and salary are not initialized. In Listing 6.3, you assigned the uninitialized fields immediately following the instantiation of an employee—but if you failed to do the initialization, you would not receive a warning from the compiler. As a result, you could end up with an Employee
object with an invalid name. (Technically, in C# 8.0, non-nullable reference types will trigger a warning suggesting that the data type be switched to nullable to avoid a default of null
. Regardless, initialization is required to avoid instantiating objects whose fields contain invalid data.)
End 8.0
To correct this problem, you need to provide a means of specifying the required data when the object is created. You do this using a constructor, as demonstrated in Listing 6.26.
public class Employee { // Employee constructor public Employee(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName{ get; set; } public string LastName{ get; set; } public string? Salary{ get; set; } = "Not Enough"; // ... }
As shown here, to define a constructor, you create a method with no return type, whose method name is identical to the class name. The constructor is the method that the runtime calls to initialize an instance of the object. In this case, the constructor takes the first name and the last name as parameters, allowing the programmer to specify these names when instantiating the Employee
object. Listing 6.27 is an example of how to call a constructor.
class Program { static void Main() { Employee employee; employee = new Employee("Inigo", "Montoya"); employee.Salary = "Too Little"; System.Console.WriteLine( "{0} {1}: {2}", employee.FirstName, employee.LastName, employee.Salary); } // ... }
Notice that the new
operator returns the type of the object being instantiated (even though no return type or return statement was specified explicitly in the constructor’s declaration or implementation). In addition, you have removed the initialization code for the first and last names because that initialization takes place within the constructor. In this example, you don’t initialize Salary
within the constructor, so the code assigning the salary still appears.
Developers should take care when using both assignment at declaration time and assignment within constructors. Assignments within the constructor will occur after any assignments are made when a field is declared (such as string Salary = "Not enough"
in Listing 6.5). Therefore, assignment within a constructor will override any value assigned at declaration time. This subtlety can lead to a misinterpretation of the code by a casual reader who assumes the value after instantiation is the one assigned in the field declaration. Therefore, it is worth considering a coding style that does not mix both declaration assignment and constructor assignment within the same class.
new
OperatorInternally, the interaction between the new
operator and the constructor is as follows. The new
operator retrieves “empty” memory from the memory manager and then calls the specified constructor, passing a reference to the empty memory to the constructor as the implicit this
parameter. Next, the remainder of the constructor chain executes, passing around the reference between constructors. None of the constructors have a return type; behaviorally they all return void
. When execution of the constructor chain is complete, the new
operator returns the memory reference, now referring to the memory in its initialized form.
When you add a constructor explicitly, you can no longer instantiate an Employee
from within Main()
without specifying the first and last names. The code shown in Listing 6.28, therefore, will not compile.
class Program { static void Main() { Employee employee; // ERROR: No overload because method 'Employee' // takes '0' arguments employee = new Employee(); // ... } }
If a class has no explicitly defined constructor, the C# compiler adds one during compilation. This constructor takes no parameters so, by definition, it is the default constructor. As soon as you add an explicit constructor to a class, the C# compiler no longer provides a default constructor. Therefore, with Employee(string firstName, string lastName)
defined, the default constructor, Employee()
, is not added by the compiler. You could manually add such a constructor, but then you would again be allowing construction of an Employee
without specifying the employee name.
It is not necessary to rely on the default constructor defined by the compiler. That is, programmers can define a default constructor explicitly—perhaps one that initializes some fields to particular values. Defining the default constructor simply involves declaring a constructor that takes no parameters.
Begin 3.0
Starting with C# 3.0, the C# language team added functionality to initialize an object’s accessible fields and properties. To do so, you can use the concept of an object initializer—a set of member initializers enclosed in curly braces following the constructor call to create the object. Each member initializer is the assignment of an accessible field or property name with a value (see Listing 6.29).
class Program { static void Main() { Employee employee1 = new Employee("Inigo", "Montoya") { Title = "Computer Nerd", Salary = "Not enough"}; // ... } }
Notice that the same constructor rules apply even when using an object initializer. In fact, the resultant CIL is exactly the same as it would be if the fields or properties were assigned within separate statements immediately following the constructor call. The order of member initializers in C# provides the sequence for property and field assignment in the statements following the constructor call within CIL.
In general, all properties should be initialized to reasonable default values by the time the constructor exits. Moreover, by using validation logic on the setter, it is possible to restrict the assignment of invalid data to a property. On occasion, the values on one or more properties may cause other properties on the same object to contain invalid values. When this occurs, exceptions from the invalid state should be postponed until the invalid interrelated property values become relevant.
DO provide sensible defaults for all properties, ensuring that defaults do not result in a security hole or significantly inefficient code.
DO allow properties to be set in any order, even if this results in a temporarily invalid object state.
Using a similar syntax to that of object initializers, collection initializers were added in C# 3.0. Collection initializers support a similar feature set as object initializers, only with collections. Specifically, a collection initializer allows the assignment of items within the collection at the time of the collection’s instantiation. Borrowing the same syntax used for arrays, the collection initializer initializes each item within the collection as part of collection creation. Initializing a list of Employee
s, for example, involves specifying each item within curly braces following the constructor call, as shown in Listing 6.30.
3.0
class Program { static void Main() { List<Employee> employees = new List<Employee>() { new Employee("Inigo", "Montoya"), new Employee("Kevin", "Bost") }; // ... } }
End 3.0
After the assignment of a new collection instance, the compiler-generated code instantiates each object in sequence and adds them to the collection via the Add()
method.
Constructors define what happens during the instantiation process of a class. To define what happens when an object is destroyed, C# provides the finalizer construct. Unlike destructors in C++, finalizers do not run immediately after an object goes out of scope. Rather, the finalizer executes at some unspecified time after an object is determined to be “unreachable.” Specifically, the garbage collector identifies objects with finalizers during a garbage collection cycle, and instead of immediately de-allocating those objects, it adds them to a finalization queue. A separate thread runs through each object in the finalization queue and calls the object’s finalizer before removing it from the queue and making it available for the garbage collector again. Chapter 10 discusses this process, along with resource cleanup, in depth.
Constructors can be overloaded—you can have more than one constructor as long as the number or types of the parameters vary. For example, as Listing 6.31 shows, you could provide a constructor that has an employee ID with first and last names, or even just the employee ID.
public class Employee { public Employee(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public Employee( int id, string firstName, string lastName ) { Id = id; FirstName = firstName; LastName = lastName; } // FirstName&LastName set inside Id property setter. #pragma warning disable CS8618 public Employee(int id) => Id = id; #pragma warning restore CS8618 public int Id { get => Id; private set { // Look up employee name... // ... } } public string FirstName { get; set; } public string LastName { get; set; } public string? Salary { get; set; } = "Not Enough"; // ... }
This approach enables Program.Main()
to instantiate an employee from the first and last names either by passing in the employee ID only or by passing both the names and the IDs. You would use the constructor with both the names and the IDs when creating a new employee in the system. You would use the constructor with only the ID to load the employee data from a file or a database.
As is the case with method overloading, multiple constructors are used to both support simple scenarios using a small number of parameters and complex scenarios with additional parameters. Consider using optional parameters in favor of overloading so that the default values for “defaulted” properties are visible in the API. For example, a constructor signature of Person(string firstName, string lastName, int? age = null)
provides signature documentation that if the Age
of a Person
is not specified, it will default to null
.
Begin 7.0
Begin 8.0
Notice also that, starting with C# 7.0, it is possible to have expression bodied member implementations of constructors, as in
// FirstName&LastName set inside Id property setter. #pragma warning disable CS8618 public Employee(int id) => Id = id;
End 7.0
End 8.0
In this case, we invoke the Id
property to assign FirstName
and LastName
. Unfortunately, the compiler doesn’t detect the assignment and, starting with C# 8.0, issues a warning to consider marking those properties as nullable. Since we are, in fact, setting them, the warning is disabled.
DO use the same name for constructor parameters (camelCase) and properties (PascalCase) if the constructor parameters are used to simply set the property.
DO provide constructor optional parameters and/or convenience constructor overloads that initialize properties with good defaults.
DO allow properties to be set in any order, even if this results in a temporarily invalid object state.
this
Notice in Listing 6.31 that the initialization code for the Employee
object is now duplicated in multiple places, so it also has to be maintained in multiple places. The amount of code is small, but there are ways to eliminate the duplication by calling one constructor from another—a practice known as constructor chaining—using constructor initializers. Constructor initializers determine which constructor to call before executing the implementation of the current constructor (see Listing 6.32).
public class Employee { public Employee(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public Employee( int id, string firstName, string lastName ) : this(firstName, lastName) { Id = id; } // FirstName&LastName set inside Id property setter. #pragma warning disable CS8618 public Employee(int id) { Id = id; // Look up employee name... // ... // NOTE: Member constructors cannot be // called explicitly inline // this(id, firstName, lastName); } #pragma warning restore CS8618 public int Id { get; private set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Salary { get; set; } = "Not Enough"; // ... }
To call one constructor from another within the same class (for the same object instance), C# uses a colon followed by the this
keyword, followed by the parameter list on the called constructor’s declaration. In this case, the constructor that takes all three parameters calls the constructor that takes two parameters. Often, this calling pattern is reversed—that is, the constructor with the fewest parameters calls the constructor with the most parameters, passing defaults for the parameters that are not known.
Notice that in the Employee(int id)
constructor implementation in Listing 6.32, you cannot call this(firstName, lastName)
because no such parameters exist on this constructor. To enable such a pattern, in which all initialization code happens through one method, you must create a separate method, as shown in Listing 6.33.
public class Employee { // FirstName&LastName set inside Initialize() method. #pragma warning disable CS8618 public Employee(string firstName, string lastName) { int id; // Generate an employee ID... // ... Initialize(id, firstName, lastName); } public Employee(int id, string firstName, string lastName ) { Initialize(id, firstName, lastName); } public Employee(int id) { string firstName; string lastName; Id = id; // Look up employee data // ... Initialize(id, firstName, lastName); } #pragma warning restore CS8618 private void Initialize( int id, string firstName, string lastName) { Id = id; FirstName = firstName; LastName = lastName; } // ... }
In this case, the method is called Initialize()
, and it takes both the names and the employee IDs. Note that you can continue to call one constructor from another, as shown in Listing 6.32.
In the same way that setting the LastName
and FirstName
via the Id
property wasn’t detected by the compiler, assignment via the Initialize
method goes undetected, so the warning is disabled.
Begin 8.0
Throughout this chapter, we have consistently disabled the C# nullable warning:
CS8618: Non-nullable field/property is uninitialized. Consider declaring as nullable.
When you declare reference type (1) non-nullable fields or (2) non-nullable automatically implemented properties, it is obvious that these fields and properties need to be initialized before the containing object is fully instantiated. Not doing so would leave those fields and properties with a default null
value—so they shouldn’t be declared as non-nullable.
The problem is that frequently the non-nullable fields and properties are initialized indirectly, outside the immediate scope of the constructor and therefore beyond the scope of the compiler’s code analysis, even if they are still, in fact, initialized, perhaps via a method or property that the constructor invokes.7 Following are some examples of this practice:
7. Or potentially via an external agent like reflection, see Chapter 18.
A simple property with validation that checks the value to be assigned to a field is not null
before assigning it to the backing field that the compiler reports is uninitialized (see Listing 6.20).
The calculated Name
property (such as Listing 6.22) sets other non-nullable properties or fields within the class.
The centralized initialization occurs in the manner shown in Listings 6.32 and 6.33.
Public properties are initialized by external agents that trigger the instantiation and then initialize the properties.8
8. Examples include the TestContext
property in MSTest or objects initialized through dependency injection.
In most cases, the reference type non-nullable field or non-nullable automatically implemented property (referred to as a non-nullable field/property in this section—“reference type” is implied) is assigned indirectly via properties or methods that the constructor invokes. Unfortunately, the C# compiler doesn’t recognize an indirect assignment of a non-nullable field/property.
Furthermore, all non-nullable fields/properties need to ensure that they are not assigned a value of null
. In the case of fields, they need to be wrapped in properties with the setter validation ensuring that a null
value will not be assigned. (Remember that field validation relies on the guideline that we do not access fields outside of the property that wraps them.) The result is that non-nullable read-write fully implemented reference-type properties should have validation preventing null
assignment.
Non-nullable automatically implemented properties need to be limited to read-only encapsulation, with any values assigned during instantiation and validated as not null
prior to assignment. Read-write non-nullable reference type automatically implemented properties should be avoided, especially with public setters, since preventing null
assignment is problematic. Although the uninitialized non-null
property compiler warning can be avoided by assigning the property from the constructor, this is not enough: The property is read-write, so it could be assigned null
after instantiation, thereby voiding your intent for it to be non-nullable.
Listing 6.34 demonstrates how to inform the compiler and avoid the false warning that a non-nullable field/property is uninitialized. The end goal is to allow the programmer to inform the compiler that the properties/fields are non-nullable so that the compiler can inform callers about the (non-) nullability of those properties/fields.
public class Employee { public Employee(string name) { Name = name; } public string Name { get => _Name!; set => _Name = value ?? throw new ArgumentNullException(nameof(value)); } private string? _Name; // ... }
The code snippet to handle a non-nullable properties/fields that are not directly initialized by the constructor has several important qualities (listed in no particular order here):
The property setter includes a check for null
that occurs before setting the value of the non-nullable field. In Listing 6.34, this is done by using the null-coalescing operator and throwing an ArgumentNullException
if the new value is null
.
The constructor invokes a method or property that indirectly assigns the non-nullable field but fails to recognize that the field is initialized to a value other than null
.
The backing field is declared as nullable to avoid the compiler warning that the field is uninitialized.
The getter returns the field with a null-forgiving operator—declaring that it is not null
thanks to the setter validation.
For a non-nullable property, it is seemingly nonsensical to declare the backing field as nullable. This is necessary, however, since the compiler is oblivious to non-nullable field/property assignments outside from the constructor. Fortunately, this is a case where you, as the programmer, are justified in using the null-forgiving operator when returning the field because of the not-null
check in the setter that ensures the field is never null
.
As pointed out earlier in this section, non-nullable automatically implemented reference-type properties need to be read-only to avoid invalid null
assignments. However, you still need to validate any parameters that may be assigned during instantiation, as shown in Listing 6.35.
public class Employee { public Employee(string name) { Name = name?? throw new ArgumentNullException(nameof(name)); } public string Name { get; } }
One could debate whether a private setter should be allowed on non-nullable automatically implemented reference-type properties. While possible, the more appropriate question to ask is whether your class could mistakenly assign null
to the property. If you don’t encapsulate the field with validation in a setter, can you be sure you won’t mistakenly assign a null
value? While the compiler will verify your intent during instantiation, it is questionable that the developer will always remember to check for null
in values coming into your class that should be non-nullable—as occurs in the constructor shown in Listing 6.35.
DO implement non-nullable read/write reference fully implemented properties with a nullable backing field, a null-forgiving operator when returning the field from the getter, and non-null validation in the property setter.
DO assign non-nullable reference-type properties before instantiation completes.
DO implement non-nullable reference-type automatically implemented properties as read-only.
DO use a nullable check for all reference-type properties and fields that are not initialized before instantiation completes.
Rather than disable nullable or nullable warnings, occasionally it is helpful to provide the compiler with hints about your nullable intent. This is possible using metadata that you can place directly into your code with a construct called an attribute (see Chapter 18). There are seven different nullable attributes, each defined in the System.Diagnostics.CodeAnalysis
namespace, and identified as either pre-conditions or post-conditions (Table 6.1).
It is helpful to have such attributes because occasionally, the nullability of the data type is insufficient. You can overcome this insufficiency with an attribute that decorates either incoming (a pre-condition nullable attribute) or outgoing (a post-condition nullable attribute) data on a method. The pre-condition communicates with the caller about whether the value specified is intended to be null
, whereas the post-condition communicates with the caller about the nullability of the outgoing data. Consider, for example, the methods that follow the try-get pattern shown in Listing 6.36.
Table 6.1: Nullable Attributes
Attribute |
Category |
Description |
|
precondition |
Non-nullable input argument may be |
|
precondition |
Nullable input argument should never be |
|
postcondition |
Non-nullable return value may be |
|
postcondition |
Nullable return value will never be |
|
postcondition |
A non-nullable input argument may be |
|
postcondition |
Nullable input argument will not be |
|
postcondition |
Return value isn’t |
|
using System.Diagnostics.CodeAnalysis; // ... static bool TryGetDigitAsText( char number, [NotNullWhen(true)]out string? text) => (text = number switch { '1' => "one", '2' => "two", // ... '9' => "nine", _ => null }) is string; [return: NotNullIfNotNull("text")] static public string? TryGetDigitsAsText(string? text) { if (text is null) return null; string result = ""; foreach (char character in text) { if (TryGetDigitAsText(character, out string? digitText)) { if (result != "") result += '-'; result += digitText.ToLower(); } } return result; }
Notice that the call to digitText.ToLower()
from TryGetDigitAsText()
has no coalescing operator and does not issue a warning even though text
is declared as nullable. This is possible because the text
parameter in TryGetDigitAsText()
is decorated with the NotNullWhen(true)
attribute, which informs the compiler that, if the method returns true (the value specified with the NotNullWhen
attribute), then your intent is that digitText
will not be null
. The NotNullWhen
attribute is a post-condition declaration, informing the caller that output (text
) is not null
if the method returns true
.
Similarly, for TryGetDigitsAsText()
, if the value specified for the text
parameter is not null
, then the value return will not be null
. This is possible because the pre-condition nullable attribute, NotNullIfNotNull
, uses whether the input value of the text
parameter is null
to determine whether the return value may potentially be null
.
When declaring a generic member or type, you will occasionally want to decorate the type parameter with a nullable modifier. The problem is that a nullable value type (a Nullable<T>
) is a different data type than a nullable reference type. As a result, type parameters decorated with nullability will require a constraint that restricts the type parameter to be either a value type or a reference type. Without this constraint, you will receive the following error:
Error CS8627 A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
However, if the logic is identical for both value types and reference types, it can be frustrating to implement two different methods—especially since different constraints do not result in different signatures to allow overloading. Consider the code in Listing 6.37.
// ... [return: MaybeNull] static public T GetObject<T>( System.Collections.Generic.IEnumerable<T> sequence, Func<T, bool> match) => // ...
Imagine that the behavior is to return an item from the collection if one satisfies the match predicate. However, if no such item exists, the intent would be to return default(T)
, which is null
for a reference type. Unfortunately, the compiler won’t allow T?
without a constraint. To avoid the warning while still declaring to callers that the return could be null
, we use the post-condition MaybeNull
attribute and leave the return type as T
(with no nullable modifier).
End 8.0
Begin 7.0
Constructors allow you to take multiple parameters and encapsulate them all into a single object. Up until C# 7.0, there was no explicit construct for implementing the reverse—unwrapping the encapsulated item into its constituent parts. Sure, you could manually assign each property to a variable; however, if there were a significant number of such variables, it would require many separate statements. With C#’s 7.0 syntax for tuples, this becomes significantly easier. You could, for example, declare a method like the Deconstruct()
method shown in Listing 6.38.
public class Employee { public void Deconstruct( out int id, out string firstName, out string lastName, out string salary) { (id, firstName, lastName, salary) = (Id, FirstName, LastName, Salary); } // ... }
class Program { static void Main() { Employee employee; employee = new Employee("Inigo", "Montoya"); employee.Salary = "Too Little"; employee.Deconstruct(out _, out string firstName, out string lastName, out string salary) System.Console.WriteLine( "{0} {1}: {2}", firstName, lastName, salary); } }
Such a method could be invoked directly, as one would expect from Chapter 5, by declaring the out
parameters inline.
Starting with C# 7.0, it became possible to invoke the Deconstruct()
method—the deconstructor—implicitly by assigning the object instance to a tuple directly (this time assuming the assigned variables are already declared):
(_, firstName, lastName, salary) = employee;
The syntax results in the identical CIL code as that highlighted in Listing 6.38—it is just a simpler syntax (and a little less indicative that the Deconstruct()
method is invoked). Note that the syntax allows for variables matching the out
parameter assignments using tuple syntax. It does not allow for the assignment of a tuple type, either
(int, string, string, string) tuple = employee;
or with named items as in
(int id, string firstName, string lastName, string salary) tuple = employee
End 7.0
To declare a deconstructor, the method name must be Deconstruct
and have a signature that returns void
and exclusively accepts two or more out
parameters. And, given such a signature, it is possible to assign an object instance directly to a tuple without the explicit method invocation.
The HelloWorld
example in Chapter 1 briefly touched on the keyword static
. This section defines the static
keyword more fully.
First, let’s consider an example. Assume that the employee Id
value needs to be unique for each employee. One way to accomplish this is to store a counter to track each employee ID. If the value is stored as an instance field, however, every time you instantiate an object, a new NextId
field will be created such that every instance of the Employee
object will consume memory for that field. The biggest problem is that each time an Employee
object is instantiated, the NextId
value on all of the previously instantiated Employee
objects needs to be updated with the next ID value. In this case, what you need is a single field that all Employee
object instances share.
Unlike many of the languages that came before it, C# does not have global variables or global functions. All fields and methods in C# appear within the context of a class. The equivalent of a global field or function within the realm of C# is a static field or function. There is no functional difference between global variables/functions and C# static fields/methods, except that static fields/methods can include access modifiers, such as private
, that can limit the access and provide better encapsulation.
To define data that is available across multiple instances, you use the static
keyword, as demonstrated in Listing 6.39.
public class Employee { public Employee(string firstName, string lastName) { FirstName = firstName; LastName = lastName; Id = NextId; NextId++; } // ... public static int NextId; public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Salary { get; set; } = "Not Enough"; // ... }
In this example, the NextId
field declaration includes the static
modifier and therefore is called a static field. Unlike Id
, a single storage location for NextId
is shared across all instances of Employee
. Inside the Employee
constructor, you assign the new Employee
object’s Id
the value of NextId
immediately before incrementing the Id
. When another Employee
class is created, NextId
will be incremented and the new Employee
object’s Id
field will hold a different value.
Just as instance fields (nonstatic fields) can be initialized at declaration time, so can static fields, as demonstrated in Listing 6.40.
public class Employee { // ... public static int NextId = 42; // ... }
Unlike with instance fields, if no initialization for a static field is provided, the static field will automatically be assigned its default value (0
, null
, false
, and so on)—the equivalent of default(T)
, where T
is the name of the type. As a result, it will be possible to access the static field even if it has never been explicitly assigned in the C# code.
Nonstatic fields, or instance fields, provide a new storage location for each object to which they belong. In contrast, static fields don’t belong to the instance, but rather to the class itself. As a result, you access a static field from outside a class via the class name. Consider the new Program
class shown in Listing 6.41 (using the Employee
class from Listing 6.39).
using System; class Program { static void Main() { Employee.NextId = 1000000; Employee employee1 = new Employee( "Inigo", "Montoya"); Employee employee2 = new Employee( "Princess", "Buttercup"); Console.WriteLine( "{0} {1} ({2})", employee1.FirstName, employee1.LastName, employee1.Id); Console.WriteLine( "{0} {1} ({2})", employee2.FirstName, employee2.LastName, employee2.Id); Console.WriteLine( $"NextId = { Employee.NextId }"); } // ... }
Output 6.8 shows the results of Listing 6.41.
Output 6.8
Inigo Montoya (1000000) Princess Buttercup (1000001) NextId = 1000002
To set and retrieve the initial value of the NextId
static field, you use the class name, Employee
, rather than a reference to an instance of the type. The only place you can omit the class name is within the class itself (or a derived class). In other words, the Employee(...)
constructor did not need to use Employee.NextId
because the code appeared within the context of the Employee
class itself, and therefore, the context was already understood. The scope of a variable is the program text in which the variable can be referred to by its unqualified name; the scope of a static field is the text of the class (and any derived classes).
Even though you refer to static fields slightly differently than you refer to instance fields, it is not possible to define a static field and an instance field with the same name in the same class. The possibility of mistakenly referring to the wrong field is high, so the C# designers decided to prevent such code. Overlap in names, therefore, introduces conflict within the declaration space.
Both classes and objects can have associated data, just as can the molds and the widgets created from them.
For example, a mold could have data corresponding to the number of widgets it created, the serial number of the next widget, the current color of the plastic injected into the mold, and the number of widgets it produces per hour. Similarly, a widget has its own serial number, its own color, and perhaps the date and time when the widget was created. Although the color of the widget corresponds to the color of the plastic within the mold at the time the widget was created, it obviously does not contain data corresponding to the color of the plastic currently in the mold, or the serial number of the next widget to be produced.
In designing objects, programmers should take care to declare fields, properties, and methods appropriately, as static or instance based. In general, you should declare methods that don’t access any instance data as static methods. Static fields store data corresponding to the class, such as defaults for new instances or the number of instances that have been created. Instance fields store data associated with the object.
Just as with static fields, you access static methods directly off the class name—for example, as Console.ReadLine()
. Furthermore, it is not necessary to have an instance to access the method.
Listing 6.42 provides another example of both declaring and calling a static method.
public static class DirectoryInfoExtension { public static void CopyTo( DirectoryInfo sourceDirectory, string target, SearchOption option, string searchPattern) { if (target[target.Length - 1] != Path.DirectorySeparatorChar) { target += Path.DirectorySeparatorChar; } if (!Directory.Exists(target)) { Directory.CreateDirectory(target); } for (int i = 0; i < searchPattern.Length; i++) { foreach (string file in Directory.GetFiles( sourceDirectory.FullName, searchPattern)) { File.Copy(file, target + Path.GetFileName(file), true); } } // Copy subdirectories (recursively) if (option == SearchOption.AllDirectories) { foreach(string element in Directory.GetDirectories( sourceDirectory.FullName)) { Copy(element, target + Path.GetFileName(element), searchPattern); } } } } // ... DirectoryInfo directory = new DirectoryInfo(".\Source"); directory.MoveTo(".\Root"); DirectoryInfoExtension.CopyTo( directory, ".\Target", SearchOption.AllDirectories, "*"); // ...
In Listing 6.42, the DirectoryInfoExtension.CopyTo()
method takes a DirectoryInfo
object and copies the underlying directory structure to a new location.
Because static methods are not referenced through a particular instance, the this
keyword is invalid inside a static method. In addition, it is not possible to access either an instance field or an instance method directly from within a static method without a reference to the particular instance to which the field or method belongs. (Note that Main()
is another example of a static method.)
One might have expected this method on the System.IO.Directory
class or as an instance method on System.IO.DirectoryInfo
. Since neither exists, Listing 6.42 defines such a method on an entirely new class. In the section “Extension Methods” later in this chapter, we show how to make it appear as an instance method on DirectoryInfo
.
In addition to static fields and methods, C# supports static constructors. Static constructors are provided as a means to initialize the class itself rather than the instances of a class. Such constructors are not called explicitly; instead, the runtime calls static constructors automatically upon first access to the class, whether by calling a regular constructor or by accessing a static method or field on the class. Because the static constructor cannot be called explicitly, no parameters are allowed on static constructors.
You use static constructors to initialize the static data within the class to a particular value, primarily when the initial value involves more complexity than a simple assignment at declaration time. Consider Listing 6.43.
public class Employee { static Employee() { Random randomGenerator = new Random(); NextId = randomGenerator.Next(101, 999); } // ... public static int NextId = 42; // ... }
Listing 6.43 assigns the initial value of NextId
to be a random integer between 100 and 1,000. Because the initial value involves a method call, the NextId
initialization code appears within a static constructor and not as part of the declaration.
If assignment of NextId
occurs within both the static constructor and the declaration, it is not obvious what the value will be when initialization concludes. The C# compiler generates CIL in which the declaration assignment is moved to be the first statement within the static constructor. Therefore, NextId
will contain the value returned by randomGenerator.Next(101, 999)
instead of a value assigned during NextId
’s declaration. Assignments within the static constructor, therefore, will take precedence over assignments that occur as part of the field declaration, as was the case with instance fields. Note that there is no support for defining a static finalizer.
Be careful not to throw an exception from a static constructor, as this will render the type unusable for the remainder of the application’s lifetime.9
9. Technically the application domain’s lifetime—the Common Language Runtime’s virtual equivalent of an operating system process.
Static constructors execute before the first access to any member of a class, whether it is a static field, another static member, or an instance constructor. To support this practice, the compiler injects a check into all type static members and constructors to ensure that the static constructor runs first.
Without the static constructor, the compiler initializes all static members to their default values and avoids adding the static constructor check. The result is that static assignment initialization is called before any static fields are accessed but not necessarily before all static methods or any instance constructor is invoked. This might provide a performance improvement if initialization of static members is expensive and is not needed before accessing a static field. For this reason, you should consider either initializing static fields inline rather than using a static constructor or initializing them at declaration time.
CONSIDER initializing static fields inline rather than explicitly using static constructors or declaration assigned values.
Begin 2.0
You also can declare properties as static. For example, Listing 6.44 wraps the data for the next ID into a property.
public class Employee { // ... public static int NextId { get { return _NextId; } private set { _NextId = value; } } public static int _NextId = 42; // ... }
It is almost always better to use a static property rather than a public static field, because public static fields are callable from anywhere, whereas a static property offers at least some level of encapsulation.
Begin 6.0
In C# 6.0, the entire NextId
implementation—including an inaccessible backing field—can be simplified down to an automatically implemented property with an initializer:
End 6.0
public static int NextId { get; private set; } = 42;
Some classes do not contain any instance fields. Consider, for example, a Math
class that has functions corresponding to the mathematical operations Max()
and Min()
, as shown in Listing 6.45.
// Static classes were introduced in C# 2.0 public static class SimpleMath { // params allows the number of parameters to vary public static int Max(params int[] numbers) { // Check that there is at least one item in numbers if(numbers.Length == 0) { throw new ArgumentException( "numbers cannot be empty", "numbers"); } int result; result = numbers[0]; foreach (int number in numbers) { if(number > result) { result = number; } } return result; } // params allows the number of parameters to vary public static int Min(params int[] numbers) { // Check that there is at least one item in numbers if(numbers.Length == 0) { throw new ArgumentException( "numbers cannot be empty", "numbers"); } int result; result = numbers[0]; foreach (int number in numbers) { if(number < result) { result = number; } } return result; } } public class Program { public static void Main(string[] args) { int[] numbers = new int[args.Length]; for (int count = 0; count < args.Length; count++) { numbers[count] = args[count].Length; } Console.WriteLine( $@"Longest argument length = { SimpleMath.Max(numbers) }"); Console.WriteLine( $@"Shortest argument length = { SimpleMath.Min(numbers) }"); } }
End 2.0
This class does not have any instance fields (or methods), so the creation of such a class would be pointless. Consequently, the class is decorated with the static
keyword. The static
keyword on a class provides two benefits. First, it prevents a programmer from writing code that instantiates the SimpleMath
class. Second, it prevents the declaration of any instance fields or methods within the class. Because the class cannot be instantiated, instance members would be pointless. The Program
class in prior listings is another good candidate for a static class because it, too, contains only static members.
Another distinguishing characteristic of the static class is that the C# compiler automatically marks it as abstract
and sealed
within the CIL. This designates the class as inextensible; in other words, no class can be derived from this class or even instantiate it.
Begin 3.0
Begin 6.0
In Chapter 5, we saw that the using static
directive can be used with static classes such as SimpleMath
. For example, adding a using static SimpleMath;
declarative at the top of Listing 6.45 would allow you to invoke Max
without the SimpleMath
prefix:
Console.WriteLine( $@"Longest argument length = { Max(numbers) }");
End 6.0
Consider the System.IO.DirectoryInfo
class, which is used to manipulate filesystem directories. This class supports functionality to list the files and subdirectories (DirectoryInfo.GetFiles()
), as well as the capability to move the directory (DirectoryInfo.Move()
). One feature it doesn’t support directly is the copy feature. If you needed such a method, you would have to implement it, as shown earlier in Listing 6.42.
The DirectoryInfoExtension.CopyTo()
method is a standard static method declaration. However, notice that calling this CopyTo()
method is different from calling the DirectoryInfo.Move()
method. This is unfortunate. Ideally, we want to add a method to DirectoryInfo
so that, given an instance, we could call CopyTo()
as an instance method: directory.CopyTo()
.
C# 3.0 simulates the creation of an instance method on a different class via extension methods. To do this, we simply change the signature of our static method so that the first parameter—that is, the data type we are extending—is prefixed with the this
keyword (see Listing 6.46).
public static class DirectoryInfoExtension { public static void CopyTo( this DirectoryInfo sourceDirectory, string target, SearchOption option, string searchPattern) { // ... } } // ... DirectoryInfo directory = new DirectoryInfo(".\Source"); directory.CopyTo(".\Target", SearchOption.AllDirectories, "*"); // ...
With extension methods, it is now possible to add “instance methods” to any class, including classes outside of your assembly. The resultant CIL code, however, is identical to what the compiler creates when calling the extension method as a normal static method.
Extension method requirements are as follows:
The first parameter corresponds to the type that the method extends or on which it operates.
To designate the extension method, prefix the first parameter with the this
modifier.
To access the method as an extension method, import the extending type’s namespace via a using
directive (or place the extending class in the same namespace as the calling code).
If the extension method signature matches a signature already found on the extended type (i.e., if CopyTo()
already existed on DirectoryInfo
), the extension method will never be called except as a normal static method.
Note that specializing a type via inheritance (covered in detail in Chapter 7) is generally preferable to using an extension method. Extension methods do not provide a clean versioning mechanism, because the addition of a matching signature to the extended type will take precedence over the extension method without warning of the change. The subtlety of this behavior is more pronounced for extended classes whose source code you don’t control. Another minor point is that although development IDEs support IntelliSense for extension methods, simply reading through the calling code does not make it obvious that a method is an extension method.
In general, you should use extension methods sparingly. Do not, for example, define them on type object
. Chapter 8 discusses how to use extension methods in association with an interface. Without such an association, defining extension methods is rare.
AVOID frivolously defining extension methods, especially on types you don’t own.
End 3.0
In addition to properties and the access modifiers we examined earlier in the chapter, there are several other specialized ways to encapsulate the data within a class. For instance, there are two more field modifiers. The first is the const
modifier, which you encountered when declaring local variables. The second is the ability to define fields as read-only.
const
Just as with const
values, a const
field contains a compile-time–determined value that cannot be changed at runtime. Values such as pi make good candidates for constant field declarations. Listing 6.47 shows an example of declaring a const
field.
class ConvertUnits { public const float CentimetersPerInch = 2.54F; public const int CupsPerGallon = 16; // ... }
Constant fields are static automatically, since no new field instance is required for each object instance. Declaring a constant field as static
explicitly will cause a compile error. Also, constant fields are usually declared only for types that have literal values (e.g., string
, int
, and double
). Types such as Program
or System.Guid
cannot be used for constant fields.
It is important that the types of values used in public
constant expressions are permanent in time. Values such as pi, Avogadro’s number, and Earth’s circumference are good examples. However, values that could potentially change over time are not. For example, population counts, the poorest country, and exchange rates would be poor choices for constants.
DO use constant fields for values that will never change.
AVOID constant fields for values that will change over time.
Publicly accessible constants should be permanent, because changing the value of a constant will not necessarily take effect in the assemblies that use it. If an assembly references a constant from a different assembly, the value of the constant is compiled directly into the referencing assembly. Therefore, if the value in the referenced assembly is changed but the referencing assembly is not recompiled, the referencing assembly will still use the original value, not the new value. Values that could potentially change in the future should be specified as readonly
instead.
readonly
Unlike const
, the readonly
modifier is available only for fields (not for local variables). It declares that the field value is modifiable only from inside the constructor or via an initializer during declaration. Listing 6.48 demonstrates how to declare a read-only field.
public class Employee { public Employee(int id) { _Id = id; } // ... public readonly int _Id; public int Id { get { return _Id; } } // Error: A readonly field cannot be assigned to (except // in a constructor or a variable initializer) // public void SetId(int id) => // _Id = id; // ... }
Unlike constant fields, readonly
-decorated fields can vary from one instance to the next. In fact, a read-only field’s value can change within the constructor. Furthermore, read-only fields occur as either instance or static fields. Another key distinction is that you can assign the value of a read-only field at execution time rather than just at compile time. Given that read-only fields must be set in the constructor or initializer, such fields are the one case where the compiler requires the fields to be accessed from code outside their corresponding property. Besides this one exception, you should avoid accessing a backing field from anywhere other than its wrapping property.
Another important feature of readonly
-decorated fields over const
fields is that read-only fields are not limited to types with literal values. It is possible, for example, to declare a readonly System.Guid
instance field:
public static readonly Guid ComIUnknownGuid = new Guid("00000000-0000-0000-C000-000000000046");
In contrast, this is not possible using a constant, because there is no C# literal representation of a GUID.
Begin 6.0
Given the guideline that fields should not be accessed from outside their wrapping property, those programming in a C# 6.0 (or later) world will discover that that there is almost never a need to use the readonly
modifier. Instead, it is preferable to use a read-only automatically implemented property, as discussed earlier in the chapter.
Listing 6.49 shows one more read-only example.
class TicTacToeBoard { // Set both players' moves to all false (blank) // | | // ---+---+--- // | | // ---+---+--- // | | public bool[,,] Cells { get; } = new bool[2, 3, 3]; // Error: The property Cells cannot // be assigned to because it is read-only // public void SetCells(bool[,,] value) => // Cells = new bool[2, 3, 3]; // ... }
Whether implemented using C# 6.0 read-only automatically implemented properties or the readonly
modifier on a field, ensuring immutability of the array reference is a useful defensive coding technique. It ensures that the array instance remains the same, while allowing the elements within the array to change. Without the read-only constraint, it would be all too easy to mistakenly assign a new array to the member, thereby discarding the existing array rather than updating individual array elements. In other words, using a read-only approach with an array does not freeze the contents of the array. Rather, it freezes the array instance (and therefore the number of elements in the array) because it is not possible to reassign the value to a new instance. The elements of the array are still writeable.
DO favor read-only automatically implemented properties in C# 6.0 (or later) over read-only fields.
DO use public static readonly
modified fields for predefined object instances prior to C# 6.0.
AVOID changing a public readonly
modified field in pre-C# 6.0 to a read-only automatically implemented property in C# 6.0 (or later) if version API compatibility is required.
End 6.0
In addition to defining methods and fields within a class, it is possible to define a class within a class. Such classes are called nested classes. You use a nested class when the class makes little sense outside the context of its containing class.
Consider a class that handles the command-line options of a program. Such a class is generally unique to each program, so there is no reason to make a CommandLine
class accessible from outside the class that contains Main()
. Listing 6.50 demonstrates such a nested class.
// CommandLine is nested within Program class Program { // Define a nested class for processing the command line private class CommandLine { public CommandLine(string[] arguments) { for(int argumentCounter=0; argumentCounter<arguments.Length; argumentCounter++) { switch (argumentCounter) { case 0: Action = arguments[0].ToLower(); break; case 1: Id = arguments[1]; break; case 2: FirstName = arguments[2]; break; case 3: LastName = arguments[3]; break; } } } public string? Action { get; }; public string? Id { get; }; public string? FirstName { get; }; public string? LastName { get; }; } static void Main(string[] args) { CommandLine commandLine = new CommandLine(args); switch (commandLine.Action) { case "new": // Create a new employee // ... break; case "update": // Update an existing employee's data // ... break; case "delete": // Remove an existing employee's file // ... break; default: Console.WriteLine( "Employee.exe " + "new|update|delete <id> [firstname] [lastname]"); break; } } }
The nested class in this example is Program.CommandLine
. As with all class members, no containing class identifier is needed from inside the containing class, so you can simply refer to it as CommandLine
.
One unique characteristic of nested classes is the ability to specify private
as an access modifier for the class itself. Because the purpose of this class is to parse the command line and place each argument into a separate field, Program.CommandLine
is relevant only to the Program
class in this application. The use of the private
access modifier defines the intended accessibility of the class and prevents access from outside the class. You can do this only if the class is nested.
The this
member within a nested class refers to an instance of the nested class, not the containing class. One way for a nested class to access an instance of the containing class is if the containing class instance is explicitly passed, such as via a constructor or a method parameter.
Another interesting characteristic of nested classes is that they can access any member on the containing class, including private members. The converse is not true, however: It is not possible for the containing class to access a private member of the nested class.
Nested classes are rare. They should not be defined if they are likely to be referenced outside the containing type. Furthermore, treat public
nested classes with suspicion; they indicate potentially poor code that is likely to be confusing and hard to discover.
AVOID publicly exposed nested types. The only exception is if the declaration of such a type is unlikely or pertains to an advanced customization scenario.
Java includes not only the concept of a nested class but also the concept of an inner class. Inner classes correspond to objects that are associated with the containing class instance, rather than having just a syntactic relationship. In C#, you can achieve the same structure by including an instance field of a nested type within the outer class. A factory method or constructor can ensure a reference to the corresponding instance of the outer class is set within the inner class instance as well.
Begin 2.0
Partial classes10 are portions of a class that the compiler can combine to form a complete class. Although you could define two or more partial classes within the same file, the general purpose of a partial class is to allow the splitting of a class definition across multiple files. Primarily this is useful for tools that are generating or modifying code. With partial classes, the tools can work on a file separate from the one the developer is manually coding.
10. Introduced with C# 2.0.
C# allows declaration of a partial class by prepending a contextual keyword, partial
, immediately before class
, as Listing 6.51 shows.
// File: Program1.cs partial class Program { } // File: Program2.cs partial class Program { }
In this case, each portion of Program
is placed into a separate file, as identified by the comment.
Besides their use with code generators, another common use of partial classes is to place any nested classes into their own files. This is in accordance with the coding convention that places each class definition within its own file. For example, Listing 6.52 places the Program.CommandLine
class into a file separate from the core Program
members.
// File: Program.cs partial class Program { static void Main(string[] args) { CommandLine commandLine = new CommandLine(args); switch (commandLine.Action) { // ... } } } // File: Program+CommandLine.cs partial class Program { // Define a nested class for processing the command line private class CommandLine { // ... } }
End 2.0
Partial classes do not allow for extending compiled classes or classes in other assemblies. They are simply a means of splitting a class implementation across multiple files within the same assembly.
Begin 3.0
Extending the concept of partial classes is the concept of partial methods,11 which are allowed only within partial classes. Like partial classes, their primary purpose is to accommodate code generation.
11. Introduced with C# 3.0.
Consider a code generation tool that generates the Person.Designer.cs
file for the Person
class based on a Person
table within a database. This tool examines the table and creates properties for each column in the table. The problem, however, is that frequently the tool cannot generate any validation logic that may be required because this logic is based on business rules that are not embedded into the database table definition. To overcome this difficulty, the developer of the Person
class needs to add the validation logic. It is undesirable to modify Person.Designer.cs
directly, because if the file is regenerated (e.g., to accommodate an additional column in the database), the changes would be lost. Instead, the structure of the code for Person
needs to be separated out so that the generated code appears in one file, and the custom code (with business rules) is placed into a separate file, unaffected by any regeneration. As we saw in the preceding section, partial classes are well suited for the task of splitting a class across multiple files, but they are not always sufficient. In many cases, we also need partial methods.
Partial methods allow for a declaration of a method without requiring an implementation. However, when the optional implementation is included, it can be located in one of the sister partial class definitions—likely in a separate file. Listing 6.53 shows the partial method declaration and the implementation for the Person
class.
// File: Person.Designer.cs public partial class Person { #region Extensibility Method Definitions partial void OnLastNameChanging(string value); partial void OnFirstNameChanging(string value); #endregion // ... public string LastName { get { return _LastName; } set { if (_LastName != value) { OnLastNameChanging(value); _LastName = value; } } } private string _LastName; // ... public string FirstName { get { return _FirstName; } set { if (_FirstName != value) { OnFirstNameChanging(value); _FirstName = value; } } } private string _FirstName; } // File: Person.cs partial class Person { partial void OnLastNameChanging(string value) { if (value is null) { throw new ArgumentNullException(nameof(value)); } if(value.Trim().Length == 0) { throw new ArgumentException( "LastName cannot be empty.", nameof(value)); } } }
In the listing of Person.Designer.cs
are declarations for the OnLastNameChanging()
and OnFirstNameChanging()
methods. Furthermore, the properties for the last and first names make calls to their corresponding changing methods. Even though the declarations of the changing methods contain no implementation, this code will successfully compile. The key is that the method declarations are prefixed with the contextual keyword partial
, in addition to the class that contains such methods.
In Listing 6.53, only the OnLastNameChanging()
method is implemented. In this case, the implementation checks the suggested new LastName
value and throws an exception if it is not valid. Notice that the signatures for OnLastNameChanging()
between the two locations match.
Partial methods must return void
. If the method didn’t return void
and the implementation was not provided, what would the expected return be from a call to a nonimplemented method? To avoid any invalid assumptions about the return, the C# designers decided to prohibit methods with returns other than void
. Similarly, out
parameters are not allowed on partial methods. If a return value is required, ref
parameters may be used.
End 3.0
In summary, partial methods allow generated code to call methods that have not necessarily been implemented. Furthermore, if no implementation is provided for a partial method, no trace of the partial method appears in the CIL. This helps keep code size small while keeping flexibility high.
This chapter explained C# constructs for classes and object orientation in C#. Its coverage included a discussion of declaring fields and how to access them on a class instance.
This chapter also discussed the key decision of whether to store data on a per-instance basis or across all instances of a type. Static data is associated with the class, whereas instance data is stored on each object.
In addition, the chapter explored encapsulation in the context of access modifiers for methods and data. The C# construct of properties was introduced, and you saw how to use it to encapsulate private fields.
The next chapter focuses on how to associate classes with each other via inheritance and explores the benefits derived from this object-oriented construct.
3.139.238.76