- Reference Types
- Object Construction/Destruction
- Object-Oriented Features
- Exception Handling
- In Brief
Object Construction/Destruction
The process of initializing and starting up a class is called construction. The opposite of this is the process of tearing down a class and cleaning it up, which is called destruction or finalization. Class construction occurs either when an object is being initialized or upon first reference to a type. Understanding this process helps resolve initialization bugs in complicated object instantiation scenarios.
Another important aspect of object lifetime management is the destruction process. Chapter 1 offered a brief explanation of memory management and garbage collection. Here, we expand on that and bring home the reality of what Common Language Runtime (CLR) memory management means when executing the object destruction/finalization process.
Constructors
Constructors initialize the state of an object. A C# class can have two types of constructors: static and instance. Static constructors (also called type constructors) initialize the state of a type, which is available to all code that accesses that type. Instance constructors initialize the state of each individual object instance.
Before looking at the syntax of static and instance constructors, it is important to understand the difference between static type access and instance access. Upon the first reference to a class, its type is loaded into memory. That type can have static members, which will be available to all code that accesses the type. In contrast, individual instances of a type can be instantiated and referenced as individual objects. Any members of a type that are not marked static are instance members. Instance members are unique for each object of that type that is instantiated. Type members, modified as static, may be accessed through the type identifier. However, instance members are accessed through a variable that the code creates as a reference to an instance of the type. The following sections will illustrate type and instance access.
Static Constructors
A static constructor will initialize type state only one time, when the type is loaded. Although instance constructors have access to static data, they shouldn't ever touch static members. This would corrupt the type state every time an instance was created. Therefore, static constructors are the only solution to initialization of type state. Listing 3.2 shows how to create a static constructor.
Listing 3.2 A Static Constructor (StaticConstructor.cs)
using System; public class MyStaticClass { public static int StaticInt; static MyStaticClass() { StaticInt = 7; } } class StaticConstructor { static void Main() { int myInt = MyStaticClass.StaticInt; Console.WriteLine("myInt: {0}", myInt); Console.ReadLine(); } }
Points to pay attention to in the static constructor in Listing 3.2 are that it begins with the static modifier, does not specify a return type, and the name, MyStaticClass, is the same as its enclosing type name. Static constructors do not take parameters either, and it would not make sense because they are never called explicitly in code. They are executed implicitly the first time a static member of the type is used.
Instance Constructors
The state of each individual instance should be initialized in instance constructors. Listing 3.3 shows how to create an instance constructor.
Listing 3.3 An Instance Constructor (InstanceConstructor.cs)
using System; public class MyInstanceClass { public int MyInstanceInt; public MyInstanceClass() { MyInstanceInt = 7; } } class InstanceConstructor { static void Main() { MyInstanceClass myClass = new MyInstanceClass(); int myInt = myClass.MyInstanceInt; Console.WriteLine("myInt: {0}", myInt); Console.ReadLine(); } }
Similar to the static constructor, an instance constructor does not return a value and has the same name as its enclosing type. The syntax differences are that there is no static modifier, and instance constructors can accept parameters and be overloaded (different types and number of parameters). Instance constructors also have visibility modifiers (discussed in a later section).
Notice in the Main method that a new instance of MyInstanceClass is being created with the new operator. This is what distinguishes usage of instance from static types, where each individual instance must be explicitly created, but static types are referenced by specifying the type and the static member to access.
Destructors
A C# destructor is a special type member that is invoked when garbage collection occurs and the instance it is a member of is available for collection. Instances become available for collection when there are no more references to them. For example, the instance could go out of scope or be explicitly set to null. The purpose of the destructor is to release unmanaged resources, such as file streams, network connections, or database connections, to name a few. If there are no unmanaged resources to release, a destructor is not necessary.
Because destructors slow down memory management and don't guarantee proper destruction of unmanaged resources, additional measures are necessary to properly manage release of unmanaged resources.
During the garbage collection (GC) process, types with a destructor have to be visited twice. The first pass of the garbage collector (GC) follows roots and identifies objects for garbage collection. If these objects don't have a destructor, they are cleaned up and done. However, objects with a destructor are placed in a temporary queue. This queue is called on a second pass of the GC, so the destructors of the object can be called.
The GC is nondeterministic and can't guarantee when or if a C# destructor will be called. The GC happens when memory pressure forces it into operation. This is by design because just-in-time garbage collection is more efficient in the general case. If a program has a small footprint, it is possible that a GC will never happen at all. Subsequently, multiple unmanaged resources may be held within the type and never released for the life of the program. If the program crashes, destructors will not be called. One would hope that the OS would help recover some of its resources, but this may not occur and such cleanup behavior is not guaranteed by other application or network resources.
The proper way to release unmanaged resources in C# is to implement the dispose pattern in a type. The IDisposable interface (discussed in the "Interfaces" section of Chapter 4) helps implement the dispose pattern. Listing 3.4 describes how to implement a destructor.
Listing 3.4 Destructor Implementation (Destructor.cs)
using System; public class MyInstanceClass { // some implementation ~MyInstanceClass() { Console.WriteLine("Releasing Resources..."); } } class Destructor { static void Main() { MyInstanceClass myClass = new MyInstanceClass(); myClass = null; GC.Collect(); Console.ReadLine(); } }
The myClass reference in the Main method of Listing 3.4 is set to null right after it is instantiated. Because there aren't any other references to this instance, setting it to null makes it available for garbage collection. Objects go out of scope and are available for garbage collection if they are allocated in a routine and there are no other references to them. However, while inside a routine, objects remain allocated. This means that long-running routines that allocate many objects increase memory pressure caused by the application and could impact performance. A way to deal with this problem, if logically possible, is by setting objects to null when they are no longer needed. This allows the garbage collector to clean up those objects and work more efficiently. However, if the routine is short and doesn't allocate many objects, setting objects to null doesn't yield much benefit.
Remember that garbage collection occurs in a nondeterministic manner; the destructor can be invoked some time in the future. Syntax for destructors includes the tilde (~) followed by the name of the destructor, which is the same as its enclosing type. Structs do not have destructors, so it is best to ensure that the disposable pattern, described in the "Interfaces" section of Chapter 4, is implemented.
A Struct Is a Custom Value Type
You can create your own custom value types, similar to the way reference types are created with class definitions. When creating a custom value type, declare the type with the struct keyword as shown following:
public struct complex { double real; double imaginary; // implementation omitted }
This struct may be declared as
complex myComplexNumber = new complex();
or
complex myComplexNumber;
You would create a struct when value type semantics are required. In addition to being allocated on the stack and contents being copied during assignment, a struct does not have a default (no parameter) constructor and does not have a destructor. This is because structs are automatically initialized with integral types set to 0, floating-point types set to 0.0, bool types set to false, and string and other reference type members set to null. Structs do not inherit other types and may not be inherited from. An exception is that structs may inherit interfaces (discussed later). Other than these differences, structs are used like classes and are manipulated as permitted by the public members they expose.
The Main method of Listing 3.4 calls GC.Collect to force a garbage collection. You shouldn't call GC.Collect yourself unless you are absolutely sure that there is a benefit. The garbage collector is optimized to provide better performance than most developers can achieve themselves, and calling GC.Collect is more likely to hurt performance. The reason this example calls GC.Collect is that the destructor in myClass would never be called if the program was left running. Recall that the garbage collector is optimized to do a garbage collection only when memory pressure forces it to do so.
Now consider the case where the call to GC.Collect was deleted or commented out. Because this program is so small, there will never be any memory pressure or other implicit event to force the garbage collection, meaning that the destructor will never be executed until the program exits. You can start this program and go get a cup of coffee, go out for dinner, or even go on vacation and that destructor will not be called. The section on interfaces in Chapter 4 will show how to fix this problem.