Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
71
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

142 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

Figure 5-1. Inserting a new C# class type

Notice that these member variables are declared using the public access modifier. Public members of a class are directly accessible once an object of this type has been created. As you may already know, the term “object” is used to represent an instance of a given class type created using the new keyword.

Note Field data of a class should seldom (if ever) be defined as public. To preserve the integrity of your state data, it is a far better design to define data as private (or possibly protected) and allow controlled access to the data via type properties (as shown later in this chapter). However, to keep this first example as simple as possible, public data fits the bill.

After you have defined the set of member variables that represent the state of the type, the next design step is to establish the members that model its behavior. For this example, the Car class will define one method named SpeedUp() and another named PrintState():

class Car

{

//The 'state' of the Car. public string petName; public int currSpeed;

//The functionality of the Car. public void PrintState()

{

Console.WriteLine("{0} is going {1} MPH.", petName, currSpeed);

}

public void SpeedUp(int delta)

{

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

143

currSpeed += delta;

}

}

As you can see, PrintState() is more or less a diagnostic function that will simply dump the current state of a given Car object to the command window. SpeedUp() will increase the speed of the Car by the amount specified by the incoming int parameter. Now, update your Main() method with the following code:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Class Types *****\n");

//Allocate and configure a Car object.

Car myCar = new Car(); myCar.petName = "Henry"; myCar.currSpeed = 10;

//Speed up the car a few times and print out the

//new state.

for (int i = 0; i <= 10; i++)

{

myCar.SpeedUp(5);

myCar.PrintState();

}

Console.ReadLine();

}

Once you run your program, you will see that the Car object (myCar) maintains its current state throughout the life of the application, as shown in Figure 5-2.

Figure 5-2. Taking the Car for a test drive (pun intended)

Allocating Objects with the new Keyword

As shown in the previous code example, objects must be allocated into memory using the new keyword. If you do not make use of the new keyword and attempt to make use of your class variable in a subsequent code statement, you will receive a compiler error:

static void Main(string[] args)

{

// Error! Forgot to use 'new'!

Car myCar;

144 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

myCar.petName = "Fred";

}

To correctly create a class type variable, you may define and allocate a Car object on a single line of code:

static void Main(string[] args)

{

Car myCar = new Car(); myCar.petName = "Fred";

}

As an alternative, if you wish to define and allocate an object on separate lines of code, you may do so as follows:

static void Main(string[] args)

{

Car myCar;

myCar = new Car(); myCar.petName = "Fred";

}

Here, the first code statement simply declares a reference to a yet-to-be-determined Car object. It is not until you assign a reference to an object via the new keyword that this reference points to a valid class instance in memory.

In any case, at this point we have a trivial class type that defines a few points of data and some basic methods. To enhance the functionality of the current Car type, we need to understand the role of class constructors.

Understanding Class Constructors

Given that objects have state (represented by the values of an object’s member variables), the object user will typically want to assign relevant values to the object’s field data before use. Currently, the Car type demands that the petName and currSpeed fields be assigned on a field-by-field basis. For the current example, this is not too problematic, given that we have only two public data points. However, it is not uncommon for a class to have dozens of fields to contend with. Clearly, it would be undesirable to author 20 initialization statements to set 20 points of data.

Thankfully, C# supports the use of class constructors, which allow the state of an object to be established at the time of creation. A constructor is a special method of a class that is called indirectly when creating an object using the new keyword. However, unlike a “normal” method, constructors never have a return value (not even void) and are always named identically to the class they are constructing.

Note As shown in Chapter 13, C# 2008 provides a new object initialization syntax, which allows you to set the values of public fields and invoke public properties at the time of construction.

The Role of the Default Constructor

Every C# class is provided with a freebee default constructor that you may redefine if need be. By definition, a default constructor never takes arguments. Beyond allocating the new object into memory, the default constructor ensures that all field data is set to an appropriate default value (see Chapter 3 for information regarding the default values of C# data types).

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

145

If you are not satisfied with these default assignments, you may redefine the default constructor to suit your needs. To illustrate, update your C# Car class as follows:

class Car

{

//The 'state' of the Car. public string petName; public int currSpeed;

//A custom default constructor. public Car()

{

petName = "Chuck"; currSpeed = 10;

}

...

}

In this case, we are forcing all Car objects to begin life named Chuck at a rate of 10 mph. With this, you are able to create a Car object set to these default values as follows:

static void Main(string[] args)

{

//Invoking the default constructor.

Car chuck = new Car();

//Prints "Chuck is going 10 MPH." chuck.PrintState();

}

Defining Custom Constructors

Typically, classes define additional constructors beyond the default. In doing so, you provide the object user with a simple and consistent way to initialize the state of an object directly at the time of creation. Ponder the following update to the Car class, which now supports a total of three class constructors:

class Car

{

//The 'state' of the Car. public string petName; public int currSpeed;

//A custom default constructor. public Car()

{

petName = "Chuck"; currSpeed = 10;

}

//Here, currSpeed will receive the

//default value of an int (zero). public Car(string pn)

{

petName = pn;

}

146CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

//Let caller set the full 'state' of the Car. public Car(string pn, int cs)

{

petName = pn; currSpeed = cs;

}

...

}

Keep in mind that what makes one constructor different from another (in the eyes of the C# compiler) is the number of and type of constructor arguments. Recall from Chapter 4, when you define a method of the same name that differs by the number or type of arguments, you have overloaded the method. Thus, the Car type has overloaded the constructor to provide a number of ways to create the object at the time of declaration. In any case, you are now able to create Car objects using any of the public constructors. For example:

static void Main(string[] args)

{

//Make a Car called Chuck going 10 MPH.

Car chuck = new Car(); chuck.PrintState();

//Make a Car called Mary going 0 MPH.

Car mary = new Car("Mary"); mary.PrintState();

//Make a Car called Daisy going 75 MPH.

Car daisy = new Car("Daisy", 75); daisy.PrintState();

}

The Default Constructor Revisited

As you have just learned, all classes are endowed with a free default constructor. Thus, if you insert a new class into your current project named Motorcycle, defined like so:

class Motorcycle

{

public void PopAWheely()

{

Console.WriteLine("Yeeeeeee Haaaaaeewww!");

}

}

you are able to create an instance of the Motorcycle type via the default constructor out of the box:

static void Main(string[] args)

{

Motorcycle mc = new Motorcycle(); mc.PopAWheely();

}

However, as soon as you define a custom constructor, the default constructor is silently removed from the class and is no longer available! Think of it this way: if you do not define a custom constructor, the C# compiler grants you a default in order to allow the object user to allocate an instance of your type with field data set to the correct default values. However, when you define

a unique constructor, the compiler assumes you have taken matters into your own hands.

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

147

Therefore, if you wish to allow the object user to create an instance of your type with the default constructor, as well as your custom constructor, you must explicitly redefine the default. To this end, understand that in a vast majority of cases, the implementation of the default constructor of a class is intentionally empty, as all you require is the ability to create an object with default values. Consider the following update to the Motorcycle class:

class Motorcycle

{

public int driverIntensity;

public void PopAWheely()

{

for (int i = 0; i <= driverIntensity; i++)

{

Console.WriteLine("Yeeeeeee Haaaaaeewww!");

}

}

//Put back the default constructor. public Motorcycle() {}

//Our custom constructor.

public Motorcycle(int intensity) { driverIntensity = intensity; }

}

The Role of the this Keyword

Like other C-based languages, C# supplies a this keyword that provides access to the current class instance. One possible use of the this keyword is to resolve scope ambiguity, which can arise when an incoming parameter is named identically to a data field of the type. Of course, ideally you would simply adopt a naming convention that does not result in such ambiguity; however, to illustrate this use of the this keyword, update your Motorcycle class with a new string field (named name) to represent the driver’s name. Next, add a method named SetDriverName() implemented as follows:

class Motorcycle

{

public int driverIntensity; public string name;

public void SetDriverName(string name) { name = name; }

...

}

Although this code will compile just fine, if you update Main() to call SetDriverName() and then print out the value of the name field, you may be surprised to find that the value of the name field is an empty string!

// Make a Motorcycle with a rider named Tiny?

Motorcycle c = new Motorcycle(5); c.SetDriverName("Tiny"); c.PopAWheely();

Console.WriteLine("Rider name is {0}", c.name); // Prints an empty name value!

148 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

The problem is that the implementation of SetDriverName() is assigning the incoming parameter back to itself given that the compiler assumes name is referring to the variable currently in the method scope rather than the name field at the class scope. To inform the compiler that you wish to set the current object’s name data field to the incoming name parameter, simply use this to resolve the ambiguity:

public void SetDriverName(string name) { this.name = name; }

Do understand that if there is no ambiguity, you are not required to make use of the this keyword when a class wishes to access its own data or members. For example, if we rename the string data member to driverName, the use of this is optional as there is no longer a scope ambiguity:

class Motorcycle

{

public int driverIntensity; public string driverName;

public void SetDriverName(string name)

{

// These two statements are functionally the same. driverName = name;

this.driverName = name;

}

...

}

Even though there is little to be gained when using this in unambiguous situations, you may still find this keyword useful when implementing members, as IDEs such as SharpDevelop and Visual Studio 2008 will enable IntelliSense when this is specified. This can be very helpful when you have forgotten the name of a class member and want to quickly recall the definition. Consider Figure 5-3.

Figure 5-3. The IntelliSense of this

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

149

Note It is a compiler error to use the this keyword within the implementation of a static member (explained shortly). As you will see, static members operate on the class (not object) level, and therefore at the class level, there is no current object (thus no this)!

Chaining Constructor Calls Using this

Another use of the this keyword is to design a class using a technique termed constructor chaining. This design pattern is helpful when you have a class that defines multiple constructors. Given the fact that constructors often validate the incoming arguments to enforce various business rules, it can be quite common to find redundant validation logic within a class’s constructor set. Consider the following updated Motorcycle:

class Motorcycle

{

public int driverIntensity; public string driverName;

public Motorcycle() { }

// Redundent constructor logic! public Motorcycle(int intensity)

{

if (intensity > 10)

{

intensity = 10;

}

driverIntensity = intensity;

}

public Motorcycle(int intensity, string name)

{

if (intensity > 10)

{

intensity = 10;

}

driverIntensity = intensity; driverName = name;

}

...

}

Here (perhaps in an attempt to ensure the safety of the rider), each constructor is ensuring that the intensity level is never greater than 10. While this is all well and good, we do have redundant code statements in two constructors. This is less than ideal, as we are now required to update code in multiple locations if our rules change (for example, if the intensity should not be greater than 5).

One way to improve the current situation is to define a method in the Motorcycle class that will validate the incoming argument(s). If we were to do so, each constructor could make a call to this method before making the field assignment(s). While this approach does allow us to isolate the code we need to update when the business rules change, we are now dealing with the following redundancy:

150CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

class Motorcycle

{

public int driverIntensity; public string driverName;

// Constructors. public Motorcycle() { }

public Motorcycle(int intensity)

{

SetIntensity(intensity);

}

public Motorcycle(int intensity, string name)

{

SetIntensity(intensity); driverName = name;

}

public void SetIntensity(int intensity)

{

if (intensity > 10)

{

intensity = 10;

}

driverIntensity = intensity;

}

...

}

A cleaner approach is to designate the constructor that takes the greatest number of arguments as the “master constructor” and have its implementation perform the required validation logic. The remaining constructors can make use of the this keyword to forward the incoming arguments to the master constructor and provide any additional parameters as necessary. In this way, we only need to worry about maintaining a single constructor for the entire class, while the remaining constructors are basically empty.

Here is the final iteration of the Motorcycle class (with one additional constructor for the sake of illustration). When chaining constructors, note how the this keyword is “dangling” off the constructor’s declaration (via a colon operator) outside the scope of the constructor itself:

class Motorcycle

{

public int driverIntensity; public string driverName;

//Constructor chaining. public Motorcycle() {}

public Motorcycle(int intensity)

:this(intensity, "") {} public Motorcycle(string name)

:this(0, name) {}

//This is the 'master' constructor that does all the real work. public Motorcycle(int intensity, string name)

{

if (intensity > 10)

{

intensity = 10;

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

151

}

driverIntensity = intensity; driverName = name;

}

...

}

Understand that using the this keyword to chain constructor calls is never mandatory. However, when you make use of this technique, you do tend to end up with a more maintainable and concise class definition. Again, using this technique you can simplify your programming tasks, as the real work is delegated to a single constructor (typically the constructor that has the most parameters), while the other constructors simply “pass the buck.”

Observing Constructor Flow

On a final note, do know that once a constructor passes arguments to the designated master constructor (and that constructor has processed the data), the constructor invoked originally by the caller will finish executing any remaining code statements. To clarify, update each of the constructors of the Motorcycle class with a fitting call to Console.WriteLine():

class Motorcycle

{

public int driverIntensity; public string driverName;

// Constructor chaining. public Motorcycle()

{

Console.WriteLine("In default ctor");

}

public Motorcycle(int intensity) : this(intensity, "")

{

Console.WriteLine("In ctor taking an int");

}

public Motorcycle(string name) : this(0, name)

{

Console.WriteLine("In ctor taking a string");

}

// This is the 'master' constructor that does all the real work. public Motorcycle(int intensity, string name)

{

Console.WriteLine("In master ctor "); if (intensity > 10)

{

intensity = 10;

}

driverIntensity = intensity; driverName = name;

}

...

}