Defining a class involves first specifying the keyword class, followed by an identifier, as shown in Listing 6.1.
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.
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.
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).
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. While you can specify the data type (Employee), it is optional if the compiler can infer the type from the left-hand side of the assignment starting in C# 9.0. In this case, the compiler can determine the targeted type is Employee and, therefore, infer that the new expression is for an Employee and allow a target-typed new expression for the instantiation—where no type is specified when invoking the constructor. That said, developers should use caution if the targeted type is not obvious from the line of code. For example, assigning text = new() gives no indication what the data type could be. And, while string could be inferred, System.Text.StringBuilder is also an obvious choice. For this reason, avoid target-typed expressions when the data type is not obvious.
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.
C# 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.