Chapter 4. Using Classes and Objects in C#

What Do We Use Classes For?

All C# programs are composed of classes. The Windows forms we have just seen are classes, derived from the basic Form class, and all the other programs we will be writing are made up exclusively of classes. C# does not have the concept of global data modules or shared data that are not part of classes.

Simply put, a class is a set of public and private methods and private data grouped inside named logical units. Usually, we write each class in a separate file, although this is not a hard and fast rule. We have already seen that these Windows forms are classes, and in this chapter we will see how we can create other useful classes.

When you create a class, it is not a single entity but a master from which you can create copies, or instances, using the new keyword. When we create these instances, we pass some initializing data into the class using its constructor. A constructor is a method that has the same name as the class name, has no return type and can have zero or more parameters that get passed into each instance of the class. We call each of these instances an object. In the sections that follow we’ll create some simple programs and use some instances of classes to simplify them.

A Simple Temperature Conversion Program

Suppose we wanted to write a visual program to convert temperatures between the Celsius and Fahrenheit temperature scales. You may remember that water freezes at 0° on the Celsius scale and boils at 100°C, whereas water freezes at 32° on the Fahrenheit scale and boils at 212°F. From these numbers you can quickly deduce the conversion formula that you may have forgotten.

The difference between freezing and boiling on one scale is 100 and 180 degrees on the other, or 100/180 or 5/9. The Fahrenheit scale is “offset” by 32, since water freezes at 32 on its scale. Thus,

image

and

image

In our visual program, we’ll allow the user to enter a temperature and select the scale on which to convert it, as in Figure 4-1.

Figure 4-1. Converting 35° Celsius to 95° Fahrenheit with our visual interface

image

Using the visual builder provided in Visual Studio.NET, we can draw the user interface in a few seconds and simply implement routines to be called when the two buttons are pressed. If we double-click on the Compute button, the program generates the btConvert_Click method. You can fill it in to have it convert the values between temperature scales.


private void> btCompute_Click(object sender,
              System.EventArgs e) {
       float temp, newTemp;
       //convert string to input value
       temp = Convert.ToSingle (txEntry.Text );
       //see which scale to convert to
       if(opFahr.Checked)
              newTemp = 9*temp/5 + 32;
       else
              newTemp = 5*(temp-32)/9;
       //put result in label text
       lbResult.Text =newTemp.ToString ();
       txEntry.Text ="";   //clear entry field
}

The preceding program is extremely straightforward and easy to understand, and it is typical of how some simple C# programs operate. However, it has some disadvantages that we might want to improve on.

The most significant problem is that the user interface and the data handling are combined in a single program module rather than handled separately. It is usually a good idea to keep the data manipulation and the interface manipulation separate so that changing interface logic doesn’t impact the computation logic and vice-versa.

Building a Temperature Class

A class in C# is a module that can contain both public and private functions and subroutines, and can hold private data values as well. These functions and subroutines in a class are frequently referred to collectively as methods.

Class modules allow you to keep a set of data values in a single named place and fetch those values using get and set functions, which we then refer to as accessor methods.

You create a class module from the C# integrated development environment (IDE) using the menu item Project | Add Class module. When you specify a filename for each new class, the IDE assigns this name as the class name as well and generates an empty class with an empty constructor. For example, if we wanted to create a Temperature class, the IDE would generate the following code for us.


namespace CalcTemp
{
       /// <summary>
       /// Summary description for Temperature
       /// </summary>
       public class Temperature
       {
             public Temperature()
             {
                    //
                    // TODO: Add constructor logic here
                    //
             }
       }
}

If you fill in the “summary description” special comment, that text will appear whenever your mouse hovers over an instance of that class. Note that the system generates the class and a blank constructor. If your class needs a constructor with parameters, you can just edit the code.

Next we want to move all of the computation and conversion between temperature scales into this new Temperature class. One way to design this class is to rewrite the calling programs that will use the class module first. In the code sample below, we create an instance of the Temperature class and use it to do whatever conversions are needed.


private void btCompute_Click(object sender, System.EventArgs e) {
       string newTemp;
       //use input value to create instance of class
       Temperature temp = new Temperature (txEntry.Text );
       //use radio button to decide which conversion
       newTemp = temp.getConvTemp (opCels.Checked );

       //get result and put in label text
       lbResult.Text =newTemp.ToString ();
       txEntry.Text ="";    //clear entry field
}

The actual class is shown following. Note that we put the string value of the input temperature into the class in the constructor and that inside the class it gets converted to a float. We do not need to know how the data are represented internally, and we could change that internal representation at any time.


public class Temperature   {
       private float temp, newTemp;
       //-------------
       //constructor for class
       public Temperature(string thisTemp)            {
             temp = Convert.ToSingle(thisTemp);
       }
       //-------------
       public string getConvTemp(bool celsius){
       if (celsius)
              return getCels();
       else
              return getFahr();
       }
       //-------------
       private string getCels() {
              newTemp= 5*(temp-32)/9;
              return newTemp.ToString() ;
       }
       //-------------
       private string getFahr() {
              newTemp = 9*temp/5 + 32;
              return Convert.ToString(newTemp) ;
       }
}

Note that the temperature variable temp is declared as private so it cannot be “seen” or accessed from outside the class. You can only put data into the class and get it back out using the constructor and the getConvTemp method. The main point to this code rearrangement is that the outer calling program does not have to know how the data are stored and how they are retrieved; that is only known inside the class.

The other important feature of the class is that it actually holds data. You can put data into it and it will return it at any later time. This class only holds the one temperature value, but classes can contain quite complex sets of data values. This is known as encapsulation.

We could easily modify this class to get temperature values out in other scales, still without requiring that the user of the class know anything about how the data are stored or how the conversions are performed.

Converting to Kelvin

Absolute zero on the Celsius scale is defined as –273.16°. This is the coldest possible temperature, since it is the point at which all molecular motion stops. The Kelvin scale is based on absolute zero, but the degrees are the same size as Celsius degrees. We can add the following function.


public string getKelvin() {
       newTemp = Convert.ToString (getCels() + 273.16)
}

What would the setKelvin method look like?

Putting the Decisions into the Temperature Class

At this point, we are still making decisions within the user interface about which methods of the Temperature class to use. It would be even better if all that complexity could disappear into the Temperature class. It would be nice if we just could write our Conversion button click method as follows.


private void btCompute_Click(object sender, System.EventArgs e) {
       Temperature temper =
              new Temperature(txEntry.Text , opCels.Checked);
       //put result in label text
       lbResult.Text = temper.getConvTemp();
       txEntry.Text ="";   //clear entry field
}

This removes the decision-making process to the Temperature class and reduces the calling interface program to just two lines of code.

The class that handles all this becomes somewhat more complex, however, but it then keeps track of what data have been passed in and what conversion must be done. We pass in the data and the state of the radio button in the constructor.


public Temperature(string sTemp, bool toCels)       {
       temp = Convert.ToSingle (sTemp);
       celsius =  toCels;
}

Now, the Celsius Boolean tells the class whether to convert and whether conversion is required on fetching the temperature value. The output routine is simply this.


public string getConvTemp(){
       if (celsius)
              return getCels();
       else
              return getFahr();
}
//-------------
private string getCels() {
       newTemp= 5*(temp-32)/9;
       return newTemp.ToString() ;
}
//-------------
private string getFahr() {
       newTemp = 9*temp/5 + 32;
       return Convert.ToString(newTemp) ;
}

In this class we have both public and private methods. The public ones are callable from other modules, such as the user interface form module. The private ones, getCels and getFahr, are used internally and operate on the temperature variable.

Note that we now also have the opportunity to return the output temperature as either a string or a single floating point value, and we could thus vary the output format as needed.

Using Classes for Format and Value Conversion

It is convenient in many cases to have a method for converting between formats and representations of data. You can use a class to handle and hide the details of such conversions. For example, you might design a program where you can enter an elapsed time in minutes and seconds with or without the colon.


315.20
3:15.20
315.2

Since all styles are likely, you’d like a class to parse the legal possibilities and keep the data in a standard format within. Figure 4-2 shows how the entries “112” and “102.3” are parsed.

Figure 4-2. A simple parsing program that uses the Times class

image

Much of the parsing work takes place in the constructor for the class. Parsing depends primarily on looking for a colon. If there is no colon, then values greater than 99 are treated as minutes.


public FormatTime(string entry)          {
errflag = false;
if (! testCharVals(entry)) {
       int i = entry.IndexOf (":");
       if (i >= 0 ) {
              mins = Convert.ToInt32 (entry.Substring (0, i));
              secs = Convert.ToSingle (entry.Substring (i+1));
              if(secs >= 60.0F ) {
                     errflag = true;
                     t = NT;
              }
              t = mins *100 + secs;
       }
       else {
              float fmins = Convert.ToSingle (entry) / 100;
              mins = (int)fmins;
              secs = Convert.ToSingle (entry) - 100 * mins;
              if (secs >= 60) {
                     errflag = true;
                     t = NT;
              }
              else
                     t = Convert.ToSingle(entry);
                }
              }
}

Since illegal time values might also be entered, we test for cases like 89.22 and set an error flag. (Remember that there are only 60 seconds in a minute.)

Depending on the kind of time measurements these represent, you might also have some non-numeric entries such as NT for no time, or in the case of athletic times, SC for scratch or DQ for disqualified. All of these are best managed inside the class. Thus, you never need to know what numeric representations of these values are used internally.


static public int NT = 10000;
static public int DQ = 20000;

Some of these are processed in the code represented by Figure 4-3.

Figure 4-3. The time entry interface, showing the parsing of symbols for Scratch, Disqualification, and No Time

image

Handling Unreasonable Values

A class is also a good place to encapsulate error handling. For example, it might be that times greater than some threshold value are unlikely, and they could be times that were entered without a decimal point. If large times are unlikely, then a number such as 123473 could be assumed to be 12:34.73.


public void setSingle(float tm) {
       t = tm;
       if((tm > minVal) && (tm < NT)) {
              t = tm / 100.0f;
       }
}

The cutoff value minVal may vary with the domain of times being considered and thus should be a variable. You can also use the class constructor to set up default values for variables.


public class FormatTime {
  public FormatTime(string entry)       {
       errflag = false;
       minVal = 1000;
       t = 0;

A String Tokenizer Class

A number of languages provide a simple method for separating strings into tokens, where each token is separated by a specified character. While C# does not exactly provide a class for this feature, we can write one quite easily using the Split method of the String class. The goal of the Tokenizer class will be to pass in a string and obtain the successive string tokens back one at a time. For example, if we had the simple string


Now is the time

our tokenizer should return the following four tokens.


Now
is
the
time

The critical part of this class is that it holds the initial string and remembers which token is to be returned next.

We use the Split function, which approximates the Tokenizer but returns an array of substrings instead of having an object interface. The class we want to write will have a nextToken method that returns string tokens or a zero length string when we reach the end of the series of tokens. This is the entire class.


//String Tokenizer class
public class StringTokenizer      {
  private string data, delimiter;
  private string[] tokens; //token array
  private int index;              //index to next token
//----------
public StringTokenizer(string dataLine)                {
       init(dataLine, " ");
}
//----------
//sets up initial values and splits string
private void init(String dataLine, string delim) {
       delimiter = delim;
       data = dataLine;
       tokens = data.Split (delimiter.ToCharArray() );
       index = 0;
}
//----------
public StringTokenizer(string dataLine, string delim) {
       init(dataLine, delim);
}
//----------
public bool hasMoreElements() {
       return (index < (tokens.Length));
}
//-----------
public string nextElement() {
       //get the next token
       if( index > tokens.Length )
              return tokens[index++];
       else
              return "";    //or none
       }
}

The class is illustrated in use in Figure 4-4.

Figure 4-4. The Tokenizer in use

image

The code that uses the Tokenizer class is just this.


//call tokenizer when button is clicked
private void btToken_Click(object sender,
             System.EventArgs e) {
       StringTokenizer tok =
             new StringTokenizer (txEntry.Text );
       while(tok.hasMoreElements () ) {
             lsTokens.Items.Add (tok.nextElement());
       }
  }

Classes as Objects

The primary difference between ordinary procedural programming and object-oriented (OO) programming is the presence of classes. A class is just a module, as we have previously shown, which has both public and private methods and that can contain data. However, classes are also unique in that there can be any number of instances of a class, each containing different data. We frequently refer to these instances as objects. We’ll see some examples of single and multiple instances later.

Suppose we have a file of results from a swimming event stored in a text data file. Such a file might look, in part, like this, where the columns represent place, names, age, club, and time.

image

If we wrote a program to display these swimmers and their times, we’d need to read in and parse this file. For each swimmer, we’d have a first and last name, an age, a club, and a time. An efficient way to group together the data for each swimmer is to design a Swimmer class and create an instance for each swimmer.

Here is how we read the file and create these instances. As each instance is created, we add it into an ArrayList object.


private void init() {
       ar = new ArrayList ();     //create array list
       csFile fl = new csFile ("500free.txt");
       //read in liens
       string s =  fl.readLine ();
       while (s != null) {
              //convert to tokens in swimmer object
              Swimmer swm = new Swimmer(s);
              ar.Add (swm);
              s= fl.readLine ();
       }
       fl.close();
       //add names to list box
       for(int i=0; i < ar.Count ; i++) {
              Swimmer swm = (Swimmer)ar[i];
              lsSwimmers.Items.Add (swm.getName ());
       }
}

The Swimmer class itself parses each line of data from the file and stores it for retrieval using the getXxx accessor functions.


public class Swimmer {
       private string frName, lName;
       private string club;
       private int age;
       private int place;
       private FormatTime tms;
//-----------
public Swimmer(String dataLine) {
       StringTokenizer tok = new StringTokenizer (dataLine);
       place =  Convert.ToInt32 (tok.nextElement());
       frName = tok.nextElement ();
       lName = tok.nextElement ();
       string s = tok.nextElement ();
       age = Convert.ToInt32 (s);
       club = tok.nextElement ();
       tms = new FormatTime (tok.nextElement ());

}
//-----------
public string getName() {
       return frName+" "+lName;
}
//-----------
public string getTime() {
       return tms.getTime();
}
}

Class Containment

Each instance of the Swimmer class contains an instance of the StringTokenizer class that it uses to parse the input string and an instance of the Times class we wrote above to parse the time and return it in formatted form to the calling program. Having a class contain other classes is a very common ploy in OO programming and is one of the main ways we can build up more complicated programs from rather simple components. The program that displays these swimmers is shown in Figure 4-5.

Figure 4-5. A list of swimmers and their times, using containment

image

When you click on any swimmer, her time is shown in the box on the right. The code for showing that time is extremely easy to write, since all the data are in the swimmer class.


private void lsSwimmers_SelectedIndexChanged(
             object sender, System.EventArgs e) {
  //get index of selected swimmer
       int i = lsSwimmers.SelectedIndex ;
       //get that swimmer
       Swimmer swm = (Swimmer)ar[i];
       //display her time
       txTime.Text =swm.getTime ();
}

Initialization

In our previous Swimmer class, note that the constructor in turn calls the constructor of the StringTokenizer class.


public Swimmer(String dataLine)         {

       StringTokenizer tok =
             new StringTokenizer (dataLine);

Classes and Properties

Classes in C# can have Property methods as well as public and private functions and subs. These correspond to the kinds of properties you associate with Forms, but they can store and fetch any kinds of values you care to use. For example, rather than having methods called getAge and setAge, you could have a single age property that then corresponds to a get and a set method.


private int Age;
//age property
public int age {
       get {
             return Age;
       }
       set {
             Age = value;
       }
}

Note that a property declaration does not contain parentheses after the property name and that the special keyword value is used to obtain the data to be stored.

To use these properties, you refer to the age property on the left side of an equal sign to set the value and refer to the age property on the right side to get the value back.


age = sw.Age;      //Get this swimmer's age
sw.Age = 12;       //Set a new age for this swimmer

Properties are somewhat vestigial, since they originally applied more to Forms in the VB language, but many programmers find them quite useful. They do not provide any features that are not already available using get and set methods, and both generate equally efficient code.

In the revised version of our SwimmerTimes display program, we convert all of the get and set methods to properties and then allow users to vary the times of each swimmer by typing in new ones. Here is the Swimmer class.


public class Swimmer
{
       private string frName, lName;
       private string club;
       private int Age;
       private int place;
       private FormatTime tms;
//-----------
public Swimmer(String dataLine)          {
       StringTokenizer tok = new StringTokenizer (dataLine);
       place =  Convert.ToInt32 (tok.nextElement());
       frName = tok.nextElement ();
       lName = tok.nextElement ();
       string s = tok.nextElement ();
       Age = Convert.ToInt32 (s);
       club = tok.nextElement ();
       tms = new FormatTime (tok.nextElement ());
}
//-----------
public string name {
       get{
       return frName+" "+lName;
       }
}
//-----------
public string time {
       get{
          return tms.getTime();
       }
       set  {
          tms = new FormatTime (value);
       }
}
//-------------------
//age property
public int age {
       get {
          return Age;
       set {
          Age = value;
       }
}
}

Then we can type in a new time for any swimmer, and when the txTime text entry field loses focus, we can store a new time as follows.


private void txTime_OnLostFocus(
       object sender, System.EventArgs e) {
       //get index of selected swimmer
       int i = lsSwimmers.SelectedIndex ;
       //get that swimmer
       Swimmer swm = (Swimmer)ar[i];
       swm.time =txTime.Text ;
}

One distinct advantage of properties is the ability to write statements like this.


sw.age += 10;

This is far more readable and friendly than this.


sw.setAge (sw.getAge() + 10);

Programming Style in C#

You can develop any of a number of readable programming styles for C#. The one we use here is partly influenced by Microsoft’s Hungarian notation (named after its originator, Charles Simonyi) and partly on styles developed for Java.

We favor using names for C# controls such as buttons and ListBoxes that have prefixes that make their purpose clear, and we will use them whenever there is more than one of them on a single form.

We will not generally create new names for labels, frames, and forms if they are never referred to directly in the code. We will begin class names with capital letters and instances of classes with lowercase letters. We will also spell instances and classes with a mixture of lowercase and capital letters to make their purpose clearer:


swimmerTime

image

Delegates

C# introduces a unique feature among the C-like languages, called a delegate. Basically, a delegate is a reference to a function in another class that you can pass around and use without knowing what class it came from as long as it satisfies the same interface.

Consider the simple program in Figure 4-6. When you click on the Process button, the text in the entry field is copied into the ListBox as either all uppercase characters or all lowercase characters. While there are a number of simple ways to write this program, we will use it here to illustrate delegates.

Figure 4-6. A simple program illustrating delegates

image

A delegate is a prototype of a class method to which you assign an actual identity later. The method can be either static or from some class instance. You declare the delegate as a sort of type declaration.


private delegate string fTxDelegate(string s);

Then you can declare one or more instances of that type and assign values to them. Here is a single delegate variable.


fTxDelegate ftx;   //instance of delegate

Note that the variable ftx represents the instance of a particular method in some class that takes a string for an input and returns a string for output.

Now we will choose the function to put in this delegate variable by determining which radio button was selected. We build a class called Capital that has a fixText method as follows.


public class Capital
{
      public string fixText(string s) {
             return s.ToUpper ();
      }
}

We also construct a Lower class that also has a fixText method, only in this case it is a static method.


public class Lower
{
      public static string fixText(string s) {
             return s.ToLower();
      }
}

Now when we click on one of the two radio buttons, we assign one of these fixText methods to this delegate reference.


private void opCap_CheckedChanged(object sender, EventArgs e) {
       btProcess.Enabled =true;
       //assign an instance method to the delegate
       //create an instance of the Capital class
       ftx = new fTxDelegate (new Capital().fixText);
}
//-----
private void opLower_CheckedChanged(object sender, EventArgs e) {
       btProcess.Enabled =true;
       //assign a static method to the delegate
       //the Lower class has a static method fixText
       ftx = new fTxDelegate (Lower.fixText);
}

Note that the syntax for creating a delegate from a class instance and from a static method varies slightly.


ftx = new fTxDelegate (new Capital().fixText); //instance
ftx = new fTxDelegate (Lower.fixText);         //static

Then when we click on the Process button, we simply execute that method using the ftx delegate.


private void btProcess_Click(object sender, EventArgs e) {
       string s  = ftx(txName.Text);
       lsBox.Items.Add ( s );
}

The ftx method then resolves to call one fixText method or the other.

While the delegate approach can give you added flexibility in programming, it is really not an entirely new approach, since you could accomplish the same thing using interfaces and calling the fixText methods directly.

Indexers

C# introduces a special kind of class method called an indexer. This allows you to access elements of data inside a class using a method that makes the data look like array elements. In the BitList class that follows, the indexer returns the value of the ith bit of the number stored in the class.


public class BitList
{
       private int number;
       public BitList(string snum) {
              number = Convert.ToInt32 (snum);
       }
       //-----
       // here is an indexer that
       // returns the i-th bit of a value
       public int this[int index] {
              get{
                     int val = number >> index;
                     return val & 1;
              }
       }
}

In Figure 4-7, each time you click on the numerical up-down control, the value of that index is used to get the value of that bit from the number in the bit class, using this simple code.

Figure 4-7. Demonstration of use of indexer to get bits from a number

image


private void numericUpDown1_ValueChanged(
             object sender, EventArgs e) {
       //get the index value from the updown control
       int index = Convert.ToInt32 (numericUpDown1.Value );
       //create an instance of the BitList class
       BitList bits = new BitList(txNum.Text );

       //get that bit value using the indexer
       int bit = bits[index];
       //add it to the list box
       lsBits.Items.Add (Convert.ToString (bit));
}

As you can see, this is a convenient trick for making arraylike references, but it offers no function that could not be implemented just as easily with a class method.

Operator Overloading

Along with the ability to use pointers within unsafe sections of C# code, the C# language also lets you overload most of the common operators.

+ - * / % & | ^ << >> == != > < >=, and <=

For any given  class, you can specify a new meaning for any of these operators. For a hypothetical Complex class, you could define the +-operator like this.


public static Complex operator +(Complex c1, Complex c2)
{
    return new Complex(c1.real + c2.real,
               c1.imaginary + c2.imaginary);
}

This tends to lead to highly unreadable code, and we will not use this feature in this book.

Summary

In this chapter, we’ve introduced C# classes and shown how they can contain public and private methods and can contain data. Each class can have many instances, and each could contain different data values. Classes can also have Property methods for setting and fetching data. These Property methods provide a simpler syntax over the usual getXxx and setXxx accessor methods but have no other substantial advantages.

Programs on the CD-ROM

image

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

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