- Item 11: Understand .NET Resource Management
- Item 12: Prefer Member Initializers to Assignment Statements
- Item 13: Use Proper Initialization for Static Class Members
- Item 14: Minimize Duplicate Initialization Logic
- Item 15: Avoid Creating Unnecessary Objects
- Item 16: Never Call Virtual Functions in Constructors
- Item 17: Implement the Standard Dispose Pattern
Item 12: Prefer Member Initializers to Assignment Statements
Classes often have more than one constructor. Over time, it’s easy for the member variables and the constructors to get out of sync. The best way to make sure this doesn’t happen is to initialize variables where you declare them instead of in the body of every constructor. You should use the initializer syntax for both static and instance variables.
Constructing a member variable when you declare that variable is natural in C#. Just initialize the variable when you declare it:
public class MyClass { // declare the collection, and initialize it. private List<string> labels = new List<string>(); }
Regardless of the number of constructors you eventually add to the MyClass type, labels will be initialized properly. The compiler generates code at the beginning of each constructor to execute all the initializers you have defined for your instance member variables. When you add a new constructor, labels get initialized. Similarly, if you add a new member variable, you do not need to add initialization code to every constructor; initializing the variable where you define it is sufficient. Equally important, the initializers are added to the compiler-generated default constructor. The C# compiler creates a default constructor for your types whenever you don’t explicitly define any constructors.
Initializers are more than a convenient shortcut for statements in a constructor body. The statements generated by initializers are placed in object code before the body of your constructors. Initializers execute before the base class constructor for your type executes, and they are executed in the order in which the variables are declared in your class.
Using initializers is the simplest way to avoid uninitialized variables in your types, but it’s not perfect. In three cases, you should not use the initializer syntax. The first is when you are initializing the object to 0, or null. The default system initialization sets everything to 0 for you before any of your code executes. The system-generated 0 initialization is done at a very low level using the CPU instructions to set the entire block of memory to 0. Any extra 0 initialization on your part is superfluous. The C# compiler dutifully adds the extra instructions to set memory to 0 again. It’s not wrong—but it can create brittle code.
public struct MyValType { // elided } MyValType myVal1; // initialized to 0 MyValType myVal2 = new MyValType(); // also 0
Both statements initialize the variable to all 0s. The first does so by setting the memory containing myVal1 to 0. The second uses the IL instruction initobj, which causes both a box and an unbox operation on the myVal2 variable. This takes quite a bit of extra time (see Item 9).
The second inefficiency comes when you create multiple initializations for the same object. You should use the initializer syntax only for variables that receive the same initialization in all constructors. This version of MyClass has a path that creates two different List objects as part of its construction:
public class MyClass2 { // declare the collection, and initialize it. private List<string> labels = new List<string>(); MyClass2() { } MyClass2(int size) { labels = new List<string>(size); } }
When you create a new MyClass2, specifying the size of the collection, you create two array lists. One is immediately garbage. The variable initializer executes before every constructor. The constructor body creates the second array list. The compiler creates this version of MyClass2, which you would never code by hand. (For the proper way to handle this situation, see Item 14 later in this chapter.)
public class MyClass2 { // declare the collection, and initialize it. private List<string> labels; MyClass2() { labels = new List<string>(); } MyClass2(int size) { labels = new List<string>(); labels = new List<string>(size); } }
You can run into the same situation whenever you use implicit properties. For those data elements where implicit properties are the right choice, Item 14 shows how to minimize any duplication when you initialize data held in implicit properties.
The final reason to move initialization into the body of a constructor is to facilitate exception handling. You cannot wrap the initializers in a try block. Any exceptions that might be generated during the construction of your member variables get propagated outside your object. You cannot attempt any recovery inside your class. You should move that initialization code into the body of your constructors so that you implement the proper recovery code to create your type and gracefully handle the exception (see Item 47).
Member initializers are the simplest way to ensure that the member variables in your type are initialized regardless of which constructor is called. The initializers are executed before each constructor you make for your type. Using this syntax means that you cannot forget to add the proper initialization when you add new constructors for a future release. Use initializers when all constructors create the member variable the same way; it’s simpler to read and easier to maintain.