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