Properties

   


Even though a property presents itself as a public instance variable to the world outside the class where it is defined, it is still implemented somewhat like a method inside the class (it has an ability to execute statements). This double life makes them highly suited for replacing accessor and mutator methods. Our path to understanding properties and why they provide more consistent, clearer, and intuitive code begins with a look at accessor and mutator methods.

Properties Versus Accessor and Mutator Methods

According to the encapsulation principle, discussed in Chapter 3, instance variables of an object must be hidden from source code written outside the object. In C#, this is typically implemented by declaring all instance variables to be private. In the examples presented so far, access from outside an object has been provided indirectly via the public accessor and mutator methods that conventionally carry names like GetDistance and SetDistance. These methods are also able to carry out actions that either protect the instance variables from being assigned incorrect values or actions that must be performed in connection with setting and getting an instance variable. Listing 14.1 demonstrates the typical use of accessor and mutator methods.

Listing 14.1 BicyclingWithAccessorMethods.cs
01: using System; 02: 03: class Bicycle 04: { 05:     const byte MaxSpeed = 40; 06:     private byte speed = 0; 07:     private int speedAccessCounter = 0; 08: 09:     public byte GetSpeed() 10:     { 11:         speedAccessCounter++; 12:         return speed; 13:     } 14: 15:     public void SetSpeed(byte newSpeed) 16:     { 17:         if (newSpeed > MaxSpeed) 18:             Console.WriteLine("Error. { 0}  exceeds the speed limit { 1} ", 19:                 newSpeed, MaxSpeed); 20:         else if (newSpeed < 0) 21:             Console.WriteLine("Error. { 0}  is less than 0", newSpeed); 22:         else 23:             speed = newSpeed; 24:     } 25: 26:     public int GetSpeedAccessCounter() 27:     { 28:         return speedAccessCounter; 29:     } 30: } 31: 32: class BicycleTester 33: { 34:     public static void Main() 35:     { 36:         byte speedInMilesPerHour; 37:         Bicycle myBike = new Bicycle(); 38: 39:         myBike.SetSpeed(60); 40:         myBike.SetSpeed(30); 41:         Console.WriteLine("Current speed of myBike: { 0} ", myBike.GetSpeed()); 42:         speedInMilesPerHour = (byte)(myBike.GetSpeed () * 0.621); 43:         Console.WriteLine("Number of times speed of myBike has been retrieved: { 0} ", 44:             myBike.GetSpeedAccessCounter()); 45:     } 46: } Error. 60 exceeds the speed limit 40 Current speed of myBike: 30 Number of times speed of myBike has been retrieved: 2 

The Bicycle class contains two private instance variables speed and speedAccessCounter, declared in lines 6 and 7. In this example, we want to prevent any outside user from assigning a value to speed that is smaller than zero or greater than MaxSpeed. This is accomplished by SetSpeed's if...else statement in lines 17 23.

speed can be accessed via the GetSpeed method, which also counts of the number of times speed has been retrieved (line 11).

The Main method in the BicycleTester class represents the source code outside the Bicycle object and must use the GetSpeed and SetSpeed methods as a means to access speed. In the output sample, Main's invalid attempt of assigning 60 to speed (see line 39) is appropriately denied by the SetSpeed method.

The number of times speed has been accessed (two, in this case once in line 41 to print out information and once in line 42 as part of a standard arbitrary calculation) is correctly counted by GetSpeed and stored in the speedAccessCounter. This value is accessed with GetSpeedAccessCounter in lines 43 and 44 and printed on the console as shown in the sample output.

Accessor and mutator methods fulfill their tasks adequately from a functional point of view (they get the work done), and they are commonly used throughout the programmer community in languages like Java and C++ that do not feature constructs similar to C#'s properties. However, they cause syntactical inconsistencies between the way instance variables are modified and retrieved when called from within the object in which they reside and the way they are modified and retrieved from an outside object. To modify and retrieve an instance variable from within its object, we simply need to write the name of the instance variable as follows:

 distance = 10;      or      time = distance / speed; 

If the syntax was consistent, we should be able to write the following line instead of line 40 in Listing 14.1:

 myBike.speed = 30; 

which uses the assignment operator, not a Set- method as in line 40.

Similarly, we should be able to write line 42 as follows:

 speedInMilesPerHour = (byte)(myBike.speed * 0.621); 

where speed is simply retrieved by writing its name after myBike, not by calling GetSpeed as in line 42.

Unfortunately, this would require us to declare speed to be public and to throw away the important statements belonging to the accessor and mutator methods, were it not for the properties. C#'s property construct allows the outside user of an object to access an instance variable by using the consistent instance variable (non-method) syntax shown previously, while permitting all instance variables inside the object to remain private. At the same time, the property contains statements, otherwise held by accessor, and mutator methods that are executed when appropriate.

Listing 14.2 uses properties to accomplish exactly the same functionality as Listing 14.1.

Listing 14.2 BicyclingWithProperties.cs
01: using System; 02: 03: class Bicycle 04: { 05:     const byte MaxSpeed = 40; 06:     private byte speed = 0; 07:     private int speedAccessCounter = 0; 08: 09:     public byte Speed 10:     { 11:         get 12:         { 13:             speedAccessCounter++; 14:             return speed; 15:         } 16: 17:         set 18:         { 19:             if (value > MaxSpeed) 20:                 Console.WriteLine("Error. { 0}  exceeds the speed limit { 1} ", 21:                     value, MaxSpeed); 22:             else if (value < 0) 23:                 Console.WriteLine("Error. { 0}  is less than 0", value); 24:             else 25:                 speed = value; 26:         } 27:     } 28: 29:     public int SpeedAccessCounter 30:     { 31:         get 32:         { 33:             return speedAccessCounter; 34:         } 35:     } 36: } 37:  38: class BicycleTester 39: { 40:     public static void Main() 41:     { 42:         byte speedInMilesPerHour; 43:         Bicycle myBike = new Bicycle (); 44: 45:         myBike.Speed = 60; 46:         myBike.Speed = 30; 47:         Console.WriteLine("Current speed of myBike: { 0} ", myBike.Speed); 48:         speedInMilesPerHour = (byte)(myBike.Speed * 0.621); 49:         Console.WriteLine("Number of times speed of myBike has been retrieved: { 0} ", 50:             myBike.SpeedAccessCounter); 51:     } 52: } Error. 60 exceeds the speed limit 40 Current speed of myBike: 30 Number of times speed of myBike has been retrieved: 2 

Note that the output is identical to that of Listing 14.1

Please refer to Syntax Box 14.1, shown later in this section, during this analysis.

The Bicycle class contains two properties. One is called Speed (lines 9 27) and provides access to the speed instance variable declared in line 6. The other, named SpeedAccessorCounter (lines 29 35) provides access to speedAccessCounter. Let's first look at the Speed property. A property consists of a header (line 9) and a block (lines 11 26) enclosed by braces (lines 10 and 27). In the header, we can specify the access modifier (in this case public), the type of the property (byte), and its name (Speed). The property block consists of a get statement block (lines 11 15) containing the statements equivalent to those found in an accessor method (compare with lines 11 and 12 of Listing 14.1) and a set statement block (lines 17 26) containing statements equivalent to those found in a mutator method (compare with lines 15 24 of Listing 14.1). The client side (which simply is the user of the class; in this case, it is the Main method) can now call the Speed property as if it is an instance variable. This is demonstrated in lines 45, 46, 47, and 48. When Speed is being assigned a value as in line 46

 myBike.Speed = 30; 

the statements of the set statement block are executed. The special parameter value, found in lines 19, 21, 22, 23, and 25, represents the value assigned, 30 in this case. value is automatically assigned the value 30 by the runtime. Line 25 thus passes 30 over to the instance variable speed.

Retrieving a value from Speed, as in line 48

 speedInMilesPerHour = (byte)(myBike.Speed * 0.621); 

triggers the statements of the get statement block to be executed. When a statement containing the return keyword is executed (line 14), the get statement block is terminated and the expression following return is returned to the retrieving call. The meaning of return in this context is identical to that of a conventional method.

If you only need to either set the value of an instance variable (write-only) or retrieve it (read-only), you can specify either just the set statement block or the get statement block; but at least one must be included. The SpeedAccessCounter property (lines 29 35) is an example of a read-only property that only allows its user to retrieve the value of an instance variable because of its missing set statement block.

Syntax Box 14.1 Property Declaration

 Property_declaration::=     [<Access_modifier>] [override | new [ virtual | abstract | static  graphics/ccc.gif] ] <Type> <Identifier>     {         [<get_statement_block>]         [<set_statement_block>]     } 

where

 <Access_modifier>                 ::= public                 ::= private                 ::= protected                 ::= internal                 ::= protected internal <get_statement_block> ::=           get           {               [<Statements>]               return <Expression>           } <set_statement_block> ::=           set           {               [<Statements (using the keyword value)>]           } 

Notes:

  • The type of the <Expression> positioned after the return keyword in the <get_statement_block> must be of the same type as specified by the <Type> of the property declaration header.

  • The parameter value in the <set_statement_block> represents the value assigned to the property and is of the same type as <Type> of the property declaration header. The name value is set by the compiler and cannot be changed.

  • A property can either have one <get_statement_block> or one <set_statement_block> or both. It cannot have zero statement blocks.

  • The <get_statement_block> is often called <get_accessor> or simply <getter>. The <set_statement_block> is often called <set_accessor> or <setter>.

  • The optional keyword new of the property declaration header is semantically different to new when you use it for creating new objects. It is, like the keywords override, virtual and abstract, related to OO concepts we haven't yet to discussed. I have included them here for completeness; they can safely be ignored for now.

  • A property does not need to represent a specific instance variable (even though this is a common use); it only pretends to be an instance variable. A property can, for example, be called Average and calculate the average of several instance variables in its get statement block.

Note

graphics/common.gif

Properties are easier for the client to use (a client is the part of a program that calls the properties and methods of a class or object) than accessor and mutator methods because

  • Properties provide for consistent syntax between accessing an instance variable residing within an object and accessing an instance variable in another object.

  • Properties save the programmer from checking whether an instance variable is public or private and whether accessor or mutator methods exist for this instance variable and save him from determining their formats.

  • There is usually one accessor and one mutator method per instance variable (which needs to be accessible), but only one property is needed per instance variable. So, instead of sifting through, for example, forty accessors and mutators, the user only needs to look at twenty properties.


Note

graphics/common.gif

Properties fit accessor and mutator parts into the same construct. An accessor and a mutator method work as a pair because they access the same instance variable. Consequently, a change in the source code in one of the methods often triggers the need for a change in the other. However, they are not treated as if they were a pair by the syntax, they could be two unrelated methods for that matter, floating around in the soup of other methods and instance variables. The property construct solves this problem by including both the accessor part (get statement block) and the mutator part (set statement block) in the same construct.


Use Pascal Casing for Properties and Camel Casing for Instance Variables

graphics/tnt.gif

Microsoft recommends the Pascal casing style for properties (example: Speed) and camel casing for instance variables (example: speed). It is important to distinguish between the two styles in your source code, because the name of an instance variable and its associated property are often so similar that casing is the only thing that differentiates them. Failure to comply will cause the compiler to mix up the two, often leading to serious errors.

Let's investigate the effect of changing speedAccessCounter in line 33 of Listing 14.2 from its current (correct) camel casing style referring to the instance variable, to the (incorrect) Pascal casing style (SpeedAccessCounter), referring to the property. After this change, the compiler will, during the execution of line 33, interpret SpeedAccessCounter as another call to the get statement block of the SpeedAccessCounter property, and thus call itself. This new execution of the get statement block will again meet the statement containing SpeedAccessCounter and call the get statement block yet again. In fact, the get statement block will call itself infinitely many times and thereby crash your program.


Note

graphics/common.gif

Properties can, like accessor and mutator methods, be called from within an object.


Properties Are Speedy

A property is executed as fast as its mutator and accessor counterparts and, in some instances, a property is executed as fast as a direct call to the instance variable it is accessing. Let's have a closer look at these two claims:

Properties are executed as fast as their equivalent accessor and mutator methods.

Both the get statement block and the set statement block of a property are turned into methods when compiled into MSIL. In MSIL, the get statement block is represented with a method called get_<Property_identifier>:... and the set statement block is represented with a method called set_<Property_identifier>:.... For example, the get statement block of the Distance property in Figure 14.1 is represented by a method equivalent construct with the header get_Distance:float64() in MSIL. It turns out that a method with the header public double get_Distance() is represented with an identical header, as illustrate in Figure 14.1. If the property and its twin methods are included in the same class (as in the Light class shown here), the result is a name clash. Consequently, if you tried to compile the Light class, you would get the following compiler error message:

    ...Class 'Light' already defines a member called 'get_Distance' with the same parameter types 
Figure 14.1. Example of identical properties and methods when compiled to MSIL.
graphics/14fig01.gif

Because of the similarity between properties and methods in MSIL, there is no performance difference between using accessor and mutator methods and their equivalent properties.

A simple get or set statement block is executed as fast as a direct call to the instance variable.

If a get statement block only contains one statement consisting of the return keyword followed by the name of the instance variable, it is called a simple get statement block. Similarly, if a set statement block only has one statement that assigns the value of the value parameter to the associated instance variable, it is called a simple set statement block. The following property Distance contains two simple statement blocks:

 ... private int distance; public int Distance {     get     {         return distance;     }     set     {         distance = value;     } } ... 

When a simple get or set statement block is called, as in the following line:

 time = moon.Distance / speed; 

the compiler performs a special optimization called inlining, whereby it replaces moon.Distance with the equivalent of a direct call to the distance instance variable. As a result, there is no performance difference between the last line and the following line (had distance been public):

 time = moon.distance / speed; 

where distance in moon.distance represents direct access to the instance variable distance.

Implementing Delayed Initialization and Lazy Updates with Properties

By using properties, it is possible to delay the initialization of an instance variable until its associated get statement block is called for the first time. This can improve the performance of a program when applied on instance variables that are rarely used and are time- and resource-consuming to initialize. Delayed initialization is a special case of another technique called lazy updates. Both techniques are discussed and demonstrated in the following discussion and example.

Sometimes, instance variables need to be updated regularly because they represent a value received from an exterior resource that changes within regular intervals. Some values might be valid for just a couple of seconds (stock prices) and others for weeks (economic indicators). Whenever this type of instance variable is needed in a calculation, it is possible to let the get statement block decide whether the program should use the old value stored in the instance variable object or whether it is time to go out and fetch a new value. This will minimize the number of times the program needs to use costly resources for updates. I will refer to these types of updates as lazy updates. The following scenario provides an example of using lazy updates.

You are writing a program for a sophisticated commodity trader (trading in oranges, wheat, and so on). The program must be able to forecast, with reasonable precision, the volume and quality of future crops in different regions of America. A rainfall prediction ten days ahead in the relevant region is one of the many parameters needed in the complex calculations that form the base of the program, so you must include a sophisticated weather forecasting component in your program to generate these data. This component provides forecasts for fifty different regions throughout the country. Each rainfall forecast is CPU intensive and can take several minutes to calculate. The component does not remember its calculations and will always need to start from scratch, no matter how many times the same parameter has been requested. Every twenty seconds, the component provides a newer, more accurate forecast; so, the same calculation will give the same result during each twenty-second interval.

Different grains and fruits have different seasons, so the user only needs to forecast a few grains or fruits at any specific time during the year, and only a limited number of regions are involved in each forecast. Some of the instance variables in your program are used to hold rainfall forecasts for the different regions. You decide to use lazy updates for each of those instance variables because of their high update costs combined with the twenty-second lifespan.

Listing 14.3 contains a simplified crop-forecasting program to illustrate the principles just discussed.

Listing 14.3 CropForecaster.cs
01: using System; 02: using System.Timers; 03: 04: class WeatherForecaster 05: { 06:     public static double CalculateRainfallDay10RegionA() 07:     { 08:          //Simulated complex calculations takes 5000 milliseconds 09:         System.Threading.Thread.Sleep(5000); 10:         Random randomizer = new Random(); 11:         return (double)randomizer.Next(40,100); 12:     } 13: } 14: 15: class CropForecaster 16: { 17:     private double rainfallDay10RegionA; 18:     private bool rainfallDay10RegionANeedUpdate = true; 19: 20:     public CropForecaster() 21:     { 22:         Timer aTimer = new Timer(); 23:         aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent); 24:         aTimer.Interval = 20000; 25:         aTimer.Enabled = true; 26:     } 27: 28:     public double RainfallDay10RegionA 29:     { 30:         get 31:         { 32:             if (rainfallDay10RegionANeedUpdate) 33:             { 34:                 rainfallDay10RegionA =  graphics/ccc.gifWeatherForecaster.CalculateRainfallDay10RegionA(); 35:                 rainfallDay10RegionANeedUpdate = false; 36:                 return rainfallDay10RegionA; 37:             } 38:             else 39:             { 40:                 return rainfallDay10RegionA; 41:             } 42:         } 43:     } 44: 45:     private double ComplexResultA() 46:     { 47:          //Arbitrary calculation involving lots of calls to rainfallDay10RegionA 48:         return ((RainfallDay10RegionA / 2) + (RainfallDay10RegionA / 3) + 49:             (RainfallDay10RegionA / 4)); 50:     } 51: 52:     private double ComplexResultB() 53:     { 54:          //Arbitrary calculation involving even more calls to rainfallDay10RegionA 55:         return (RainfallDay10RegionA / 10 - 100) + (RainfallDay10RegionA / 100); 56:     } 57:  58:     public double WheatCropSizeInTonsInRegionA () 59:     { 60:          //More arbitrary calculations returning a nice big result 61:         Console.WriteLine("Commencing forecast calculations. Please wait..."); 62:         return (ComplexResultA() / 2 + ComplexResultB() / 4 + 63:             ComplexResultA()) * 100000; 64:     } 65: 66:     public void OnTimedEvent(object source, ElapsedEventArgs e) 67:     { 68:          //This method is currently called automatically every 20 seconds 69:         Console.WriteLine("\n\nNew Update Needed\nPerform  another forecast?"); 70:         rainfallDay10RegionANeedUpdate = true; 71:     } 72: } 73: 74: class ForecastTester 75: { 76:     public static void Main() 77:     { 78:         string answer; 79:         CropForecaster myForecaster = new CropForecaster(); 80: 81:         Console.Write("Would you like to perform a crop forecast? Y)es N)o "); 82:         answer = Console.ReadLine().ToUpper(); 83:         while (!(answer == "N")) 84:         { 85:             Console.WriteLine("Wheat crop size in tons: { 0:N2} ", 86:                 myForecaster.WheatCropSizeInTonsInRegionA()); 87:             Console.Write("Would you like to perform another crop forecast? Y)es N)o  graphics/ccc.gif"); 88:             answer = Console.ReadLine().ToUpper(); 89:         } 90:     } 91: } 

Note

graphics/common.gif

The numbers in the sample output are based on randomly produced values and will vary between different sample outputs.


 Would you like to perform a crop forecast? Y)es N)o Y<enter> Commencing forecast calculations. Please wait... Wheat crop size in tons: 10,389,500.00 Would you like to perform another crop forecast? Y)es N)o Y<enter> Commencing forecast calculations. Please wait... Wheat crop size in tons: 10,389,500.00 Would you like to perform another crop forecast? Y)es N)o Y<enter> Commencing forecast calculations. Please wait... Wheat crop size in tons: 10,389,500.00 Would you like to perform another crop forecast? Y)es N)o New Update Needed Perform another forecast? Y<enter> Commencing forecast calculations. Please wait... Wheat crop size in tons: 13,694,500.00 Would you like to perform another crop forecast? Y)es N)o N<enter> 

Listing 14.3 contains the CropForecaster class (lines 15 72) that, through the WheatCropSizeInTonsInRegionA () method in lines 58 64, provides what pretends to be a forecast of a wheat crop in region A. This method utilizes the results of two other methods ComplexResultA (lines 45 50) and ComplexResultB (lines 52 56). The calculations in all three methods are arbitrary and are not meant to resemble a real forecast; their important role in the program is simply to call the RainfallDay10RegionA property (lines 28 43) a few times, just as their serious counterparts would.

The RainFallDay10RegionA property performs lazy updates and minimizes the times the WeatherForecaster's costly (it pretends to take 5 seconds to perform its calculations, see line 9) method CalculateRainFallDay10RegionA needs to be called. Lazy updates are achieved by the RainFallDay10RegionA in the following fashion: Every 20 seconds, the OnTimedEvent method (lines 66 71) will, regardless of what happens in the rest of the program, automatically be called and executed by the .NET runtime (it has been instructed to do so by lines 22 25, but we don't need to worry about why or how here). As a result, the rainfallDay10RegionANeedUpdate variable is automatically assigned the value true (in line 70) every 20 seconds. This, in turn, allows the get statement block to perform its lazy updates, because it only calls the WeatherForecaster the first time rainfallDay10RegionA is requested after rainfallDay10RegionANeedUpdate has been set to true (rainfallDay10RegionANeedUpdate is set to false in line 35 when RainfallDay10RegionA is accessed).

When you run the program, try to make several forecasts quickly, one after the other. You will see that only the first one is slow and the rest are swift…that is, until the following text is printed onscreen:

 New Update Needed... 

The first calculation after this message will be slow again, and the following ones will again be faster.

You don't need to know the meaning of the code in lines 22 25. You can simply regard it as a special way of instructing the runtime to call the OnTimedEvent (lines 66 71) method automatically every twenty seconds. You can adjust this time interval in line 24, if you want.

By substituting 20000 with 30000, for example, you are telling the runtime to make the call every thirty seconds instead of every twenty seconds. To convince yourself that lines 66 71 are automatically called, you can try not to interact with the program for at least twenty seconds after you have started it. You should see the following text written onscreen every twenty seconds (printed by line 69):

 New Update Needed Perform another forecast? Y<enter> 

No, I haven't forgotten the delayed initialization. Even though the true initialization of rainfallDay10RegionA is performed automatically by C# (it is initially set to 0.0), the first call to RainfallDay10RegionA can be regarded as a delayed initialization, and every call after that can be regarded as a lazy update.


   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net