Chapter 8
Introduction to Object-Oriented Programming

Wrox.com Code Downloads for this Chapter

You can find the wrox.com code downloads for this chapter at www.wrox.com/go/beginningvisualc#2015programming on the Download Code tab. The code is in the Chapter 8 download and individually named according to the names throughout the chapter.

At this point in the book, you've covered all the basics of C# syntax and programming, and have learned how to debug your applications. Already, you can assemble usable console applications. However, to access the real power of the C# language and the .NET Framework, you need to make use of object-oriented programming (OOP) techniques. In fact, as you will soon see, you've been using these techniques already, although to keep things simple we haven't focused on this.

This chapter steers away from code temporarily and focuses instead on the principles behind OOP. This leads you back into the C# language because it has a symbiotic relationship with OOP. All of the concepts introduced in this chapter are revisited in later chapters, with illustrative code — so don't panic if you don't grasp everything in the first read-through of this material.

To start with, you'll look at the basics of OOP, which include answering that most fundamental of questions, “What is an object?” You will quickly find that a lot of terminology related to OOP can be confusing at first, but plenty of explanations are provided. You will also see that using OOP requires you to look at programming in a different way.

As well as discussing the general principles of OOP, this chapter looks at an area requiring a thorough understanding of OOP: desktop applications. This type of application relies on the Windows environment, with features such as menus, buttons, and so on. As such, it provides plenty of scope for description, and you will be able to observe OOP points effectively in the Windows environment.

What Is Object-Oriented Programming?

Object-oriented programming seeks to address many of the problems with traditional programming techniques. The type of programming you have seen so far is known as procedural programming, which often results in so-called monolithic applications, meaning all functionality is contained in a few modules of code (often just one). With OOP techniques, you often use many more modules of code, with each offering specific functionality. Also, each module can be isolated or even completely independent of the others. This modular method of programming gives you much more versatility and provides more opportunity for code reuse.

To illustrate this further, imagine that a high-performance application on your computer is a top-of-the-range race car. Written with traditional programming techniques, this sports car is basically a single unit. If you want to improve this car, then you have to replace the whole unit by sending it back to the manufacturer and getting their expert mechanics to upgrade it, or by buying a new one. If OOP techniques are used, however, you can simply buy a new engine from the manufacturer and follow their instructions to replace it yourself, rather than taking a hacksaw to the bodywork.

In a more traditional application, the flow of execution is often simple and linear. Applications are loaded into memory, begin executing at point A, end at point B, and are then unloaded from memory. Along the way various other entities might be used, such as files on storage media, or the capabilities of a video card, but the main body of the processing occurs in one place. The code along the way is generally concerned with manipulating data through various mathematical and logical means. The methods of manipulation are usually quite simple, using basic types such as integers and Boolean values to build more complex representations of data.

With OOP, things are rarely so linear. Although the same results are achieved, the way of getting there is often very different. OOP techniques are firmly rooted in the structure and meaning of data, and the interaction between that data and other data. This usually means putting more effort into the design stages of a project, but it has the benefit of extensibility. After an agreement is made as to the representation of a specific type of data, that agreement can be worked into later versions of an application, and even entirely new applications. The fact that such an agreement exists can reduce development time dramatically. This explains how the race car example works. The agreement here is how the code for the “engine” is structured, such that new code (for a new engine) can be substituted with ease, rather than requiring a trip back to the manufacturer. It also means that the engine, once created, can be used for other purposes. You could put it in a different car, or use it to power a submarine, for example.

OOP often simplifies things by providing an agreement about the approach to data representation, as well as about the structure and usage of more abstract entities. For example, an agreement can be made not just on the format of data that should be used to send output to a device such as a printer, but also on the methods of data exchange with that device, including what instructions it understands, and so on. In the race car analogy, the agreement would include how the engine connects to the fuel tank, how it passes drive power to the wheels, and so on.

As the name of the technology suggests, this is achieved using objects.

What Is an Object?

An object is a building block of an OOP application. This building block encapsulates part of the application, which can be a process, a chunk of data, or a more abstract entity.

In the simplest sense, an object can be very similar to a struct type such as those shown earlier in the book, containing members of variable and function types. The variables contained make up the data stored in the object, and the functions contained allow access to the object's functionality. Slightly more complex objects might not maintain any data; instead, they can represent a process by containing only functions. For example, an object representing a printer might be used, which would have functions enabling control over a printer (so you can print a document, a test page, and so on).

Objects in C# are created from types, just like the variables you've seen already. The type of an object is known by a special name in OOP, its class. You can use class definitions to instantiate objects, which means creating a real, named instance of a class. The phrases instance of a class and object mean the same thing here; but class and object mean fundamentally different things.

In this chapter, you work with classes and objects using Unified Modeling Language (UML) syntax. UML is designed for modeling applications, from the objects that build them to the operations they perform to the use cases that are expected. Here, you use only the basics of this language, which are explained as you go along. UML is a specialized subject to which entire books are devoted, so it's more complex aspects are not covered here.

Figure 8.1 shows a UML representation of your printer class, called Printer. The class name is shown in the top section of this box (you learn about the bottom two sections a little later).

Image described by surrounding text.

Figure 8.1

Figure 8.2 shows a UML representation of an instance of this Printer class called myPrinter.

Image described by surrounding text.

Figure 8.2

Here, the instance name is shown first in the top section, followed by the name of its class. The two names are separated by a colon.

Properties and Fields

Properties and fields provide access to the data contained in an object. This object data differentiates separate objects because it is possible for different objects of the same class to have different values stored in properties and fields.

The various pieces of data contained in an object together make up the state of that object. Imagine an object class that represents a cup of coffee, called CupOfCoffee. When you instantiate this class (that is, create an object of this class), you must provide it with a state for it to be meaningful. In this case, you might use properties and fields to enable the code that uses this object to set the type of coffee used, whether the coffee contains milk and/or sugar, whether the coffee is instant, and so on. A given coffee cup object would then have a given state, such as “Colombian filter coffee with milk and two sugars.”

Both fields and properties are typed, so you can store information in them as string values, as int values, and so on. However, properties differ from fields in that they don't provide direct access to data. Objects can shield users from the nitty-gritty details of their data, which needn't be represented on a one-to-one basis in the properties that exist. If you used a field for the number of sugars in a CupOfCoffee instance, then users could place whatever values they liked in the field, limited only by the limits of the type used to store this information. If, for example, you used an int to store this data, then users could use any value between −2147483648 and 2147483647, as shown in Chapter 3. Obviously, not all values make sense, particularly the negative ones, and some of the large positive amounts might require an inordinately large cup. If you use a property for this information, you could limit this value to, say, a number between 0 and 2.

In general, it is better to provide properties rather than fields for state access because you have more control over various behaviors. This choice doesn't affect code that uses object instances because the syntax for using properties and fields is the same.

Read/write access to properties can also be clearly defined by an object. Certain properties can be read-only, allowing you to see what they are but not change them (at least not directly). This is often a useful technique for reading several pieces of state simultaneously. You might have a read-only property of the CupOfCoffee class called Description, returning a string representing the state of an instance of this class (such as the string given earlier) when requested. You might be able to assemble the same data by interrogating several properties, but a property such as this one might save you time and effort. You might also have write-only properties that operate in a similar way.

As well as this read/write access for properties, you can also specify a different sort of access permission for both fields and properties, known as accessibility. Accessibility determines which code can access these members — that is, whether they are available to all code (public), only to code within the class (private), or should use a more complex scheme (covered in more detail later in the chapter, when it becomes pertinent). One common practice is to make fields private and provide access to them via public properties. This means that code within the class has direct access to data stored in the field, while the public property shields external users from this data and prevents them from placing invalid content there. Public members are said to be exposed by the class.

One way to visualize this is to equate it with variable scope. Private fields and properties, for example, can be thought of as local to the object that possesses them, whereas the scope of public fields and properties also encompasses code external to the object.

In the UML representation of a class, you use the second section to display properties and fields, as shown in Figure 8.3.

UML representation of a class named CupOfCoffee with displayed properties and fields in the second section.

Figure 8.3

This is a representation of the CupOfCoffee class, with five members (properties or fields, because no distinction is made in UML) defined as discussed earlier. Each of the entries contains the following information:

  • Accessibility — A + symbol is used for a public member, a − symbol is used for a private member. In general, though, private members are not shown in the diagrams in this chapter because this information is internal to the class. No information is provided as to read/write access.
  • The member name.
  • The type of the member.

A colon is used to separate the member names and types.

Methods

Method is the term used to refer to functions exposed by objects. These can be called in the same way as any other function and can use return values and parameters in the same way — you looked at functions in detail in Chapter 6.

Methods are used to provide access to the object's functionality. Like fields and properties, they can be public or private, restricting access to external code as necessary. They often make use of an object's state to affect their operations, and have access to private members, such as private fields, if required. For example, the CupOfCoffee class might define a method called AddSugar(), which would provide a more readable syntax for incrementing the amount of sugar than setting the corresponding Sugar property.

In UML, class boxes show methods in the third section, as shown in Figure 8.4.

UML representation of a class named CupOfCoffee with displayed properties and fields in the second section and methods in the third section.

Figure 8.4

The syntax here is similar to that for fields and properties, except that the type shown at the end is the return type, and method parameters are shown. Each parameter is displayed in UML with one of the following identifiers: return, in, out, or inout. These are used to signify the direction of data flow, where out and inout roughly correspond to the use of the C# keywords out and ref described in Chapter 6. in roughly corresponds to the default C# behavior, where neither the out nor ref keyword is used and return signifies that a value is passed back to the calling method.

Everything's an Object

At this point, it's time to come clean: You have been using objects, properties, and methods throughout this book. In fact, everything in C# and the .NET Framework is an object! The Main() function in a console application is a method of a class. Every variable type you've looked at is a class. Every command you have used has been a property or a method, such as <String>.Length, <String>.ToUpper(), and so on. (The period character here separates the object instance's name from the property or method name, and methods are shown with () at the end to differentiate them from properties.)

Objects really are everywhere, and the syntax to use them is often very simple. It has certainly been simple enough for you to concentrate on some of the more fundamental aspects of C# up until now. From this point on, you'll begin to look at objects in detail. Bear in mind that the concepts introduced here have far-reaching consequences — applying even to that simple little int variable you've been happily playing around with.

The Life Cycle of an Object

Every object has a clearly defined life cycle. Apart from the normal state of “being in use,” this life cycle includes two important stages:

  • Construction — When an object is first instantiated it needs to be initialized. This initialization is known as construction and is carried out by a constructor function, often referred to simply as a constructor for convenience.
  • Destruction — When an object is destroyed, there are often some clean-up tasks to perform, such as freeing memory. This is the job of a destructor function, also known as a destructor.

Constructors

Basic initialization of an object is automatic. For example, you don't have to worry about finding the memory to fit a new object into. However, at times you will want to perform additional tasks during an object's initialization stage, such as initializing the data stored by an object. A constructor is what you use to do this.

All class definitions contain at least one constructor. These constructors can include a default constructor, which is a parameter-less method with the same name as the class itself. A class definition might also include several constructor methods with parameters, known as nondefault constructors. These enable code that instantiates an object to do so in many ways, perhaps providing initial values for data stored in the object.

In C#, constructors are called using the new keyword. For example, you could instantiate a CupOfCoffee object using its default constructor in the following way:

CupOfCoffee myCup = new CupOfCoffee();

Objects can also be instantiated using nondefault constructors. For example, the CupOfCoffee class might have a nondefault constructor that uses a parameter to set the bean type at instantiation:

CupOfCoffee myCup = new CupOfCoffee("Blue Mountain");

Constructors, like fields, properties, and methods, can be public or private. Code external to a class can't instantiate an object using a private constructor; it must use a public constructor. In this way, you can, for example, force users of your classes to use a nondefault constructor (by making the default constructor private).

Some classes have no public constructors, meaning it is impossible for external code to instantiate them (they are said to be noncreatable). However, that doesn't make them completely useless, as you will see shortly.

Destructors

Destructors are used by the .NET Framework to clean up after objects. In general, you don't have to provide code for a destructor method; instead, the default operation does the work for you. However, you can provide specific instructions if anything important needs to be done before the object instance is deleted.

For example, when a variable goes out of scope, it may not be accessible from your code; however, it might still exist somewhere in your computer's memory. Only when the .NET runtime performs its garbage collection clean-up is the instance completely destroyed.

Static and Instance Class Members

As well as having members such as properties, methods, and fields that are specific to object instances, it is also possible to have static (also known as shared, particularly to our Visual Basic brethren) members, which can be methods, properties, or fields. Static members are shared between instances of a class, so they can be thought of as global for objects of a given class. Static properties and fields enable you to access data that is independent of any object instances, and static methods enable you to execute commands related to the class type but not specific to object instances. When using static members, in fact, you don't even need to instantiate an object.

For example, the Console.WriteLine() and Convert.ToString() methods you have been using are static. At no point do you need to instantiate the Console or Convert classes (indeed, if you try, you'll find that you can't, as the constructors of these classes aren't publicly accessible, as discussed earlier).

There are many situations such as these where static properties and methods can be used to good effect. For example, you might use a static property to keep track of how many instances of a class have been created. In UML syntax, static members of classes appear with underlining, as shown in Figure 8.5.

Image described by surrounding text.

Figure 8.5

Static Constructors

When using static members in a class, you might want to initialize these members beforehand. You can supply a static member with an initial value as part of its declaration, but sometimes you might want to perform a more complex initialization, or perhaps perform some operations before assigning values or allowing static methods to execute.

You can use a static constructor to perform initialization tasks of this type. A class can have a single static constructor, which must have no access modifiers and cannot have any parameters. A static constructor can never be called directly; instead, it is executed when one of the following occurs:

  • An instance of the class containing the static constructor is created.
  • A static member of the class containing the static constructor is accessed.

In both cases, the static constructor is called first, before the class is instantiated or static members accessed. No matter how many instances of a class are created, its static constructor will be called only once. To differentiate between static constructors and the constructors described earlier in this chapter, all nonstatic constructors are also known as instance constructors.

Static Classes

Often, you will want to use classes that contain only static members and cannot be used to instantiate objects (such as Console). A shorthand way to do this, rather than make the constructors of the class private, is to use a static class. A static class can contain only static members and can't have instance constructors, since by implication it can never be instantiated. Static classes can, however, have a static constructor, as described in the preceding section.

OOP Techniques

Now that you know the basics, and what objects are and how they work, you can spend some time looking at some of the other features of objects. This section covers all of the following:

  • Interfaces
  • Inheritance
  • Polymorphism
  • Relationships between objects
  • Operator overloading
  • Events
  • Reference versus value types

Interfaces

An interface is a collection of public instance (that is, nonstatic) methods and properties that are grouped together to encapsulate specific functionality. After an interface has been defined, you can implement it in a class. This means that the class will then support all of the properties and members specified by the interface.

Interfaces cannot exist on their own. You can't “instantiate an interface” as you can a class. In addition, interfaces cannot contain any code that implements its members; it just defines the members. The implementation must come from classes that implement the interface.

In the earlier coffee example, you might group together many of the more general-purpose properties and methods into an interface, such as AddSugar(), Milk, Sugar, and Instant. You could call this interface something like IHotDrink (interface names are normally prefixed with a capital I). You could use this interface on other objects, perhaps those of a CupOfTea class. You could therefore treat these objects in a similar way, and they can still have their own individual properties (BeanType for CupOfCoffee and LeafType for CupOfTea, for example).

Interfaces implemented on objects in UML are shown using lollipop syntax. In Figure 8.6, members of IHotDrink are split into a separate box using class-like syntax.

Image described by surrounding text.

Figure 8.6

A class can support multiple interfaces, and multiple classes can support the same interface. The concept of an interface, therefore, makes life easier for users and other developers. For example, you might have some code that uses an object with a certain interface. Provided that you don't use other properties and methods of this object, it is possible to replace one object with another (code using the IHotDrink interface shown earlier could work with both CupOfCoffee and CupOfTea instances, for example). In addition, the developer of the object itself could supply you with an updated version of an object, and as long as it supports an interface already in use, it would be easy to use this new version in your code.

Once an interface is published — that is, it has been made available to other developers or end users — it is good practice not to change it. One way of thinking about this is to imagine the interface as a contract between class creators and class consumers. You are effectively saying, “Every class that supports interface X will support these methods and properties.” If the interface changes later, perhaps due to an upgrade of the underlying code, this could cause consumers of that interface to run it incorrectly, or even fail. Instead, you should create a new interface that extends the old one, perhaps including a version number, such as X2. This has become the standard way of doing things, and you are likely to come across numbered interfaces frequently.

Disposable Objects

One interface of particular interest is IDisposable. An object that supports the IDisposable interface must implement the Dispose() method — that is, it must provide code for this method. This method can be called when an object is no longer needed (just before it goes out of scope, for example) and should be used to free up any critical resources that might otherwise linger until the destructor method is called on garbage collection. This gives you more control over the resources used by your objects.

C# enables you to use a structure that makes excellent use of this method. The using keyword enables you to initialize an object that uses critical resources in a code block, where Dispose() is automatically called at the end of the code block:

<ClassName> <VariableName> = new <ClassName>();
…
using (<VariableName>)
{
   …
}

Alternatively, you can instantiate the object <VariableName> as part of the using statement:

using (<ClassName> <VariableName> = new <ClassName>())
{
   …
}

In both cases, the variable <VariableName> will be usable within the using code block and will be disposed of automatically at the end (that is, Dispose() is called when the code block finishes executing).

Inheritance

Inheritance is one of the most important features of OOP. Any class may inherit from another, which means that it will have all the members of the class from which it inherits. In OOP terminology, the class being inherited from (derived from) is the parent class (also known as the base class). Classes in C# can derive only from a single base class directly, although of course that base class can have a base class of its own, and so on.

Inheritance enables you to extend or create more specific classes from a single, more generic base class. For example, consider a class that represents a farm animal (as used by ace octogenarian developer Old MacDonald in his livestock application). This class might be called Animal and possess methods such as EatFood() or Breed(). You could create a derived class called Cow, which would support all of these methods but might also supply its own, such as Moo() and SupplyMilk(). You could also create another derived class, Chicken, with Cluck() and LayEgg()methods.

In UML, you indicate inheritance using arrows, as shown in Figure 8.7.

Image described by surrounding text.

Figure 8.7

When using inheritance from a base class, the question of member accessibility becomes an important one. Private members of the base class are not accessible from a derived class, but public members are. However, public members are accessible to both the derived class and external code. Therefore, if you could use only these two levels of accessibility, you couldn't have a member that was accessible both by the base class and the derived class but not external code.

To get around this, there is a third type of accessibility, protected, in which only derived classes have access to a member. As far as external code is aware, this is identical to a private member — it doesn't have access in either case.

As well as defining the protection level of a member, you can also define an inheritance behavior for it. Members of a base class can be virtual, which means that the member can be overridden by the class that inherits it. Therefore, the derived class can provide an alternative implementation for the member. This alternative implementation doesn't delete the original code, which is still accessible from within the class, but it does shield it from external code. If no alternative is supplied, then any external code that uses the member through the derived class automatically uses the base class implementation of the member.

In the animals example, you could make EatFood() virtual and provide a new implementation for it on any derived class — for example, just on the Cow class, as shown in Figure 8.8. This displays the EatFood() method on the Animal and Cow classes to signify that they have their own implementations.

UML of abstract class Animal displaying method EatFood() and Breed(), and two derived classes (Chicken and Cow) with methods Cluck() and LayEgg() for chicken, and Moo(), SupplyMilk() and EatFood() for Cow.

Figure 8.8

Base classes may also be defined as abstract classes. An abstract class can't be instantiated directly; to use it you need to inherit from it. Abstract classes can have abstract members, which have no implementation in the base class, so an implementation must be supplied in the derived class. If Animal were an abstract class, then the UML would look as shown in Figure 8.9.

Similar image in Figure 8-8 but with added method EatFood in Chicken, and Breed on both Chicken and Cow.

Figure 8.9

In Figure 8.9, both EatFood()and Breed()are shown in the derived classes Chicken and Cow, implying that these methods are either abstract (and, therefore, must be overridden in derived classes) or virtual (and, in this case, have been overridden in Chicken and Cow). Of course, abstract base classes can provide implementation of members, which is very common. The fact that you can't instantiate an abstract class doesn't mean you can't encapsulate functionality in it.

Finally, a class may be sealed. A sealed class cannot be used as a base class, so no derived classes are possible.

C# provides a common base class for all objects called object (which is an alias for the System.Object class in the .NET Framework). You take a closer look at this class in Chapter 9.

Polymorphism

One consequence of inheritance is that classes deriving from a base class have an overlap in the methods and properties that they expose. Because of this, it is often possible to treat objects instantiated from classes with a base type in common using identical syntax. For example, if a base class called Animal has a method called EatFood(), then the syntax for calling this method from the derived classes Cow and Chicken will be similar:

Cow myCow = new Cow();
Chicken myChicken = new Chicken();
myCow.EatFood();
myChicken.EatFood();

Polymorphism takes this a step further. You can assign a variable that is of a derived type to a variable of one of the base types, as shown here:

Animal myAnimal = myCow;

No casting is required for this. You can then call methods of the base class through this variable:

myAnimal.EatFood();

This results in the implementation of EatFood() in the derived class being called. Note that you can't call methods defined on the derived class in the same way. The following code won't work:

myAnimal.Moo();

However, you can cast a base type variable into a derived class variable and call the method of the derived class that way:

Cow myNewCow = (Cow)myAnimal;
myNewCow.Moo();

This casting causes an exception to be raised if the type of the original variable was anything other than Cow or a class derived from Cow. There are ways to determine the type of an object, which you'll learn in the next chapter.

Polymorphism is an extremely useful technique for performing tasks with a minimum of code on different objects descending from a single class. It isn't just classes sharing the same parent class that can make use of polymorphism. It is also possible to treat, say, a child and a grandchild class in the same way, as long as there is a common class in their inheritance hierarchy.

As a further note here, remember that in C# all classes derive from the base class object at the root of their inheritance hierarchies. It is therefore possible to treat all objects as instances of the class object. This is how WriteLine()can process an almost infinite number of parameter combinations when building strings. Every parameter after the first is treated as an object instance, allowing output from any object to be written to the screen. To do this, the method ToString() (a member of object) is called. You can override this method to provide an implementation suitable for your class, or simply use the default, which returns the class name (qualified according to any namespaces it is in).

Interface Polymorphism

Although you can't instantiate interfaces in the same way as objects, you can have a variable of an interface type. You can then use the variable to access methods and properties exposed by this interface on objects that support it.

For example, suppose that instead of an Animal base class being used to supply the EatFood() method, you place this EatFood() method on an interface called IConsume. The Cow and Chicken classes could both support this interface, the only difference being that they are forced to provide an implementation for EatFood() because interfaces contain no implementation. You can then access this method using code such as the following:

Cow myCow = new Cow();
Chicken myChicken = new Chicken();
IConsume consumeInterface;
consumeInterface = myCow;
consumeInterface.EatFood();
consumeInterface = myChicken;
consumeInterface.EatFood();

This provides a simple way for multiple objects to be called in the same manner, and it doesn't rely on a common base class. For example, this interface could be implemented by a class called VenusFlyTrap that derives from Vegetable instead of Animal:

VenusFlyTrap myVenusFlyTrap = new VenusFlyTrap();
IConsume consumeInterface;
consumeInterface = myVenusFlyTrap;
consumeInterface.EatFood();

In the preceding code snippets, calling consumeInterface.EatFood() results in the EatFood() method of the Cow, Chicken, or VenusFlyTrap class being called, depending on which instance has been assigned to the interface type variable.

Note here that derived classes inherit the interfaces supported by their base classes. In the first of the preceding examples, it might be that either Animal supports IConsume or that both Cow and Chicken support IConsume. Remember that classes with a base class in common do not necessarily have interfaces in common, and vice versa.

Relationships between Objects

Inheritance is a simple relationship between objects that results in a base class being completely exposed by a derived class, where the derived class can also have some access to the inner workings of its base class (through protected members). There are other situations in which relationships between objects become important.

This section takes a brief look at the following

  • Containment — One class contains another. This is similar to inheritance but allows the containing class to control access to members of the contained class and even perform additional processing before using members of a contained class.
  • Collections — One class acts as a container for multiple instances of another class. This is similar to having arrays of objects, but collections have additional functionality, including indexing, sorting, resizing, and more.

Containment

Containment is simple to achieve by using a member field to hold an object instance. This member field might be public, in which case users of the container object have access to its exposed methods and properties, much like with inheritance. However, you won't have access to the internals of the class via the derived class, as you would with inheritance.

Alternatively, you can make the contained member object a private member. If you do this, then none of its members will be accessible directly by users, even if they are public. Instead, you can provide access to these members using members of the containing class. This means that you have complete control over which members of the contained class to expose, if any, and you can perform additional processing in the containing class members before accessing the contained class members.

For example, a Cow class might contain an Udder class with the public method Milk(). The Cow object could call this method as required, perhaps as part of its SupplyMilk() method, but these details will not be apparent (or important) to users of the Cow object.

Contained classes can be visualized in UML using an association line. For simple containment, you label the ends of the lines with 1s, showing a one-to-one relationship (one Cow instance will contain one Udder instance). You can also show the contained Udder class instance as a private field of the Cow class for clarity (see Figure 8.10).

Image described by surrounding text.

Figure 8.10

Collections

Chapter 5 described how you can use arrays to store multiple variables of the same type. This also works for objects (remember, the variable types you have been using are really objects, so this is no real surprise). Here's an example:

Animal[] animals = new Animal[5];

A collection is basically an array with bells and whistles. Collections are implemented as classes in much the same way as other objects. They are often named in the plural form of the objects they store — for example, a class called Animals might contain a collection of Animal objects.

The main difference from arrays is that collections usually implement additional functionality, such as Add() and Remove() methods to add and remove items to and from the collection. There is also usually an Item property that returns an object based on its index. More often than not this property is implemented in such a way as to allow more sophisticated access. For example, it would be possible to design Animals so that a given Animal object could be accessed by its name.

In UML you can visualize this as shown in Figure 8.11. Members are not included in Figure 8.11 because it's the relationship that is being illustrated. The numbers on the ends of the connecting lines show that one Animals object will contain zero or more Animal objects. You take a more detailed look at collections in Chapter 11.

Image described by surrounding text.

Figure 8.11

Operator Overloading

Earlier in the book, you saw how operators can be used to manipulate simple variable types. There are times when it is logical to use operators with objects instantiated from your own classes. This is possible because classes can contain instructions regarding how operators should be treated.

For example, you might add a new property to the Animal class called Weight. You could then compare animal weights using the following:

if (cowA.Weight > cowB.Weight)
{
   …
}

Using operator overloading, you can provide logic that uses the Weight property implicitly in your code, so that you can write code such as the following:

if (cowA > cowB)
{
   …
}

Here, the greater-than operator (>) has been overloaded. An overloaded operator is one for which you have written the code to perform the operation involved — this code is added to the class definition of one of the classes that it operates on. In the preceding example, you are using two Cow objects, so the operator overload definition is contained in the Cow class. You can also overload operators to work with different classes in the same way, where one (or both) of the class definitions contains the code to achieve this.

You can only overload existing C# operators in this way; you can't create new ones. However, you can provide implementations for both unary (single operand) and binary (two operands) usages of operators such as + or >. You see how to do this in C# in Chapter 13.

Events

Objects can raise (and consume) events as part of their processing. Events are important occurrences that you can act on in other parts of code, similar to (but more powerful than) exceptions. You might, for example, want some specific code to execute when an Animal object is added to an Animals collection, where that code isn't part of either the Animals class or the code that calls the Add() method. To do this, you need to add an event handler to your code, which is a special kind of function that is called when the event occurs. You also need to configure this handler to listen for the event you are interested in.

You can create event-driven applications, which are far more prolific than you might think. For example, bear in mind that Windows-based applications are entirely dependent on events. Every button click or scroll bar drag you perform is achieved through event handling, as the events are triggered by the mouse or keyboard.

Later in this chapter you will see how this works in Windows applications, and there is a more in-depth discussion of events in Chapter 13.

Reference Types versus Value Types

Data in C# is stored in a variable in one of two ways, depending on the type of the variable. This type will fall into one of two categories: reference or value. The difference is as follows:

  • Value types store themselves and their content in one place in memory.
  • Reference types hold a reference to somewhere else in memory (called the heap) where content is stored.

In fact, you don't have to worry about this too much when using C#. So far, you've used string variables (which are reference types) and other simple variables (most of which are value types, such as int) in pretty much the same way.

One key difference between value types and reference types is that value types always contain a value, whereas reference types can be null, reflecting the fact that they contain no value. It is, however, possible to create a value type that behaves like a reference type in this respect (that is, it can be null) by using nullable types. These are described in Chapter 12, when you look at the advanced technique of generic types (which include nullable types).

The only simple types that are reference types are string and object, although arrays are implicitly reference types as well. Every class you create will be a reference type, which is why this is stressed here.

OOP in Desktop Applications

In Chapter 2, you created a simple desktop application in C# using Windows Presentation Foundation (WPF). WPF desktop applications are heavily dependent on OOP techniques, and this section takes a look at this to illustrate some of the points made in this chapter. The following Try It Out enables you to work through a simple example.


EXERCISES

  1. 8.1 Which of the following are real levels of accessibility in OOP?

    1. (a) Friend

    2. (b) Public

    3. (c) Secure

    4. (d) Private

    5. (e) Protected

    6. (f) Loose

    7. (g) Wildcard

  2. 8.2 “You must call the destructor of an object manually or it will waste memory.” True or false?

  3. 8.3 Do you need to create an object to call a static method of its class?

  4. 8.4 Draw a UML diagram similar to the ones shown in this chapter for the following classes and interface:

    • An abstract class called HotDrink that has the methods Drink, AddMilk, and AddSugar, and the properties Milk and Sugar
    • An interface called ICup that has the methods Refill and Wash, and the properties Color and Volume
    • A class called CupOfCoffee that derives from HotDrink, supports the ICup interface, and has the additional property BeanType
    • A class called CupOfTea that derives from HotDrink, supports the ICup interface, and has the additional property LeafType
  5. 8.5 Write some code for a function that will accept either of the two cup objects in the preceding example as a parameter. The function should call the AddMilk, Drink, and Wash methods for any cup object it is passed.

    Answers to the exercises can be found in Appendix A.


image What You Learned in This Chapter

Topic Key Concepts
Objects and classes Objects are the building blocks of OOP applications. Classes are type definitions that are used to instantiate objects. Objects can contain data and/or expose operations that other code can use. Data can be made available to external code through properties, and operations can be made available to external code through methods. Both properties and methods are referred to as class members. Properties can allow read access, write access, or both. Class members can be public (available to all code), or private (available only to code inside the class definition). In .NET, everything is an object.
Object life cycle An object is instantiated by calling one of its constructors. When an object is no longer needed, it is destroyed by executing its destructor. To clean up after an object, it is often necessary to manually dispose of it.
Static and instance members Instance members are available only on object instances of a class. Static members are available only through the class definition directly, and are not associated with an instance.
Interfaces Interfaces are a collection of public properties and methods that can be implemented on a class. An instance-typed variable can be assigned a value of any object whose class definition implements that interface. Only the interface-defined members are then available through the variable.
Inheritance Inheritance is the mechanism through which one class definition can derive from another. A class inherits members from its parent, of which it can have only one. Child classes cannot access private members in its parent, but it is possible to define protected members that are available only within a class or classes that derive from that class. Child classes can override members that are defined as virtual in a parent class. All classes have an inheritance chain that ends in System.Object, which has the alias object in C#.
Polymorphism All objects instantiated from a derived class can be treated as if they were instances of a parent class.
Object relationships and features Objects can contain other objects, and can also represent collections of other objects. To manipulate objects in expressions, you often need to define how operators work with objects, through operator overloading. Objects can expose events that are triggered due to some internal process, and client code can respond to events by providing event handlers.
..................Content has been hidden....................

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