Objects in Programming

 
Appendix A - Principles of Object-Oriented Programming
bySimon Robinsonet al.
Wrox Press 2002
  

We've now established what an object is in general terms and seen a couple of examples from everyday life. We now need to see more specifically how to apply the concepts to programming.

If you've programmed on Windows before then you've almost certainly already been using objects extensively in your programs. For example, think about the various controls that you can place in windows textboxes, listboxes, buttons , and so on. Microsoft has written these controls for you, so that you don't need to know, for example, how a textbox works internally. You just know that it does certain things. For example, you can set its Text property and the new text will appear on the screen, or you can set its Width property, and the textbox will immediately resize itself for you.

In programming, we need to distinguish between a class and an object . A class is the generic definition of what an object is the template. For example, a class could be "car radio" the abstract idea of a car radio. The class specifies what properties an object should have to be a car radio.

Class Members

So far, we've emphasized that there are two sides to an object what it does, which is usually publicly known, and how it works, which is usually hidden. In programming, the "what it does" is normally represented in the first instance by methods , which are blocks of functionality that you can use. A method is just C# parlance for a function. The "how it works" is represented both by methods, and by any data (variables) that the object stores. In Java and C++, this data is described as member variables, while in VB this data would be represented by any module-level variables in the class module. In C# the terminology is fields . In general, a class is defined by its fields and methods.

We'll also use the term member by itself to denote anything that is part of a class, be it a field, method, or any of the other items just mentioned that can be defined within a class.

Defining a Class

The easiest way to understand how to code up a class is by looking at an example. Therefore, over the next few sections of this Appendix, we're going to develop a simple class called Authenticator . We'll assume we're in the process of writing a large application, which at some point requires users to log in, supplying a password. Authenticator is the name of the class that will handle this aspect of the program. We won't worry about the rest of the application we'll just concentrate on writing this class, but we will also write a small piece of test harness code to check that Authenticator works as intended.

Authenticator allows us to do two things: set a new password, and check whether a password is valid. The C# code we need to define the class looks like this:

   public class Authenticator     {     private string password  = "";         public bool IsPasswordCorrect(string tryPassword)     {     return (tryPassword == password) ? true : false;     }     public bool ChangePassword(string oldPassword, string newPassword)     {     if (oldPassword == password)     {     password = newPassword;     return true;     }     else     return false;     }     }   

The keyword class in C# indicates that we are going to define a new class (type of object). The word immediately following class is the name we're going to use for this class. Then the actual definition of the object follows in braces the definition consisting of variables (fields) and methods in our case one field, password , and two methods, IsPasswordCorrect() and ChangePassword() .

Access Modifiers

The only field in Authenticator , password , stores the current password (initially an empty string when an Authenticator object is created), and is marked by the keyword private . This means that it is not visible outside the class, only to code that is part of the Authenticator class itself. Marking a field or method as private effectively ensures that that field or method will be part of the internal working of the class, as opposed to the external interface. The advantage of this is that if you decide to change the internal working (perhaps you later decide not to store password as a string but to use some other more specialized data type), you can just make the change, and you know it won't break or affect any other code outside the Authenticator class definition. This is because any other code couldn't possibly have been accessing this field anyway.

Any code that uses the Authenticator class can only access the methods that have been marked with the keyword public in this case the IsPasswordCorrect() and ChangePassword() methods. Both of these methods have been implemented in such a way that nothing will be done (other than returning true or false ) unless the calling code supplies the current correct password, as you'd expect for software that implements security. The implementations of these functions both access the password field, but that's fine because this code forms part of the Authenticator class itself. Notice that these public functions simultaneously give us the interface to the external world (in other words, any other code that uses the Authenticator class), and define what the Authenticator class does, as viewed by the rest of the world.

private and public are not the only access modifiers available to define what code is allowed to know about the existence of a member. Later in this Appendix we'll encounter protected , which makes the member available to this class and certain related classes. C# also allows members to be declared as internal and protected internal , which restrict access to other code within the same assembly.

Instantiating and Using Objects

Now we've defined the Authenticator class, how do we use it in our code? The easiest way to understand that is to think of the class as a new type of variable. You're used to the predefined variable types in C# these are things like int , float , double , and so on. Well, by defining the Authenticator class, we've effectively told the compiler that there's a new type of variable called an Authenticator . The class definition contains everything the compiler needs to know to be able to process this variable type. Therefore, just as the compiler knows that a double contains a floating-point number stored in a certain format, and you can do things with doubles such as adding them together, we've told the compiler that a variable of type Authenticator contains a string and allows you to call the IsPasswordCorrect() and ChangePassword() methods.

Although we've described a class as a new type of variable, the more common terminology is data type , or simply type .

Creating a user -defined variable (an object) is known as instantiation , because we create an instance of the object. An instance is simply any particular occurrence of the object. So, if our Authenticator object is simply another kind of variable, we should be able to use it just like any other variable. We can, and in the next example we demonstrate this.

Create the MainEntryPoint class, as shown below, and place it in the Wrox.ProCSharp.OOProg namespace along with the Authenticator class we created earlier:

   using System;     namespace Wrox.ProCSharp.OOProg     {     class MainEntryPoint     {     static void Main()     {     Authenticator simon = new Authenticator();     bool done;     done = simon.ChangePassword("", "MyNewPassword");     if (done == true)     Console.WriteLine("Password for Simon changed");     else     Console.WriteLine("Failed to change password for Simon");     done = simon.ChangePassword("", "AnotherPassword");     if (done == true)     Console.WriteLine("Password for Simon changed");     else     Console.WriteLine("Failed to change password for Simon");     if (simon.IsPasswordCorrect("WhatPassword"))     Console.WriteLine("Verified Simon\'s password");     else     Console.WriteLine("Failed to verify Simon\'s password");     }     }     public class Authenticator     {     // implementation as shown earlier     }     }   

The MainEntryPoint class is a full-blown class like Authenticator it can have its own members (that is, its own fields, methods and so on). However, we've chosen to use this class solely as a container for the program entry point the Main() method. Doing it this way means that the Authenticator class can sit as a class in its own right, able to be used in other programs if we wish (either by cutting and pasting the code or by compiling it separately into an assembly). MainEntryPoint only really exists as a class because of the syntactical requirement of C# that even the program's main entry point has to be defined within a class, rather than sitting as an independent function.

Since all the action is happening in the Main() method, let's take a closer look at it. The first line of interest is:

 Authenticator simon = new Authenticator(); 

Here we are declaring and instantiating a new Authenticator object instance. Don't worry too much about the = new Authenticator() bit. It's part of C# syntax, and is there because in C#, classes are always accessed by reference. We could actually use the following line if we just wanted to declare a new Authenticator object called simon :

   Authenticator simon;   

This can hold a reference to an Authenticator object, without actually creating any object (in much the same way that the line Dim obj As Object in VB doesn't actually create any object). The new operator in C# is what actually instantiates an Authenticator object.

Calling class methods is done using the period symbol ( . ) appended to the name of the variable:

 done = simon.ChangePassword("", "MyNewPassword"); 

Here we have called the ChangePassword() method on the simon instance, and fed the return value into the done Boolean variable. We can retrieve class fields in a similar way. Note, however, that we could not do this:

   string SimonsPassword = simon.password;   

This code will actually cause a compilation error, because the password field was explicitly marked as private , so other code outside the Authenticator class cannot explicitly access it. If we changed the password field to be public , then the line above would compile, and would feed the value of password into the string variable.

You should note that if you are accessing member methods or fields from inside the same class, you can simply give the name of the member directly.

Now that you understand how to instantiate objects, call class methods, and retrieve public fields, the logic in the Main() method should be pretty clear. If we save this code as Authenticator.cs , then compile and run it, this is what we see:

  Authenticator  Password for Simon changed Failed to change password for Simon Failed to verify Simons password 

There are a couple of points to note from the code. Firstly, you'll notice that so far we're not actually doing anything new compared to say how you'd code up a VB class module, or to the basic C# syntax that we covered in Chapter 2. The reason for going over this code here was to make sure we are clear about the concepts behind classes.

Second, the above example uses the Authenticator class directly in other code within the same source file. You'll often want to write classes that are used by other projects that you or others work on. In order to do this, you write the class in exactly the same way, but compile the code for the class into a library. How to do this is covered in Chapter 8.

Using Static Members

You may have noticed in our example that the Main() method was declared as static . In this section we are going to discuss what effect this static keyword has.

Creating Static Fields

It's important to understand that each instance of a class (each object) has its own set of all the fields you've defined in the class by default. For example, if you write:

   Authenticator julian = new Authenticator();     Authenticator karli = new Authenticator();     karli.ChangePassword("OldKarliPassword", "NewKarliPassword")     julian.ChangePassword("OldJulianPassword", "NewJulianPassword")   

The instances karli and julian each contain their own string called password . Changing the password in karli has no effect on the password in julian , and vice versa (unless the two references happen to be pointing to the same address in memory, which is something we'll come to later). The situation is a bit like this:

click to expand

There are some cases in which this might not be the behavior you want. For example, suppose in our Authenticator class we wished to define a minimum length for all password s (and therefore for all of the password fields in all instances). We do not want each password to have its own minimum length. Therefore, we really want the minimum length to be stored only once in memory, no matter how many instances of Authenticator we create.

To indicate that a field should only be stored once, no matter how many instances of the class we create, we place the keyword static in front of the field declaration in our code:

 public class Authenticator    {   private static uint minPasswordLength = 6;   private string password = ""; 

Storing a copy of minPasswordLength with each Authenticator instance would not only have wasted memory, but also caused problems if we wanted to be able to change its value! By declaring the field as static , we ensure that it will only be stored once, and this field is shared among all instances of the class. Note that in this code snippet we also set an initial value. Fields declared with the static keyword are referred to as static fields or static data , while fields that are not declared as static are referred to as instance fields or instance data . Another way of looking at it is that an instance field belongs to an object, while a static field belongs to the class.

Important 

VB developers shouldn't confuse this with static variables in VB, which are variables whose values remain between invocations of a method.

If a field has been declared as static , then it exists when your program is running from the moment that the particular module or assembly containing the definition of the class is loaded. This will be as soon as your code tries to use something from that assembly, so you can always guarantee a static variable is there when you want to refer to it. This is independent of whether you actually create any instances of that class. By contrast, instance fields only exist when there are variables of that class currently in scope one set of instance fields for each variable.

In some ways static fields perform the same functions as global variables performed for older languages such as C and FORTRAN.

You should note that the static keyword is independent of the accessibility of the member to which it applies. A class member can be public static or private static .

Creating Static Methods

As we saw in the example earlier, by default a method such as ChangePassword() is called against a particular instance, as indicated by the name of the variable in front of the " . " operator. That method then implicitly has access to all the members (fields, methods, and so on) of that particular instance.

However, just as with fields, it is possible to declare methods as static , provided that they do not attempt to access any instance data or other instance methods. For example, we might wish to provide a method to allow users to view the minimum password length:

 public class Authenticator    {       private static uint minPasswordLength = 6;   public static uint GetMinPasswordLength()     {     return minPasswordLength;     }   ... 

The code for Authenticator with this modification is available on the Wrox Press web site as the Authenticator2 sample.

In our earlier Authenticator example, the Main() method of the MainEntryPoint class was declared as static . This allows it to be invoked as the entry point to the program, despite the fact that no instance of the MainEntryPoint class was ever created.

Accessing Static Members

The fact that static methods and fields are associated with a class rather than an object is reflected in how you access them. Instead of specifying the name of a variable before the " . " operator, you specify the name of the class, like this:

   Console.WriteLine(Authenticator.GetMinPasswordLength());   

Also notice that in the above code, we access the Console.WriteLine() method by specifying the name of the class, Console . That is because WriteLine() is a static method too we don't need to instantiate a Console object to use WriteLine() .

How Instance and Static Methods are Implemented in Memory

We said earlier that each object stores its own copy of that class's instance fields. This is, however, not the case for methods. If each object had its own copy of the code for a method it would be incredibly wasteful of memory, since the code for the methods remains the same across all object instances. Therefore, instance methods, just like static methods, are stored only once, and associated with the class as a whole. Later on, we'll learn about other types of class member (constructors, properties, and so on) that contain code rather than data, and the same applies to these.

The full picture looks something like this:

click to expand

If instance methods are only stored once, how is a method able to access the correct copy of each field? In other words, when you write:

   karli.ChangePassword("OldKarliPassword", "NewKarliPassword")     julian.ChangePassword("OldJulianPassword", "NewJulianPassword")   

How is the compiler able to generate code that accesses Karli's password with the first method call and Julian's with the second? The answer is that instance methods actually take an extra implicit parameter, which is a reference to where in memory the relevant class instance is stored. You can almost think of it as that the above code is the user-friendly version that you have to write, because it's how C# syntax works, but what's actually happening in your compiled code is:

   ChangePassword(karli, "OldKarliPassword", "NewKarliPassword")     ChangePassword(julian "OldJulianPassword", "NewJulianPassword")   

Declaring a method as static makes calling it slightly more efficient, because it will not be passed this extra parameter. On the other hand, if a method is declared as static , but attempts to access any instance data, the compiler will raise an error for the obvious reason that you can't access instance data unless you have the address of a class instance!

This means that in our Authenticator sample we could not declare either ChangePassword() or IsPasswordCorrect() as static, because both of these methods access the password field, which is not static .

Interestingly, although the hidden parameter that comes with instance methods is never declared explicitly, you do actually have access to it in your code. You can get to it using the keyword this . As another example, we could rewrite the code for the ChangePassword() method as follows:

 public bool ChangePassword(string oldPassword, string newPassword)       {   if (oldPassword == this.password)   {   this.password = newPassword;   return true;          }          else             return false;       } 

Generally, you wouldn't write your code like this unless you need to distinguish variable names all we've achieved here is to make the method longer and slightly harder to read.

A Note About Reference Types

Before we leave the discussion of classes, we ought to point out one potential gotcha that can occur in C# because C# regards all classes as reference types. This can have some unexpected effects when it comes to comparing instances of classes for equality, and setting instances of classes equal to each other. For example, look at this:

   Authenticator User1;     Authenticator User2 = new Authenticator();     Authenticator User3 = new Authenticator();     User1 = User2;     User2.ChangePassword ("", "Tardis");  // This sets password for User1 as well!     User3.ChangePassword ("", "Tardis");     if (User2 == User3)     {     // contents of this if block will NOT be executed even though     // objects referred to by User2 and User3 are contain identical values,     // because the variables refer to different objects     }     if (User2 == User1)     {     // any code here will be executed because User1 and User2 refer     // to the same memory     }   

In this code we declare three variables of type Authenticator : User1 , User2 , and User3 . However, we only actually instantiate two objects of the Authenticator class, because we only use the new operator twice. Then we set the variable User1 equal to User2 . Unlike with a value type, this does not copy any of the contents of User2 . Rather, it means that User1 is set to refer to the same memory as User2 is referring to. What that means is that any changes we make to User2 also affect User1 , because they are not separate objects; both variables refer to the same data. We can also say that they point to the same data, and the actual data referred to is sometimes described as the referent . So when we set the password of User2 to Tardis , we are implicitly also setting the password of User1 to Tardis . This is very different from how value types would behave.

The situation gets even less intuitive when we try to compare User2 and User3 in the next statement:

 if (User2 == User3) 

You might expect that this condition would return true , since User2 and User3 have both been set to the same password, so both instances contain identical data. The comparison operator for reference types, however, doesn't compare the contents of the data by default it simply tests to see whether the two references are referring to the same address in memory. Because they are not, this test will return false , which means anything inside this if block will not be executed. By contrast, comparing User2 with User1 will return true because these variables do point to the same address in memory.

Note that this behavior does not apply to strings, because the == operator has been overloaded for strings. Comparing two strings with " == " will always compare string content (any other behavior for strings would be extremely confusing!).

Overloading Methods

To overload a method is to create several methods each with the same name, but each with a different signature. The reason why you might want to use overloading is best seen with an example. Consider how in C# we write data to the command line, using the Console.WriteLine() method. For example, if we want to display the value of an integer, we can write this:

   int x = 10;     Console.WriteLine(x);   

While to display a string we can write:

   string message = "Hello";     Console.WriteLine(message);   

Even though we are passing different data types to the same method, both of these examples compile. This is because there are actually lots of Console.WriteLine() methods, but each has a different signature one of them takes an int as a parameter, while another one takes a string , and so on. There is even a two parameter overload of the method that allows for formatted output, and lets you write code like this:

   string Message = "Hello";     Console.WriteLine("The message is {0}", Message);   

Obviously, Microsoft provides all of these Console.WriteLine() methods because it realizes that there are many different data types that you might want to display the value of.

Method overloading is very useful, but there are some pitfalls to be aware of when using it. Suppose we write:

   short y = 10;     Console.WriteLine(y);   

A quick look at the documentation will reveal that no overload of WriteLine() takes a short . So what will the compiler do? Well in principle, it could generate code that converts the short to an int , and call the int version of Console.WriteLine() . Or it could convert the short to a long and call Console.WriteLine(long) . Or it could even convert the short to a string .

In this situation, each language will have a set of rules for what conversion will be the one that is actually performed (for C#, the conversion to an int is the preferred one). However you can see the potential for confusion. For this reason, if you define method overloads, you need to take care to do so in a way that won't cause any unpredictable results. This issue is discussed in more detail for C# in Chapter 6.

When to Use Overloading

Generally, you should consider overloading a method when you need a number of methods that take different parameters, but conceptually do the same thing, as with Console.WriteLine() above. The main situations in which you will normally use overloading are as follows.

Optional Parameters

One common use of method overloads is to allow certain parameters to a method to be optional and to have default values if the client code chooses not to specify their values explicitly. For example, consider this code:

   public void DoSomething(int x, int y)     {     // do whatever     }     public void DoSomething(int x)     {     DoSomething(x, 10);     }   

These overloads allow client code to call DoSomething() , supplying one required parameter and one optional parameter. If the optional parameter isn't supplied, we effectively assume the second int is 10 . Most modern compilers will also inline method calls in this situation so there is no performance loss. This is certainly true of the .NET JIT compiler.

Some languages, including VB and C++, allow default parameters to be specified explicitly in function declarations, with a syntax that looks like public void DoSomething(int X, int Y=10) . C# does not allow this, so in C# you need to simulate default parameters by providing multiple overloads of methods as shown in the above example.

Different Input Types

We have already seen an example ( Console.WriteLine() ) of this very common reason for defining overloads.

Different Output Types

This situation is far less common, but you may occasionally have a method that calculates or obtains some quantity, and depending on the circumstances, you might wish this to be returned in more than one different way. For example, in an airline company, you might have a class that represents aircraft timetables, and you might wish to define a method that tells you where an aircraft should be at a particular time. Depending on the situation, you might want the method to return either a string description of the position ("over Atlantic Ocean en route to London") or the latitude and longitude of the position.

We cannot distinguish overloads using the return type of a method. However, you can do so using out parameters. So you could define these:

   void GetAircraftLocation(DateTime Time, out string Location)     {     ...     }     void GetAircraftLocation(DateTime Time, out float Latitude, out float Longitude)     {     ...     }   

Note, however, that in most cases, using overloads to obtain different out parameters does not lead to an architecturally neat design. In the above example, a better design would perhaps involve defining a Location struct that contains the location string as well as the latitude and longitude, and returning this from the method call, hence avoiding the need for overloads.

Properties

Earlier we said that, in general, a class is defined by its fields and methods. However, there are some other types of class members constructors, indexers, properties, delegates, and events. For the most part these other items are used only in more advanced situations, and are not essential to understanding the principles of object-oriented design. For that reason, we are not going to discuss most of them in this appendix. They are introduced as required in 7. Properties are in extremely common use, however, and can significantly simplify the external user interface exposed by classes. For this reason, we'll discuss them here.

VB programmers will find that C# properties correspond almost exactly to properties in VB class modules and are used in just the same way.

Properties exist for the situation in which you wish to make a method call look like a field. We can see what a property is by looking again at the minPasswordLength field in our Authenticator class. Let's extend the class so that users can read and modify this field without having to use a GetMinPasswordLength() method like the one we introduced earlier. We can do this using properties.

A property is a method or pair of methods that are exposed to the outside world as if they are fields. To create a property for the minimum password length we modify the code for the Authenticator class as follows:

   public static uint MinPasswordLength     {     get     {     return minPasswordLength;     }     set     {     minPasswordLength = value;     }     }   

As we can see from this, we define a property in much the same way as a field, except that after the name of the property, we have a code block enclosed by curly braces. In the code block there may be two methods called get and set . These are known as the get accessor and the set accessor . Note that although no parameter is explicitly mentioned in the definition of the set accessor, there is an implicit parameter passed in, and referred to by the name value . Also, the get accessor always returns the same data type as the property was declared as (in this case a uint ).

Now, to retrieve the value of minPasswordLength using this property, we use this syntax:

   uint i = Authenticator.MinPasswordLength;   

What will actually happen here is that MinPasswordLength property's get accessor is called. In this case, this method is implemented to simply return the value of the minPasswordLength field.

To set the MinPasswordLength field using the property, we would use the following code:

   Authenticator.MinPasswordLength = 7;   

This code will cause the MinPasswordLength 's set accessor to be called, which is implemented to assign the required value (seven here) to the minPasswordLength field. We said earlier that the set accessor has an implicit parameter, called value .

Note that in this particular example, the property in question happens to be static. In general that is not necessary. Just as for methods, you will normally declare properties as static only if they only refer to static data.

Data Encapsulation

You may wonder what the point of all the above code is. Wouldn't it have been simpler to simply make the minPasswordLength field public , so that we could access it directly and not have to bother about any properties? The answer is that fields represent the internal data of an object, so they are anintegral part of the functionality of an object. Now, in object-oriented programming, we aim to make it so that users of objects only need to know what an object does, not how it does it. So making fields directly accessible to users defeats the ideology behind object-oriented programming.

Ideology is all very well, but there must be practical reasons behind it. One reason is this: if we make fields directly visible to external users, we lose control over what they do to the fields. They might modify the fields in such a way as to break the intended functionality of the object (give the fields inappropriate values, say). However, if we use properties to control access to a field, this is not a problem because we can add functionality to the property that checks for inappropriate values. Related to this, we can also provide read-only properties by omitting the set accessor completely. The principle of hiding fields from client code in this way is known as data encapsulation .

You should only use properties to do something that appears only to set or retrieve a value otherwise you should use methods. That means that the set accessor must only take one parameter and return a void, while the get accessor cannot take any parameters. For example, it would not be possible to rewrite the IsPasswordValid() method in the Authenticator class as a property. The parameter types and return value for this method are not of the correct type.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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