Classes and Objects |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
In this chapter we will explore the creation of object from classes, and how to get rid of the objects once they are not longer needed. Creation of objects - instantiation of classes - is tightly connected with initialization of new objects. Object initialization is therefore also an important theme in this chapter.
|
12.1. Creating and Deleting Objects
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Our goal in this section is to obtain an overall understanding of object creation and deletion, in particular in relation to the dimension of explicit/implicit creation and deletion. If you dislike such overall discussion, please proceed to Section 12.2. We identify the following approaches to creation and deletion of objects:
|
The most important way to create objects is to instantiate a class. Instantiation takes place when we use the class as a template for creating a new object. We may have an explicit way to express this (such as the operator new), or it may be implicitly done via declaration of a variable of the type of the class. Relative to this understanding, C# uses explicit creation of objects from classes, and implicit creation of objects (values) from structs.
|
Instantiation comes in two flavors:
|
Static instantiation is implicit. The object is automatically created (and destroyed) when the surrounding object or block is created. Dynamic instantiation is explicit. The object is created on demand, by executing a command. In C# and similar language we call the new operator for the purpose of dynamic class instantiation.
We should also be aware of the possibility of object copying. If we already have a nice object, say obj, we can create a new object (of the same type as obj) by copying obj. Some object-oriented programming languages (most notably Self) use this as the only way of creating objects. The original objects in Self are called prototypes, and they are created directly by the programmer (instead of classes).
Older object-oriented programming languages, such as C++, use explicit deleting of objects. Most newer object-oriented programming languages use implicit object deleting, by means of garbage collection. The use of garbage collection turns out to be a major quality of an object-oriented programming language. C# relies on garbage collection.
Modern object-oriented languages support explicit object creation and implicit object deletion
(by means of garbage collection) |
12.2. Instantiation of classes in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We illustrate instantiation of classes in C# using a client of a Point class, such as Program 11.2, or even better a similar class with non-public instance variables. The accompanying slide shows such a class.
Classes must be instantiated dynamically with use of the new operator The new operator returns a reference to the new object |
The class Application in Program 12.1 uses class Point. Recall that class Application is said to be a client of class Point. We have three Point variables p0, p1, and p2. The two latter variables are local variables in Main. p0 is static, because it is used from a static method.
We see a single instantiation of class Point at the purple place. p0 is automatically initialized to null and p1 is uninitialized before the assignments p0 = p1 = p2. After the assignments all three variables refer to the same Point object , and therefore you should be able to understand the program output shown in Listing 12.2. Notice the Move message in line 12 and the implementation of Move in line 13-15 of Program 11.2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using System; public class Application{ private static Point p0; // Initialized to null public static void Main(){ Point p1, // NOT initialized p2 = new Point(1.1, 2.2); p0 = p1 = p2; p2.Move(3.3, 0); Console.WriteLine("{0} {1} {2}", p0, p1, p2); } } | |||
|
Move in line 12 moves the object referred by the three variables p0, p1, and p2. If you have problems with this, you are encouraged to review this example when you have read Section 13.2.
1 | Point: (4,4, 2,2). Point: (4,4, 2,2). Point: (4,4, 2,2). | |||
|
12.3. Initialization of objects
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Initialization should always follow class instantiation.
|
There are several ways to do initialization. We recommend that you are explicit about initialization in your programs. With use of explicit initialization you signal that you have actually thought about the initialization. If you rely on default values, it may equally well be the case that you have not considered the initialization at all!
Initialization of an object of type T can be done
|
In C# you can denote the default value of a type T by use of the expression default(T). For a reference type RT, default(RT) is is null. For a value type VT, default(VT) is the default value of VT. The default value of numeric types is zero, the default value of bool is false, and the default char value is the null character. The default value of a struct type is aggregated by the default values of the fields of the struct.
In Program 12.1 we have seen that local variables are not initialized to the default value of their types. Instance variables (fields) in classes are, however. This is confusing, and it may easily lead to errors if you forget the exact rules of the language.
An initializer is, for instance, the expression following '=' in a declaration such as int i = 5 + j;
It is not recommended to initialize instance variables via initializers. Initializers are static code, and from static code you cannot refer to the current object, and you cannot refer to other instance variables.
You should write one or more constructors of every class, and you should explicitly initialize all instance variables in your constructors. By following this rule you do not have to care about default values.
It is very important that a newly born object is initialized to a healthy citizen in the population of objects Explicit initialization is always preferred over implicit initialization Always initialize instance variables in constructors |
12.4. Constructors in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
As recommended in Section 12.3, initialization of instance variables takes place in constructors.
|
|
There is no 'constructor' keyword in C#. By the way, there is no 'method' keyword either. So how do we recognize constructors? The answer is given in first two bullet points above: A constructor has the same name as the surrounding class, and it specifies no return type.
Overloading takes place if we have two constructors (or methods) of the same name. Overloaded constructors are distinguished by different types of parameters. In Program 12.3 there are three overloaded constructors. Overload resolution takes place at compile time. It means that a constructor used in new C(...) is determined and bound at compile time - not at run time.
The special delegation mentioned in bullet point four is illustrated by the difference between Program 12.3 and Program 12.4. In the latter, the two first constructors activate the third constructor. The third constructor in Program 12.4 is the most general one, because it can handle the jobs of the two first mentioned constructors as special cases. Notice the this(...) syntax in between the constructor head and body.
As already stressed, I recommend that you always supply at least one constructor in the classes you program. In that case, there will be no parameterless default constructor available to you. You can always, however, program a parameterless constructor yourself. The philosophy is that if you have started to program constructors in your class, you should finish the job. It is not sound to mix your own, "custom" constructors (which are based on a deep knowledge about the class) with the system's default initialization (based on very little knowledge of the class).
In Program 12.3 and Program 12.4 we show a BankAccount class with three constructors. The three constructors reflect different ways to initialize a new bank account. They provide convenience to clients of the BankAccount class. Program 12.4 is better than Program 12.3 because there is less overlap between the constructors. Thus, Program 12.4 is easier to maintain than Program 12.3. (Just count the lines and compare). Make sure to program your constructors like in Program 12.4.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using System; public class BankAccount { private double interestRate; private string owner; private decimal balance; public BankAccount(string owner) { this.interestRate = 0.0; this.owner = owner; this.balance = 0.0M; } public BankAccount(string owner, double interestRate) { this.interestRate = interestRate; this.owner = owner; this.balance = 0.0M; } public BankAccount(string owner, double interestRate, decimal balance) { this.interestRate = interestRate; this.owner = owner; this.balance = balance; } public decimal Balance () { return balance; } public void Withdraw (decimal amount) { balance -= amount; } public void Deposit (decimal amount) { balance += amount; } public void AddInterests() { balance = balance + balance * (decimal)interestRate; } public override string ToString() { return owner + "'s account holds " + + balance + " kroner"; } } | |||
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; public class BankAccount { private double interestRate; private string owner; private decimal balance; public BankAccount(string owner): this(owner, 0.0, 0.0M) { } public BankAccount(string owner, double interestRate): this(owner, interestRate, 0.0M) { } public BankAccount(string owner, double interestRate, decimal balance) { this.interestRate = interestRate; this.owner = owner; this.balance = balance; } // BankAccount methods here } | |||
|
We also show and emphasize the constructors in the Die class, which we meet in Program 10.1 of Section 10.1. Below, in Program 12.5, the first Die constructor call the second one, hereby making a six eyed die. Notice that the second Die constructor creates a new Random object. It is typical that a constructor in a class instantiates a number of other classes, which again may instantiate other classes, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System; public class Die { private int numberOfEyes; private Random randomNumberSupplier; private readonly int maxNumberOfEyes; public Die (): this(6){} public Die (int maxNumberOfEyes){ randomNumberSupplier = new Random(unchecked((int)DateTime.Now.Ticks)); this.maxNumberOfEyes = maxNumberOfEyes; numberOfEyes = NewTossHowManyEyes(); } // Die methods here } | |||
|
12.5. Copy constructors
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Copy constructors can be used for making copies of existing objects. A copy constructor can be recognized by the fact that it takes a parameter of the same type as the class to which it belongs. Object copying is an intricate matter, because we will have to decide if the referred object should be copied too (shallow copying, deep copying, or something in between, see more details in Section 13.4 and Section 32.6).
It is sometimes useful to have a constructor that creates an identical copy of an existing object |
In Program 12.6 we show the Die class with an emphasized copy constructor. Notice that the Random object is shared between the original Die and the copy of the Die. This is shallow copying.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; public class Die { private int numberOfEyes; private Random randomNumberSupplier; private readonly int maxNumberOfEyes; public Die (Die d){ numberOfEyes = d.numberOfEyes; randomNumberSupplier = d.randomNumberSupplier; maxNumberOfEyes = d.maxNumberOfEyes; } public Die (): this(6){} public Die (int maxNumberOfEyes){ randomNumberSupplier = new Random(unchecked((int)DateTime.Now.Ticks)); this.maxNumberOfEyes = maxNumberOfEyes; numberOfEyes = randomNumberSupplier.Next(1,maxNumberOfEyes + 1); } // Die methods here } | |||
|
The use of copy constructors is particularly useful when we deal with mutable objects |
Objects are mutable if their state can be changed after the constructor has been called. It is often necessary to copy a mutable object. Why? Because of aliasing, an object may be referred from several different places. If the object is mutable, all these places will observe a change, and this is not always what we want. Therefore, we can protect against this by copying certain objects.
The observation from above is illustrated by means of an example - privacy leak - in Section 16.5.
12.6. Initialization of class variables
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
It is too late - and not natural - to initialize class variables in ordinary constructors |
Constructors initialize new instances of classes. Class instances are objects. Class variables (static fields) do not belong to any object. They belong to the class as such, but they can be used from instances of the class as well. Class variables can be useful even in the case where no instances of the class will ever be made.
Therefore we will need other means than constructors to initialize class variables in C#. Initialization of a class variable of type T takes place at class load time
|
Initialization of class variable (static fields) v of type T takes place implicitly. The variable v is, at load time, bound the distinguished default value of type T.
A static initializer is the expression at the right-hand side of "=" in a static field declaration. In Program 12.7 we have emphasized four examples of static initializers from line 13 to 16. The static initializers are executed in the order of appearance at class load time.
In Program 12.7 we show a simple playing card class called Card in which we organize all spade cards, all heart cards, all club cards, and all diamond cards in static arrays. The arrays are created in static initializers from line 13 to 16. It is convenient to initialize the elements of the arrays in a for loops. The right place of these for loops is in a static constructor. We show a static constructor in line 18-25 of Program 12.7. A static constructor is always without parameters.
Notice in line 19 of Program 12.7 how we get access to all enumeration values in a given enumeration type ET by the expression Enum.GetValues(typeof(ET)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using System; public class Card{ public enum CardSuite { Spade, Heart, Club, Diamond}; public enum CardValue { Ace = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, Nine = 9, Ten = 10, Jack = 11, Queen = 12, King = 13, }; private CardSuite suite; private CardValue value; public static Card[] allSpades = new Card[14]; public static Card[] allHearts = new Card[14]; public static Card[] allClubs = new Card[14]; public static Card[] allDiamonds = new Card[14]; static Card(){ foreach(CardValue cv in Enum.GetValues(typeof(CardValue))){ allSpades[(int)cv] = new Card(CardSuite.Spade, cv); allHearts[(int)cv] = new Card(CardSuite.Heart, cv); allClubs[(int)cv] = new Card(CardSuite.Club, cv); allDiamonds[(int)cv] = new Card(CardSuite.Diamond, cv); } } public Card(CardSuite suite, CardValue value){ this.suite = suite; this.value = value; } public CardSuite Suite{ get { return this.suite; } } public CardValue Value{ get { return this.value; } } public override String ToString(){ return String.Format("Suite:{0}, Value:{1}", suite, value); } } | |||
|
We also show how the static arrays can be used, see Program 12.8 and the output of the program, see Listing 12.9 (only on web).
1 2 3 4 5 6 7 8 9 10 | using System; class Client{ public static void Main(){ foreach (Card c in Card.allSpades) Console.WriteLine(c); } } | |||
|
1 2 3 4 5 6 7 8 9 10 11 12 13 | Suite:Spade, Value:Ace Suite:Spade, Value:Two Suite:Spade, Value:Three Suite:Spade, Value:Four Suite:Spade, Value:Five Suite:Spade, Value:Six Suite:Spade, Value:Seven Suite:Spade, Value:Eight Suite:Spade, Value:Nine Suite:Spade, Value:Ten Suite:Spade, Value:Jack Suite:Spade, Value:Queen Suite:Spade, Value:King | |||
|
We recommend explicit initialization of all variables in a class, including static variables. It is recommended to initialize all instance variables in (instance) constructors. Most static variables can and should be initialized via use of initializers, directly associated with their declaration. In some special cases it is convenient to do a systematic initialization of class variables, for instance in a for loop. This can be done in a static constructor.