Core C# and .NET
< Day Day Up > |
Constants, fields, and properties are the members of a class that maintain the content or state of the class. As a rule of thumb, use constants for values that will never change; use fields to maintain private data within a class; and use properties to control access to data in a class. Let's now look at the details of these three class members. Constants
C# uses the const keyword to declare variables that have a fixed, unalterable value. Listing 3-1 provides an example of using constants. Although simple, the code illustrates several basic rules for defining and accessing constants. Listing 3-1. Constants
using System; class Conversions { public const double Cm = 2.54; public const double Grams = 454.0 , km = .62 ; public const string ProjectName = "Metrics"; } class ShowConversions { static void Main() { double pounds, gramWeight; gramWeight = 1362; pounds = gramWeight / Conversions.Grams; Console.WriteLine( "{0} Grams= {1} Pounds", gramWeight,pounds); Console.WriteLine("Cm per inch {0}", Conversions.Cm); Conversions c= new Conversions(); // Create class // instance // This fails to compile. Cannot access const from object Console.WriteLine("Cm per inch {0}", c.Cm); } }
The most important thing to recognize about a constant is that its value is determined at compile time. This can have important ramifications. For example, suppose the Furniture class in Figure 3-1 is contained in a DLL that is used by other assemblies as a source for the sales tax rate. If the rate changes, it would seem logical that assigning the new value to SalesTax and recompiling the DLL would then make the new value to external assemblies. However, the way .NET handles constants requires that all assemblies accessing the DLL must also be recompiled. The problem is that const types are evaluated at compile time. When any calling routine is compiled against the DLL, the compiler locates all constant values in the DLL's metadata and hardcodes them in the executable code of the calling routine. This value is not changed until the calling routine is recompiled and reloads the value from the DLL. In cases such as tax, where a value is subject to change, it's preferable to define the value as a readonly field sometimes referred to as a runtime constant. Core Approach
Fields
A field is also used to store data within a class. It differs from a const in two significant ways: Its value is determined at runtime, and its type is not restricted to primitives. Field Modifiers
In addition to the access modifiers, fields have two additional modifiers: static and readonly (see Table 3-3).
As a rule of thumb, fields should be defined with the private attribute to ensure that the state of an object is safe from outside manipulation. Methods and properties should then be used to retrieve and set the private data if outside access is required. Core Note
There is one case where setting a field to public makes sense: when your program requires a global constant value. By declaring a field to be public static readonly, you can create a runtime constant. For example, this declaration in Figure 3-1: const double salesTax = .065; can be replaced with a field public static readonly double SalesTax = .065;
A method then references this field as Furniture.SalesTax, and the readonly modifier ensures the value cannot be changed. Note that if you create an instance of this class, you cannot access salesTax as a member of that instance. An attempt to do so results in the compile error "static member cannot be accessed with an instance reference". Furniture chair = new Furniture("Broyhill","12422",225.00); double itemTax = chair.SalesTax; // Raises an error
Using Static Read-Only Fields to Reference Class Instances
Static readonly fields can be used to represent groups of related constant data by declaring them a reference type such as a class instance or array. This is of use when the data can be represented by a limited number of objects with unchanging values. This example presents the interesting concept of fields defined as instances of their containing class. The static modifier makes this possible, because it designates the field as part of the class and not its instances. Note that the private constructor prevents clients outside the scope of the class from creating new class instances. Thus, only those objects exposed by the fields are available outside the class. The class also contains two instance fields, yardPrice and deliveryWeeks, that are declared as private. Access to them is controlled though a public method and property: Upholstery.silk.FabCost(10); // Value from method Upholstery.silk.DeliveryTime; // Value from property
Listing 3-2. Using Static Read-Only Fields as Reference Types
public class Upholstery { // fields to contain price and delivery time for fabrics public static readonly Upholstery silk = new Upholstery(15.00, 8); public static readonly Upholstery wool = new Upholstery(12.00, 6); public static readonly Upholstery cotton = new Upholstery(9.00, 6); private double yardPrice; private int deliveryWeeks; // constructor - set price per yard and delivery time // private modifier prevents external class instantiation private Upholstery ( double yrPrice, int delWeeks) { yardPrice = yrPrice; deliveryWeeks = delWeeks; } // method to return total cost of fabric public double FabCost(double yards) { return yards * this.yardPrice; } // property to return delivery time public int DeliveryTime {get { return deliveryWeeks;}} // property to return price per yard public double PricePerYard {get {return yardPrice;}} }
Properties
A property is used to control read and write access to values within a class. Java and C++ programmers create properties by writing an accessor method to retrieve field data and a mutator method to set it. Unlike these languages, the C# compiler actually recognizes a special property construct and provides a simplified syntax for creating and accessing data. In truth, the syntax is not a whole lot different than a comparable C++ implementation, but it does allow the compiler to generate more efficient code. Syntax: [attributes] <modifier> <data type> <property name> { [access modifier] get { ... return(propertyvalue) } [access modifier] set { ... Code to set a field to the keyword value } }
Note:
Listing 3-3. Creating and Accessing a Property
public class Upholstery { private double yardPrice; // Property to return or set the price public double PricePerYard { get {return yardPrice;} // Returns a property value set { // Sets a property value if ( value <= 0 ) throw new ArgumentOutOfRangeException( "Price must be greater than 0."); yardPrice = value } } ... } The syntax for accessing the property of a class instance is the same as for a field: // fabObj is instance of Upholstery class double fabricPrice = fabObj.PricePerYard; fabObj.PricePerYard = 12.50D; The get block of code serves as a traditional accessor method and the set block as a mutator method. Only one is required. Leave out the get block to make the property write-only or the set block to make it read-only. All return statements in the body of a get block must specify an expression that is implicitly convertible to the property type. In this example, the code in the set block checks to ensure that the property is set to a value greater than 0. This capability to check for invalid data is a major argument in favor of encapsulating data in a property. If you were to examine the underlying code generated for this example, you would find that C# actually creates a method for each get or set block. These names are created by adding the prefix get or set to the property name for example, get_PricePerYard. In the unlikely case you attempt to create a method with the same name as the internal one, you will receive a compile-time error. The use of properties is not necessarily any less efficient than exposing fields directly. For a non-virtual property that contains only a small amount of code, the JIT (Just-in-Time) compiler may replace calls to the accessor methods with the actual code contained in the get or set block. This process, known as inlining, reduces the overhead of making calls at runtime. The result is code that is as efficient as that for fields, but much more flexible. Indexers
An indexer is often referred to as a parameterized property. Like a property, it is declared within a class, and its body may contain get and set accessors that share the same syntax as property accessors. However, an indexer differs from a property in two significant ways: It accepts one or more parameters, and the keyword this is used as its name. Here is the formal syntax: Syntax: [attributes] <modifier><return type> this[parameter(s)] {
Example: public int this [int ndx] {
Note: The static modifier is not supported because indexers work only with instances. In a nutshell, the indexer provides a way to access a collection of values maintained within a single class instance. The parameters passed to the indexer are used as a single- or multi-dimensional index to the collection. The example in Listing 3-4 should clarify the concept. Listing 3-4. Using Indexer to Expose an Array of Objects
using System; using System.Collections; // Namespace containing ArrayList public class Upholstery { // Class to represent upholstery fabric private double yardPrice; private int deliveryWeeks; private string fabName; // Constructor public Upholstery (double price, int delivery, string fabric) { this.yardPrice = price; this.deliveryWeeks = delivery; this.fabName = fabric; } // Three readonly properties to return Fabric information public int DeliveryTime {get {return deliveryWeeks;}} public double PricePerYard {get {return yardPrice;}} public string FabricName {get {return fabName;}} } public class Fabrics { // Array to hold list of objects private ArrayList fabricArray = new ArrayList(); // Indexer to add or return an object from the array public Upholstery this[int ndx] { get { if(!(ndx<0 || ndx > fabricArray.Count-1)) return (Upholstery)fabricArray[ndx]; // Return empty object else return(new Upholstery(0,0,"")); } set { fabricArray.Insert(ndx, value);} } } public class IndexerApp { public static void Main() { Fabrics sofaFabric = new Fabrics(); // Use Indexer to create array of Objects sofaFabric[0] = new Upholstery(15.00, 8, "Silk"); sofaFabric[1] = new Upholstery(12.00, 6, "Wool"); sofaFabric[2] = new Upholstery(9.00, 6, "Cotton"); // Next statement prints "Fabric: Silk" Console.WriteLine("Fabric: {0} ", sofaFabric[0].FabricName); } }
The Fabrics class contains an indexer that uses the get and set accessors to control access to an internal array of Upholstery objects. A single instance of the Fabrics class is created and assigned to sofaFabric. The indexer allows the internal array to be directly accessed by an index parameter passed to the object: Fabrics sofaFabric = new Fabrics(); // Use Indexer to create array of Objects sofaFabric[0] = new Upholstery(15.00, 8, "Silk"); sofaFabric[1] = new Upholstery(12.00, 6, "Wool");
The advantage of using an indexer is that it hides the array handling details from the client, and it can also perform any validation checking or data modification before returning a value. Indexers are best used with objects whose properties can be represented as a collection, rather than a scalar value. In this example, the various fabrics available for sofas form a collection. We could also use the indexer to create a collection of fabrics used for curtains: Fabrics curtainFabric = new Fabrics(); curtainFabric[0] = new Upholstery(11.00, 4, "Cotton"); curtainFabric[1] = new Upholstery(7.00, 5, "Rayon");
|
< Day Day Up > |