Chapter 10. Classes and Objects: A Deeper Look

 

But what, to serve our private ends, Forbids the cheating of our friends?

 
 --Charles Churchill
 

This above all: to thine own self be true.

 
 --William Shakespeare.
<feature> <supertitle>Objectives</supertitle>

In this chapter you’ll learn:

<objective>

Encapsulation and data hiding.

</objective>
<objective>

To use keyword this.

</objective>
<objective>

To use static variables and methods.

</objective>
<objective>

To use readonly fields.

</objective>
<objective>

To take advantage of C#’s memory-management features.

</objective>
<objective>

To use the IDEs Class View and Object Browser windows.

</objective>
<objective>

To use object initializers to create an object and initialize it in the same statement.

</objective>
</feature>
<feature> <supertitle>Outline</supertitle> </feature>

Introduction

In this chapter, we take a deeper look at building classes, controlling access to members of a class and creating constructors. We discuss composition—a capability that allows a class to have references to objects of other classes as members. We reexamine the use of properties. The chapter also discusses static class members and readonly instance variables in detail. We investigate issues such as software reusability, data abstraction and encapsulation. We also discuss several miscellaneous topics related to defining classes.

Time Class Case Study

Time1 Class Declaration

Our first example consists of two classes—Time1 (Fig. 10.1) and Time1Test (Fig. 10.2). Class Time1 represents the time of day. Class Time1Test is a testing class in which the Main method creates an object of class Time1 and invokes its methods. The output of this application appears in Fig. 10.2.

Example 10.1. Time1 class declaration maintains the time in 24-hour format.

 1   // Fig. 10.1: Time1.cs
 2   // Time1 class declaration maintains the time in 24-hour format.
 3   using System; // namespace containing ArgumentOutOfRangeException
 4
 5   public class Time1
 6   {
 7      private int hour; // 0 - 23  
 8      private int minute; // 0 - 59
 9      private int second; // 0 - 59
10
11      // set a new time value using universal time; throw an
12      // exception if the hour, minute or second is invalid
13      public void SetTime( int h, int m, int s )
14      {
15         // validate hour, minute and second
16         if ( ( h >= 0 && h < 24 ) && ( m >= 0 && m < 60 ) &&
17            ( s >= 0 && s < 60 ) )
18         {
19            hour = h;
20            minute = m;
21            second = s;
22         } // end if
23         else
24            throw new ArgumentOutOfRangeException();
25      } // end method SetTime
26
27      // convert to string in universal-time format (HH:MM:SS)
28      public string ToUniversalString()
29      {
30         return string.Format( "{0:D2}:{1:D2}:{2:D2}",
31            hour, minute, second );                   
32      } // end method ToUniversalString
33
34      // convert to string in standard-time format (H:MM:SS AM or PM)
35      public override string ToString()
36      {
37         return string.Format( "{0}:{1:D2}:{2:D2} {3}",      
38            ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 ),
39            minute, second, ( hour < 12 ? "AM" : "PM" ) );   
40      } // end method ToString
41   } // end class Time1

Example 10.2. Time1 object used in an application.

 1   // Fig. 10.2: Time1Test.cs
 2   // Time1 object used in an application.
 3   using System;
 4
 5   public class Time1Test
 6   {
 7      public static void Main( string[] args )
 8      {
 9         // create and initialize a Time1 object
10         Time1 time = new Time1(); // invokes Time1 constructor
11
12         // output string representations of the time
13         Console.Write( "The initial universal time is: " );
14         Console.WriteLine( time.ToUniversalString() );
15         Console.Write( "The initial standard time is: " );
16         Console.WriteLine( time.ToString() );
17         Console.WriteLine(); // output a blank line
18
19         // change time and output updated time
20         time.SetTime( 13, 27, 6 );
21         Console.Write( "Universal time after SetTime is: " );
22         Console.WriteLine( time.ToUniversalString() );
23         Console.Write( "Standard time after SetTime is: " );
24         Console.WriteLine( time.ToString() );
25         Console.WriteLine(); // output a blank line
26
27         // attempt to set time with invalid values
28         try
29         {
30            time.SetTime( 99, 99, 99 );
31         } // end try
32         catch ( ArgumentOutOfRangeException ex )
33         {
34            Console.WriteLine( ex.Message + "n" );
35         } // end catch
36
37         // display time after attempt to set invalid values
38         Console.WriteLine( "After attempting invalid settings:" );
39         Console.Write( "Universal time: " );
40         Console.WriteLine( time.ToUniversalString() );
41         Console.Write( "Standard time: " );
42         Console.WriteLine( time.ToString() );
43      } // end Main
44   } // end class Time1Test

The initial universal time is: 00:00:00
The initial standard time is: 12:00:00 AM

Universal time after SetTime is: 13:27:06
Standard time after SetTime is: 1:27:06 PM

Specified argument was out of the range of valid values.
After attempting invalid settings:
Universal time: 13:27:06
Standard time: 1:27:06 PM

Class Time1 contains three private instance variables of type int (Fig. 10.1, lines 7–9)—hour, minute and second—that represent the time in universal-time format (24-hour clock format, in which hours are in the range 0–23). Class Time1 contains public methods SetTime (lines 13–25), ToUniversalString (lines 28–32) and ToString (lines 35–40). These are the public services or the public interface that the class provides to its clients.

In this example, class Time1 does not declare a constructor, so the class has a default constructor that is supplied by the compiler. Each instance variable implicitly receives the default value 0 for an int. When instance variables are declared in the class body, they can be initialized using the same initialization syntax as a local variable.

Method SetTime and Throwing Exceptions

Method SetTime (lines 13–25) is a public method that declares three int parameters and uses them to set the time. Lines 16–17 tests each argument to determine whether the value is in the proper range, and, if so, lines 19–21 assign the values to the hour, minute and second instance variables. The hour value (line 13) must be greater than or equal to 0 and less than 24, because universal-time format represents hours as integers from 0 to 23 (e.g., 1 PM is hour 13 and 11 PM is hour 23; midnight is hour 0 and noon is hour 12). Similarly, both minute and second values must be greater than or equal to 0 and less than 60. For values outside these ranges, SetTime throws an exception of type ArgumentOutOfRangeException (lines 23–24), which notifies the client code that an invalid argument was passed to the method. As you learned in Chapter 8, you can use try...catch to catch exceptions and attempt to recover from them, which we’ll do in Fig. 10.2. The throw statement (line 24) creates a new object of type ArgumentOutOfRangeException. The parentheses following the class name indicate a call to the ArgumentOutOfRangeException constructor. After the exception object is created, the throw statement immediately terminates method SetTime and the exception is returned to the code that attempted to set the time.

Method ToUniversalString

Method ToUniversalString (lines 28–32) takes no arguments and returns a string in universal-time format, consisting of six digits—two for the hour, two for the minute and two for the second. For example, if the time were 1:30:07 PM, method ToUniversalString would return 13:30:07. The return statement (lines 30–31) uses static method Format of class string to return a string containing the formatted hour, minute and second values, each with two digits and, where needed, a leading 0 (specified with the D2 format specifier—which pads the integer with leading 0s if it has less than two digits). Method Format is similar to the string formatting in method Console.Write, except that Format returns a formatted string rather than displaying it in a console window. The formatted string is returned by method ToUniversalString.

Method ToString

Method ToString (lines 35–40) takes no arguments and returns a string in standard-time format, consisting of the hour, minute and second values separated by colons and followed by an AM or PM indicator (e.g., 1:27:06 PM). Like method ToUniversalString, method ToString uses static string method Format to format the minute and second as two-digit values with leading 0s, if necessary. Line 38 uses a conditional operator (?:) to determine the value for hour in the string—if the hour is 0 or 12 (AM or PM), it appears as 12—otherwise, it appears as a value from 1 to 11. The conditional operator in line 39 determines whether AM or PM will be returned as part of the string.

Recall from Section 7.4 that all objects in C# have a ToString method that returns a string representation of the object. We chose to return a string containing the time in standard-time format. Method ToString is called implicitly when an object’s value is output with a format item in a call to Console.Write. Remember that to enable objects to be converted to their string representations, we need to declare method ToString with keyword override—the reason for this will become clear when we discuss inheritance in Chapter 11.

Using Class Time1

As you learned in Chapter 4, each class you declare represents a new type in C#. Therefore, after declaring class Time1, we can use it as a type in declarations such as

Time1 sunset; // sunset can hold a reference to a Time1 object

The Time1Test application class (Fig. 10.2) uses class Time1. Line 10 creates a Time1 object and assigns it to local variable time. Operator new invokes class Time1’s default constructor, since Time1 does not declare any constructors. Lines 13–17 output the time, first in universal-time format (by invoking time’s ToUniversalString method in line 14), then in standard-time format (by explicitly invoking time’s ToString method in line 16) to confirm that the Time1 object was initialized properly. Line 20 invokes method SetTime of the time object to change the time. Then lines 21–25 output the time again in both formats to confirm that the time was set correctly.

Calling Time Method SetTime with Invalid Values

To illustrate that method SetTime validates its arguments, line 30 calls method SetTime with invalid arguments of 99 for the hour, minute and second. This statement is placed in a try block (lines 28–31) in case SetTime throws an ArgumentOutOfRangeException, which it will do since the arguments are all invalid. When this occurs, the exception is caught at lines 32–35 and the exception’s Message property is displayed. Lines 38–42 output the time again in both formats to confirm that SetTime did not change the time when invalid arguments were supplied.

Notes on the Time1 Class Declaration

Consider several issues of class design with respect to class Time1. The instance variables hour, minute and second are each declared private. The actual data representation used within the class is of no concern to the class’s clients. For example, it would be perfectly reasonable for Time1 to represent the time internally as the number of seconds since midnight or the number of minutes and seconds since midnight. Clients could use the same public methods and properties to get the same results without being aware of this. (Exercise 10.4 asks you to represent the time as the number of seconds since midnight and show that indeed no change is visible to the clients of the class.)

Software Engineering Observation 10.1

Software Engineering Observation 10.1

Classes simplify programming because the client can use only the public members exposed by the class. Such members are usually client oriented rather than implementation oriented. Clients are neither aware of, nor involved in, a class’s implementation. Clients generally care about what the class does but not how the class does it. Clients do, of course, care that the class operates correctly and efficiently.

Software Engineering Observation 10.2

Software Engineering Observation 10.2

Interfaces change less frequently than implementations. When an implementation changes, implementation-dependent code must change accordingly. Hiding the implementation reduces the possibility that other application parts become dependent on class-implementation details.

Controlling Access to Members

The access modifiers public and private control access to a class’s variables, methods and properties. (In Chapter 11, we’ll introduce the additional access modifier protected.) As we stated in Section 10.2, the primary purpose of public methods is to present to the class’s clients a view of the services the class provides (that is, the class’s public interface). Clients of the class need not be concerned with how the class accomplishes its tasks. For this reason, a class’s private variables, properties and methods (i.e., the class’s implementation details) are not directly accessible to the class’s clients.

Figure 10.3 demonstrates that private class members are not directly accessible outside the class. Lines 9–11 attempt to directly access private instance variables hour, minute and second of Time1 object time. When this application is compiled, the compiler generates error messages stating that these private members are not accessible. [Note: This application uses the Time1 class from Fig. 10.1.]

Example 10.3. Private members of class Time1 are not accessible outside the class.

 1   // Fig. 10.3: MemberAccessTest.cs
 2   // Private members of class Time1 are not accessible outside the class.
 3   public class MemberAccessTest
 4   {
 5      public static void Main( string[] args )
 6      {
 7         Time1 time = new Time1(); // create and initialize Time1 object
 8
 9         time.hour = 7; // error: hour has private access in Time1
10         time.minute = 15; // error: minute has private access in Time1
11         time.second = 30; // error: second has private access in Time1
12      } // end Main
13   } // end class MemberAccessTest
Private members of class Time1 are not accessible outside the class.

Notice that members of a class—for instance, properties, methods and instance variables—do not need to be explicitly declared private. If a class member is not declared with an access modifier, it has private access by default. For clarity, we always explicitly declare private members.

Referring to the Current Object’s Members with the this Reference

Every object can access a reference to itself with keyword this (also called the this reference). When a non-static method is called for a particular object, the method’s body implicitly uses keyword this to refer to the object’s instance variables and other methods. As you’ll see in Fig. 10.4, you can also use keyword this explicitly in a non-static method’s body. Section 10.5 shows a more interesting use of keyword this. Section 10.9 explains why keyword this cannot be used in a static method.

Example 10.4. this used implicitly and explicitly to refer to members of an object.

 1   // Fig. 10.4: ThisTest.cs
 2   // this used implicitly and explicitly to refer to members of an object.
 3   using System;
 4
 5   public class ThisTest
 6   {
 7      public static void Main( string[] args )
 8      {
 9         SimpleTime time = new SimpleTime( 15, 30, 19 );
10         Console.WriteLine( time.BuildString() );
11      } // end Main
12   } // end class ThisTest
13
14   // class SimpleTime demonstrates the "this" reference
15   public class SimpleTime
16   {
17      private int hour; // 0-23
18      private int minute; // 0-59
19      private int second; // 0-59
20
21      // if the constructor uses parameter names identical to
22      // instance-variable names, the "this" reference is
23      // required to distinguish between names
24      public SimpleTime( int hour, int minute, int second )
25      {
26         this.hour = hour; // set "this" object's hour instance variable
27         this.minute = minute; // set "this" object's minute            
28         this.second = second; // set "this" object's second            
29      } // end SimpleTime constructor
30
31      // use explicit and implicit "this" to call ToUniversalString
32      public string BuildString()
33      {
34         return string.Format( "{0,24}: {1}
{2,24}: {3}",
35            "this.ToUniversalString()", this.ToUniversalString(),
36            "ToUniversalString()", ToUniversalString() );
37      } // end method BuildString
38
39      // convert to string in universal-time format (HH:MM:SS)
40      public string ToUniversalString()
41      {
42         // "this" is not required here to access instance variables,
43         // because method does not have local variables with same
44         // names as instance variables
45         return string.Format( "{0:D2}:{1:D2}:{2:D2}",
46            this.hour, this.minute, this.second );
47      } // end method ToUniversalString
48   } // end class SimpleTime

this.ToUniversalString(): 15:30:19
     ToUniversalString(): 15:30:19

We now demonstrate implicit and explicit use of the this reference to enable class ThisTest’s Main method to display the private data of a class SimpleTime object (Fig. 10.4). For the sake of brevity, we declare two classes in one file—class ThisTest is declared in lines 5–12, and class SimpleTime is declared in lines 15–48.

Class SimpleTime declares three private instance variables—hour, minute and second (lines 17–19). The constructor (lines 24–29) receives three int arguments to initialize a SimpleTime object. For the constructor we used parameter names that are identical to the class’s instance-variable names (lines 17–19). We don’t recommend this practice, but we intentionally did it here to hide the corresponding instance variables so that we could illustrate explicit use of the this reference. Recall from Section 7.11 that if a method contains a local variable with the same name as a field, that method will refer to the local variable rather than the field. In this case, the parameter hides the field in the method’s scope. However, the method can use the this reference to refer to the hidden instance variable explicitly, as shown in lines 26–28 for SimpleTime’s hidden instance variables.

Method BuildString (lines 32–37) returns a string created by a statement that uses the this reference explicitly and implicitly. Line 35 uses the this reference explicitly to call method ToUniversalString. Line 36 uses the this reference implicitly to call the same method. Programmers typically do not use the this reference explicitly to reference other methods in the current object. Also, line 46 in method ToUniversalString explicitly uses the this reference to access each instance variable. This is not necessary here, because the method does not have any local variables that hide the instance variables of the class.

Common Programming Error 10.1

Common Programming Error 10.1

It’s often a logic error when a method contains a parameter or local variable that has the same name as an instance variable of the class. In such a case, use reference this if you wish to access the instance variable of the class—otherwise, the method parameter or local variable will be referenced.

Error-Prevention Tip 10.1

Error-Prevention Tip 10.1

Avoid method-parameter names or local-variable names that conflict with field names. This helps prevent subtle, hard-to-locate bugs.

Class ThisTest (Fig. 10.4, lines 5–12) demonstrates class SimpleTime. Line 9 creates an instance of class SimpleTime and invokes its constructor. Line 10 invokes the object’s BuildString method, then displays the results.

Performance Tip 10.1

Performance Tip 10.1

C# conserves memory by maintaining only one copy of each method per class—this method is invoked by every object of the class. Each object, on the other hand, has its own copy of the class’s instance variables (i.e., non-static variables). Each method of the class implicitly uses the this reference to determine the specific object of the class to manipulate.

Time Class Case Study: Overloaded Constructors

As you know, you can declare your own constructor to specify how objects of a class should be initialized. Next, we demonstrate a class with several overloaded constructors that enable objects of that class to be initialized in different ways. To overload constructors, simply provide multiple constructor declarations with different signatures.

Class Time2 with Overloaded Constructors

By default, instance variables hour, minute and second of class Time1 (Fig. 10.1) are initialized to their default values of 0—midnight in universal time. Class Time1 doesn’t enable the class’s clients to initialize the time with specific nonzero values. Class Time2 (Fig. 10.5) contains overloaded constructors for conveniently initializing its objects in a variety of ways. In this application, one constructor invokes the other constructor, which in turn calls SetTime to set the hour, minute and second. The compiler invokes the appropriate Time2 constructor by matching the number and types of the arguments specified in the constructor call with the number and types of the parameters specified in each constructor declaration.

Example 10.5. Time2 class declaration with overloaded constructors.

 1   // Fig. 10.5: Time2.cs
 2   // Time2 class declaration with overloaded constructors.
 3   using System; // for class ArgumentOutOfRangeException
 4
 5   public class Time2
 6   {
 7      private int hour; // 0 - 23
 8      private int minute; // 0 - 59
 9      private int second; // 0 - 59
10
11      // constructor can be called with zero, one, two or three arguments
12      public Time2( int h = 0, int m = 0, int s = 0 )          
13      {                                                        
14         SetTime( h, m, s ); // invoke SetTime to validate time
15      } // end Time2 three-argument constructor                
16
17      // Time2 constructor: another Time2 object supplied as an argument
18      public Time2( Time2 time )                          
19         : this( time.Hour, time.Minute, time.Second ) { }
20
21      // set a new time value using universal time; ensure that
22      // the data remains consistent by setting invalid values to zero
23      public void SetTime( int h, int m, int s )
24      {
25         Hour = h; // set the Hour property
26         Minute = m; // set the Minute property
27         Second = s; // set the Second property
28      } // end method SetTime
29
30      // property that gets and sets the hour
31      public int Hour
32      {
33         get
34         {
35            return hour;
36         } // end get
37         set
38         {
39            if ( value >= 0 && value < 24 )
40               hour = value;
41            else
42               throw new ArgumentOutOfRangeException(
43                  "Hour", value, "Hour must be 0-23" );
44         } // end set
45      } // end property Hour
46
47      // property that gets and sets the minute
48      public int Minute
49      {
50         get
51         {
52            return minute;
53         } // end get
54         set
55         {
56            if ( value >= 0 && value < 60 )
57               minute = value;
58            else
59               throw new ArgumentOutOfRangeException(
60                  "Minute", value, "Minute must be 0-59" );
61         } // end set
62      } // end property Minute
63
64      // property that gets and sets the second
65      public int Second
66      {
67         get
68         {
69            return second;
70         } // end get
71         set
72         {
73            if ( value >= 0 && value < 60 )
74               second = value;
75            else
76               throw new ArgumentOutOfRangeException(
77                  "Second", value, "Second must be 0-59" );
78         } // end set
79      } // end property Second
80
81      // convert to string in universal-time format (HH:MM:SS)
82      public string ToUniversalString()
83      {
84         return string.Format(
85            "{0:D2}:{1:D2}:{2:D2}", Hour, Minute, Second );
86      } // end method ToUniversalString
87
88      // convert to string in standard-time format (H:MM:SS AM or PM)
89      public override string ToString()
90      {
91         return string.Format( "{0}:{1:D2}:{2:D2} {3}",
92            ( ( Hour == 0 || Hour == 12 ) ? 12 : Hour % 12 ),
93            Minute, Second, ( Hour < 12 ? "AM" : "PM" ) );
94      } // end method ToString
95   } // end class Time2

Class Time2’s Constructors

Lines 12–15 declare a constructor with three default parameters. This constructor is also considered to be the class’s parameterless constructor—a constructor invoked without arguments—because you can call the constructor without arguments and the compiler will automatically provide the default parameter values. This constructor can also be called with one argument for the hour, two arguments for the hour and minute, or three arguments for the hour, minute and second. This constructor calls SetTime to set the time.

Common Programming Error 10.2

Common Programming Error 10.2

A constructor can call methods of its class. Be aware that the instance variables might not yet be initialized, because the constructor is in the process of initializing the object. Using instance variables before they have been initialized properly is a logic error.

Lines 18–19 declare a Time2 constructor that receives a reference to a Time2 object. In this case, the values from the Time2 argument are passed to the three-parameter constructor at lines 12–15 to initialize the hour, minute and second. In this constructor, we use this in a manner that is allowed only in the constructor’s header. In line 19, the usual constructor header is followed by a colon (:), then the keyword this. The this reference is used in method-call syntax (along with the three int arguments) to invoke the Time2 constructor that takes three int arguments (lines 12–15). The constructor passes the values of the time argument’s Hour, Minute and Second properties to set the hour, minute and second of the Time2 object being constructed. Additional initialization code can be placed in this constructor’s body and it will execute after the other constructor is called.

The use of the this reference as shown in line 19 is called a constructor initializer. Constructor initializers are a popular way to reuse initialization code provided by one of the class’s constructors rather than defining similar code in another constructor’s body. This syntax makes the class easier to maintain, because one constructor reuses the other. If we needed to change how objects of class Time2 are initialized, only the constructor at lines 12–15 would need to be modified. Even that constructor might not need modification—it simply calls the SetTime method to perform the actual initialization, so it’s possible that the changes the class might require would be localized to this method.

Line 19 could have directly accessed instance variables hour, minute and second of the constructor’s time argument with the expressions time.hour, time.minute and time.second—even though they’re declared as private variables of class Time2.

Software Engineering Observation 10.3

Software Engineering Observation 10.3

When one object of a class has a reference to another object of the same class, the first object can access all the second object’s data and methods (including those that are private).

Class Time2’s SetTime Method

Method SetTime (lines 23–28) invokes the set accessors of the new properties Hour (lines 31–45), Minute (lines 48–62) and Second (lines 65–79), which ensure that the value supplied for hour is in the range 0 to 23 and that the values for minute and second are each in the range 0 to 59. If a value is out of range, each set accessor throws an ArgumentOutOfRangeException (lines 42–43, 59–60 and 76–77). In this example, we use the ArgumentOutOfRangeException constructor that receives three arguments—the name of the item that was out of range, the value that was supplied for that item and an error message.

Notes Regarding Class Time2’s Methods, Properties and Constructors

Time2’s properties are accessed throughout the class’s body. Method SetTime assigns values to properties Hour, Minute and Second in lines 25–27, and methods ToUniversalString and ToString use properties Hour, Minute and Second in line 85 and lines 92–93, respectively. These methods could have accessed the class’s private data directly. However, consider changing the representation of the time from three int values (requiring 12 bytes of memory) to a single int value representing the total number of seconds that have elapsed since midnight (requiring only 4 bytes of memory). If we make such a change, only the bodies of the methods that access the private data directly would need to change—in particular, the individual properties Hour, Minute and Second. There would be no need to modify the bodies of methods SetTime, ToUniversalString or ToString, because they do not access the private data directly. Designing the class in this manner reduces the likelihood of programming errors when altering the class’s implementation.

Similarly, each constructor could be written to include a copy of the appropriate statements from method SetTime. Doing so may be slightly more efficient, because the extra constructor call and the call to SetTime are eliminated. However, duplicating statements in multiple methods or constructors makes changing the class’s internal data representation more difficult and error-prone. Having one constructor call the other or even call SetTime directly requires any changes to SetTime’s implementation to be made only once.

Software Engineering Observation 10.4

Software Engineering Observation 10.4

When implementing a method of a class, use the class’s properties to access the class’s private data. This simplifies code maintenance and reduces the likelihood of errors.

Using Class Time2’s Overloaded Constructors

Class Time2Test (Fig. 10.6) creates six Time2 objects (lines 9–13 and 42) to invoke the overloaded Time2 constructors. Lines 9–13 demonstrate passing arguments to the Time2 constructors. C# invokes the appropriate overloaded constructor by matching the number and types of the arguments specified in the constructor call with the number and types of the parameters specified in each constructor declaration. Lines 9–12 each invoke the constructor at lines 12–15 of Fig. 10.5. Line 9 invokes the constructor with no arguments, which causes the compiler to supply the default value 0 for each of the three parameters. Line 10 invokes the constructor with one argument that represents the hour—the compiler supplies the default value 0 for the minute and second. Line 11 invokes the constructor with two arguments that represent the hour and minute—the compiler supplies the default value 0 for the second. Line 12 invoke the constructor with values for all three parameters. Line 13 invokes the constructor at lines 18–19 of Fig. 10.5. Lines 16–37 display the string representation of each initialized Time2 object to confirm that each was initialized properly.

Example 10.6. Overloaded constructors used to initialize Time2 objects.

 1   // Fig. 10.6: Time2Test.cs
 2   // Overloaded constructors used to initialize Time2 objects.
 3   using System;
 4
 5   public class Time2Test
 6   {
 7      public static void Main( string[] args )
 8      {
 9         Time2 t1 = new Time2(); // 00:00:00            
10         Time2 t2 = new Time2( 2 ); // 02:00:00         
11         Time2 t3 = new Time2( 21, 34 ); // 21:34:00    
12         Time2 t4 = new Time2( 12, 25, 42 ); // 12:25:42
13         Time2 t5 = new Time2( t4 ); // 12:25:42        
14         Time2 t6; // initialized later in the program
15
16         Console.WriteLine( "Constructed with:
" );
17         Console.WriteLine( "t1: all arguments defaulted" );
18         Console.WriteLine( "   {0}", t1.ToUniversalString() ); // 00:00:00
19         Console.WriteLine( "   {0}
", t1.ToString() ); // 12:00:00 AM
20
21         Console.WriteLine(
22            "t2: hour specified; minute and second defaulted" );
23         Console.WriteLine( "   {0}", t2.ToUniversalString() ); // 02:00:00
24         Console.WriteLine( "   {0}
", t2.ToString() ); // 2:00:00 AM
25
26         Console.WriteLine(
27            "t3: hour and minute specified; second defaulted" );
28         Console.WriteLine( "   {0}", t3.ToUniversalString() ); // 21:34:00
29         Console.WriteLine( "   {0}
", t3.ToString() ); // 9:34:00 PM
30
31         Console.WriteLine( "t4: hour, minute and second specified" );
32         Console.WriteLine( "   {0}", t4.ToUniversalString() ); // 12:25:42
33         Console.WriteLine( "   {0}
", t4.ToString() ); // 12:25:42 PM
34
35         Console.WriteLine( "t5: Time2 object t4 specified" );
36         Console.WriteLine( "   {0}", t5.ToUniversalString() ); // 12:25:42
37         Console.WriteLine( "   {0}", t5.ToString() ); // 12:25:42 PM
38
39         // attempt to initialize t6 with invalid values
40         try
41         {
42            t6 = new Time2( 27, 74, 99 ); // invalid values
43         } // end try
44         catch ( ArgumentOutOfRangeException ex )
45         {
46            Console.WriteLine( "
Exception while initializing t6:" );
47            Console.WriteLine( ex.Message );
48         } // end catch
49      } // end Main
50   } // end class Time2Test
Constructed with:

t1: all arguments defaulted
   00:00:00
   12:00:00 AM

t2: hour specified; minute and second defaulted
   02:00:00
   2:00:00 AM
t3: hour and minute specified; second defaulted
   21:34:00
   9:34:00 PM

t4: hour, minute and second specified
   12:25:42
   12:25:42 PM

t5: Time2 object t4 specified
   12:25:42
   12:25:42 PM

Exception while initializing t6:
hour must be 0-23
Parameter name: hour
Actual value was 27.

Line 42 attempts to intialize t6 by creating a new Time2 object and passing three invalid values to the constructor. When the constructor attempts to use the invalid hour value to initialize the object’s Hour property, an ArgumentOutOfRangeException occurs. We catch this exception at line 44 and display its Message property, which results in the last three lines of the output in Fig. 10.6. Because we used the three-argument ArgumentOutOfRangeException constructor when the exception object was created, the exception’s Message property also includes the information about the out-of-range value.

Default and Parameterless Constructors

Every class must have at least one constructor. Recall from Section 4.10 that if you do not provide any constructors in a class’s declaration, the compiler creates a default constructor that takes no arguments when it’s invoked. In Section 11.4.1, you’ll learn that the default constructor implicitly performs a special task.

The compiler will not create a default constructor for a class that explicitly declares at least one constructor. In this case, if you want to be able to invoke the constructor with no arguments, you must declare a parameterless constructor—as in line 12 of Fig. 10.5. Like a default constructor, a parameterless constructor is invoked with empty parentheses. The Time2 parameterless constructor explicitly initializes a Time2 object by passing to the three-parameter constructor 0 for each parameter. Since 0 is the default value for int instance variables, the parameterless constructor in this example could actually omit the constructor initializer. In this case, each instance variable would receive its default value when the object is created. If we omit the parameterless constructor, clients of this class would not be able to create a Time2 object with the expression new Time2().

Common Programming Error 10.3

Common Programming Error 10.3

If a class has constructors, but none of the public constructors are parameterless constructors, and an attempt is made to call a parameterless constructor to initialize an object of the class, a compilation error occurs. A constructor can be called with no arguments only if the class does not have any constructors (in which case the default constructor is called) or if the class has a visible parameterless constructor.

Composition

A class can have references to objects of other classes as members. This is called composition and is sometimes referred to as a has-a relationship. For example, an object of class AlarmClock needs to know the current time and the time when it’s supposed to sound its alarm, so it’s reasonable to include two references to Time objects in an AlarmClock object.

Software Engineering Observation 10.5

Software Engineering Observation 10.5

One form of software reuse is composition, in which a class has as members references to objects of other classes.

Class Date

Our example of composition contains three classes—Date (Fig. 10.7), Employee (Fig. 10.8) and EmployeeTest (Fig. 10.9). Class Date (Fig. 10.7) declares instance variables month and day (lines 7–8) and auto-implemented property Year (line 11) to represent a date. The constructor receives three int parameters. Line 17 invokes the set accessor of property Month (lines 24–38) to validate the month—if the value is out-of-range the accessor throws an exception. Line 18 uses property Year to set the year. Since Year is an auto-implemented property, we’re assuming in this example that the value for Year is correct. Line 19 uses property Day (lines 41–63), which validates and assigns the value for day based on the current month and Year (by using properties Month and Year in turn to obtain the values of month and Year). The order of initialization is important, because the set accessor of property Day validates the value for day based on the assumption that month and Year are correct. Line 53 determines whether the day is correct based on the number of days in the particular Month. If the day is not correct, lines 56–57 determine whether the Month is February, the day is 29 and the Year is a leap year. Otherwise, if the parameter value does not contain a correct value for day, the set accessor throws an exception. Line 20 in the constructor outputs the this reference as a string. Since this is a reference to the current Date object, the object’s ToString method (lines 66–69) is called implicitly to obtain the object’s string representation.

Example 10.7. Date class declaration.

 1   // Fig. 10.7: Date.cs
 2   // Date class declaration.
 3   using System;
 4
 5   public class Date
 6   {
 7      private int month; // 1-12
 8      private int day; // 1-31 based on month
 9
10      // auto-implemented property Year
11      public int Year { get; private set; }
12
13      // constructor: use property Month to confirm proper value for month;
14      // use property Day to confirm proper value for day
15      public Date( int theMonth, int theDay, int theYear )
16      {
17         Month = theMonth; // validate month
18         Year = theYear;  // could validate year
19         Day = theDay;  // validate day
20         Console.WriteLine(  "Date object constructor for date {0}", this );
21      } // end Date constructor
22
23      // property that gets and sets the month
24      public int Month
25      {
26         get
27         {
28            return month;
29         }  // end get
30         private set // make writing inaccessible outside the class
31         {
32            if ( value > 0 && value <= 12 ) // validate month
33               month = value;
34            else // month is invalid
35               throw new ArgumentOutOfRangeException(
36                  "Month", value, "Month must be 1-12" );
37         } // end set
38      } // end property Month
39
40      // property that gets and sets the day
41      public int Day
42      {
43         get
44         {
45            return day;
46         } // end get
47         private set // make writing inaccessible outside the class
48         {
49            int[ ] daysPerMonth = { 0, 31, 28, 31, 30, 31, 30,
50                                     31, 31, 30, 31, 30, 31 };
51
52            // check if day in range for month
53            if ( value > 0 && value <= daysPerMonth[ Month ] )
54               day = value;
55            // check for leap year
56            else if ( Month == 2 && value == 29 &&
57               ( Year % 400 == 0 || ( Year % 4 == 0 && Year % 100 != 0 ) ) )
58               day = value;
59            else // day is invalid
60               throw new ArgumentOutOfRangeException(
61                  "Day", value, "Day out of range for current month/year" );
62         } // end set
63      } // end property Day
64
65      // return a string of the form month/day/year
66      public override string ToString()
67      {
68         return string.Format( "{0}/{1}/{2}", Month, Day, Year );
69      } // end method ToString
70   } // end class Date

Class Date’s private set Accessors

Class Date uses access modifiers to ensure that clients of the class must use the appropriate methods and properties to access private data. In particular, the properties Year, Month and Day declare private set accessors (lines 11, 30 and 47, respectively) to restrict the use of the set accessors to members of the class. We declare these private for the same reasons that we declare the instance variables private—to simplify code maintenance and control access to the class’s data. Although the constructor, method and properties in class Date still have all the advantages of using the set accessors to perform validation, clients of the class must use the class’s constructor to initialize the data in a Date object. The get accessors of properties Year, Month and Day are implicitly declared public because their properties are declared public—when there’s no access modifier before a get or set accessor, the accessor inherits the access modifier preceding the property name.

Class Employee

Class Employee (Fig. 10.8) has public auto-implemented properties FirstName, LastName, BirthDate and HireDate. BirthDate and HireDate (lines 7–8) manipulate Date objects, demonstrating that a class can have references to objects of other classes as members. This, of course, is also true of the properties FirstName and LastName, which manipulate String objects. The Employee constructor (lines 11–18) takes four parameters—first, last, dateOfBirth and dateOfHire. The objects referenced by parameters dateOfBirth and dateOfHire are assigned to the Employee object’s BirthDate and HireDate properties, respectively. When class Employee’s ToString method is called, it returns a string containing the string representations of the two Date objects. Each of these strings is obtained with an implicit call to the Date class’s ToString method.

Example 10.8. Employee class with references to other objects.

 1   // Fig. 10.8: Employee.cs
 2   // Employee class with references to other objects.
 3   public class Employee
 4   {
 5      public string FirstName { get; private set; }
 6      public string LastName { get; private set; }
 7      public Date BirthDate { get; private set; }
 8      public Date HireDate { get; private set; } 
 9
10      // constructor to initialize name, birth date and hire date
11      public Employee( string first, string last,
12         Date dateOfBirth, Date dateOfHire )
13      {
14         firstName = first;
15         lastName = last;
16         birthDate = dateOfBirth;
17         hireDate = dateOfHire;
18      } // end Employee constructor
19
20      // convert Employee to string format
21      public override string ToString()
22      {
23         return string.Format( "{0}, {1}  Hired: {2}  Birthday: {3}",
24            lastName, firstName, hireDate, birthDate );
25      } // end method ToString
26   } // end class Employee

Class EmployeeTest

Class EmployeeTest (Fig. 10.9) creates two Date objects (lines 9–10) to represent an Employee’s birthday and hire date, respectively. Line 11 creates an Employee and initializes its instance variables by passing to the constructor two strings (representing the Employee’s first and last names) and two Date objects (representing the birthday and hire date). Line 13 implicitly invokes the Employee’s ToString method to display the values of its instance variables and demonstrate that the object was initialized properly.

Example 10.9. Composition demonstration.

 1   // Fig. 10.9: EmployeeTest.cs
 2   // Composition demonstration.
 3   using System;
 4
 5   public class EmployeeTest
 6   {
 7      public static void Main( string[] args )
 8      {
 9         Date birth = new Date( 7, 24, 1949 );
10         Date hire = new Date( 3, 12, 1988 );
11         Employee employee = new Employee( "Bob", "Blue", birth, hire );
12
13         Console.WriteLine( employee );
14      } // end Main
15   } // end class EmployeeTest
Date object constructor for date 7/24/1949
Date object constructor for date 3/12/1988
Blue, Bob Hired: 3/12/1988 Birthday: 7/24/1949

Garbage Collection and Destructors

Every object you create uses various system resources, such as memory. In many programming languages, these system resources are reserved for the object’s use until they’re explicitly released by the programmer. If all the references to the object that manages the resource are lost before the resource is explicitly released, the application can no longer access the resource to release it. This is known as a resource leak.

We need a disciplined way to give resources back to the system when they’re no longer needed, thus avoiding resource leaks. The Common Language Runtime (CLR) performs automatic memory management by using a garbage collector to reclaim the memory occupied by objects that are no longer in use, so the memory can be used for other objects. When there are no more references to an object, the object becomes eligible for destruction. Every object has a special member, called a destructor, that is invoked by the garbage collector to perform termination housekeeping on an object before the garbage collector reclaims the object’s memory. A destructor is declared like a parameterless constructor, except that its name is the class name, preceded by a tilde (~), and it has no access modifier in its header. After the garbage collector calls the object’s destructor, the object becomes eligible for garbage collection. The memory for such an object can be reclaimed by the garbage collector. With .NET 4, Microsoft has introduced a new background garbage collector that manages memory more efficiently than the garbage collectors in earlier .NET versions.

Memory leaks, which are common in other languages such as C and C++ (because memory is not automatically reclaimed in those languages), are less likely in C# (but some can still happen in subtle ways). Other types of resource leaks can occur. For example, an application could open a file on disk to modify its contents. If the application does not close the file, no other application can modify (or possibly even use) the file until the application that opened it terminates.

A problem with the garbage collector is that it doesn’t guarantee that it will perform its tasks at a specified time. Therefore, the garbage collector may call the destructor any time after the object becomes eligible for destruction, and may reclaim the memory any time after the destructor executes. In fact, it’s possible that neither will happen before the application terminates. Thus, it’s unclear whether, or when, the destructor will be called. For this reason, destructors are rarely used.

Software Engineering Observation 10.6

Software Engineering Observation 10.6

A class that uses system resources, such as files on disk, should provide a method to eventually release the resources. Many Framework Class Library classes provide Close or Dispose methods for this purpose. Section 13.5 introduces the Dispose method, which is then used in many later examples. Close methods are typically used with objects that are associated with files (Chapter 17) and other types of so-called streams of data.

static Class Members

Every object has its own copy of all the instance variables of the class. In certain cases, only one copy of a particular variable should be shared by all objects of a class. A static variable is used in such cases. A static variable represents classwide information—all objects of the class share the same piece of data. The declaration of a static variable begins with the keyword static.

Let’s motivate static data with an example. Suppose that we have a video game with Martians and other space creatures. Each Martian tends to be brave and willing to attack other space creatures when it’s aware that there are at least four other Martians present. If fewer than five Martians are present, each Martian becomes cowardly. Thus each Martian needs to know the martianCount. We could endow class Martian with martianCount as an instance variable. If we do this, every Martian will have a separate copy of the instance variable, and every time we create a new Martian, we’ll have to update the instance variable martianCount in every Martian. This wastes space on redundant copies, wastes time updating the separate copies and is error prone. Instead, we declare martianCount to be static, making martianCount classwide data. Every Martian can access the martianCount as if it were an instance variable of class Martian, but only one copy of the static martianCount is maintained. This saves space. We save time by having the Martian constructor increment the static martianCount—there’s only one copy, so we do not have to increment separate copies of martianCount for each Martian object.

Software Engineering Observation 10.7

Software Engineering Observation 10.7

Use a static variable when all objects of a class must use the same copy of the variable.

The scope of a static variable is the body of its class. A class’s public static members can be accessed by qualifying the member name with the class name and the member access (.) operator, as in Math.PI. A class’s private static class members can be accessed only through the methods and properties of the class. Actually, static class members exist even when no objects of the class exist—they’re available as soon as the class is loaded into memory at execution time. To access a private static member from outside its class, a public static method or property can be provided.

Common Programming Error 10.4

Common Programming Error 10.4

It’s a compilation error to access or invoke a static member by referencing it through an instance of the class, like a non-static member.

Software Engineering Observation 10.8

Software Engineering Observation 10.8

Static variables, methods and properties exist, and can be used, even if no objects of that class have been instantiated.

Class Employee

Our next application declares two classes—Employee (Fig. 10.10) and EmployeeTest (Fig. 10.11). Class Employee declares private static auto-implemented property Count. We declare Count’s set accessor private, because we don’t want clients of the class to be able to modify the property’s value. The compiler automatically creates a private static variable that property Count will manage. If a static variable is not initialized, the compiler assigns a default value to the variable—in this case, the static variable for the auto-implemented Count property is initialized to 0, the default value for type int. Property Count maintains a count of the number of objects of class Employee that have been created.

Example 10.10. static property used to maintain a count of the number of Employee objects that have been created.

 1   // Fig. 10.10: Employee.cs
 2   // Static variable used to maintain a count of the number of
 3   // Employee objects that have been created.
 4   using System;
 5
 6   public class Employee
 7   {
 8      public static int Count { get; private set; } // objects in memory
 9
10      // read-only auto-implemented property FirstName
11      public string FirstName { get; private set; }
12
13      // read-only auto-implemented property LastName
14      public string LastName { get; private set; }
15
16      // initialize employee, add 1 to static Count and
17      // output string indicating that constructor was called
18      public Employee( string first, string last )
19      {
20         FirstName = first;
21         LastName = last;
22         ++Count; // increment static count of employees
23         Console.WriteLine( "Employee constructor: {0} {1}; Count = {2}",
24            FirstName, LastName, Count );
25      } // end Employee constructor
26   } // end class Employee

Example 10.11. static member demonstration.

 1   // Fig. 10.11: EmployeeTest.cs
 2   // Static member demonstration.
 3   using System;
 4
 5   public class EmployeeTest
 6   {
 7      public static void Main( string[ ] args )
 8      {
 9         // show that Count is 0 before creating Employees
10         Console.WriteLine( "Employees before instantiation: {0}",
11            Employee.Count );
12
13         // create two Employees; Count should become 2
14         Employee e1 = new Employee( "Susan", "Baker" );
15         Employee e2 = new Employee( "Bob", "Blue" );   
16
17         // show that Count is 2 after creating two Employees
18         Console.WriteLine( "
Employees after instantiation: {0}",
19            Employee.Count );
20
21         // get names of Employees
22         Console.WriteLine( "
Employee 1: {0} {1}
Employee 2: {2} {3}
",
23            e1.FirstName, e1.LastName,
24            e2.FirstName, e2.LastName );
25
26         // in this example, there is only one reference to each Employee,
27         // so the following statements cause the CLR to mark each        
28         // Employee object as being eligible for garbage collection      
29         e1 = null; // good practice: mark object e1 no longer needed     
30         e2 = null; // good practice: mark object e2 no longer needed     
31      } // end Main
32   } // end class EmployeeTest

Employees before instantiation: 0
Employee constructor: Susan Baker; Count = 1
Employee constructor: Bob Blue; Count = 2

Employees after instantiation: 2

Employee 1: Susan Baker
Employee 2: Bob Blue

When Employee objects exist, Count can be used in any method of an Employee object—this example increments Count in the constructor (line 22). Client code can access the Count with the expression Employee.Count, which evaluates to the number of Employee objects currently in memory.

Class EmployeeTest

EmployeeTest method Main (Fig. 10.11) instantiates two Employee objects (lines 14–15). When each Employee object’s constructor is invoked, lines 20–21 of Fig. 10.10 assign the Employee’s first name and last name to properties FirstName and LastName. These two statements do not make copies of the original string arguments. Actually, string objects in C# are immutable—they cannot be modified after they’re created. Therefore, it’s safe to have many references to one string object. This is not normally the case for objects of most other classes in C#. If string objects are immutable, you might wonder why we’re able to use operators + and += to concatenate string objects. String-concatenation operations actually result in a new string object containing the concatenated values. The original string objects are not modified.

Lines 18–19 display the updated Count. When Main has finished using the two Employee objects, references e1 and e2 are set to null at lines 29–30, so they no longer refer to the objects that were instantiated in lines 14–15. The objects become “eligible for destruction” because there are no more references to them in the application. After the objects’ destructors are called, the objects become “eligible for garbage collection.”

Eventually, the garbage collector might reclaim the memory for these objects (or the operating system will reclaim the memory when the application terminates). C# does not guarantee when, or even whether, the garbage collector will execute. When the garbage collector does run, it’s possible that no objects or only a subset of the eligible objects will be collected.

A method declared static cannot access non-static class members directly, because a static method can be called even when no objects of the class exist. For the same reason, the this reference cannot be used in a static method—the this reference must refer to a specific object of the class, and when a static method is called, there might not be any objects of its class in memory.

readonly Instance Variables

The principle of least privilege is fundamental to good software engineering. In the context of an application, the principle states that code should be granted the amount of privilege and access needed to accomplish its designated task, but no more. Let’s see how this principle applies to instance variables.

Some instance variables need to be modifiable, and some do not. In Section 8.4, we used keyword const for declaring constants. These constants must be initialized to a constant value when they’re declared. Suppose, however, we want to initialize a constant belonging to an object in the object’s constructor. C# provides keyword readonly to specify that an instance variable of an object is not modifiable and that any attempt to modify it after the object is constructed is an error. For example,

private readonly int INCREMENT;

declares readonly instance variable INCREMENT of type int. Like constants, readonly variables are declared with all capital letters by convention. Although readonly instance variables can be initialized when they’re declared, this isn’t required. Readonly variables should be initialized by each of the class’s constructors. Each constructor can assign values to a readonly instance variable multiple times—the variable doesn’t become unmodifiable until after the constructor completes execution. A constructor does not initialize the readonly variable, the variable receives the same default value as any other instance variable (0 for numeric simple types, false for bool type and null for reference types), and the compiler generates a warning.

Software Engineering Observation 10.9

Software Engineering Observation 10.9

Declaring an instance variable as readonly helps enforce the principle of least privilege. If an instance variable should not be modified after the object is constructed, declare it to be readonly to prevent modification.

Members that are declared as const must be assigned values at compile time. Therefore, const members can be initialized only with other constant values, such as integers, string literals, characters and other const members. Constant members with values that cannot be determined at compile time must be declared with keyword readonly, so they can be initialized at execution time. Variables that are readonly can be initialized with more complex expressions, such as an array initializer or a method call that returns a value or a reference to an object.

Common Programming Error 10.5

Common Programming Error 10.5

Attempting to modify a readonly instance variable anywhere but in its declaration or the object’s constructors is a compilation error.

Error-Prevention Tip 10.2

Error-Prevention Tip 10.2

Attempts to modify a readonly instance variable are caught at compilation time rather than causing execution-time errors. It’s always preferable to get bugs out at compile time, if possible, rather than allowing them to slip through to execution time (where studies have found that repairing bugs is often many times more costly).

Software Engineering Observation 10.10

Software Engineering Observation 10.10

If a readonly instance variable is initialized to a constant only in its declaration, it’s not necessary to have a separate copy of the instance variable for every object of the class. The variable should be declared const instead. Constants declared with const are implicitly static, so there will only be one copy for the entire class.

Data Abstraction and Encapsulation

Classes normally hide the details of their implementation from their clients. This is called information hiding. As an example, let’s consider the stack data structure introduced in Section 7.6. Recall that a stack is a last-in, first-out (LIFO) data structure—the last item pushed (inserted) on the stack is the first item popped (removed) off the stack.

Stacks can be implemented with arrays and with other data structures, such as linked lists. (We discuss stacks and linked lists in Chapters 21 and 23.) A client of a stack class need not be concerned with the stack’s implementation. The client knows only that when data items are placed in the stack, they’ll be recalled in last-in, first-out order. The client cares about what functionality a stack offers, not about how that functionality is implemented. This concept is referred to as data abstraction. Even if you know the details of a class’s implementation, you shouldn’t write code that depends on these details as they may later change. This enables a particular class (such as one that implements a stack and its push and pop operations) to be replaced with another version—perhaps one that runs faster or uses less memory—without affecting the rest of the system. As long as the public services of the class do not change (i.e., every original method still has the same name, return type and parameter list in the new class declaration), the rest of the system is not affected.

Earlier non-object-oriented programming languages like C emphasize actions. In these languages, data exists to support the actions that applications must take. Data is “less interesting” than actions. Data is “crude.” Only a few simple types exist, and it’s difficult for programmers to create their own types. C# and the object-oriented style of programming elevate the importance of data. The primary activities of object-oriented programming in C# are creating types (e.g., classes) and expressing the interactions among objects of those types. To create languages that emphasize data, the programming-languages community needed to formalize some notions about data. The formalization we consider here is the notion of abstract data types (ADTs), which improve the application-development process.

Consider the type int, which most people associate with an integer in mathematics. Actually, an int is an abstract representation of an integer. Unlike mathematical integers, computer ints are fixed in size. Type int in C# is limited to the range −2,147,483,648 to +2,147,483,647. If the result of a calculation falls outside this range, an error occurs, and the computer responds in some appropriate manner. It might “quietly” produce an incorrect result, such as a value too large to fit in an int variable—commonly called arithmetic overflow. It also might throw an exception, called an OverflowException. (We show how to deal with arithmetic overflow in Section 13.8.) Mathematical integers do not have this problem. Therefore, the computer int is only an approximation of the real-world integer. Simple types like int, double, and char are all examples of abstract data types—representations of real-world concepts to some satisfactory level of precision within a computer system.

An ADT actually captures two notions: a data representation and the operations that can be performed on that data. For example, in C#, an int contains an integer value (data) and provides addition, subtraction, multiplication, division and remainder operations—division by zero is undefined.

Software Engineering Observation 10.11

Software Engineering Observation 10.11

Programmers create types through the class mechanism. New types can be designed to be as convenient to use as the simple types. Although the language is easy to extend via new types, you cannot alter the base language itself.

Another ADT we discuss is a queue, which is similar to a “waiting line.” Computer systems use many queues internally. A queue offers well-understood behavior to its clients: Clients place items in a queue one at a time via an enqueue operation, then retrieve them one at a time via a dequeue operation. A queue returns items in first-in, first-out (FIFO) order—the first item inserted in a queue is the first removed. Conceptually, a queue can become infinitely long, but real queues are finite.

The queue hides an internal data representation that keeps track of the items currently waiting in line, and it offers enqueue and dequeue operations to its clients. The clients are not concerned about the implementation of the queue—they simply depend on the queue to operate “as advertised.” When a client enqueues an item, the queue should accept that item and place it in some kind of internal FIFO data structure. Similarly, when the client wants the next item from the front of the queue, the queue should remove the item from its internal representation and deliver it in FIFO order—the item that has been in the queue the longest should be returned by the next dequeue operation.

The queue ADT guarantees the integrity of its internal data structure. Clients cannot manipulate this data structure directly—only the queue ADT has access to its internal data. Clients are able to perform only allowable operations on the data representation—the ADT rejects operations that its public interface does not provide. We’ll discuss stacks and queues in greater depth in Chapter 21, Data Structures.

Class View and Object Browser

Now that we have introduced key concepts of object-oriented programming, we present two features that Visual Studio provides to facilitate the design of object-oriented applications—Class View and Object Browser.

Using the Class View Window

The Class View displays the fields, methods and properties for all classes in a project. To access this feature, you must first enable the IDE’s “expert features.” To do so, select Tools > Settings > Expert Settings. Next, select View > Class View. Figure 10.12 shows the Class View for the Time1 project of Fig. 10.1 (class Time1) and Fig. 10.2 (class Time1Test). The view follows a hierarchical structure, positioning the project name (Time1) as the root and including a series of nodes that represent the classes, variables, methods and properties in the project. If a Using the Class View Window appears to the left of a node, that node can be expanded to show other nodes. If a Using the Class View Window appears to the left of a node, that node can be collapsed. According to the Class View, project Time1 contains class Time1 and class Time1Test as children. When class Time1 is selected, the class’s members appear in the lower half of the window. Class Time1 contains methods SetTime, ToString and ToUniversalString (indicated by purple boxes, Using the Class View Window) and instance variables hour, minute and second (indicated by blue boxes, Using the Class View Window). The lock icons to the left of the blue box icons for the instance variables specify that the variables are private. Both class Time1 and class Time1Test contain the Base Types node. If you expand this node, you’ll see class Object in each case, because each class inherits from class System.Object (discussed in Chapter 11).

Class View of class Time1 (Fig. ) and class Time1Test (Fig. ).

Figure 10.12. Class View of class Time1 (Fig. 10.1) and class Time1Test (Fig. 10.2).

Using the Object Browser

Visual C# Express’s Object Browser lists all classes in the C# library. You can use the Object Browser to learn about the functionality provided by a specific class. To open the Object Browser, select Other Windows from the View menu and click Object Browser. Figure 10.13 depicts the Object Browser when the user navigates to the Math class in namespace System. To do this, we expanded the node for mscorlib (Microsoft Core Library) in the upper-left pane of the Object Browser, then expanded its subnode for System. [Note: The most common classes from the System namespace, such as System.Math, are in mscorlib.]

Object Browser for class Math.

Figure 10.13. Object Browser for class Math.

The Object Browser lists all methods provided by class Math in the upper-right frame—this offers you “instant access” to information regarding the functionality of various objects. If you click the name of a member in the upper-right frame, a description of that member appears in the lower-right frame. The Object Browser lists all the classes of the Framework Class Library. The Object Browser can be a quick mechanism to learn about a class or one of its methods. Remember that you can also view the complete description of a class or a method in the online documentation available through the Help menu in Visual C# Express.

Object Initializers

Visual C# provides object initializers that allow you to create an object and initialize its public properties (and public instance variables, if any) in the same statement. This can be useful when a class does not provide an appropriate constructor to meet your needs, but does provide properties that you can use to manipulate the class’s data. The following statements demonstrate object initializers using the class Time2 from Fig. 10.5.

// create a Time2 object and initialize its properties
Time2 aTime = new Time2 { Hour = 14, Minute = 30, Second = 12 };

// create a Time2 object and initialize only its Minute property
Time2 anotherTime = new Time2 { Minute = 45 };

The first statement creates a Time2 object (aTime), initializes it with class Time2’s constructor that can be called with no arguments, then uses an object initializer to set its Hour, Minute and Second properties. Notice that new Time2 is immediately followed by an object-initializer list—a comma-separated list in curly braces ({ }) of properties and their values. Each property name can appear only once in the object-initializer list. The object initializer executes the property initializers in the order in which they appear.

The second statement creates a new Time2 object (anotherTime), initializes it with class Time2’s constructor that can be called with no arguments, then sets only its Minute property using an object initializer. When the Time2 constructor is called with no arguments, it initializes the time to midnight. The object initializer then sets each specified property to the supplied value. In this case, the Minute property is set to 45. The Hour and Second properties retain their default values, because no values are specified for them in the object initializer.

Wrap-Up

In this chapter, we discussed additional class concepts. The Time class case study presented a complete class declaration consisting of private data, overloaded public constructors for initialization flexibility, properties for manipulating the class’s data and methods that returned string representations of a Time object in two different formats. You learned that every class can declare a ToString method that returns a string representation of an object of the class and that this method is invoked implicitly when an object of a class is output as a string or concatenated with a string.

You learned that the this reference is used implicitly in a class’s non-static methods to access the class’s instance variables and other non-static methods. You saw explicit uses of the this reference to access the class’s members (including hidden fields) and learned how to use keyword this in a constructor to call another constructor of the class.

You saw that composition enables a class to have references to objects of other classes as members. You learned about C#’s garbage-collection capability and how it reclaims the memory of objects that are no longer used. We explained the motivation for static variables in a class and demonstrated how to declare and use static variables and methods in your own classes. You also learned how to declare and initialize readonly variables.

We also showed how to use Visual Studio’s Class View and Object Browser windows to navigate the classes of the Framework Class Library and your own applications to discover information about those classes. Finally, you learned how to initialize an object’s properties as you create it with an object-initializer list.

In the next chapter, you’ll learn about inheritance. You’ll see that all classes in C# are related directly or indirectly to the object class and begin to understand how inheritance enables you to build more powerful applications faster.

Summary

Section 10.2 Time Class Case Study

  • The public methods of a class are the public services or the public interface that the class provides to its clients.

  • Methods and properties that modify the values of private variables should verify that the intended new values are valid.

  • A class’s methods and properties can throw exceptions to indicate invalid data.

  • The actual data representation used within the class is of no concern to the class’s clients. This allows you to change the implementation of the class. Clients could use the same public methods and properties to get the same results without being aware of this change.

  • Clients are neither aware of, nor involved in, a class’s implementation. Clients generally care about what the class does but not how the class does it.

Section 10.3 Controlling Access to Members

  • Access modifiers public and private control access to a class’s variables, methods and properties. A class’s private variables, methods and properties are not directly accessible to the class’s clients.

  • If a client attempts to use the private members of another class, the compiler generates error messages stating that these private members are not accessible.

  • If a class member is not declared with an access modifier, it has private access by default.

Section 10.4 Referring to the Current Object’s Members with the this Reference

  • Every object can access a reference to itself with keyword this. When a non-static method is called for a particular object, the method’s body implicitly uses keyword this to refer to the object’s instance variables, other methods and properties.

  • If a method contains a local variable with the same name as a field, that method will refer to the local variable rather than the field. However, a non-static method can use the this reference to refer to a hidden instance variable explicitly.

  • Avoid method-parameter names or local-variable names that conflict with field names. This helps prevent subtle, hard-to-locate bugs.

Section 10.5 Time Class Case Study: Overloaded Constructors

  • To overload constructors, provide multiple constructor declarations with different signatures.

  • Following the constructor header with the constructor initializer : this ( args ) invokes the matching overloaded constructor in the same class.

  • Constructor initializers are a popular way to reuse initialization code provided by one of the class’s constructors rather than defining similar code in another constructor’s body.

  • When one object of a class has a reference to another object of the same class, the first object can access all the second object’s data and methods (including those that are private).

  • When implementing a method of a class, use the class’s properties to access the class’s private data. This simplifies code maintenance and reduces the likelihood of errors.

  • The ArgumentOutOfRangeException constructor with three arguments lets you specify the name of the item that is out of range, the value that was out of range and an error message.

Section 10.6 Default and Parameterless Constructors

  • Every class must have at least one constructor. If there are no constructors in a class’s declaration, the compiler creates a default constructor for the class.

  • The compiler will not create a default constructor for a class that explicitly declares at least one constructor. In this case, if you want to be able to invoke the constructor with no arguments, you must declare a parameterless constructor.

Section 10.7 Composition

  • A class can have references to objects of other classes as members. Such a capability is called composition and is sometimes referred to as a has-a relationship.

Section 10.8 Garbage Collection and Destructors

  • Every object you create uses various system resources, such as memory. The CLR performs automatic memory management by using a garbage collector to reclaim the memory occupied by objects that are no longer in use.

  • The destructor is invoked by the garbage collector to perform termination housekeeping on an object before the garbage collector reclaims the object’s memory.

  • Memory leaks, which are common in other languages like C and C++ (because memory is not automatically reclaimed in those languages), are less likely in C#.

  • A problem with the garbage collector is that it’s not guaranteed to perform its tasks at a specified time. Therefore, the garbage collector may call the destructor any time after the object becomes eligible for destruction, making it unclear when, or whether, the destructor will be called.

Section 10.9 static Class Members

  • A static variable represents classwide information—all objects of the class share the variable.

  • The scope of a static variable is the body of its class. A class’s public static members can be accessed by qualifying the member name with the class name and the member access (.) operator.

  • Static class members exist even when no objects of the class exist—they’re available as soon as the class is loaded into memory at execution time.

  • String objects in C# are immutable—they cannot be modified after they’re created. Therefore, it’s safe to have many references to one string object.

  • It’s possible that no objects or only a subset of the eligible objects will be collected.

  • A method declared static cannot access non-static class members directly, because a static method can be called even when no objects of the class exist. For the same reason, the this reference cannot be used in a static method.

Section 10.10 readonly Instance Variables

  • The principle of least privilege is fundamental to good software engineering. In the context of an application, the principle states that code should be granted only the amount of privilege and access needed to accomplish its designated task, but no more.

  • Any attempt to modify a readonly instance variable after its object is constructed is an error.

  • Although readonly instance variables can be initialized when they’re declared, this is not required. A readonly variable can be initialized by each of the class’s constructors.

  • Members that are declared as const must be assigned values at compile time. Constant members with values that cannot be determined at compile time must be declared with keyword readonly, so they can be initialized at execution time.

Section 10.11 Data Abstraction and Encapsulation

  • Classes use information hiding to hide the details of their implementation from their clients.

  • The client cares about what functionality a class offers, not about how that functionality is implemented. This is referred to as data abstraction. Programmers should not write code that depends on these details as the details may later change.

  • The primary activities of object-oriented programming in C# are the creation of types (e.g., classes) and the expression of the interactions among objects of those types.

  • Types like int, double, and char are all examples of abstract data types. They’re representations of real-world notions to some satisfactory level of precision within a computer system.

  • An ADT actually captures two notions: a data representation and the operations that can be performed on that data.

Section 10.12 Class View and Object Browser

  • The Class View displays the variables, methods and properties for all classes in a project. The view follows a hierarchical structure, positioning the project name as the root and including a series of nodes that represent the classes, variables, methods and properties in the project.

  • The Object Browser lists all classes of the Framework Class Library. The Object Browser can be a quick mechanism to learn about a class or method of a class.

Section 10.13 Object Initializers

  • Object initializers allow you to create an object and initialize its public properties (and public instance variables, if any) in the same statement.

  • An object-initializer list is a comma-separated list in curly braces ({}) of properties and their values.

  • Each property and instance variable name can appear only once in the object-initializer list.

  • An object initializer first calls the class’s constructor, then sets the value of each property and variable specified in the object-initializer list.

Self-Review Exercises

10.1

Fill in the blanks in each of the following statements:

  1. string class static method _______ is similar to method Console.Write, but returns a formatted string rather than displaying a string in a console window.

  2. If a method contains a local variable with the same name as one of its class’s fields, the local variable _______ the field in that method’s scope.

  3. The _______ is called by the garbage collector before it reclaims an object’s memory.

  4. If a class declares constructors, the compiler will not create a(n) _______.

  5. An object’s _______ method can be called implicitly when an object appears in code where a string is needed.

  6. Composition is sometimes referred to as a(n) _______ relationship.

  7. A(n) _______ variable represents classwide information that is shared by all the objects of the class.

  8. The _______ states that code should be granted only the amount of access needed to accomplish its designated task.

  9. Declaring an instance variable with keyword _______ specifies that the variable is not modifiable.

  10. A(n) _______ consists of a data representation and the operations that can be performed on the data.

  11. The public methods of a class are also known as the class’s _______ or _______.

10.1

  1. Format.

  2. hides.

  3. destructor.

  4. default constructor.

  5. ToString.

  6. has-a.

  7. static.

  8. principle of least privilege.

  9. readonly.

  10. abstract data type (ADT).

  11. public services, public interface.

10.2

Suppose class Book defines properties Title, Author and Year. Use an object initializer to create an object of class Book and initialize its properties.

10.2

new Book { Title = "Visual C# 2010 HTP",
    Author = "Deitel", Year = 2010 }

Answers to Self-Review Exercises

Exercises

10.3

(Rectangle Class) Create class Rectangle. The class has attributes length and width, each of which defaults to 1. It has read-only properties that calculate the Perimeter and the Area of the rectangle. It has properties for both length and width. The set accessors should verify that length and width are each floating-point numbers greater than 0.0 and less than 20.0. Write an application to test class Rectangle.

10.4

(Modifying the Internal Data Representation of a Class) It would be perfectly reasonable for the Time2 class of Fig. 10.5 to represent the time internally as the number of seconds since midnight rather than the three integer values hour, minute and second. Clients could use the same public methods and properties to get the same results. Modify the Time2 class of Fig. 10.5 to implement the Time2 as the number of seconds since midnight and show that no change is visible to the clients of the class by using the same test application from Fig. 10.6.

10.5

(Savings-Account Class) Create the class SavingsAccount. Use the static variable annualInterestRate to store the annual interest rate for all account holders. Each object of the class contains a private instance variable savingsBalance, indicating the amount the saver currently has on deposit. Provide method CalculateMonthlyInterest to calculate the monthly interest by multiplying the savingsBalance by annualInterestRate divided by 12—this interest should be added to savingsBalance. Provide static method ModifyInterestRate to set the annualInterestRate to a new value. Write an application to test class SavingsAccount. Create two savingsAccount objects, saver1 and saver2, with balances of $2000.00 and $3000.00, respectively. Set annualInterestRate to 4%, then calculate the monthly interest and display the new balances for both savers. Then set the annualInterestRate to 5%, calculate the next month’s interest and display the new balances for both savers.

10.6

(Enhancing Class Date) Modify class Date of Fig. 10.7 to perform error checking on the initializer values for instance variables month, day and year (class Date currently validates only the month and day). You’ll need to convert the auto-implemented property Year into instance variable year with an associated Year property. Provide method NextDay to increment the day by 1. The Date object should always maintain valid data and throw exceptions when attempts are made to set invalid values. Write an application that tests the NextDay method in a loop that displays the date during each iteration of the loop to illustrate that the NextDay method works correctly. Test the following cases:

  1. incrementing to the next month and

  2. incrementing to the next year.

10.7

(Complex Numbers) Create a class called Complex for performing arithmetic with complex numbers. Complex numbers have the form realPart + imaginaryPart * i where i is Exercises. Write an application to test your class. Use floating-point variables to represent the private data of the class. Provide a constructor that enables an object of this class to be initialized when it’s declared. Provide a parameterless constructor with default values in case no initializers are provided. Provide public methods that perform the following operations:

  1. Add two Complex numbers: The real parts are added together and the imaginary parts are added together.

  2. Subtract two Complex numbers: The real part of the right operand is subtracted from the real part of the left operand, and the imaginary part of the right operand is subtracted from the imaginary part of the left operand.

  3. Return a string representation of a Complex number in the form (a, b), where a is the real part and b is the imaginary part.

10.8

(Set of Integers) Create class IntegerSet. Each IntegerSet object can hold integers in the range 0–100. The set is represented by an array of bools. Array element a[i] is true if integer i is in the set. Array element a[j] is false if integer j is not in the set. The parameterless constructor initializes the array to the “empty set” (i.e., a set whose array representation contains all false values).

Provide the following methods:

  1. Method Union creates a third set that is the set-theoretic union of two existing sets (i.e., an element of the third set’s array is set to true if that element is true in either or both of the existing sets—otherwise, the element of the third set is set to false).

  2. Method Intersection creates a third set which is the set-theoretic intersection of two existing sets (i.e., an element of the third set’s array is set to false if that element is false in either or both of the existing sets—otherwise, the element of the third set is set to true).

  3. Method InsertElement inserts a new integer k into a set (by setting a[k] to true).

  4. Method DeleteElement deletes integer m (by setting a[m] to false).

  5. Method ToString returns a string containing a set as a list of numbers separated by spaces. Include only those elements that are present in the set. Use --- to represent an empty set.

  6. Method IsEqualTo determines whether two sets are equal.

Write an application to test class IntegerSet. Instantiate several IntegerSet objects. Test that all your methods work properly.

10.9

(Rational Numbers) Create a class called Rational for performing arithmetic with fractions. Write an application to test your class. Use integer variables to represent the private instance variables of the class—the numerator and the denominator. Provide a constructor that enables an object of this class to be initialized when it’s declared. The constructor should store the fraction in reduced form. The fraction

2/4

is equivalent to 1/2 and would be stored in the object as 1 in the numerator and 2 in the denominator. Provide a parameterless constructor with default values in case no initializers are provided. Provide public methods that perform each of the following operations (all calculation results should be stored in a reduced form):

  1. Add two Rational numbers.

  2. Subtract two Rational numbers.

  3. Multiply two Rational numbers.

  4. Divide two Rational numbers.

  5. Display Rational numbers in the form a/b, where a is the numerator and b is the denominator.

  6. Display Rational numbers in floating-point format. (Consider providing formatting capabilities that enable the user of the class to specify the number of digits of precision to the right of the decimal point.)

10.10

(HugeInteger Class) Create a class HugeInteger which uses a 40-element array of digits to store integers as large as 40 digits each. Provide methods Input, ToString, Add and Subtract. For comparing HugeInteger objects, provide the following methods: IsEqualTo, IsNotEqualTo, IsGreaterThan, IsLessThan, IsGreaterThanOrEqualTo and IsLessThanOrEqualTo. Each of these is a method that returns true if the relationship holds between the two HugeInteger objects and returns false if the relationship does not hold. Provide method IsZero. If you feel ambitious, also provide methods Multiply, Divide and Remainder. In the Input method, use the string method ToCharArray to convert the input string into an array of characters, then iterate through these characters to create your HugeInteger. [Note: The .NET Framework Class Library now includes type BigInteger for arbitrary sized integer values.]

10.11

(Tic-Tac-Toe) Create class TicTacToe that will enable you to write a complete application to play the game of Tic-Tac-Toe. The class contains a private 3-by-3 rectangular array of integers. The constructor should initialize the empty board to all 0s. Allow two human players. Wherever the first player moves, place a 1 in the specified square, and place a 2 wherever the second player moves. Each move must be to an empty square. After each move, determine whether the game has been won and whether it’s a draw. If you feel ambitious, modify your application so that the computer makes the moves for one of the players. Also, allow the player to specify whether he or she wants to go first or second. If you feel exceptionally ambitious, develop an application that will play three-dimensional Tic-Tac-Toe on a 4-by-4-by-4 board.

10.12

What happens when a return type, even void, is specified for a constructor?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.157.6