Deconstructors

Constructors allow you to take multiple parameters and encapsulate them all into a single object. Until now, we haven’t introduced any constructs 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 tuples, however, this becomes significantly easier. You could, for example, declare a method like the Deconstruct() method shown in Listing 6.43.

Listing 6.43: Defining and Using a Deconstructor
1. public class Employee
2. {
3.     // ...
4.     public void Deconstruct(
5.         out int id, out string firstName, 
6.         out string lastName, out string? salary)
7.     {
8.        (id, firstName, lastName, salary) = 
9.             (Id, FirstName, LastName, Salary);
10.     }
11.     // ...
12. }
13.  
14. public class Program
15. {
16.     public static void Main()
17.     {
18.         Employee employee;
19.         employee = new ("Inigo""Montoya")
20.         {
21.             // Leveraging object initializer syntax
22.             Salary = "Too Little"
23.         };
24.         // ...
25.  
26.         employee.Deconstruct(out _, out string firstName,
27.             out string lastName, out string? salary);
28.  
29.         System.Console.WriteLine(
30.             "{0} {1}: {2}",
31.             firstName, lastName, salary);
32.     }
33. }

Such a method could be invoked directly, as one would expect from Chapter 5, by declaring the out parameters inline. However, you can invoke the Deconstruct() method—the deconstructor12—implicitly by assigning the object instance to a tuple directly as shown in Listing 6.44.

Listing 6.44: Implicitly Invoking Deconstructors
1. public class Program
2. {
3.     public static void Main()
4.     {
5.         Employee employee;
6.         employee = new ("Inigo""Montoya")
7.         {
8.             // Leveraging object initializer syntax
9.             Salary = "Too Little"
10.         };
11.  
12.         // ...
13.  
14.         (_, string firstName, string lastName, string? salary) = employee;
15.  
16.         System.Console.WriteLine(
17.             "{0} {1}: {2}",
18.             firstName, lastName, salary);
19.     }
20. }

The syntax results in the identical CIL code as that highlighted in Listing 6.43—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

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.

Note that prior to C# 10, all the assigned values needed to be newly declared or pre-declared—you couldn’t mix newly declared with existing variables. With C# 10, this restriction was removed.

Static Members

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.

Language Contrast: C++—Global Variables and Functions

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.

Static Fields

To define data that is available across multiple instances, you use the static keyword, as demonstrated in Listing 6.45.

Listing 6.45: Declaring a Static Field
1. public class Employee
2. {
3.     public Employee(string firstName, string lastName)
4.     {
5.         FirstName = firstName;
6.         LastName = lastName;
7.         Id = NextId;
8.         NextId++;
9.     }
10.  
11.     // ...
12.  
13.     public static int NextId;
14.     public int Id { getprivate set; }
15.     public string FirstName { getset; }
16.     public string LastName { getset; }
17.     public string? Salary { getset; } = "Not Enough";
18.  
19.     // ...
20. }

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 instance of the 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 (non-static fields) can be initialized at declaration time, so can static fields, as demonstrated in Listing 6.46.

Listing 6.46: Assigning a Static Field at Declaration
1. public class Employee
2. {
3.     // ...
4.     public static int NextId = 42;
5.     // ...
6. }

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.

Non-static 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.47 (using the Employee class from Listing 6.45 along with Output 6.8).

Listing 6.47: Accessing a Static Field
1. using System;
2.  
3. public class Program
4. {
5.     public static void Main()
6.     {
7.         Employee.NextId = 1000000;
8.  
9.         Employee employee1 = new(
10.             "Inigo""Montoya");
11.         Employee employee2 = new(
12.             "Princess""Buttercup");
13.  
14.         Console.WriteLine(
15.             "{0} {1} ({2})",
16.             employee1.FirstName,
17.             employee1.LastName,
18.             employee1.Id);
19.         Console.WriteLine(
20.             "{0} {1} ({2})",
21.             employee2.FirstName,
22.             employee2.LastName,
23.             employee2.Id);
24.  
25.         Console.WriteLine(
26.             $"NextId = {Employee.NextId}");
27.     }
28.     // ...
29. }
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 implied. The scope of a variable is the program context in which the variable can be referred to by its unqualified name; the scope of a static field is the context 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.

Beginner Topic
Data Can Be Associated with Both a Class and an Object

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.

Static Methods

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.48 provides another example of both declaring and calling a static method.

Listing 6.48: Defining a Static Method on DirectoryInfoExtension
1. public static class DirectoryInfoExtension
2. {
3.     public static void CopyTo(
4.         DirectoryInfo sourceDirectory, string target,
5.         SearchOption option, string searchPattern)
6.     {
7.         if (target[^1] !=
8.             Path.DirectorySeparatorChar)
9.         {
10.             target += Path.DirectorySeparatorChar;
11.         }
12.         Directory.CreateDirectory(target);
13.  
14.         for (int i = 0; i < searchPattern.Length; i++)
15.         {
16.             foreach (string file in
17.                 Directory.EnumerateFiles(
18.                     sourceDirectory.FullName, searchPattern))
19.             {
20.                 File.Copy(file,
21.                     target + Path.GetFileName(file), true);
22.             }
23.         }
24.  
25.         // Copy subdirectories (recursively)
26.         if (option == SearchOption.AllDirectories)
27.         {
28.             foreach (string element in
29.                 Directory.EnumerateDirectories(
30.                     sourceDirectory.FullName))
31.             {
32.                 Copy(element,
33.                     target + Path.GetFileName(element),
34.                     searchPattern);
35.             }
36.         }
37.     }
38.     // ...
39. }
40.  
41. public class Program
42. {
43.     public static void Main(params string[] args)
44.     {
45.         DirectoryInfo source = new(args[0]);
46.         string target = args[1];
47.  
48.         DirectoryInfoExtension.CopyTo(
49.             source, target,
50.             SearchOption.AllDirectories, "*");
51.     }
52. }

In Listing 6.48, 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 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.48 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 like an instance method on DirectoryInfo.

Static Constructors

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.49.

Listing 6.49: Declaring a Static Constructor
1. public class Employee
2. {
3.     static Employee()
4.     {
5.         Random randomGenerator = new();
6.         NextId = randomGenerator.Next(101, 999);
7.     }
8.  
9.     // ...
10.     public static int NextId = 42;
11.     // ...
12. }

Listing 6.49 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.13

AdVanced Topic
Favor Static Initialization during Declaration

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 runtime checks that all 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.

Guidelines
CONSIDER initializing static fields inline rather than explicitly using static constructors or declaration assigned values.
Static Properties

You also can declare properties as static. For example, Listing 6.50 wraps the data for the next ID into a property.

Listing 6.50: Declaring a Static Property
1. public class Employee
2. {
3.     // ...
4.     public static int NextId
5.     {
6.         get
7.         {
8.             return _NextId;
9.         }
10.         private set
11.         {
12.             _NextId = value;
13.         }
14.     }
15.     public static int _NextId = 42;
16.     // ...
17. }

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.

The entire NextId implementation—including an inaccessible backing field—can be simplified to an automatically implemented property with an initializer:14

public static int NextId { get; private set; } = 42;

Static Classes

Some classes do not contain any instance fields. Consider, for example, a SimpleMath class that has functions corresponding to the mathematical operations Max() and Min(), as shown in Listing 6.51.

Listing 6.51: Declaring a Static Class
1. public static class SimpleMath
2. {
3.     // params allows the number of parameters to vary
4.     public static int Max(params int[] numbers)
5.     {
6.         // Check that there is at least one item in numbers
7.         if(numbers.Length == 0)
8.         {
9.             throw new ArgumentException(
10.                 "numbers cannot be empty"nameof(numbers));
11.         }
12.  
13.         int result;
14.         result = numbers[0];
15.         foreach(int number in numbers)
16.         {
17.             if(number > result)
18.             {
19.                 result = number;
20.             }
21.         }
22.         return result;
23.     }
24.  
25.     // params allows the number of parameters to vary
26.     public static int Min(params int[] numbers)
27.     {
28.         // Check that there is at least one item in numbers
29.         if(numbers.Length == 0)
30.         {
31.             throw new ArgumentException(
32.                 "numbers cannot be empty"nameof(numbers));
33.         }
34.  
35.         int result;
36.         result = numbers[0];
37.         foreach(int number in numbers)
38.         {
39.             if(number < result)
40.             {
41.                 result = number;
42.             }
43.         }
44.         return result;
45.     }
46. }
47.  
48. public class Program
49. {
50.     public static void Main(string[] args)
51.     {
52.         int[] numbers = new int[args.Length];
53.         for (int index = 0; index < args.Length; index++)
54.         {
55.             numbers[index] = args[index].Length;
56.         }
57.  
58.         Console.WriteLine(
59.             $@"Longest argument length = {
60.                 Max(numbers) }");
61.  
62.         Console.WriteLine(
63.             $@"Shortest argument length = {
64.                 Min(numbers) }");
65.     }
66. }

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. (The sealed and abstract keywords are covered in more detail in Chapter 7.)

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.51 would allow you to invoke Max without the SimpleMath prefix:

       Console.WriteLine(

           $@"Longest argument length = { Max(numbers) }");

________________________________________

12. Introduced in C# 7.0.
13. Technically the application domain’s lifetime—the Common Language Runtime’s virtual equivalent of an operating system process.
14. Starting in C# 6.0.
{{ snackbarMessage }}
;