10.5 Time Class Case Study: Overloaded Constructors

Next, we demonstrate a class with several overloaded constructors that enable objects of that class to be conveniently initialized in different ways. To overload constructors, simply provide multiple constructor declarations with different signatures.

10.5.1 Class Time2 with Overloaded Constructors

By default, the properties 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, because it does not define such a constructor. Class Time2 (Fig. 10.5) contains overloaded constructors. In this app, one constructor invokes the other, which in turn calls SetTime to set the private instance variables hour, minute and second via the class’s Hour, Minute and Second properties, which perform validation. 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.

Fig. 10.5 Time2 class declaration with overloaded constructors.

Alternate View

 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 hour = 0, int minute = 0, int second = 0)
13       {
14          SetTime(hour, minute, second); // invoke SetTime to validate time
15       }
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; invalid values
22        // cause the properties' set accessors to throw exceptions
23       public void SetTime(int hour, int minute, int second)
24       {
25          Hour = hour; // set the Hour property
26          Minute = minute; // set the Minute property
27          Second = second; // set the Second property
28       }
29
30       // property that gets and sets the hour
31       public int Hour
32       {
33          get
34          {
35             return hour;
36          }
37          set
38          {
39              if (value < 0 || value > 23)
40              {
41                 throw new ArgumentOutOfRangeException(nameof(value),
42                    value, $"{nameof(Hour)} must be 0-23");
43              }
44
45              hour = value;
46           }
47      }
48
49       // property that gets and sets the minute
50       public int Minute
51      {
52          get
53         {
54          return minute;
55          }
56          set
57          {
58            if (value < 0 || value > 59)
59            {
60              throw new ArgumentOutOfRangeException(nameof(value),
61                 value, $"{nameof(Minute)} must be 0-59");
62            }
63
64              minute = value;
65         }
66       }
67
68       // property that gets and sets the second
69       public int Second
70       {
71          get
72          {
73             return second;
74          }
75          set
76          {
77             if (value < 0 || value > 59)
78             {
79                throw new ArgumentOutOfRangeException(nameof(value),
80                   value, $"{nameof(Second)} must be 0-59");
81             }
82
83             second = value;
84           }
85         }
86
87          // convert to string in universal-time format (HH:MM:SS)
88          public string ToUniversalString() =>
89             $"{Hour:D2}:{Minute:D2}:{Second:D2}";
90
91           // convert to string in standard-time format (H:MM:SS AM or PM)
92          public override string ToString() =>
93             $"{((Hour == 0 || Hour == 12) ? 12 : Hour % 12)}:" +
94             $"{Minute:D2} : {Second:D2}{(Hour < 12 ? "AM" : "PM")}";
95       }

Class Time2’s Three-Argument Constructor

Lines 12–15 declare a constructor with three default parameters. We did not define a constructor with an empty parameter list, so for class Time2 the constructor at lines 12–15 is also considered to be the class’s parameterless constructor—you can call the constructor without arguments and the compiler will provide the default values. This constructor also can be called with one argument for the hour, two for the hour and minute, or three for the hour, minute and second. This constructor calls SetTime to set the time.

Common Programming Error 10.1

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.

Constructor Initializers

Lines 18–19 declare another 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’s allowed only in the constructor’s header. In line 19,


: this(time.Hour, time.Minute, time.Second) { }

: this followed by parentheses containing arguments indicates a call to one of the class’s other constructors—in this case, the Time2 constructor that takes three int arguments (lines 12–15). Line 19 passes the values of the time argument’s Hour, Minute and Second properties to initialize the Time2 object being constructed. Any initialization code in the body of the constructor at lines 18–19 would execute after the other constructor is called.

Using this as in line 19 is called a constructor initializer. It enables a class to reuse initialization code provided by a constructor, rather than defining similar code in another constructor. 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 SetTime.

Software Engineering Observation 10.5

Constructor initializers make classes easier to maintain, modify and debug, because the common initialization code can be defined in one constructor and called by others.

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

When executing a method of a class, if that method has a reference to another object of the same class (typically received via a parameter), the method can access all of that other object’s data and methods (including those that are private).

SetTime Method and the Hour, Minute and Second Properties

Method SetTime (lines 23–28) invokes the set accessors of the properties Hour (lines 31–47), Minute (lines 50–66) and Second (lines 69–85), which ensure that the hour is in the range 0 to 23 and that the values for the 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 41–42, 60–61 and 79–80). In this example, we use the exception class’s overloaded constructor that receives three arguments:

  • the string name of the item that was out of range

  • the value that was supplied for that item and

  • a string error message.

It’s common to include in an exception’s error message a variable’s or property’s identifier. This information can help a client-code programmer understand the context in which the exception occurred. Prior to C# 6, you had to hard code these identifiers into your error-message strings. As of C# 6, you can instead use the nameof operator (lines 41–42, 60–61 and 79–80), which returns a string representation of the identifier enclosed in parentheses. For example, the expression


nameof(value)

in line 41 returns the string "value" and the expression


nameof(Hour)

in line 42 returns the string "Hour".

Good Programming Practice 10.1

When you need to include an identifier in a string literal, use the nameof operator rather than hard coding the identifier’s name in the string. If you right click an identifier in Visual Studio then use the Rename… option to change the identifier’s name throughout your code, the string that nameof returns will be updated automatically with the identifier’s new name.

Notes Regarding Class Time2’s Methods, Properties and Constructors

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

Similarly, each constructor could 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 allows any changes to SetTime’s implementation to be made only once.

Software Engineering Observation 10.7

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

10.5.2 Using Class Time2’s Overloaded Constructors

Class Time2Test (Fig. 10.6) creates six Time2 objects (lines 9–13 and 41) to invoke the overloaded Time2 constructors.

Fig. 10.6 Overloaded constructors used to initialize Time2 objects.

Alternate View

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

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: value
Actual value was 27.

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 in the constructor call with the number and types of the parameters in each constructor declaration. Lines 9–12 each invoke the constructor at lines 12–15 of Fig. 10.5:

  • Line 9 of Fig. 10.6 invokes the constructor with no arguments—the compiler supplies 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 the hour, minute and second.

Line 13 invokes the constructor at lines 18–19 of Fig. 10.5. Lines 15–36 (Fig. 10.6) display the string representation of each initialized Time2 object to confirm that each was initialized properly.

Line 41 attempts to initialize 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 Hour property, an ArgumentOutOfRangeException occurs. We catch this exception at line 43 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 Argument-OutOfRangeException constructor when the exception object was created, the exception’s Message property also includes the information about the out-of-range value.

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

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