OOP CSharp Part 2
OOP CSharp Part 2
These require Windows NT 4.0 with Service Pack 6, Windows 2000, Windows XP Professional, or
Windows .NET Server to be installed.
Note that there are some limitations to Visual C# .NET Standard Edition, which mean that not all of
the project types used in this book can be easily generated with this edition. In particular, Chapter 8,
Creating Custom Controls, requires the full edition of Visual Studio .NET.
We're also assuming you have access to either a SQL Server or MSDE database server. If you're using
the Standard Edition of Visual C# .NET, you will need MSDE to connect to the database from Server
Explorer. MSDE can be installed with the samples shipped with the .NET Framework SDK; this is itself
installed with Visual C# .NET.
Chapter 11, Integrating VB6 and C#, requires access to a VB6 installation.
Summary of Contents
Introduction
Chapter 1: .NET, C#, and Visual Studio. NET
Chapter 2: A Windows Application with Visual Studio .NET and C#
Chapter 3: The C# Language
Chapter 4: Object-Oriented Programming : Part I
Chapter 5: Object-Oriented Programming : Part II
Chapter 6: Building a Windows Application
Chapter 7: Working with ActiveX Controls
Chapter 8: Creating Custom Controls
Chapter 9: Displaying Data in Windows Forms
Chapter 10: Class Libraries
Chapter 11: Integrating VB6 and C#
Chapter 12: Data Access with ADO.NET
Chapter 13: Advanced ADO.NET
Chapter 14: Deploying Windows Applications
Index
1
9
33
65
129
167
245
277
307
331
359
375
415
441
491
513
Object-Oriented Programming
Part II
In the previous chapter we went through the basics of object-oriented programming with C#. We
covered several new concepts. Now is the time to continue our journey into more advanced topics of
OOP, and how it is implemented in C#.
More specifically, in this chapter we'll cover:
All of these really stem from increasing our knowledge of classes and objects, and what we can do with
them. Let's start by learning more about base classes and derived classes.
Chapter 5
168
Note that here we are not adding any new functionality to the Student class, and we're not changing
any of the existing functionality we will do that later. For now, Student and Person behave in
exactly the same way. Now modify MyExecutableClass.cs to read as follows:
using System;
namespace PersonExample
{
class MyExecutableClass
{
static void Main(string[] args)
{
Person dave = new Person("Dave");
dave.DescribeYourself();
Person claire = new Student("Claire");
claire.DescribeYourself();
Student mark = new Student("Mark");
mark.DescribeYourself();
}
}
}
Type the code and make sure it works. When executed, the program should display these lines:
Hi there!
Hi there!
Hi there!
Press any
There are two things you may have already noticed about this program: first, the code is not very
complicated, and second, the output is very boring. It is not particularly interesting to see Person and
Student objects that describe themselves exactly the same way.
169
Chapter 5
Another important thing to note about the code is how we implement polymorphism when creating the
objects in MyExecutableClass. We create dave as Person, and ask him to describe himself like this:
Person dave = new Person("Dave");
dave.DescribeYourself();
If with the two boys things are very clear, our girl Claire is a bit more complicated just like in real life:
Person claire = new Student("Claire");
claire.DescribeYourself();
Remember that polymorphism allows us to declare objects of a generic type, and use them to access
objects of a more specialized type (that is, objects of a derived class). That's what we do now: claire is
declared as a generic Person, but then we assign a Student object to it. Although this doesn't appear
to be particularly interesting or useful right now, you'll soon understand why we declared and created
claire like this.
Now let's get back to the main topic changing behavior in the derived class.
{}
170
Before analyzing the results, let's remind ourselves once again how we defined our three objects:
Person dave = new Person("Dave");
Person claire = new Student("Claire");
Student mark = new Student("Mark");
dave's behavior is easy to understand: dave is a Person, so any changes in Student doesn't affect it
in any way. mark is a Student object, and will naturally use Student's implementation of
DescribeYourself().
With Claire we have a problem. Although she is a Student, she describes herself as a Person. Let's
understand now why this is.
claire, which holds a Person reference, doesn't see the new implementation of
DescribeYourself() from Student, even if its underlying object is actually Student. When we
use hiding, the new method in the derived class can't be seen using an object that has initially been
declared as Person even if we assign a Student object to it.
This has implications for methods that use polymorphism. In the previous chapter we added a
Describe() method to MyExecutableClass, which took a Person as a parameter. We saw that we
could pass a Student into this method, and it would still work. But if we passed a Student into it
now, we would get a Person description instead of a Student description. Usually this is not the
behavior we want we would want the Student description. Fortunately, overriding offers us a way to
do this. Let's look at overriding now.
And now, in the Student class, replace the new keyword with override:
public override void DescribeYourself()
{
Console.WriteLine
("Hello man, I'm {0}. I'm a cool student.", name);
}
171
Chapter 5
Now we will get the following output:
Hi there! My name is Dave and I am a nice person.
Hello man, I'm Claire. I'm a cool student.
Hello man, I'm Mark. I'm a cool student.
Press any key to continue
Now calling DescribeYourself() on claire displays the student greeting. We still declared claire
as a Person, and then assigned a Student object to it. But this time, DescribeYourself() was not
hidden in the derived class instead, in was overridden. When a method is overridden, the compiler will
always look for its most derived implementation. In the case of claire, the most derived implementation
is the one in Student. This demonstrates the difference between overriding and hiding.
In order to be able to override a method in a derived class, it must be declared as virtual, because in
C# methods and properties are not virtual by default. If we don't explicitly mark a base class's
member as virtual, we cannot override it in the derived class. If you think that certain members of
your class should or could be overridden instead of hidden in derived classes, make sure you mark them
as virtual.
Keep in mind that just because you declare a method as virtual does not mean it will always be
overridden in the derived class. By declaring a class member as virtual, the derived classes will have
a choice to either override the member (using override), to hide the member (using new), or to use
the original implementation. If the method is not virtual, it cannot be overriden. To test method
hiding for virtual methods, modify the Student class again by using new instead of override:
public new void DescribeYourself()
{
Console.WriteLine
("Hello man, I'm {0}. I'm a cool student.", name);
}
Now, Claire will describe herself again as a person, demonstrating that DescribeYourself() was
hidden and not overridden.
Because there are three ways we can define the method in the derived class (with new, override, or
neither), and two ways we can have our method in the base class (virtual or non-virtual), you may be a
bit confused about the combinations that can be made. The following table should make all options
clear.
172
There is no special
modifier for derived class's
method
There are four combinations that lead to method hiding (because this is the default behavior), and only
one for method overriding (C# forces us to be explicit when we want to use overriding).
Many people confuse overloading with overriding. They usually know the difference,
but say the wrong thing without thinking. Remember that overloading refers to the
situation when we have different methods with the same name in the same class, and
overriding is when we have different methods with the same name and with the same
signature, in the base class and derived classes. You may find that some publications
particularly web sites will get this wrong. However, it's usually easy to tell what is
meant by the context.
173
Chapter 5
Now, when we use method hiding (with new) for DescribeYourself(), we obtain this output:
Hi there! My name is Dave and I am a nice person.
Hi there! My name is Claire and I am a nice person.
Press any key to continue
If we use method overriding (with override) instead, the output changes to:
Hi there! My name is Dave and I am a nice person.
Hello man, I'm Claire. I'm a cool student.
Press any key to continue
Before we leave polymorphism, let's look at how we could modify the Describe() method so that it
still accepts any Person as a parameter, but can do different things depending on whether we pass a
Person, Student, or Instructor.
Actually this makes sense we can't call methods specific to Student on Person objects! We have to
make an explicit cast to the Student data type before we can call methods specific to that class. To see
this in action, add the following method to the Student class:
public void Learn()
{
Console.WriteLine
("Student wakes up, yawns, scratches head, and tries to look interested.");
}
174
Besides DescribeYourself(), which is overridden in both classes (so make sure it is virtual in
the Person class), Student now has a method called Learn, and Instructor has a method called
Teach. We want to improve the Describe() method like this:
If the object received is a simple Person, we want to call DescribeYourself() and nothing
else
How do we implement this functionality? The problem is that our person parameter is of type Person,
so we can't call Learn or Teach directly on it the compiler wouldn't recognize these methods.
175
Chapter 5
Let's see the solution first, and then we'll explain how it works. Update MyExecutableClass like this:
class MyExecutableClass
{
static void Main(string[] args)
{
Person person = new Person("Person");
Student student = new Student("Student");
Instructor instructor = new Instructor("Mr. Instructor");
Describe(person);
Describe(student);
Describe(instructor);
}
static void Describe(Person person)
{
person.DescribeYourself();
if (person is Student)
{
Student s = (Student) person;
s.Learn();
}
if (person is Instructor)
{
Instructor i = (Instructor) person;
i.Teach();
}
}
}
In order to achieve the desired results, we used the is operator to test if a certain expression is
compatible with a specific type:
if (person is Student)
{
If we find out that person is a Student, then it's safe to make an explicit cast to Student. Once we save
person using a Student reference we are free to call the Learn() method, which is specific to Student:
176
Making conversions using the as operator is more convenient in some cases because if the types are not
compatible, it returns null instead of raising an exception. In our example, we tested whether the
object received as a parameter is compatible with the type we want to cast to using the if statement, so
here it is just a matter of taste what syntax you prefer to use for casting.
We also did the same magic for the Instructor class, as we've seen in the above code snippet.
177
Chapter 5
If we create an object of DerivedClass type and execute its WriteValues() method, the output is:
Value of BaseClass: 5
Value of DerivedClass: 10
Press any key to continue
Note that we didn't have to use this, but this makes the difference between the base value and the
this value obvious. The program works the same if we use Value instead of this.Value. The example
demonstrates using base and this for fields, but they work the same way for methods or properties.
For a more interesting example, let's modify the DescribeYourself() method of Instructor like
this:
public override void DescribeYourself()
{
base.DescribeYourself();
Console.WriteLine
("Good morning class. My name is {0}, and I am your instructor.",name);
}
In order to test this, we need only two lines in the Main method of MyExecutableClass. Don't
remove the Describe method, because we will use it in the following examples:
static void Main(string[] args)
{
Instructor instructor = new Instructor("Mr. Instructor");
instructor.DescribeYourself();
}
178
Abstract Classes
Abstract classes are classes that cannot be directly instantiated, but can be inherited from. When talking
about inheritance in the previous chapter, we mentioned that derived classes are more specialized
versions of the base class. Indeed, we can see that a student is a "more specialized" person, and the same
for an instructor. Applying the same idea the reverse way, it's fair to say that the base class serves as a
generalized version of its derived classes. You will sometimes find that in your class hierarchies there
will be classes that are so general, that it wouldn't make much sense to directly instantiate them. In
regard to our Person example, say that we want to create an application for a school that must keep
track of all its students, teachers, secretaries, managers, and so on. We decide to use Person as a base
class to create the other classes like Student, Instructor, Teacher or Secretary. While it does
make sense to create objects of type Student or Instructor, we decide that nobody should ever
create objects of type Person.
Abstract classes provide us with the means to achieve this behavior. If we declare Person as abstract,
we will not be able to instantiate it, but we will be free to derive from it, thus ensuring that we'll have a
set of classes that are guaranteed to support a set of functionality the functionality inherited from
Person. Before going into other details, let's first see how to implement an abstract class. Modify our
Person class by adding abstract to its definition:
abstract class Person
Now compile and execute the program, using the same code we used in the previous section, Accessing
Members of the Base Class. If everything works well, the output should be the same.
An important point to understand about abstract classes is that we aren't allowed to instantiate them, but
we can still declare objects of their type. Moreover, we can assign to them created objects of derived
classes, thus enabling us to use polymorphism on them. To demonstrate this, let's revisit polymorphism
using the following code in the Main method of MyExecutableClass:
static void Main(string[] args)
{
Person person;
Instructor instructor = new Instructor("Mr. Bean");
Student student = new Student("Joey");
person = instructor;
person.DescribeYourself();
person = student;
person.DescribeYourself();
}
179
Chapter 5
Run the program, and if you have the version of Student and Instructor as presented in the
previous examples, you should see the following output:
Hi there! My name is Mr. Bean and I am a nice person.
Good morning class. My name is Mr. Bean, and I am your instructor.
Hello man, I'm Joey. I'm a cool student.
Press any key to continue
If we had tried to instantiate the object with the new keyword, we would have received the following
compile-time error: Cannot create an instance of the abstract class or interface
'PersonExample.Person'.
Because the Instructor and Student classes are not abstract, we are allowed to declare and
instantiate them, as instructor and student in our example. Then we are able to assign objects of
derived classes to the Person object:
person = instructor;
person.DescribeYourself();
The result of this code snippet depends of course of whether DescribeYourself() is overridden or
hidden in Instructor. Alternatively it might not be defined at all in Instructor, in which case the
Person implementation of the method will be used.
Also, keep in mind that you can still use the Describe() method we used in our previous examples.
This method takes a Person parameter. We used to send Person, Student and Instructor objects
to this method. Now that Person is abstract, we can only send Student and Instructor objects, or
objects of other classes derived from Person. Obviously we can't send Person objects, since we can't
create Person objects.
180
Abstract class members can only be part of abstract classes. Since they don't have an implementation, they
must be overridden in the derived classes, except for derived classes that are also abstract. In order to
understand their use better, let's imagine again the scenario where we create an application for a school.
After the initial design of the School application, we decide that a great number of classes like
Student, Instructor, or Secretary have many things in common. For example, each of them
must have a name, and must know how to describe itself.
You have already learned that inheritance is a great way to implement code reuse. If we have a great
deal of code that all our classes share, then it might be good to place that code in the base class
(Person in our case). Still, there may be times when we don't care too much about code reuse, but from
a design point of view we want to ensure that a number of classes (Student, Instructor, and so on)
have a similar behavior, so we can benefit from polymorphism by having a base class from which they
all inherit.
By using an abstract base class using abstract members, we can provide a pattern to be followed by other
classes. Classes that derive from an abstract class must provide an implementation for every abstract
member of that class. In order to understand this better, let's use an example. Modify Person like this:
abstract class Person
{
protected string name;
public Person(string name)
{
this.name = name;
}
public abstract void DescribeYourself();
}
Note that the constructor is still there, and it does it job of storing the name to a private instance field.
This functionality is still inherited by derived classes. But the major change is in the way
DescribeYourself() is defined: notice the abstract modifier, and the fact that the method has no
implementation.
Now, all non-abstract derived classes will have to override DescribeYourself(). In a previous
exercise, we called the base class's DescribeYourself() method from Instructor, in a line like
this:
base.DescribeYourself();
181
Chapter 5
If you still have that line, you need to remove it, because it's not possible to call an abstract method.
This is obvious, since an abstract member doesn't have an implementation. Because both Student and
Instructor override DescribeYourself() in their current implementation, the program should
still run and produce the same results:
Good morning class. My name is Mr. Bean, and I am your instructor.
Hello man, I'm Joey. I'm a cool student.
Press any key to continue
Abstract in Action
In more complicated examples, you'll often find abstract classes that derive from other abstract classes.
An abstract class that derives from another abstract class can override any number of abstract members
of the base class, but finally there has to be a non-abstract, derived class, otherwise a hierarchy with
only abstract classes wouldn't be too useful. To demonstrate this idea, let's complicate our example a
little bit more. Take a look at this diagram, which we will implement after discussing it:
Person
#name : string
+Person(in name : string)
+DescribeYourself()
Employee
Student
+Employee(in name : string)
+StartWorking()
+DescribeYourself()
+Student(in name : string)
+Learn()
Instructor
Manager
+DescribeYourself()
+StartWorking()
+Instructor(in name : string)
+Teach()
+DescribeYourself()
+StartWorking()
+Manager(in name : string)
Visio shows abstract classes or abstract class members using italics. Person, the base class, contains a constructor, a
protected class field (name) and an abstract method DescribeYourself(). Because DescribeYourself()
is abstract, it is overridden in the non-abstract class student Student. Even though Instructor and Manager
are not directly derived from Person, they must also override DescribeYourself() because it is not
overridden in any of the intermediary classes (Employee in this case).
182
{}
183
Chapter 5
class Instructor: Employee
{
public Instructor (string name) : base (name) {}
public override void DescribeYourself()
{
Console.WriteLine
("Good morning class. My name is {0}, and I am your instructor.",name);
}
public override void StartWorking()
{
Console.WriteLine("Going to my dear students...");
}
public void Teach()
{
Console.WriteLine("Instructor babbles incomprehensibly.");
}
}
class Manager: Employee
{
public Manager(string name): base(name){}
public override void DescribeYourself()
{
Console.WriteLine("I'm {0}. I'm the boss around here.", name);
}
public override void StartWorking()
{
Console.WriteLine("No, it's my golf day.");
}
}
}
For a quick test of these classes, implement the Main() method of MyExecutableClass as follows:
static void Main(string[] args)
{
Person person;
Employee employee;
Student student = new Student("Joey");
Instructor instructor = new Instructor("Mr. Bean");
Manager manager = new Manager("Mr. Listentomecarefully");
// apply polymorphism using Person reference
person = student;
person.DescribeYourself();
184
As were at the end of this section, let's summarize the key ideas you should have learned about abstract
members in C#:
Abstract classes can't be instantiated. They can only be used as base classes for other classes.
If a class has at least one abstract member, it must also be declared as abstract in other
words, non-abstract classes aren't allowed to contain abstract members.
Even if we have a full working class, we can declare it as abstract if we want to make sure
nobody will use it directly (we force other programmers to inherit from our class).
A non-abstract class that derives from an abstract class must override every abstract member
inherited from the base class.
Abstract class members are very similar to virtual class members in that they can be
overridden in derived classes. However, abstract properties and method are not allowed to
have implementations, and they must be overridden in the non-abstract derived class we are
not allowed to hide an abstract member.
185
Chapter 5
Now, when you try to compile the solution, you'll receive many compiling errors mentioning that one
class or another cannot inherit from sealed class 'PersonExample.Person'.
Remember that only overriding methods can be sealed. To test sealed on a method, we can modify
the Student class's DescribeYourself() as:
public sealed override void DescribeYourself()
So far, all of our discussion has assumed that methods, properties, and fields refer to a particular object.
However, the world doesn't always work this way. Often we want a particular piece of functionality that
does not neatly apply to a particular object. In the next section, we are going to discuss static members,
and how to use them.
186
Interfaces
An interface is set of property and method signatures, and serves as a contract for classes that
implement it. When a class implements an interface, it guarantees that it will implement every signature
defined in that interface.
Technically, an interface is very much like an abstract class, so let's first review what we know about
abstract classes. In the previous sections we learned that an abstract class cannot be instantiated it is
meant to be inherited from, and serve as a base class for a number of classes. In our examples, we
created the Person and Employee classes. Neither of them could be instantiated, because they were
abstract. Person had an abstract member named DescribeYourself() that had to be overridden in
derived classes, but also had a constructor (implementation), which instantiated the name protected
field. Employee only had an abstract method signature called StartWorking().
Let's see now the main similarities and differences between interfaces and abstract classes:
Neither interfaces nor abstract classes can be instantiated. Still, we can create variables of their
types, and then assign to these variables objects of derived classes.
Both interfaces and abstract classes can contain method and property definitions. In fact,
interfaces are allowed to contain only definitions, unlike abstract classes which are also allowed
to contain method implementations. Additionally, interfaces aren't allowed to contain fields or
constants, constructors or destructors (we'll cover these a bit later), and static members.
Because C# supports only single implementation inheritance, a class cannot derive from more
than one (abstract or non-abstract) class. Still, a class can implement any number of interfaces.
We'll learn soon what this means. While we're here, note that the terminology is different with
interfaces: a class inherits from another class, but it implements a number of interfaces.
Because the purpose of interfaces is to enable interface inheritance, let's see what it's all about.
Interface Inheritance
From a design standpoint, the power of abstract classes is that by preventing us from using them
directly, they help enforce certain rules about how the classes in our class hierarchy can be used. In the
latest version of our program, Person and Employee were abstract classes. We could not implement
them, but they defined a set of methods that had to be overridden in all derived classes.
187
Chapter 5
Interfaces provide an alternative way to enforce certain rules for classes through interface inheritance.
Note that when we simply say "inheritance", this usually refers to implementation inheritance. This is
the only kind of inheritance we have used so far in this chapter, and when I simply say "inheritance",
implementation inheritance is what I have in mind.
In order to understand interface inheritance, let's first quickly analyze (once again) implementation
inheritance, in a different way from what we have done before. Implementation inheritance achieves
two important goals that you rely on when designing your application:
Derived classes are guaranteed to support all functionality exposed by the base class's default
interface. The default interface consists of all the public members of a class. In other words, if
the base class has a public method named DescribeYourself(), we can safely call
DescribeYourself() on any of the derived classes. We are guaranteed that any derived
class will expose DescribeYourself(), regardless of whether it overrides the method,
hides it, or simply relies on the base class's implementation. As a side note, this idea is at the
heart of polymorphism.
Derived classes inherit all the implementations provided by the base class. A base class can
have abstract members, for which we need to provide an implementation in the derived
classes. But if a public member of a base class has an implementation (so it's not abstract), we
are not required to provide an implementation in the derived class.
It is very important to realize the difference between the two points I highlighted here.
With implementation inheritance, we inherit not only the interface, but also the
implementation.
With interface inheritance, we only inherit the interface all the interface is allowed to have is
signatures with no implementations. Thus with interface inheritance we can't inherit any
implementations.
At the first sight, interface inheritance is not very useful it requires us to write code for every interface
signature in each class that implements the interface. With interface inheritance, it would seem that all
the benefits of code reuse provided by implementation inheritance are no longer with us. So you may
ask why do we need interface inheritance?
Well, there might be times when that's exactly what you'll want to do: to force all derived classes to
implement all inherited behavior for themselves, as you don't want to provide any "base"
implementation. But that's what you could achieve with implementation inheritance, using abstract
classes that contained exclusively abstract members.
There is a subtle, but very important difference between inheriting from abstract base classes, and
inheriting from interfaces. Remember that implementation inheritance could be verified with the "is a"
rule. A person is a student, an instructor is an employee. This was true because a class could only derive
from exactly one other class. With interface inheritance, a class can implement more interfaces, while
still being able to derive from a base class interfaces are "smaller" pieces of "functionality
requirements" that we apply to a class in order to make sure the class implements all the interface's
members. Inheritance relationship established by using interfaces cannot usually be verified with the "is
a" rule; instead, "acts as" is perhaps better suited for this.
188
189
Chapter 5
Person
<<interface>>
IEmployee
#name : string
+Person(in name : string)
+DescribeYourself()
Student
+DescribeYourself()
+Student(in name : string)
+Learn()
+StartWorking()
Manager
Instructor
+DescribeYourself()
+StartWorking()
+Manager(in name : string)
+DescribeYourself()
+StartWorking()
+Instructor(in name : string)
+Teach()
IEmployee
IEmployee
As I warned you, we won't be using Employee any more. Student, Manager, and Instructor derive
directly from Person. Manager and Instructor implement the IEmployee interface. Before
proceeding, please make sure you have the code we used in the Abstract Classes, Methods, and Properties section.
Since there are more steps that need to be taken in order to upgrade the existing program, I'll mark
them so they are easier to follow:
1.
No matter how painful this might be, remove the abstract Employee class.
2.
Add the IEmployee interface. For more clarity, I suggest you to add it at the beginning of the
file. Interfaces, like base classes, are generally situated before other derived classes.
interface IEmployee
{
void StartWorking();
}
3.
190
4.
5.
Now execute the program. The output should be the same as the last time we ran the program.
Hello man, I'm Joey. I'm a cool student.
Good morning class. My name is Mr. Bean, and I am your instructor.
I'm Mr. Listentomecarefully. I'm the boss around here.
Going to my dear students...
No, it's my golf day.
Press any key to continue
Probably the most important change we made to the code was the addition of IEmployee:
interface IEmployee
{
void StartWorking();
}
191
Chapter 5
The syntax to declare an interface is simple, using the interface keyword. The interface members
look like abstract members, because they don't have an implementation. Still, they aren't allowed to be
explicitly declared as abstract in fact, we can only specify a return value. They are automatically
public, but we can't use the public (or any other) modifier with them.
Now that we have seen how to declare an interface, let's see how to actually implement it in a class:
class Instructor: Person, IEmployee
This line specifies that Instructor derives from Person, and implements IEmployee. We don't
have to specify a class to derive from before specifying the interfaces: in fact, Person and IEmployee
could both be interfaces, but we know that in our case Person is a class. Anyway, if there's a class we
inherit from, it must be specified first, before any interfaces.
Then we defined the StartWorking() method.
public void StartWorking()
Notice that compared to its previous version, we removed the override keyword from it this is
required, since we aren't overriding any method. Also note that, because we're implementing
IEmployee, we need to provide an implementation of StartWorking() in Instructor and
Manager if we didn't implement this method, we would break the contract we agree to when we said
we would implement the interface. If you remove StartWorking() from the Manager class, you'll
receive an error message specifying that 'PersonExample.Manager' does not implement interface
member 'PersonExample.IEmployee.StartWorking()'.
After we successfully implement the interfaces, we can use an interface reference to access its members.
These lines are extracted from the Main() method, and show how an interface reference can be used to
access an implementing class's members through polymorphism.
IEmployee employee;
Instructor instructor = new Instructor("Mr. Bean");
Manager manager = new Manager("Mr. Listentomecarefully");
// apply polymorphism using IEmployee reference
employee = instructor;
employee.StartWorking();
employee = manager;
employee.StartWorking();
Note that through an IEmployee reference we can only call the StartWorking() method, plus the
four members of System.Object, which every object has and we talk about later in this chapter. For
now, just think that through an interface reference we can only access the members defined by that
interface. The following calls would result in a compile-time error, no matter what the real object
behind employee is:
employee.Teach();
employee.DescribeYourself();
192
We decide that it would be very useful to have an interface, named ISelfDescribingObject, that
would contain the DescribeYourself() method. We would implement this interface on classes that
should have the ability to describe themselves.
At this point, I think you may be wondering about one thing: Why do we need an interface? Why don't
we simply implement DescribeYourself() in the classes we want, without the trouble of adding an
interface? For now, please just follow the steps to implement this idea. We'll see in the next section how
this can be useful in real-world situations.
This is what we want to achieve:
Person
#name : string
+Person(in name : string)
Student
+DescribeYourself()
+Student(in name : string)
+Learn()
<<interface>>
ISelfDescribingObject
<<interface>>
IEmployee
+DescribeYourself()
+StartWorking()
Instructor
Manager
+DescribeYourself()
+StartWorking()
+Instructor(in name : string)
+Teach()
+StartWorking()
+Manager(in name : string)
IEmployee
ISelfDescribingObject
IEmployee
ISelfDescribingObject
193
Chapter 5
Note that with this new version, we deliberately didn't implement ISelfDescribingObject in
Manager. For our experiments, we'll need a class that doesn't implement this interface. In order to
implement this design, follow the steps:
1.
Remove the definition for DescribeYourself() from the Person class. The class should
now look like:
2.
interface ISelfDescribingObject
{
void DescribeYourself();
}
3.
Note that the rest of the code doesn't need to change. Since we have a DescribeYourself()
method, we are honoring the ISelfDescribingObject contract.
4.
5.
194
We don't want to provide the Manager class with the ability to describe itself. Just remove or
comment out the DescribeYourself() method.
The final step is to modify the Main() method of MyExecutableClass, because the old
version calls DescribeYourself() on Person objects, which is no longer possible.
Modify Main() to:
static void Main(string[] args)
{
IEmployee employee;
ISelfDescribingObject sdo;
Student student = new Student("Joey");
Instructor instructor = new Instructor("Mr. Bean");
Manager manager = new Manager("Mr. Listentomecarefully");
sdo = instructor;
sdo.DescribeYourself();
sdo = student;
sdo.DescribeYourself();
employee = instructor;
employee.StartWorking();
employee = manager;
employee.StartWorking();
}
If you understood the previous example, you should have no problem understanding this one too.
Probably the most interesting part is the new Instructor's definition, which now derives from
Person, but also implements ISelfDescribingObject and IEmployee.
Interface Recap
Before we move on, let's review some of the key points about interfaces:
An interface is set of property and method signatures, and serves as a contract for classes that
implement it.
When a class implements an interface, it guarantees that it will implement every signature
defined in that interface.
With interface inheritance, we only inherit the interface the interface is only allowed to have
signatures with no implementations. Thus with interface inheritance we can't inherit any
implementations.
195
Chapter 5
Here, we access Name and DescribeYourself() on an object named student. Name and
DescribeYourself() are instance members of the Student class. Name is an instance field (or
perhaps property), and DescribeYourself() is an instance method.
Static class members (fields, methods, and properties) are members that don't belong to a particular
class instance, but rather to the class as a whole. Before implementing static members in an example,
let's first look at a very popular static member that we used many times:
Console.WriteLine ("very interesting text here");
The WriteLine method of the Console class is one of our dearest friends, at least in this chapter.
However, we never instantiate a Console object, and then call the WriteLine method on the object:
Console c = new Console(); // this never happens
c.WriteLine(); // this never happens
Instead, we call WriteLine directly on the Console class. This is a hint for us that WriteLine is a
static method of Console. The Console class was created as a utility class that shouldn't be
instantiated, and provides us with some general-use members. There are a few more things to note
about the Console class, in regard to the previous code snippet:
196
Console cannot be inherited from because it is sealed. Utility classes are a good example
of classes that are in their 'final' version and we don't want anybody to derive from them.
Even if we could create a Console instance, WriteLine() could not be called on that
instance. Static members can't be accessed using a class's instance.
Now that we have met a few popular static fields, let's play a bit with our dear Person and create our
own static members.
197
Chapter 5
private static int howManyPersons = 0;
public Person(string name)
{
this.name = name;
howManyPersons++;
}
public static void ShowHowManyPersons()
{
Console.WriteLine("{0} persons created so far.", howManyPersons);
}
public virtual void DescribeYourself()
{
Console.WriteLine
("Hello! My name is {0} and I am a nice person.", name);
}
}
The important lines are highlighted. Note that DescribeYourself() is not abstract any more (as it
was in our last example). For the purposes of this example, it's better to have an implementation for
DescribeYourself(). In this example we'll also use the Student class. The Student class should
already look like this, if it doesn't then please change it:
class Student: Person
{
public Student(string name): base(name)
{}
198
What's going on here? The whole magic is being done in the Person class, so we'll analyze it first. At
the heart of the counting mechanism is a static field named howManyPersons. Static class members
are declared using the static modifier:
private static int howManyPersons = 0;
The initial value is zero, and we increase it in Person's instance constructor meaning that it will be
increased every time we create a new instance of Person:
public Person(string name)
{
this.name = name;
howManyPersons++;
}
199
Chapter 5
howManyPersons is a field private, so we won't need to deal with it directly from outside. Instead, we
have a static method that writes the number of Person objects created to the console:
public static void ShowHowManyPersons()
{
Console.WriteLine("{0} persons created so far.", howManyPersons);
}
Looking at this method, you can see that it looks just like a non-static method. But because it is static,
we can't call it like we call an instance method instead we call it using the class's name. We can also
call ShowHowManyPersons() on Student, since Student inherits from Person:
static void Main(string[] args)
{
Person firstPerson = new Person("Eddie");
firstPerson.DescribeYourself();
Person.ShowHowManyPersons();
// ...
Student secondStudent = new Student("Chris");
secondStudent.DescribeYourself();
Student.ShowHowManyPersons();
// ...
}
The method is not static any more, but it can still access howManyPersons, which is a static field. This
is because there's no reason why a static field wouldn't be accessible to the class's instance members.
200
We learned earlier in the previous chapter that when we create a Student object, Person's
constructor is called first. The program demonstrates that value of howManyPersons increases even
when we create Student objects, demonstrating that the Person constructor does get called.
201
Chapter 5
Static Constructors
C# possess a feature called static constructors. Static constructors are used to initialize any static
members the class might have. We're not allowed to access any instance members from the static
constructor, so we can only play with class's static data.
A static constructor belongs to the class as a whole, so it gets called only once in an application, not
every time an instance of the class is created. It's hard to be sure exactly when the static constructor gets
called, but we know it will be before any static members are used, and before any instances of the class
are created. Most of the time the static constructor is called immediately before the class is used for the
first time.
Static constructors are not allowed to have a return type, parameters, or access modifiers. None of these
would make sense in the context of a static constructor. A class can have a static constructor in addition
to its instance constructors. There is no conflict between a parameterless instance constructor and a
static constructor.
For a simple example, we will continue to work on the scenario we imagined in the Adding Static
Members to the Person Class section. This time we want to improve our Person class so that it knows the
maximum number of persons that can be created overall. This number would be loaded from a
database, and stored in a static class field named maxPersons. If the maximum number of Persons is
exceeded, a warning message should be displayed on the screen.
If this seems scary, don't worry. We won't really use a database in this chapter although we will learn
how to later in the book. For this example, we'll assume that the maximum number of persons read
from the database is 2. Modify the Person class like this:
class Person
{
protected string name;
private static int howManyPersons;
private static int maxPersons;
public Person(string name)
{
this.name = name;
if (++howManyPersons>maxPersons)
Console.WriteLine("Warning! Maximum number of persons exceeded!");
}
static Person()
{
howManyPersons = 0;
maxPersons = 2;
}
public static void ShowHowManyPersons()
{
Console.WriteLine
("{0} persons created so far.", Person.howManyPersons);
202
Since we've learned a lot about static fields, the way that this works should be obvious. Just notice the
syntax used to declare the static constructors, and observe that the static fields really get initialized.
After the first two persons are created, the instance constructor of Person displays the warning message
for every additional person created.
Destructors
The destructor is a special kind of method that gets called when an object is removed from memory.
This usually happens when the object goes out of scope, and the destructor's role is to allow the object
to free any resources it has been using, before being finally deleted from memory. In Visual Basic there
was a method called Class_Finalize that served a similar purpose, although C# destructors work in
a very different way.
The most important thing to know about C# destructors is that their execution is not deterministic: after
the object goes out of scope, it is only marked for destruction. The Garbage Collector, which is a
sophisticated memory management mechanism implemented by the .NET runtime, every so often frees
the memory consumed by unused objects. The object's destructor is invoked by the Garbage Collector
right before the object is actually removed from memory.
If your object holds important resources, such as database connections to a server, it is important for the
object to start the resource-releasing sequence as soon as it goes out of scope. This kind of behavior is
implemented using a method named Dispose(). This is an advanced topic which will not be covered
in this book. Don't worry there are still many interesting things to learn.
203
Chapter 5
Let's implement a destructor in the Person class to provide some functionality that has long been
needed. In our previous examples we used a static field named howManyPersons that increased
every time we created a new person. However, the design didn't consider the possibility that some
objects might actually be destroyed. howManyPersons would never decrease. This would reduce the
chance of the warning message appearing, because out-of-scope objects would no longer count towards
the total.
Please update the Person class from the previous example by adding a destructor like this:
class Person
{
protected string name;
private static int howManyPersons;
private static int maxPersons;
public Person(string name)
{
this.name = name;
if (++howManyPersons>maxPersons)
Console.WriteLine("Warning! Maximum number of persons exceeded!");
}
static Person()
{
howManyPersons = 0;
maxPersons = 2;
}
~Person()
{
howManyPersons--;
Console.WriteLine
("{0} going offline. {1} active persons remaining.", name, howManyPersons);
}
public static void ShowHowManyPersons()
{
Console.WriteLine("{0} persons created so far.", Person.howManyPersons);
}
public virtual void DescribeYourself()
{
Console.WriteLine
("Hello! My name is {0} and I am a nice person.", name);
}
}
204
Even if destructors are usually called at mysterious times chosen by the Garbage Collector, they are
always executed before the program completes execution. This explains why all destructors are called
when our program terminates executing. The order in which the objects are destroyed cannot be always
predetermined, so you might see your objects 'disappear' in a different order.
That would be it no more, no less you could have a fully working program, even with some
functionality, with just three lines of code. Indeed, Visual Basic 6 does a very good job of hiding what
happens behind the scenes. This is very good because it makes application programming easy but it
places many constraints on programmers who want more flexibility.
205
Chapter 5
Visual Studio .NET does the same great job of automatically generating code, still the code is more involved
now and you can no longer have a fully working button with only three lines of code. This section aims to
teach you what you need to know in order to use delegates and events in your programs.
Because Visual C# does a good job at hiding most details of handling events and delegates, it is not
required to understand them in order to create Windows Form Applications. But as with any other
concept presented in this chapter, learning delegates will make you a stronger programmer, because
it allows you to understand why button1_Click() is called when you click on that button.
Delegates
A bit earlier I said that delegates represent a special kind of data type. Why are they so special, you
might ask? Well, while int variables store numbers, and objects store class instances, delegates store
references to methods.
Just as we can send an integer, a string, or an object as a method parameter, delegates allow us to send
methods as parameters. Sounds exciting? It really is!
Delegates are used when a class must call a method on another class, but doesn't know at compile-time
what that method is. Since a delegate instance references a particular method, it can be sent as a
parameter to another method; the method that receives the delegate as a parameter can use it to call the
original method the delegate is pointing to.
Although delegates are references to methods, thus acting like methods, technically they are a kind of
classes in fact, all delegates derive from a class named System.Delegate. There is yet another kind
of delegate, called a multicast delegate, that derives from System.MulticastDelegate which in
turn derives from System.Delegate. Multicast delegates are a special kind of delegates that can hold
references for more than one method we'll see an example soon.
Also, since a delegate is a data type, it means that it can be instantiated, just like a class. Unlike a
classes, we don't have terminology to differentiate between a delegate and a delegate instance. The
delegate type specifies the kind of methods its instances will reference. For example, we can have a
delegate that is defined to work for methods that take no parameters and have no output values. We can
then create any number of delegate instances, every instance referencing (probably different, but not
necessarily) methods that comply with the delegate's definition (no parameters, no return values).
Since this is your first exposure to delegates, all this theory might be a bit confusing at first. Let's start
writing some code, and everything should become clearer.
206
1.
Note that in a real-world application, the signal sent by the Person would contain a lot of mysterious
codes and data that would be understood by MyExecutableClass; for our current example, we'll just
send out the warning message's text.
In this example the method to be called is a static method, but be aware that delegates also work with
instance methods.
2.
We will need to instruct Person to pass the warning message to this method instead of
displaying it on the screen. For this, we need to declare a delegate. Add
WarningEventHandler to the PersonExample namespace, not inside any class. It must
be visible from all classes that use it, so it's easier to place it in the same namespace with
Person and MyExecutableClass (otherwise, you would need to import its namespace or
use its fully qualified name in those classes).
207
Chapter 5
This defines a delegate type to be used for methods that have no return values, and receive a string as
a parameter. Since ProcessWarningMessage exactly fits this description, we'll be able to create an
instance of the WarningEventHandler delegate and make it reference
ProcessWarningMessage().
Note that the delegate type WarningEventHandler does not reference any method by itself; it is just
a type, a "blueprint", just like a class. In order to make it reference actual methods, we must create
instances of this delegate.
3.
Now that we have a delegate to use for transmitting the warning messages, let's modify
Person to use it. There are three sub-steps that we must follow here:
First, we need to add in Person a private field of the type of the delegate. We'll use this
delegate to call ProcessWarningMessage() of MyExecutableClass:
class Person
{
protected string name;
private static int howManyPersons;
private static int maxPersons;
private WarningEventHandler warningMethodToCall;
Let me highlight once again that both WarningEventHandler and warningMethodToCall are
referred to as delegates even if WarningEventHandler is the type, and warningMethodToCall is
an instance of that type. With classes, we have a separate name for their instances objects. For
delegates, we use the same name to refer to the type or to its instances. Keep this in mind when reading
technical documentation about delegates, including this section of the chapter.
Our third and final sub-step is to instruct the destructor of Person to use the delegate instead
of writing the destruction message to screen:
~Person()
{
howManyPersons--;
warningMethodToCall(
String.Format("{0} going offline. {1} active persons remaining.",
name, howManyPersons));
}
208
At this point it is interesting to see the syntax used to create a new delegate instance:
WarningEventHandler warning = new WarningEventHandler(ProcessWarningMessage);
It looks similar to the syntax for creating objects. The method to be referenced is sent as a parameter.
Note that in this case we're using a static method, so no class instance is required. If
ProcessWarningMessage() was an instance method, we would need to create a class instance, and
attach the method of that instance to the delegate.
OK, that's it. We have done quite a number of changes, so here is the whole program, implemented as a
single file:
using System;
namespace PersonExample
{
delegate void WarningEventHandler(string message);
class MyExecutableClass
{
static void Main(string[] args)
{
WarningEventHandler warning = new
WarningEventHandler(ProcessWarningMessage);
Person firstPerson = new Person("Eddie", warning);
firstPerson.DescribeYourself();
Person secondPerson = new Person("Mike", warning);
secondPerson.DescribeYourself();
209
Chapter 5
Person thirdPerson = new Person("Doug", warning);
thirdPerson.DescribeYourself();
}
static void ProcessWarningMessage(string warning)
{
// very complicated code here
Console.WriteLine("Warning: " + warning);
}
}
class Person
{
protected string name;
private static int howManyPersons;
private static int maxPersons;
private WarningEventHandler warningMethodToCall;
public Person(string name, WarningEventHandler method)
{
warningMethodToCall = method;
this.name = name;
if (++howManyPersons>maxPersons)
warningMethodToCall("Maximum number of persons exceeded!");
}
static Person()
{
howManyPersons = 0;
maxPersons = 2;
}
~Person()
{
howManyPersons--;
warningMethodToCall
(String.Format("{0} going offline. {1} active persons remaining.",
name, howManyPersons));
}
public static void ShowHowManyPersons()
{
Console.WriteLine("{0} persons created so far.", Person.howManyPersons);
}
public virtual void DescribeYourself()
{
Console.WriteLine
("Hello! My name is {0} and I am a nice person.", name);
}
}
}
210
Multicast Delegates
A multicast delegate is a delegate that holds a reference not to one, but multiple methods. We won't go
into too much detail here, and using a multicast delegate doesn't help too much in our Person example
anyway. In order to use a multicast delegate, modify MyExecutableClass to:
class MyExecutableClass
{
static void Main(string[] args)
{
WarningDelegate delegate1 = new WarningDelegate(FirstMethod);
WarningDelegate delegate2 = new WarningDelegate(SecondMethod);
WarningDelegate multicast = delegate1 + delegate2;
Person firstPerson = new Person("Eddie", multicast);
firstPerson.DescribeYourself();
Person secondPerson = new Person("Mike", multicast);
secondPerson.DescribeYourself();
Person thirdPerson = new Person("Doug", multicast);
thirdPerson.DescribeYourself();
}
static void FirstMethod(string warning)
{
Console.WriteLine("Method1: " + warning);
}
static void SecondMethod(string warning)
{
Console.WriteLine("Method2: " + warning);
}
}
211
Chapter 5
The output is:
Hello! My name is Eddie and I am a nice person.
Hello! My name is Mike and I am a nice person.
Method1: Maximum number of persons exceeded!
Method2: Maximum number of persons exceeded!
Hello! My name is Doug and I am a nice person.
Method1: Doug going offline. 2 active persons remaining.
Method2: Doug going offline. 2 active persons remaining.
Method1: Eddie going offline. 1 active persons remaining.
Method2: Eddie going offline. 1 active persons remaining.
Method1: Mike going offline. 0 active persons remaining.
Method2: Mike going offline. 0 active persons remaining.
Press any key to continue
Events
Events play a very important role in programming for the Windows platform because they are the main
means by which an application interacts with the user, but they are not limited to this area. They can be
used in situations where an executing object needs to send signals to a number of other objects that may
be listening.
Let's briefly see how events work by looking at the typical Button example. When a user clicks on a
Button, that button raises an event saying that it was clicked on. If the program that includes the
Button has subscribed to its Click event (meaning that it is listening for this event), it responds to the
event by executing its associated event handler method.
Any class can generate many different kinds of events. Programs that use that class are able to subscribe
to a number of those events; when a program subscribes to an event, it will be notified when the class
generates that event. The class notifies its listeners that it's raising the event by calling a method on
them (sounds familiar?), and this is implemented by using delegates.
After you have seen how to use delegates and how we can allow a class to call a method of another class
through a delegate, you might be wondering why events are useful, after all the same functionality can
be achieved by only using delegates. Why complicate things further by introducing yet another concept?
212
1.
Let's first add the class that will wrap the text message. As I told you earlier, this is a
requirement for events. This class could contain many methods and properties, but in our case
it is very simple because we only want to store a simple string. The naming
recommendation for this class is to add EventArgs as a suffix on the event's name, so let me
present the WarningEventArgs class:
213
Chapter 5
class WarningEventArgs : EventArgs
{
private string message;
public string Message
{
get { return message; }
}
public WarningEventArgs(string warning)
{
this.message = warning;
}
}
2.
Now we need to modify the delegate definition. In the previous program, it looked like this:
The delegate now has two parameters, and no return value. This is a requirement to use this delegate for
the generation of events. The first parameter is the object that raised the event and the second must be
an object derived from System.EventArgs. The suggested naming convention for the event handler
is to append EventHandler to the event's name.
Observe that the second parameter is a WarningEventArgs object. This is no great surprise, because
I mentioned earlier we can't send the warning message directly, but a WarningEventArgs object that
contains the warning message. But what about the first parameter? Since events are very flexible in
nature and work for various, unrelated classes, it's important to know which object has raised the event.
As you'll see, in MyExecutableClass we'll create several Person objects that will use a single event
handler method.
When a method is used as an event handler for more than one object, we'll use the sender argument to
find out what object generated the event. We'll see a bit later how to do that.
3.
Now, let's add the Warning event declaration to the Person class:
214
In the Delegates section code, we used warningMethodToCall, which was an instance of the
WarningEventHandler delegate. Now, instead of using a delegate instance, we're raising events.
Warning can also be seen as an instance of WarningEventHandler, and it is used in a similar way,
but now it is an event and is defined using the event keyword.
1.
Instead of raising the event directly using Warning, it is recommended to add an additional
method in your code, which in turn raises the Warning event. This method is typically
named with On as a prefix to the event's name:
The important thing to note here is that we check to see if Warning is not null this is a check to see
if anybody is actually listening for the event with a suitable event handler. If there were no methods
associated with the WarningEventHandler delegate, a run-time error would be generated, and so the
check to see if Warning is not null will avoid this.
As you can see, the recommendation to implement this method is quite useful. It's easier to call
OnWarning() instead of calling Warning() directly and testing every time to see if there is somebody
actually listening to the event.
2.
We need to perform some housekeeping in Person, and update it to use OnWarning(). The
Person instance constructor should become:
As you can see, there is no sign of the delegate instance as a parameter now. Events can be associated at
any point of time, not just in the constructor or in a separate method. We'll see how to listen to events a
bit later, when coding MyExecutableClass.
In the previous version, we sent a warning message in the Person constructor. We aren't doing the
same here, because a Person event can be assigned an event handler in MyExecutableClass only
after Person is created. So if we tried to raise the event here in the constructor, it wouldn't make much
difference, because nobody's listening yet.
215
Chapter 5
Feel free to remove static int maxPersons and the static constructor from Person, as we won't use
them any more. Also, feel free to remove ShowHowManyPersons() which will not be used any more.
3.
Modify the Person destructor as follows. We want it to raise an event and warn us when the
object is being garbage-collected:
~Person()
{
howManyPersons--;
string str = String.Format("{0} going offline. {1} active persons remaining.",
name, howManyPersons);
OnWarning (new WarningEventArgs(str));
}
4.
Right now, the only place in Person that generates events is the destructor. It isn't much fun
to send warning messages only from the destructor, and using our current Person there
wouldn't be many places where it would make sense to raise a warning event. Add the
following method to Person:
When we want Person to raise an event, we can ask, politely, the PleaseGenerateEvent()
method. Now we have two places in Person that raise the Warning event the destructor and our
brand-new coded method.
5.
Now that we have a well behaved Person that generates events, let's modify
MyExecutableClass to catch these events.
using System;
namespace PersonExample
{
class MyExecutableClass
{
static void Main(string[] args)
{
Person eddie = new Person("Eddie");
eddie.Warning += new WarningEventHandler (Warn);
eddie.PleaseGenerateEvent();
Person mike = new Person("Mike");
mike.Warning += new WarningEventHandler (Warn);
Person julia = new Person("Julia");
julia.Warning += new WarningEventHandler (GirlsWarn);
julia.PleaseGenerateEvent();
}
216
OK, that's it. Execute the program, and expect to see this output:
Warning: Eddie generated this event!
A Girl warning: Julia generated this event!
A Girl warning: Julia going offline. 2 active persons remaining.
Warning: Eddie going offline. 1 active persons remaining.
Warning: Mike going offline. 0 active persons remaining.
Press any key to continue
The most important thing to note about this code is how a program can enlist to receive events
generated by an object:
eddie.Warning += new WarningEventHandler (Warn);
This line instructs that whenever eddie generates a Warning event, the Warn() method of
MyExecutableClass gets called. Note that for two objects we use one event handler method
(Warn()), and for our third object we used another event handler (GirlsWarn()).
Now where have you seen this syntax before? That's right, it's the same syntax that you'll find in the
code automatically generated by Visual Studio .NET when, for example, you double-click on a Button
in the designer and the event-handler is added:
this.button1.Click += new System.EventHandler(this.button1_Click);
Hopefully, this section should have helped you begin to understand what Visual Studio .NET is creating
for you behind the scenes.
I mentioned earlier that if the same method is used as an event handler for multiple objects, then we can
use the sender parameter to discover who the original sender of the event is. For example, we could
change Warn() to:
static void Warn(object sender, WarningEventArgs warn)
{
if (sender is Person)
{
Console.WriteLine("Message from:");
Person p = sender as Person;
217
Chapter 5
p.DescribeYourself();
Console.WriteLine("Warning: " + warn.Message);
}
else
{
Console.WriteLine("Message from unknown type of sender");
Console.WriteLine("Warning: " + warn.Message);
}
}
The method now checks if sender is a Person object (we saw the is keyword in Chapter 3 and
earlier in this chapter), and if so, retrieves the Person object using the as keyword, and then calls an
appropriate method on this object. The sender is converted into a Person object with sender as
Person we've seen this technique earlier in the chapter as well.
In Chapter 3 we actually made extensive use of this technique in SweepCSharp remember how we
cast sender to a Button to extract information about the Button that was clicked, and also how we
used the System.Windows.Forms.MouseEventArgs parameter of our MouseDown event handler,
OnMouseDown(), to determine which mouse button was clicked? These examples demonstrated the
kind of information that can be carried in the two parameters of an event handler.
Constants
Constants are fields marked with the const keyword, and represent a value that cannot be changed at
run-time. Their value must be known at compile-time. The C# compiler replaces occurrences of each
constant in your code with the literal value, which means that we can only use basic data types (such as
strings or numbers) as constants. A constant cannot be set based on the value of a variable, or the result
of a method:
public int value = 13;
public const int MinAge = value; // not allowed
but it can be based on another constant, or simple calculations, because these can be evaluated at
compile-time:
public const int MinAge = 15;
public const int MaxAge = MinAge + 100; // allowed
218
219
Chapter 5
The output should be:
Age set to: 50
Invalid value: 150
Minimum age: 15
Press any key to continue
The MinAge and MaxAge constants are public. We can see in the Main method that constants are,
indeed, static:
Console.WriteLine ("Minimum age: " + Person.MinAge);
If you try to reference MinAge or MaxAge using a Person instance, you'll receive a compiler error:
Console.WriteLine ("Minimum age: " + myPerson.MinAge); // error!
Because constants are static, we can't use the this keyword to reference them within their class. This is
not correct:
if (value >= this.MinAge && value <= this.MaxAge) // error!
Constants are great because they make code easier to read and easier to modify. Every programmer has
bad memories of times when they haven't used constants properly. However, often we want an
unchangeable value that we can only figure out at run-time (for example, if it's based on user input). In
this case, we can use readonly fields let's look at them now.
Read-Only Fields
We've seen that with constants, we need to know their exact value at write time. Sometimes, though, we
want to be able to set the value of a field based on the result of a method, or something else not known
when we're writing the program, and then prevent it from changing again. To achieve this we can use
readonly fields.
A readonly field provides more flexibility than a constant because it allows you to set its initial value
in the constructor of the class that contains it. In fact, we can change its value as many times as we like,
but only in the constructor. Another degree of a readonly field's flexibility over constants is that we
are allowed to have readonly fields of complex types (such as Person), while constants are limited to
simple types.
220
To demonstrate using readonly fields, let's modify the previous example to use instance readonly fields:
using System;
class MyExecutableClass
{
static void Main(string[] args)
{
Person person = new Person(18, 65);
person.Age = 22;
person.Age = 15;
person.Age = 70;
}
}
class Person
{
private readonly int MinAge;
private readonly int MaxAge;
private int age;
public Person (int MinAge, int MaxAge)
{
Console.WriteLine ("Instance constructor of Person:");
Console.WriteLine ("Setting MinAge to: " + MinAge);
this.MinAge = MinAge;
Console.WriteLine ("Setting MaxAge to: " + MaxAge);
this.MaxAge = MaxAge;
}
public int Age
{
get
{
return age;
}
set
221
Chapter 5
{
const int x = 10;
if (value >= this.MinAge && value <= this.MaxAge)
{
age = value;
Console.WriteLine ("Age set to: " + value);
}
else
Console.WriteLine ("Invalid value: " + value);
}
}
}
If you try to modify readonly fields somewhere else in the program than in the constructor, the
compiler will throw this error: A readonly field cannot be assigned to (except in a constructor or a
variable initializer).
222
In .NET, the heap is called a managed heap because programmers don't have to explicitly destroy (free
memory used by) objects they created. With lower-level programming languages such as C++, after
finishing working with an object, we must take care of releasing the memory that was occupied. The
object itself must know how to release all the resources and free all the memory it occupies. In .NET the
Garbage Collector takes care of freeing the memory when we no longer use an object that was
previously stored on the heap (a typical example is an object that was created in a method, and went out
of scope after the method finished executing). We already met the Garbage Collector a few pages ago,
in the Destructors section.
In the following sections we'll see several code samples that help us better understand the nature of
value types and reference types. We won't investigate the low-level differences between storing data on
the stack versus storing on the heap, but we'll see enough to provide a solid understanding of the
implications of using value types and reference types.
When we declare two variables like this, we'll have the value '10' stored in two different places in
memory (on the stack). On the other hand, when we copy an object to another object, only the
reference is copied. We will have two separate copies of the reference on the stack, but both pointing to
the same block of memory on the heap:
Person firstObject = new Person(10);
Person secondObject = firstObject;
The objects we create are likely to have many value types among their members. These value types are
also stored on the heap, as part of the object they belong to. However, most of the principles about
value types continue to apply, even if the value type happens to be stored on the heap.
We can also copy an object reference over the top of another object that has already been created in
memory. In this case, the object constructed with Person(50) is lost and its memory will be freed by the
Garbage Collector. We end up again by having two separate variables referencing the same object in
memory:
Person firstObject = new Person(10);
Person secondObject = new Person(50);
secondObject = firstObject;
223
Chapter 5
Let's start playing with value types and reference types using a simple example. We use two variables of
type int and two objects of type Person, and we apply the same actions on them. However, in the end
the results are not the same:
using System;
class MyExecutableClass
{
static void Main(string[] args)
{
int firstValue = 10;
Person firstObject = new Person(10);
int secondValue = firstValue;
Person secondObject = firstObject;
secondValue = 20;
secondObject.Age = 20;
Console.WriteLine("firstValue = " + firstValue);
Console.WriteLine("secondValue = " + secondValue);
Console.WriteLine("firstObject.Age = " + firstObject.Age);
Console.WriteLine("secondObject.Age = " + secondObject.Age);
}
}
class Person
{
public int Age;
public Person(int Age)
{
this.Age = Age;
}
}
The key to understanding this program's output is to analyze what happens in these two lines:
int secondValue = firstValue;
Person secondObject = firstObject;
224
In C# all method parameters, regardless of whether they are value types or reference
types, are passed by value by default. Visual Basic 6 passes parameters by reference by
default.
But passing by value for value types has a different meaning from passing by value for reference types.
For more clarification, let's examine each scenario, one at a time.
When executed, this program would show a message box containing the value 100. This demonstrates
that changing the value in DoSomething method has effect on the original variable defined in
Form_Load. This means that the value was sent by reference. Now change the definition of
DoSomething like this:
Public Sub DoSomething(ByVal parameter As Integer)
225
Chapter 5
The message would now contain the value of 50. This time the parameter was sent by value, meaning
that a copy of the value was sent to the subroutine, not a reference to the original variable.
By default, Visual Basic 6 passes parameters by reference and C# passes by value. To demonstrate this
difference, let's translate the code to C#:
using System;
class MyExecutableClass
{
static void Main(string[] args)
{
int value = 50;
DoSomething (value);
Console.WriteLine (value);
}
static void DoSomething(int parameter)
{
parameter = 100;
}
}
The program's output (50) confirms that the variable is sent by value, not by reference. We didn't need
to add any special keyword we relied on the default behavior of C# passing a value type by value.
We must use the ref keyword when passing the parameter as well:
DoSomething (ref value);
Here's the complete code that demonstrates using ref parameters for value types:
using System;
class MyExecutableClass
{
226
227
Chapter 5
public Person(int Age)
{
this.Age = Age;
}
}
The output shows 100, exactly what we expect. However, now try changing the DoSomething()
method to this:
static void DoSomething(Person somePerson)
{
somePerson = new Person (100);
}
When you rerun the program, the result is again 50. But why? Didn't we send the reference to the
method?
The answer is: yes, we did send the reference. This is great as long as we don't change the reference.
Remember that reference types have two elements to them the reference, and the object. Now, when
we called the DoSomething() method we created a copy of the reference. It still pointed to the same
object, so changes to the object did affect the main program. But changes to the reference do not
when the method finishes, the copy of the reference simply disappears.
When we use the somePerson.Age property to change the age, we are changing the object itself. But
in the second attempt we create a new object, and change the reference to point to it this change to the
reference will be lost.
So what should be done? You guessed it: the solution is to send the reference type by reference. By
doing that, if we change the reference stored by somePerson, there is another "parent" reference that
is automatically updated. Sounds confusing? Let's look at the idea in more detail.
228
This time, the program's output will be 100, which is what we want. Let's take a look at what's going on
here. The person variable is really a reference to a Person object that is on the heap. When we call
DoSomething(), the compiler creates a reference to the person reference (rather than a reference to
the Person object that it points to). Within the DoSomething() method, then, somePerson is a
reference to the Person reference, which in turn references the object on the heap. However,
DoSomething() knows that the value was passed by reference and therefore any changes made to
somePerson are really made to person. The practical upshot of this is that somePerson behaves as
if it were the person reference, not a copy of it.
So far we've concentrated on designing classes and creating objects. We've seen how objects are
reference types, and primitive data types are value types. However, C# enables us to create our own
value types called structs, as we will see now.
Structs
A struct is a user-defined value data type very similar to the class it can contain constructors, fields,
methods, and properties. Structs are declared using the struct keyword instead of class.
Let's examine the most important differences between structs and classes:
Inheritance doesn't work with structs: a struct cannot derive from a class or another struct; a
class cannot derive from a struct. All structs are implicitly derived from System.ValueType
(which in turn is derived from System.Object we'll see more details about this a bit later
in this chapter).
Structs always contain a parameterless, default constructor. You are allowed to add more
overloads, but you can't add a parameterless constructor.
You are not allowed to provide default values for structs' fields.
You don't have to create a new struct using the new keyword (because the struct is a value
type). If you don't use new, the default, parameterless constructor is called when creating the
struct. However, you'll need to use new keyword if you want to supply parameters to a struct's
constructor, just like you do for classes.
229
Chapter 5
Although structs are very powerful, they are mainly designed to act as containers for data rather than as
full-featured objects. Because structs are value types, they are stored on the stack. We didn't discuss in
depth the difference between the stack (where value types are stored) and the managed heap (where
reference types are stored), but keep in mind that storing on the stack can be very fast if you're working
with small amounts of data. MSDN says that data structures smaller than 16 bytes may be handled more
efficiently as structs rather than as classes.
To demonstrate using a struct, we'll again use the example program from the Passing a Reference Type by
Value section. This is the program:
using System;
class MyExecutableClass
{
static void Main(string[] args)
{
Person person = new Person(50);
DoSomething (person);
Console.WriteLine (person.Age);
}
static void DoSomething(Person SomePerson)
{
SomePerson.Age = 100;
}
}
class Person
{
public int Age;
public Person(int Age)
{
this.Age = Age;
}
}
Remember that running this program resulted with the value 100 printed on the screen. Now instead of
a class use a struct:
struct Person
That's the only change you need to do! Rerun the program and admire the new output that shows 50.
This simple but interesting experiment shows us that structs are indeed value types, not reference types.
It's also very simple to test all the other properties of the struct. For example, try to add a parameterless
constructor in the Person struct:
public Person()
{
Age = 0;
}
The error generated by the compiler is: Structs cannot contain explicit parameterless constructors.
230
The error message is: 'Person.Age': cannot have instance field initializers in structs.
Of course, trying to initialize Age to zero would be completely useless, as it is automatically initialized
to zero anyway. But as you can see, you must use a parameterized constructor in order to initialize the
fields to some other values than their default ones. As a final experiment, you can even make
MyExecutableClass into a struct:
struct MyExecutableClass
Execute the program, and you'll see that it works just as before!
Enums
Enumerations are user-defined value data types used to store a set of named constants that are all of the
same data type. Enumerations were present in Visual Basic 6, and they are also present in C#. Also, like
in Visual Basic 6, we use the enum keyword to declare an enumeration.
The enum element's data type can be any of the predefined integral data types (signed or unsigned, 8-,
16-, 32- or 64-bit integer data types). The default enumeration's data type is int, and the default value
for the first element is 0.
Take a look at this example, where we have upgraded the Person struct (you can make it a class if you
want, it doesn't matter) to use enums:
using System;
public enum Age {From13to18, From18to25, From25to40, From41to65};
public enum Sex {Female, Male};
class MyExecutableClass
{
static void Main(string[] args)
{
Person person = new Person(Age.From41to65, Sex.Female);
Console.WriteLine("Age (string): " + person.Age);
Console.WriteLine("Age (int)
: " + (int)person.Age);
Console.WriteLine("Sex (string): " + person.Sex);
Console.WriteLine("Sex (int)
: " + (int)person.Sex);
}
}
231
Chapter 5
struct Person
{
public Age Age;
public Sex Sex;
public Person(Age age, Sex sex)
{
this.Age = age;
this.Sex = sex;
}
}
The output shows that an enum element knows both its string representation and its internal int value:
Age (string):
Age (int)
:
Sex (string):
Sex (int)
:
Press any key
From41to65
3
Female
0
to continue
We don't have to use an enum element's internal value in our program although the values are
internally stored as numeric values, we typically only use their string representation:
Person person = new Person(Age.From41to65, Sex.Female);
The program's output also shows that the default value for the first element of the enum is zero, and for
each following element it is increased by one. We can manually set the value of an element like this:
public enum Age {From13to18 = 10, From18to25, From25to40, From41to65};
From41to65
13
Female
0
to continue
We can choose the data type used for each enum value like this:
public enum Sex: byte {Female, Male};
232
233
Chapter 5
Don't worry, it's not important to know what every class in this list does. Actually, there are quite a few
classes that you'll use directly and often in your programs. Most of them are used in quite rare
situations. Anyway, the point is to observe that at the base of the whole hierarchy is System.Object.
Another important class you should know about is System.ValueType. Value types, including handmade structs and predefined types like int and char, implicitly derive from a class named
System.ValueType, which in turn derive from System.Object. We know that in C# int is an
alias for System.Int32. MSDN reveals the inheritance hierarchy for this object:
System.Object
System.ValueType
System.Int32
System.Object Members
Analyzing the System.Object members is very important, because all its public members
automatically become part of each class's public interface. Since every class derives from Object, we
can access each object's public member on any class.
The designers of .NET decided that there is a minimal functionality that must exist in every entity that
we call an object. Understanding this basic functionality that exists in each object in the .NET world
helps us understand that vision, and program in the .NET spirit.
In the next sections, we'll cover the System.Object public members:
Equals()
ReferenceEquals()
GetHashCode()
GetType()
ToString()
234
Equals()
Equals() returns a Boolean value that specifies if two object instances are equal. The method comes in
two flavors: a static version, and an instance version. Let's see an example that uses both:
class MyExecutableClass
{
static void Main(string[] args)
{
Person first = new Person(5);
Person second = new Person(5);
Person third = first;
Console.Write("first = second: ");
Console.WriteLine (first.Equals(second));
Console.Write("first = third: ");
Console.WriteLine (Object.Equals(first,third));
}
}
class Person
{
public int Age;
public Person(int age)
{
this.Age = age;
}
}
Keep in mind that for value types the value is compared, while for reference types the reference is
compared. The instance version of Equals() is declared as virtual, so we can override it in our own
classes. You may want to do this in your classes if you want to compare internal values rather than the
instance to make them behave just like it does for value types.
235
Chapter 5
In our example, both first and second objects are initialized with the same value:
Person first = new Person(5);
Person second = new Person(5);
Still, they are different objects, located in different memory locations. Because Person is a reference
type, Equals() evaluates the equality of object's reference, not their internal values, so the result is
that they are note equal:
first = second: False
However, if we "transform" our Person class to a value type, by making it a struct, the result changes:
first = second: True
first = third: True
Press any key to continue
This demonstrates that for value types, Equals() compares their value rather than their particular
instance, or in other words their location in memory.
We can override Equals() in our classes to make them behave like value types (at least as far as the
Equals() method is concerned). Let's override Equals() in our Person class. This is perfectly
possible, since Person derives from System.Object, and the instance implementation of Equals()
in System.Object is virtual:
public virtual bool Equals(object);
Since we have the complete method's definition, it is easy to override it. Modify Person like this:
class Person
{
public int Age;
public override bool Equals(object other)
{
if (other == null) return false;
if (GetType() == other.GetType())
{
Person otherPerson = (Person) other;
return (otherPerson.Age == this.Age);
}
return false;
}
public Person(int age)
{
this.Age = age;
}
}
236
Note that when you compile the program, you receive the following warning: 'Person' overrides
Object.Equals(object o) but does not override Object.GetHashCode(). This warning appears because
when you override Object.Equals(), sometimes you should also override
Object.GetHashCode(), so that when Object.Equals() returns true,
Object.GetHashCode() should also return true. You can read more details in the GetHashCode()
section.
ReferenceEquals()
ReferenceEquals() is also used to test for equality, but it always compares the reference if we supply
objects or variables located at different places in memory, they will not be seen as equal. For reference
types, their location on the heap is compared; for value types, their location on the stack is compared. The
consequence is that two value type objects (like two structs or two ints) will always be different, while
two reference type objects will be equal if they both point to the same location on the heap.
ReferenceEquals() is supplied only as a static method of Object. The most important difference
between Equals() and ReferenceEquals() you'll notice in this example will be about value types,
which are not reported as being equal when using Equals(), value types with the same value are always
equal.
using System;
class MyExecutableClass
{
static void Main(string[] args)
{
Person first = new Person(5);
Person second = new Person(5);
Person third = first;
Console.Write("first = second: ");
Console.WriteLine (Object.ReferenceEquals(first,second));
Console.Write("first = third: ");
Console.WriteLine (Object.ReferenceEquals(first,third));
}
}
class Person
{
public int Age;
public Person(int Age)
{
this.Age = Age;
}
}
237
Chapter 5
When Person is a class, the result is:
first = second: False
first = third: True
Press any key to continue
GetHashCode()
GetHashCode() returns the hash value of a specific object. The hash code of an object is an integer
value that ideally would uniquely identify that object. We will not cover the details of hash codes in this
book for now, the important thing to keep in mind is that if two objects are equal, then their hash
codes will be identical. The reverse is not necessarily true two objects with the same hash code are not
necessarily identical, but the probability of them being identical is very high.
For this reason, when we override Equals() in our class, we should also override GetHashCode() in
order to make sure the rules are still followed. For a demonstration, use this class:
class MyExecutableClass
{
static void Main(string[] args)
{
Person first = new Person(5);
Person second = new Person(5);
Person third = first;
Console.Write("Hash Code for first: ");
Console.WriteLine (first.GetHashCode());
Console.Write("Hash Code for second: ");
Console.WriteLine (second.GetHashCode());
Console.Write("Hash Code for third: ");
Console.WriteLine (third.GetHashCode());
}
}
238
for
for
for
key
first: 3
second: 5
third: 3
to continue
for
for
for
key
first: 5
second: 5
third: 5
to continue
GetType()
GetType() returns an object of System.Type type, that contains information about our object. We
met GetType() in Chapter 3, where we obtained an object's type as a string representation. Keep in
mind that System.Type provides us with very powerful techniques to programmatically get detailed
data about an object's type in .NET this is called reflection. As a hint, think about Microsoft's
IntelliSense technology: Visual Studio is capable of providing us with hints about a specific object by
getting its type information. However, this is considered an advanced topic, and we won't be covering
reflection in this book.
This is how the type of an object can be found out:
Person first = new Person(5);
Console.WriteLine("Type of first: " + first.GetType());
ToString()
This is probably the "simplest" member of the Object data type, and you'll find it very useful in your
programs. ToString() is used to return the text representation of a class. Object's implementation of
ToString() returns the class name on which the method is called. For example, this piece of code will
write Person on the screen:
static void Main(string[] args)
{
Person obj = new Person(5);
Console.WriteLine(obj.ToString());
}
It is particularly useful to override ToString() because there are certain situations when it is
automatically called. Take a look a this piece of code:
static void Main(string[] args)
{
Person obj = new Person(5);
Console.WriteLine("obj is " + obj);
}
Because of C#'s type safety, the second line should never compile, because we try to add a string with a
Person. But in fact, the program compiles and works very well, and the output is obj is Person. When we
try to add a string with a non-string type, the non-string type's ToString() method is automatically called,
and this explains the output of the previous code snip. This is another way of obtaining the same result:
239
Chapter 5
static void Main(string[] args)
{
Person obj = new Person(5);
string strA = "obj is ";
string strB = strA + obj;
Console.WriteLine(strB);
}
For basic data types like int, ToString() returns the string representation of the number. Take a
look a this short example:
static void Main(string[] args)
{
int a = 5;
string b = " + ";
int c = 5;
string d = " = 10";
Console.WriteLine(a+b+c+d);
}
Thanks to the ToString() method, it is possible to "add" an integer with a string this behavior
might seem normal to you, the Visual Basic 6 developer, but as you learned in Chapter 3, in reality C#
is very strict about type casts.
Boxing
We know by now that all C# types derive from System.Object. While it's pretty obvious how this
works for classes, it might be a mystery for you how it's possible to call a method on a basic data type.
Try to execute the following line of code, and you'll see that the output is True:
Console.WriteLine(true.ToString());
So, the ToString() method is overloaded in System.Boolean, and it returns "True" for true values
and "False" otherwise.
We know that reference types are stored on the heap, while value types are stored on the stack (unless they
are fields of a reference type). We know that the performance of value types is very good, especially for
basic data types that have direct support in the CPU. Basic data types that are stored on the stack occupy
the exact size in bytes of the data type. For example, an int in C# occupies 32 bits (4 bytes), while a short
takes exactly 16 bits (2 bytes).
240
So we store the variable i in an object of the reference type object. Then, using the object reference,
we're able to call any of its members, like the ToString() method. These are in fact the steps that
happen in the background in the following example, which has exactly the same effect:
static void Main(string[] args)
{
Console.WriteLine(5.ToString());
}
It is important to understand that in the process of boxing, the object does not hold a reference to the
original variable. In fact, it couldn't, because the original variable is stored on the stack, while the object
is stored on the heap. So the original variable and the object used to box it are two independent entities.
Here's an example that demonstrates this theory:
static void Main(string[] args)
{
int i = 5;
// original value of i is
object o = i;
// original value in o is
i = 10;
// does not affect the value
o = 20;
// does not affect the value
Console.WriteLine("i = " + i);
Console.WriteLine("o = " + o);
5
5
in o
in i
// i = 10
// o = 20
Unboxing
Unboxing refers to casting from a reference type to a value type. This is a typical example for unboxing:
int i = 5;
object o = i;
// boxing
int j = (int) i;
// unboxing
241
Chapter 5
If the boxed value cannot be stored in the destination variable, an InvalidCastException is
generated:
static void Main(string[] args)
{
try
{
float i = 5;
object o = i;
// boxing
int j = (int) o;
// unboxing
Console.WriteLine("j = " + j);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
When you execute the code above, the text of the error is typed to the screen:
System.InvalidCastException: Specified cast is not valid.
Boxing to ValueType
It is possible to cast any value type (or any other type) to Object, because all type derives from
Object. If you remember the section about polymorphism well, you know that we can cast any object
to any base class.
Since all value types derive from System.ValueType, it's also possible to "box" your values to
System.ValueType objects, not necessarily to System.Object objects. Although I'm not sure that
you'll actually need to do this in your programs, it's good to know that this is possible. Here's a short
example:
static void Main(string[] args)
{
int i = 5;
ValueType vt = i;
Console.WriteLine(vt);
}
Summary
Well, that's the end of our two-part journey into object-oriented programming. We have built on the
basics of object-oriented programming that we learned in the previous chapter, and looked at more
advanced topics of object-oriented programming, and at how these features are implemented in C#.
242
Method and property hiding, and overriding, and the difference between these concepts
Destructors
That brings us to the end of our voyage into the C# language and object-oriented programming, for
now. As we move on to the next chapter, and begin to construct a more sizeable Windows application
to which we will add further features over the following three chapters, we will continue to learn more
about the C# language, and see the concepts that we have learned about in these two chapters in action.
243
Chapter 5
244