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

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

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

152 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

Now, ensure your Main() method exercises a Motorcycle object as follows:

static void Main(string[] args)

{

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

// Make a Motorcycle.

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

Console.WriteLine("Rider name is {0}", c.driverName); Console.ReadLine();

}

With this, ponder the output in Figure 5-4.

Figure 5-4. Constructor chaining at work

As you can see, the flow of constructor logic is as follows:

We create our object by invoking the constructor requiring a single int.

This constructor forwards the supplied data to the master constructor and provides any additional startup arguments not specified by the caller.

The master constructor assigns the incoming data to the object’s field data.

Control is returned to the constructor originally called, and executes any remaining code statements.

Great! At this point you are able to define a class with field data (aka member variables) and various members that can be created using any number of constructors. Next up, let’s formalize the role of the static keyword.

Source Code The SimpleClassExample project is included under the Chapter 5 subdirectory.

Understanding the static Keyword

A C# class (or structure) may define any number of static members via the static keyword. When you do so, the member in question must be invoked directly from the class level, rather than from a

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

153

type instance. To illustrate the distinction, consider our good friend System.Console. As you have seen, you do not invoke the WriteLine() method from the object level:

// Error! WriteLine() is not an instance level method!

Console c = new Console(); c.WriteLine("I can't be printed...");

but instead simply prefix the type name to the static WriteLine() member:

// Correct! WriteLine() is a static method.

Console.WriteLine("Thanks...");

Simply put, static members are items that are deemed (by the type designer) to be so commonplace that there is no need to create an instance of the type when invoking the member. While any class (or structure) can define static members, they are most commonly found within “utility classes.” For example, if you were to use the Visual Studio 2008 object browser (via the View Object Browser menu item) and examine the members of System.Console, System.Math, System.Environment, or System.GC (to name a few), you will find that all of their functionality is exposed from static members.

Defining Static Methods (and Fields)

Assume you have a new Console Application project named StaticMethods and have inserted a class named Teenager that defines a static method named Complain(). This method returns a random string, obtained in part by calling a static helper function named GetRandomNumber():

class Teenager

{

public static Random r = new Random();

public static int GetRandomNumber(short upperLimit)

{

return r.Next(upperLimit);

}

public static string Complain()

{

string[] messages = {"Do I have to?", "He started it!", "I'm too tired...", "I hate school!",

"You are sooooooo wrong!"}; return messages[GetRandomNumber(5)];

}

}

Notice that the System.Random member variable and the GetRandomNumber() helper function method have also been declared as static members of the Teenager class, given the rule that static members can operate only on other static members.

Note Allow me to repeat myself: static members can operate only on static data and call static methods of the defining class. If you attempt to make use of nonstatic class data or call a nonstatic method of the class within a static member’s implementation, you’ll receive compile-time errors.

154 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

Like any static member, to call Complain(), prefix the name of the defining class:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Static Methods *****\n");

for(int i =0; i < 5; i++) Console.WriteLine(Teenager.Complain());

Console.ReadLine();

}

Source Code The StaticMethods application is located under the Chapter 5 subdirectory.

Defining Static Data

In addition to static members, a type may also define static field data (such as the Random member variable seen in the previous Teenager class). Understand that when a class defines nonstatic data (properly referred to as instance data), each object of this type maintains an independent copy of the field. For example, assume a class that models a savings account is defined in a new Console Application project named StaticData:

// A simple savings account class. class SavingsAccount

{

public double currBalance;

public SavingsAccount(double balance)

{

currBalance = balance;

}

}

When you create SavingsAccount objects, memory for the currBalance field is allocated for each class instance. Static data, on the other hand, is allocated once and shared among all objects of the same type. To illustrate the usefulness of static data, add a static point of data named currInterestRate to the SavingsAccount class, which is set to a default value of 0.04:

// A simple savings account class. class SavingsAccount

{

public double currBalance;

// A static point of data.

public static double currInterestRate = 0.04;

public SavingsAccount(double balance)

{

currBalance = balance;

}

}

If you were to create three instances of SavingsAccount as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Static Data *****\n");

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

155

Dim s1 As new SavingsAccount(50); Dim s2 As new SavingsAccount(100);

Dim s3 As new SavingsAccount(10000.75); Console.ReadLine();

}

the in-memory data allocation would look something like Figure 5-5.

Savings Account:S1

Figure 5-5. Static data is allocated once and shared among all instances of the class.

Let’s update the SavingsAccount class to define two static methods to get and set the interest rate value:

// A simple savings account class. class SavingsAccount

{

public double currBalance;

// A static point of data.

public static double currInterestRate = 0.04;

public SavingsAccount(double balance)

{

currBalance = balance;

}

// Static members to get/set interest rate.

public static void SetInterestRate(double newRate )

{currInterestRate = newRate; } public static double GetInterestRate()

{return currInterestRate; }

}

Now, observe the following usage and the output in Figure 5-6:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Static Data *****\n");

SavingsAccount s1 = new SavingsAccount(50); SavingsAccount s2 = new SavingsAccount(100);

// Print the current interest rate.

Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate());

156CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

//Make new object, this does NOT 'reset' the interest rate.

SavingsAccount s3 = new SavingsAccount(10000.75); Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate());

Console.ReadLine();

}

Figure 5-6. Static data is allocated only once.

As you can see, when you create new instances of the SavingsAccount class, the value of the static data is not reset, as the CLR will allocate the data into memory exactly one time. After that point, all objects of type SavingsAccount operate on the same value.

As stated, static methods can operate only on static data. However, a nonstatic method can make use of both static and nonstatic data. This should make sense, given that static data is available to all instances of the type. To illustrate, update SavingsAccount with the following instance-level members:

class SavingsAccount

{

public double currBalance;

public static double currInterestRate = 0.04;

// Instance members to get/set interest rate. public void SetInterestRateObj(double newRate)

{currInterestRate = newRate; } public double GetInterestRateObj()

{return currInterestRate; }

...

}

Here, SetInterestRateObj() and GetInterestRateObj() are operating on the same static field as the static SetInterestRate()/GetInterestRate() methods. Thus, if one object were to change the interest rate, all other objects report the same value:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Static Data *****\n");

SavingsAccount.SetInterestRate(0.09); SavingsAccount s1 = new SavingsAccount(50); SavingsAccount s2 = new SavingsAccount(100);

// All three lines print out "Interest Rate is: 0.09"

Console.WriteLine("Interest Rate is: {0}", s1.GetInterestRateObj()); Console.WriteLine("Interest Rate is: {0}", s2.GetInterestRateObj()); Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate()); Console.ReadLine();

}

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

157

In this case, the value 0.09 is returned regardless of which SavingsAccount object we ask (including asking via the static GetInterestRate() method).

Defining Static Constructors

As explained earlier in this chapter, constructors are used to set the value of a type’s data at the time of creation. Thus, if you were to assign a value to a static data member within an instance-level constructor, you may be surprised to find that the value is reset each time you create a new object! For example, assume you have updated the SavingsAccount class as follows:

class SavingsAccount

{

public double currBalance;

public static double currInterestRate;

public SavingsAccount(double balance)

{

currInterestRate = 0.04; currBalance = balance;

}

...

}

If you execute the previous Main() method, notice how the currInterestRate variable is reset each time you create a new SavingsAccount object (see Figure 5-7).

Figure 5-7. Assigning static data in an instance level constructor “resets” the value.

While you are always free to establish the initial value of static data using the member initialization syntax, what if the value for your static data needed to be obtained from a database or external file? To perform such tasks requires a method scope (such as a constructor) to execute the code statements. For this very reason, C# allows you to define a static constructor:

class SavingsAccount

{

public double currBalance;

public static double currInterestRate;

public SavingsAccount(double balance)

{

currBalance = balance;

}

// A static constructor. static SavingsAccount()

{

Console.WriteLine("In static ctor!");

158 CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

currInterestRate = 0.04;

}

...

}

Simply put, a static constructor is a special constructor that is an ideal place to initialize the values of static data when the value is not known at compile time (e.g., you need to read in the value from an external file, generate a random number, etc.). Here are a few points of interest regarding static constructors:

A given class (or structure) may define only a single static constructor.

A static constructor does not take an access modifier and cannot take any parameters.

A static constructor executes exactly one time, regardless of how many objects of the type are created.

The runtime invokes the static constructor when it creates an instance of the class or before accessing the first static member invoked by the caller.

The static constructor executes before any instance-level constructors.

Given this modification, when you create new SavingsAccount objects, the value of the static data is preserved, as the static member is set only one time within the static constructor, regardless of the number of objects created.

Defining Static Classes

Since the release of .NET 2.0, the C# language expanded the scope of the static keyword by introducing static classes. When a class has been defined as static, it is not creatable using the new keyword, and it can contain only members or fields marked with the static keyword (if this is not the case, you receive compiler errors).

At first glance, this might seem like a fairly useless feature, given that a class that cannot be created does not appear all that helpful. However, if you create a class that contains nothing but static members and/or constant data, the class has no need to be allocated in the first place. Consider the following new static class type:

//Static classes can only

//contain static members! static class TimeUtilClass

{

public static void PrintTime()

{Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDate()

{Console.WriteLine(DateTime.Today.ToShortDateString()); }

}

Given that this class has been defined with the static keyword, we cannot create an instance of TimeUtilClass using the new keyword:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Static Data *****\n");

TimeUtilClass.PrintDate();

// Compiler error! Can't create static classes.

TimeUtilClass u = new TimeUtilClass ();

...

}

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

159

Prior to .NET 2.0, the only way to prevent the creation of a class type was to either redefine the default constructor as private or mark the class as an abstract type using the C# abstract keyword (full details regarding abstract types are in Chapter 6):

class TimeUtilClass

{

//Redefine the default ctor as private

//to prevent creation.

private TimeUtilClass (){}

public static void PrintTime()

{Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDate()

{Console.WriteLine(DateTime.Today.ToShortDateString()); }

}

//Define type as abstract to prevent

//creation

abstract class TimeUtilClass

{

public static void PrintTime()

{Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDate()

{Console.WriteLine(DateTime.Today.ToShortDateString()); }

}

While these constructs are still permissible, the use of static classes is a cleaner solution and more type-safe, given that the previous two techniques allowed nonstatic members to appear within the class definition without error.

On a related note, a project’s application object (e.g., the class defining the Main() method) is often defined as a static class, to ensure it only contains static members and cannot be directly created. For example:

// Define the application object as static. static class Program

{

static void Main(string[] args)

{

...

}

}

At this point in the chapter you hopefully feel comfortable defining simple class types containing constructors, fields, and various static (and nonstatic) members. Now that you have the basics under your belt, we can formally investigate the three pillars of object-oriented programming.

Source Code The StaticData project is located under the Chapter 5 subdirectory.

Defining the Pillars of OOP

All object-based languages must contend with three core principals of object-oriented programming, often called the “pillars of object-oriented programming (OOP)”:

160CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

Encapsulation: How does this language hide an object’s internal implementation details and preserve data integrity?

Inheritance: How does this language promote code reuse?

Polymorphism: How does this language let you treat related objects in a similar way?

Before digging into the syntactic details of each pillar, it is important that you understand the basic role of each. Here is an overview of each pillar, which will be examined in full detail over the remainder of this chapter and the next.

The Role of Encapsulation

The first pillar of OOP is called encapsulation. This trait boils down to the language’s ability to hide unnecessary implementation details from the object user. For example, assume you are using a class named DatabaseReader, which has two primary methods: Open() and Close():

//This type encapsulates the details of opening and closing a database.

DatabaseReader dbReader = new DatabaseReader(); dbReader.Open(@"C:\MyCars.mdf");

//Do something with data file and close the file.

dbReader.Close();

The fictitious DatabaseReader class encapsulates the inner details of locating, loading, manipulating, and closing the data file. Object users love encapsulation, as this pillar of OOP keeps programming tasks simpler. There is no need to worry about the numerous lines of code that are working behind the scenes to carry out the work of the DatabaseReader class. All you do is create an instance and send the appropriate messages (e.g., “Open the file named MyCars.mdf located on my C drive”).

Closely related to the notion of encapsulating programming logic is the idea of data hiding. Ideally, an object’s state data should be specified using the private (or possibly protected) keyword. In this way, the outside world must ask politely in order to change or obtain the underlying value. This is a good thing, as publicly declared data points can easily become corrupted (hopefully by accident rather than intent!). You will formally examine this aspect of encapsulation in just a bit.

The Role of Inheritance

The next pillar of OOP, inheritance, boils down to the language’s ability to allow you to build new class definitions based on existing class definitions. In essence, inheritance allows you to extend the behavior of a base (or parent) class by inheriting core functionality into the derived subclass (also called a child class). Figure 5-8 shows a simple example.

You can read the diagram in Figure 5-8 as “A Hexagon is-a Shape that is-an Object.” When you have classes related by this form of inheritance, you establish “is-a” relationships between types. The “is-a” relationship is termed classical inheritance.

Here, you can assume that Shape defines some number of members that are common to all descendents. Given that the Hexagon class extends Shape, it inherits the core functionality defined by Shape and Object, as well as defines additional hexagon-related details of its own (whatever those may be).

Note Under the .NET platform, System.Object is always the topmost parent in any class hierarchy, which defines some bare-bones functionality fully described in Chapter 6.

CHAPTER 5 DEFINING ENCAPSULATED CLASS TYPES

161

Figure 5-8. The “is-a” relationship

There is another form of code reuse in the world of OOP: the containment/delegation model (also known as the “has-a” relationship or aggregation). This form of reuse is not used to establish parent/child relationships. Rather, the “has-a” relationship allows one class to define a member variable of another class and expose its functionality (if required) to the object user indirectly.

For example, assume you are again modeling an automobile. You might want to express the idea that a car “has-a” radio. It would be illogical to attempt to derive the Car class from a Radio, or vice versa (a Car “is-a” Radio? I think not!). Rather, you have two independent classes working together, where the Car class creates and exposes the Radio’s functionality:

class Radio

{

public void Power(bool turnOn)

{

Console.WriteLine("Radio on: {0}", turnOn);

}

}

class Car

{

// Car 'has-a' Radio

private Radio myRadio = new Radio();

public void TurnOnRadio(bool onOff)

{

// Delegate call to inner object. myRadio.Power(onOff);

}

}

Notice that the object user has no clue that the Car class is making use of an inner Radio object.

static void Main(string[] args)

{

// Call is forwarded to Radio internally.

Car viper = new Car(); viper.TurnOnRadio(false);

}