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. |
In this chapter you’ll learn:
<objective>Encapsulation and data hiding.
</objective> <objective>To use keyword this
.
To use static
variables and methods.
To use readonly
fields.
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>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.
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
(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
(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 0
s 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
(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 0
s, 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.
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.
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.
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.)
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.
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.
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
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.
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
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.
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.
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.
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.
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.
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
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.
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
.
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.
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.
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.
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()
.
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.
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.
One form of software reuse is composition, in which a class has as members references to objects of other classes.
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
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
(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 string
s 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
(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 string
s (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 |
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.
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.
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 Martian
s 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 Martian
s present. If fewer than five Martian
s 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.
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.
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.
Static variables, methods and properties exist, and can be used, even if no objects of that class have been instantiated.
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
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.
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.
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.
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.
Attempting to modify a readonly
instance variable anywhere but in its declaration or the object’s constructors is a compilation error.
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).
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.
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 int
s 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.
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.
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.
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 appears to the left of a node, that node can be expanded to show other nodes. If a 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, ) and instance variables hour
, minute
and second
(indicated by blue boxes, ). 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).
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
.]
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
simple name of a class, field or method
10.3 | (Rectangle Class) Create class |
10.4 | (Modifying the Internal Data Representation of a Class) It would be perfectly reasonable for the |
10.5 | (Savings-Account Class) Create the class |
10.6 | (Enhancing Class
|
10.7 | (Complex Numbers) Create a class called
|
10.8 | (Set of Integers) Create class Provide the following methods:
Write an application to test class |
10.9 | (Rational Numbers) Create a class called 2/4 is equivalent to
|
10.10 | ( |
10.11 | (Tic-Tac-Toe) Create class |
10.12 | What happens when a return type, even |