Chapter 3 discusses the intrinsic types, built into the C# language. As you may recall, these simple types allow you to hold and manipulate numeric values and strings. The true power of C#, however, lies in its capacity to let the programmer define new types to suit particular problems. It is this ability to create new types that characterizes an object-oriented language. You specify new types in C# by declaring and defining classes.
Particular instances of a class are called objects. The difference between a class and an object is the same as the difference between the concept of a Dog and the particular dog who is sitting at your feet as you read this. You can’t play fetch with the definition of a Dog, only with an instance.
A Dog class describes what dogs are like; they have weight, height, eye color, hair color, disposition, and so forth. They also have actions they can take, such as eat, walk, bark, and sleep. A particular dog (such as my dog, Milo) will have a specific weight (62 pounds), height (22 inches), eye color (black), hair color (yellow), disposition (angelic), and so forth. He is capable of all the actions—methods, in programming parlance—of any dog (though if you knew him, you might imagine that eating is the only method he implements).
The huge advantage of classes in object-oriented programming is that classes encapsulate the characteristics and capabilities of a type in a single, self-contained unit.
Suppose, for instance, you want to sort the contents of an instance of a Windows listbox control. The listbox control is defined as a class. One of the properties of that class is that it knows how to sort itself. Sorting is encapsulated within the class, and the details of how the listbox sorts itself are not made visible to other classes. If you want a listbox sorted, you just tell the listbox to sort itself, and it takes care of the details.
So, you simply write a method that tells the listbox to sort itself—and that’s what happens. How it sorts is of no concern; that it does so is all you need to know.
As noted in Chapter 6, this is called encapsulation, which, along with polymorphism and specialization, is one of three cardinal principles of object-oriented programming. Chapter 11 discusses polymorphism and inheritance.
An old programming joke asks, how many object-oriented programmers does it take to change a light bulb? Answer: none, you just tell the light bulb to change itself. This chapter explains the C# language features that are used to specify new classes . The elements of a class—its behaviors and its state—are known collectively as its class members.
Class behavior is created by writing methods (sometimes called
member functions). A method is a routine that every object of the class
can execute. For example, a Dog
class
might have a Bark
method, and a
listbox
class might have a Sort
method.
Class state is maintained by fields (sometimes
called member variables). Fields may be primitive types (an int
to hold the age of the dog or a set of
strings to hold the contents of the listbox), or fields may be objects of
other classes (for example, an Employee
class may have a field of type Address
).
Finally, classes may also have properties, which act like methods to the creator of the class, but look like fields to clients of the class. A client is any object that interacts with instances of the class.
When you define a new class, you define the characteristics of all objects of that class, as well as their behaviors. For example, if you create your own windowing operating system, you might want to create screen widgets (known as controls in Windows). One control of interest might be a listbox, a control that is very useful for presenting a list of choices to the user and enabling the user to select from the list.
Listboxes have a variety of characteristics: height, width, location, and text color, for example. Programmers have also come to expect certain behaviors of listboxes—they can be opened, closed, sorted, and so on.
Object-oriented programming allows you to create a new type,
ListBox
, which encapsulates these
characteristics and capabilities.
To define a new type or class, you first declare it and then
define its methods and fields. You declare a class using the class
keyword. The complete syntax is:
[attributes
] [access-modifiers
] classidentifier
[:base-class
] {class-body
}
Attributes are used to provide special metadata about a class (that is, information about the structure or use of the class). You will not need attributes for routine C# programming.
Access modifiers are discussed later in this chapter. (Typically, your
classes will use the keyword public
as an access modifier.)
The identifier is the name of the class that you provide. Typically, C# classes are named with nouns (Dog, Employee, ListBox). The naming convention (not required, but strongly encouraged) is to use Pascal notation. In Pascal notation, you don’t use underbars or hyphens, but if the name has two words (Golden Retriever), you push the two words together, each word beginning with an uppercase letter (GoldenRetriever).
As mentioned earlier, inheritance is one of the pillars of object-oriented programming. The optional base class is explained when inheritance is discussed in Chapter 11.
The member definitions that make up the
class-body are enclosed by open and closed curly braces ({}
):
class Dog { int age; // the dog's age int weight; // the dog's weight Bark( ) { //... } Eat( ) { // ... } }
Methods within the class definition of Dog describe all the things a dog can do. The fields (member variables) such as age and weight describe all the dog’s attributes or state.
To make an actual instance, or object, of the Dog class, you must declare the object and allocate memory for the object. These two steps combined are necessary to create, or instantiate, the object. Here’s how you do it.
First, you declare the object by writing the name of the class
(Dog
) followed by an identifier
(name) for the object or instance of that class:
Dog milo; // declare milo to be an instance of Dog
This is not unlike the way you create a local variable; you
declare the type (in this case, Dog
), followed by the identifier (milo
). Notice also that (as with variables)
by convention, the identifier for the object uses Camel notation.
Camel notation is just like Pascal notation except that the very first
letter is lowercase. Thus, a variable or object name might be myDog
, designatedDriver
, or plantManager
.
The declaration alone doesn’t actually create an instance,
however. To create an instance of a class, you must also allocate
memory for the object using the keyword new
.
milo = new Dog( ); // allocate memory for milo
You can combine the declaration of the Dog
type with the memory allocation into a
single line:
Dog milo = new Dog( );
This code declares milo
to be
an object of type Dog
and also
creates a new instance of Dog
.
You’ll see what the parentheses are for later in this chapter in the
discussion of the constructor.
In C#, everything happens within a class.
No methods can run outside of a class, not even Main( )
. The Main( )
method is the entry point for your program; it is called
by the operating system, and it is where execution of your program
begins. Typically, you’ll create a small class to house Main( )
, because like every method, Main( )
must live within a class. Some of
the examples in this book use a class named Tester
to house Main( )
:
public class Tester { public static void Main( ) { //... } }
Even though Tester
was
created to house the Main( )
method, you’ve not yet instantiated any objects of type Tester
. To do so, you would write:
Tester myTester = new Tester( ); // instantiate an object of type Tester
As you’ll see later in this chapter, creating an instance of the
Tester
class allows you to call
other methods on the object you’ve created (myTester
).
Now consider a class to keep track of and display the time of day. The internal state of the class must be able to represent the current year, month, date, hour, minute, and second. You probably would also like the class to display the time in a variety of formats.
The .NET Framework provides a fully functional DateTime
class. The creation of a
simplified Time
class here is
used only to illustrate how such a class might be designed and
implemented.
You might implement such a class by defining a single method and six variables, as shown in Example 7-1.
using System; public class Time { // private variables private int year; private int month; private int date; private int hour; private int minute; private int second; // public methods public void DisplayCurrentTime( ) { Console.WriteLine( "stub for DisplayCurrentTime" ); } } public class Tester { static void Main( ) { Time timeObject = new Time( ); timeObject.DisplayCurrentTime( ); } }
This code creates a new user-defined type: Time
. The Time
class definition begins with the
declaration of a number of member variables: Year
, Month
, Date
, Hour
, Minute
, and Second
.
The keyword private
indicates
that these values can only be called by methods of this class. The
private
keyword is an access
modifier, explained later in this chapter.
Many C# programmers prefer to put all of the member fields together, either at the very top or the very bottom of the class declaration, though that is not required by the language.
The only method declared within the Time
class is the method DisplayCurrentTime( )
. The DisplayCurrentTime( )
method is defined to return void
; that is, it will not return a value to
the method that invokes it. For now, the body of this method has been
“stubbed out.”
Stubbing out a method is a temporary measure you might use when you first write a program to allow you to think about the overall structure without filling in every detail when you create a class. When you stub out a method body, you leave out the internal logic and just mark the method, perhaps with a message to the console:
public void DisplayCurrentTime( ) { Console.WriteLine( "stub for DisplayCurrentTime"); }
After the closing brace, a second class, Tester
, is defined. Tester
contains our now familiar Main( )
method. In Main( )
, an instance of
Time
is created, named timeObject
:
Time timeObject = new Time( );
Technically, an unnamed instance of Time
is created on the heap and a
reference to that object is returned and used to initialize the
Time
reference named timeObject
. Because that is cumbersome,
we’ll simply say that a Time
instance named timeObject
was
created.
Because timeObject
is an
instance of Time
, Main( )
can make use of the DisplayCur-rentTime( )
method available with
objects of that type and call it to display the time:
timeObject.DisplayCurrentTime( );
You invoke a method on an object by writing the name of the
object (timeObject
) followed by the
dot operator (.
), the method name
(DisplayCurrentTime
), and the
parameter list in parentheses (in this case, empty). You’ll see how to
pass in values to initialize the member variables in the discussion of
constructors, later in this chapter.
An access modifier determines which class methods —including methods of other classes—can see and use a member variable or method within a class. Table 7-1 summarizes the C# access modifiers .
Access modifier | Restrictions |
| No restrictions. Members that are
marked |
| The members in class A that are
marked |
| The members in class A that are
marked |
| The members in class A that are
marked |
| The members in class A that are
marked |
Public methods are part of the class’s public interface: they define how this class behaves. Private methods are “helper methods” used by the public methods to accomplish the work of the class. Because the internal workings of the class are private, helper methods need not (and should not) be exposed to other classes.
The Time
class and its method
DisplayCurrentTime( )
are both
declared public
so that any other
class can make use of them. If DisplayCurrentTime( )
had been private
, it would not be possible to invoke
DisplayCurrentTime( )
from any
method of any class other than methods of Time
. In Example 7-1, DisplayCurrentTime( )
was invoked from a
method of Tester
(not Time
), and this was legal because both the
class (Time
) and the method
(DisplayCurrentTime
) were marked
public
.
The behavior of a class is defined by the methods of that
class. To make your methods as flexible as possible, you can define
parameters : information passed into the method when the method is
invoked. Thus, rather than having to write one method when you want to
sort your listbox from A to Z and a second method when you want to sort
it from Z to A, you define a more general Sort( )
method and pass in a parameter specifying the order of the
sort.
Methods can take any number of parameters . The parameter list follows the method name and is enclosed in parentheses. Each parameter’s type is identified before the name of the parameter.
The terms “argument” and “parameter” are often used interchangeably, though some programmers insist on differentiating between the parameter declaration and the arguments passed in when the method is invoked.
For example, the following declaration defines a method named
MyMethod( )
that returns void
(that is, it returns no value at all) and
takes two parameters (an int
and a
Button
):
void MyMethod (int firstParam, Button secondParam) { // ... }
Within the body of the method, the parameters act as local
variables, as if you had declared them in the body of the method and
initialized them with the values passed in. Example 7-2 illustrates how you
pass values into a method; in this case, values of type int
and float
.
using System; public class MyClass { public void SomeMethod( int firstParam, float secondParam ) { Console.WriteLine("Here are the parameters received: {0}, {1}", firstParam, secondParam ); } } public class Tester { static void Main( ) { int howManyPeople = 5; float pi = 3.14f; MyClass mc = new MyClass( ); mc.SomeMethod( howManyPeople, pi ); } }
Here is the output:
Here are the parameters received: 5, 3.14
Note that when you pass in a float with a decimal part (3.14), you must append the letter f (3.14f) to signal to the compiler that the value is a float and not a double.
The method SomeMethod( )
takes
two parameters, firstParam
and
secondParam
, and displays them using
Console.WriteLine( )
. FirstParam
is an int
, and secondParam
is a float
. These parameters are treated as local
variables within SomeMethod( )
. You
can manipulate these values within the method, but they go out of scope
and are destroyed when the method ends.
In the calling method (Main
),
two local variables (howManyPeople
and pi
) are created and initialized.
These variables are passed as the parameters to SomeMethod( )
. The compiler maps howManyPeople
to firstParam
and pi
to secondParam
, based on their relative positions
in the parameter list.
In Example
7-1, notice that the statement that creates the Time
object looks as though it is invoking a
Time( )
method:
Time timeObject = new Time( );
In fact, a member method is invoked whenever you instantiate an object. This method is called a constructor. Each time you define a class, you are free to define your own constructor, but if you don’t, the compiler will provide one for you invisibly and automatically.
The job of a constructor is to create an instance of the object specified by a class and to put it into a valid state. Before the constructor runs, the object is just a blob of memory; after the constructor completes, the memory holds a valid instance of the class.
The Time
class of Example 7-2 does not define a
constructor, so the compiler implicitly provides one. The constructor
provided by the compiler creates the object but takes no other
action.
Any constructor that takes no arguments is called a default constructor. The constructor provided by the compiler takes no arguments, and hence is a default constructor. This terminology has caused a great deal of confusion. You can create your own default constructor, and if you do not create a constructor at all, the compiler will create a default constructor for you, by default.
If you do not explicitly initialize your member variables, they are initialized to innocuous values (integers to 0, strings to the empty string, etc.). Table 7-2 lists the default values assigned to various types.
Type | Default value |
Numeric ( | 0 |
| False |
| '\0’ (null) |
| 0 |
Reference | Null |
Typically, you’ll want to define your own constructor and provide it with arguments, so that the constructor can set the initial state for your object. In Example 7-3, you want to pass in the current year, month, date, and so forth, so that the object is created with meaningful data.
You declare a constructor like any other member method, except:
The name of the constructor must be the same as the name of the class.
Constructors have no return type (not even void).
If there are arguments to be passed, you define an argument list
just as you would for any other method. Example 7-3 declares a
constructor for the Time
class that
accepts six arguments, one each for the year, month, date, hour, minute,
and second for the new Time
object
you are creating.
using System; public class Time { // private member variables int year; int month; int date; int hour; int minute; int second; // public method public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", month, date, year, hour, minute, second ); } // constructor public Time( int theYear, int theMonth, int theDate, int theHour, int theMinute, int theSecond ) { year = theYear; month = theMonth; date = theDate; hour = theHour; minute = theMinute; second = theSecond; } } public class Tester { static void Main( ) { Time timeObject = new Time( 2008, 8, 1, 9, 35, 20 ); timeObject.DisplayCurrentTime( ); } }
The output looks like this:
8/1/2008 9:35:20
In this example, the constructor takes a series of integer values and initializes all the member variables based on these parameters.
In this, as in virtually all the demonstration code, we’ve taken out all the error checking to simplify the presentation. Of course, this allows you to create a date such as July 45th 2005 at 29:32 a.m. Don’t do that.
When the constructor finishes, the Time
object exists and the values have been
initialized. When DisplayCurrentTime( )
is called in Main( )
, the
values are displayed.
Try commenting out one of the assignments and running the program again. You’ll find that each member variable that isn’t assigned-to by you is initialized by the compiler to zero. Integer member variables are set to zero if you don’t otherwise assign them. Remember that value types (such as integers) must be initialized; if you don’t tell the constructor what to do, it sets innocuous values.
It is possible to initialize the values of member variables in an initializer, instead of having to do so in the constructor. You create an initializer by assigning an initial value to a class member:
private int Second = 30; // initializer
Assume that the semantics of the Time
object are such that no matter what time
is set, the seconds are always initialized to 30. You might rewrite your
Time
class to use an initializer so
that the value of Second
is always
initialized, as shown in Example
7-4.
using System; public class Time { // private member variables int year; int month; int date; int hour; int minute; int second = 30; // public method public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", month, date, year, hour, minute, second ); } // constructor public Time( int theYear, int theMonth, int theDate, int theHour, int theMinute ) { year = theYear; month = theMonth; date = theDate; hour = theHour; minute = theMinute; } } public class Tester { static void Main( ) { Time timeObject = new Time( 2008, 8, 1, 9, 35 ); timeObject.DisplayCurrentTime( ); } }
The output looks like this:
8/1/2008 9:35:30
If you do not provide a specific initializer, the constructor initializes each integer member variable to zero (0). In the case shown, however, the Second member is initialized to 30:
private int Second = 30; // initializer
Later in this chapter, you will see that you can have more than one
constructor. If you assign 30 to Second
in more than one of these, you can
avoid the problem of having to keep all the constructors consistent with
one another by initializing the Second
member, rather than assigning 30 in
each of the constructors.
The keyword this
refers
to the current instance of an object. The this
reference is a hidden parameter in every
nonstatic method of a class (static methods are discussed later in this
chapter). There are three ways in which the this
reference is typically used. The first
way is to qualify instance members that have the same name as
parameters, as in the following:
public void SomeMethod (int hour) { this.hour = hour; }
In this example, SomeMethod( )
takes a parameter (hour) with the same name as a member variable of the
class. The this
reference is used to
resolve the ambiguity. While this.hour
refers to the member variable,
hour
refers to the parameter.
You can, for example, use the this
reference to make assigning to a field
more explicit:
public void SetTime( year, month, date, newHour, newMinute, newSecond) { this.year = year; // use of "this" required this.month = month; // required this.date = date; // required this.hour = hour; // use of "this" optional this.minute = newMinute; // optional second = newSecond; // also ok }
If the name of the parameter is the same as the name of the member
variable, then you must use the this
reference to distinguish between the two,
but if the names are different (such as newMinute
and newSecond
), then the use of the this
reference is optional.
The argument in favor of naming the argument to a method the same as the name of the member is that the relationship between the two is made explicit. The counterargument is that using the same name for both the parameter and the member variable can be confusing as to which one you are referring to at any given moment.
The second use of the this
reference is to pass the current object as a parameter to another
method, as in the following code:
Class SomeClass { public void FirstMethod(OtherClass otherObject) { otherObject.SecondMethod(this); } // ... }
This code snippet establishes two classes, SomeClass
and OtherClass
. SomeClass
has a method named FirstMethod( )
, and OtherClass
has a method named SecondMethod( )
.
Inside FirstMethod( )
, we’d
like to invoke SecondMethod( )
,
passing in the current object (an instance of SomeClass
) for further processing. To do so,
you pass in the this
reference, which
refers to the current instance of SomeClass
.
The third use of this
is with
indexers, which are covered in Chapter 12.
The fields, properties, and methods of a class can be either
instance members or static members . Instance members are associated with instances of a
type, while static members are associated with the class and not with
any particular instance. Methods are instance methods unless you
explicitly mark them with the keyword static
.
The vast majority of methods will be instance methods. The semantics of an instance method are that you are taking an action on a specific object. From time to time, however, it is convenient to be able to invoke a method without having an instance of the class, and for that, you will use a static method.
You access a static member through the name of the class in which
it is declared. For example, suppose you have a class named Button
and have instantiated objects of that
class named btnUpdate
and btnDelete
.
Suppose that the Button
class
has an instance method Draw( )
and a
static method GetButtonCount( )
. The
job of Draw( )
is to draw the current
button, and the job of GetButtonCount( )
is to return the number of buttons currently visible on the
form.
You access an instance method through an instance of the class—that is, through an object:
btnUpdate.SomeMethod( );
You access a static method through the class name, not through an instance:
Button.GetButtonCount( );
Static methods are said to operate on the class, rather
than on an instance of the class. They do not have a this
reference, as there is no instance to
point to.
Static methods cannot directly access nonstatic members. You
will remember that Main( )
is
marked static. For Main( )
to call
a nonstatic method of any class, including its own class, it must
instantiate an object.
For the next example, use Visual Studio 2005 to create a new
console application named StaticTester
. VS.NET creates a namespace
StaticTester
and a class named
Class1
. Rename Class1
to Tester
. Get rid of all the comments and the
attribute [STATThread]
that Visual
Studio .NET puts above Main( )
.
Delete the args
parameter to
Main( )
. When you are done, your
source code should look like this:
using System; namespace StaticTester { class Tester { static void Main( ) { } } }
That is a good starting point. Until now, you’ve always done all
the work of the program right in the Main( )
method, but now you’ll create an instance method, Run( )
. The work of the program will now be
done in the Run( )
method, rather
than in the Main( )
method.
Within the class, but not within the Main( )
method, declare a new instance
method named Run( )
. When you
declare a method, you write the accessor (public
), followed by the return type, the
identifier, and then parentheses:
public void Run( )
The parentheses will hold parameters, but Run( )
won’t have any parameters, so you can
just leave the parentheses empty. Create braces for the method, and
within the braces, just print “Hello World” to the console:
public void Run( ) { Console.WriteLine("Hello world"); }
Run( )
is an instance method. Main( )
is a static method and cannot invoke Run( )
directly. You will therefore create
an instance of the Tester
class and
call Run( )
on that
instance:
Tester t = new Tester( );
When you type the keyword new
, IntelliSense tries to help you with the
class name. You’ll find that Tester
is in the list; it is a legitimate class like any other.
On the next line, invoke Run( )
on your Tester
object
t
. When you type t
followed by the dot operator, IntelliSense
presents all the public methods of the Tester
class, as shown in Figure 7-1.
Notice that the Tester
class has a number of methods you did not create (Equals
, Finalize
, and others). Every class in C#
is an object, and these methods are part of the Object
class. This is covered in Chapter 11.
When your program is complete, it looks like Example 7-5.
using System; namespace StaticTester { // create the class class Tester { // Run is an instance method public void Run( ) { Console.WriteLine( "Hello world" ); } // Main is static static void Main( ) { // create an instance Tester t = new Tester( ); // invoke the instance method t.Run( ); } } }
The output looks like this:
Hello world
This is the model you’ll use from now on in most console
applications. The Main( )
method
will be limited to instantiating an object and then invoking the
Run( )
method.
A common use of static member variables, or fields, is
to keep track of the number of instances/objects that currently exist
for your class. In the next example, you create a Cat
class. The Cat
class might be used in a pet-store
simulation.
For this example, the Cat
class has been stripped to its absolute essentials. The complete
listing is shown in Example
7-6. An analysis follows the example.
using System; namespace Test { // declare a Cat class // stripped down public class Cat { // a private static member to keep // track of how many Cat objects have // been created private static int instances = 0; private int weight; private String name; // cat constructor // increments the count of Cats public Cat( String name, int weight ) { instances++; this.name = name; this.weight = weight; } // Static method to retrieve // the current number of Cats public static void HowManyCats( ) { Console.WriteLine( "{0} cats adopted", instances ); } public void TellWeight( ) { Console.WriteLine( "{0} is {1} pounds", name, weight ); } } class Tester { public void Run( ) { Cat.HowManyCats( ); Cat frisky = new Cat( "Frisky", 5 ); frisky.TellWeight( ); Cat.HowManyCats( ); Cat whiskers = new Cat( "Whisky", 7 ); whiskers.TellWeight( ); Cat.HowManyCats( ); } static void Main( ) { Tester t = new Tester( ); t.Run( ); } } }
Here is the output:
0 cats adopted Frisky is 5 pounds 1 cats adopted Whisky is 7 pounds 2 cats adopted
The Cat
class begins by
defining a static member variable, instances
, that is initialized to zero. This
static member field will keep track of the number of Cat
objects created. Each time the
constructor runs (creating a new object), the instances
field is incremented.
The Cat
class also defines
two instance fields: name
and
weight
. These track the name and
weight of each individual Cat
object.
The Cat
class defines two
methods: HowManyCats( )
and
TellWeight( )
. HowManyCats( )
is static. The number of
Cats
is not an attribute of any
given Cat
; it is an attribute of
the entire class. TellWeight( )
is
an instance method. The name and weight of each cat is per instance
(each Cat
has his own name and
weight).
The Main( )
method accesses
the static HowManyCats( )
method
directly, through the class:
Cat.HowManyCats( );
Main( )
then creates an
instance of Cat
and accesses the
instance method, TellWeight( )
,
through the instance of Cat
:
Cat frisky = new Cat( ) frisky.TellWeight( );
Each time a new Cat is created, HowManyCats( )
reports the increase.
Unlike many other programming languages (C, C++, Pascal, etc.), C# provides garbage collection . Your objects are destroyed after you are done with them. You do not need to worry about cleaning up after your objects unless you use unmanaged or scarce resources. An unmanaged resource is an operating-system feature outside of the .NET Framework; a scarce resource is a resource that you have in limited quantity, perhaps because of licensing limitations (such as database connections).
If you do control an unmanaged resource, you need to explicitly free that resource when you are done with it. Implicit control over this resource is provided with a destructor, which is called by the garbage collector when your object is destroyed.
This material is fairly advanced and is included here for completeness. Feel free to skip this section and come back if and when you need it. It’s your book; you paid for it. (You did, right?)
You declare a C# destructor with a tilde, as follows:
~MyClass( ){}
This syntax is actually translated by the compiler into:
protected override void Finalize() { try { // do work here } finally { base.Finalize(); } }
For this reason, some programmers refer to the destructor as a finalizer.
It is not legal to call a destructor explicitly—your destructor
(finalizer) will be called by the garbage collector. If you do handle
precious unmanaged resources (such as file handles) that you want to
close and dispose of as quickly as possible, you ought to implement the
IDisposable
interface. (You will learn more about interfaces in Chapter 13.)
The IDisposable
interface
requires you to create a method named Dispose( )
, which will be called by your clients.
If you provide a Dispose( )
method, you should stop the garbage collector from calling your object’s
destructor. To stop the garbage collector, call the static method
GC.SuppressFinalize( )
, passing in the this
reference for your object. Your destructor can then call your Dispose( )
method. Thus, you might
write:
using System; class Testing : IDisposable { bool is_disposed = false; protected virtual void Dispose( bool disposing ) { if ( !is_disposed ) // only dispose once! { if ( disposing ) { Console.WriteLine( "Not in destructor, OK to reference other objects" ); } // perform cleanup for this object Console.WriteLine( "Disposing..." ); } this.is_disposed = true; } public void Dispose( ) { Dispose( true ); // tell the GC not to finalize GC.SuppressFinalize( this ); } ~Testing( ) { Dispose( false ); Console.WriteLine( "In destructor." ); } }
For some objects, you’d rather have your clients call the Close( )
method. (For example, Close( )
makes more sense than Dispose( )
for file objects.) You can implement this by creating a
private Dispose( )
method and a
public Close( )
method and having
your Close( )
method invoke Dispose( )
.
Because you cannot be certain that your user will call Dispose( )
reliably, and because finalization
is nondeterministic (that is, you can’t control when the garbage
collector will run), C# provides a using
statement to ensure that Dispose( )
is called at the earliest possible
time. The idiom is to declare which objects you are using and then to
create a scope for these objects with curly braces. When the close brace
is reached, the Dispose( )
method
will be called on the object automatically, as illustrated here:
using System.Drawing; class Tester { public static void Main( ) { using (Font theFont = new Font("Arial", 10.0f)) { // use the font } } }
Because Windows only lets you have a small number of Font
objects, we want to dispose of it at the
earliest opportunity. In this code snippet, the Font
object is created within the using
statement. When the using
statement ends, Dispose( )
is guaranteed to be called on the
Font
object.
Objects created within methods are called local variables . They are local to the method, as opposed to belonging to the object, as member variables do. The object is created within the method, used within the method, and then destroyed when the method ends. Local objects are not part of the object’s state—they are temporary value holders, useful only within the particular method.
Local variables of intrinsic types such as int
are
created on a portion of memory known as the stack.
The stack is allocated and de-allocated as methods are invoked. When you
start a method, all the local variables are created on the stack. When the method ends,
local variables are destroyed.
These variables are referred to as local because they exist (and are visible) only during the lifetime of the method. They are said to have local scope . When the method ends, the variable goes out of scope and is destroyed.
C# divides the world of types into value types and reference
types. Value types are created on the stack. All
the intrinsic types (int
, long
) are value types (as are structs,
discussed later in the chapter), and thus are created on the
stack.
Classes, on the other hand, are reference types. Reference types are created on an undifferentiated block of memory known as the heap. When you declare an instance of a reference type, what you are actually declaring is a reference, which is a variable that refers to another object. The reference acts like an alias for the object.
That is, when you write:
Dog milo = new Dog( );
the new
operator creates a
Dog
object on the heap and returns a
reference to it. That reference is assigned to milo
. Thus, milo
is a reference object that refers to a
Dog
object on the heap. It is common
to say that milo
is a reference to a
dog, or even that milo
is a Dog
object, but technically that is incorrect.
milo
is actually a reference object
that refers to an (unnamed) Dog
object on the heap.
The reference milo
acts as an
alias for that unnamed object. For all practical purposes, however, you
can treat milo
as if it were the
Dog
object itself.
The implication of using references is that you can have more than one reference to the same object. To see this difference between creating value types and reference types, examine Example 7-7. A complete analysis follows the output.
using System; namespace heap { public class Dog { public int weight; } class Tester { public void Run( ) { // create an integer int firstInt = 5; // create a second integer int secondInt = firstInt; // display the two integers Console.WriteLine( "firstInt: {0} secondInt: {1}", firstInt, secondInt ); // modify the second integer secondInt = 7; // display the two integers Console.WriteLine( "firstInt: {0} secondInt: {1}", firstInt, secondInt ); // create a dog Dog milo = new Dog( ); // assign a value to weight milo.weight = 5; // create a second reference to the dog Dog fido = milo; // display their values Console.WriteLine( "Milo: {0}, fido: {1}", milo.weight, fido.weight ); // assign a new weight to the second reference fido.weight = 7; // display the two values Console.WriteLine( "Milo: {0}, fido: {1}", milo.weight, fido.weight ); } static void Main( ) { Tester t = new Tester( ); t.Run( ); } } }
The output looks like this:
firstInt: 5 secondInt: 5 firstInt: 5 secondInt: 7 Milo: 5, fido: 5 Milo: 7, fido: 7
The program begins by creating an integer, firstInt
, and initializing it with the value
5. The second integer, secondInt
, is
then created and initialized with the value in firstInt
. Their values are displayed as
output:
firstInt: 5 secondInt: 5
These values are identical. Because int
is a value type, a copy of the firstInt
value is made and assigned to
secondInt
; secondInt
is an independent second variable,
as illustrated in Figure
7-2.
Then the program assigns a new value to secondInt
:
secondInt = 7;
Because these variables are value types, independent of one another, the first variable is unaffected. Only the copy is changed, as illustrated in Figure 7-3.
When the values are displayed, they are different:
firstInt: 5 secondInt: 7
Your next step is to create a simple Dog
class with only one member variable
(field) called weight
. Note that this
field is given a keyword, public
,
which specifies that any method of any class can access this field.
public
is what is known as an
access modifier. (Generally, you will not make
member variables public. The weight
field was made public to simplify this example.) Access modifiers are
covered in detail later in this chapter.
You instantiate a Dog
object
and save a reference to that dog in the reference milo
.
Dog milo = new Dog( );
You assign the value 5 to milo
’s weight field:
milo.weight = 5;
You commonly say that you’ve set milo
’s weight to 5, but actually you’ve set
the weight of the unnamed object on the heap to which milo
refers, as shown in Figure 7-4.
Next, you create a second reference to Dog
and initialize it by setting it equal to
milo
. This creates a new reference to
the same object on the heap.
Dog fido = milo;
Notice that this is syntactically similar to creating a second
int
variable and initializing it with
an existing int
, as you did
before:
int secondInt = firstInt; Dog fido = milo;
The difference is that Dog
is a
reference type, so fido
is not a copy
of milo
—it is a second reference to
the same object to which milo
refers.
That is, you now have an object on the heap with two references to it,
as illustrated in Figure
7-5.
When you change the weight of that object through the fido
reference:
fido.weight = 7;
you change the weight of the same object to which milo
refers. The output reflects this:
Milo: 7, fido: 7
It isn’t that fido
is changing
milo
; it is that by changing the
(unnamed) object on the heap to which fido
refers, you simultaneously change the
value of milo
because they refer to
the same unnamed object.
If you had used the keyword new
when creating fido
, you’d have created a new instance of
Dog
on the heap, and fido
and milo
would not point to the same Dog
object.
If you need a class that acts as a value object, you can create a struct (see the "Structs" sidebar). The use of structs is so unusual that they are not covered beyond the sidebar for the rest of this book. In five years of professional C# programming, my principal use of structs has been to teach what they are, not to actually use them.
When you define a new class, you declare its name with the
class
keyword, and then define
its methods, fields, delegates, events, and properties.
To instantiate an object, you declare the name of the class,
followed by an identifier for the object, much as you would a local
variable. You then need to allocate memory for the actual (unnamed)
object that will be created on the heap; you do so with the keyword
new
.
You can define a reference to an existing object by declaring the class and an identifier and then assigning to that identifier an existing object; the two identifiers now both refer to the same (unnamed) object on the heap.
You invoke a method on an object by writing the name of the object, followed by the dot operator, and the method name followed by parentheses. Parameters, if any, are placed within the parentheses.
Access modifiers dictate which methods can see and use a variable or method within a class. All members of the class are visible to all methods of the class.
Members marked public
have
no restrictions, and are visible to methods of any class.
Members marked private
are
only visible to methods within the same class.
Members marked protected
are visible to methods within the same class, and methods in derived
classes.
A constructor is a special method invoked when a new object is created. If you do not define any constructors at all for your class, the compiler will provide a default constructor that does nothing. A default constructor is a constructor that takes no parameters. You are free to create your own default constructor for your class.
You can initialize the values of your member variables when you define them in your class.
The this
keyword is used to
refer to the current instance of an object.
Every non-static method of a class has an implicit “this” variable passed into the method.
Static members are associated with the class itself, not with
a particular instance. Static members are declared with the keyword
static
, and are invoked through
the class name. Static methods do not have a this
parameter because there is no
instance to refer to.
C# does not specifically require a destructor method in your classes because the framework will destroy any object that is not in use.
You should provide a Dispose( )
method if your class uses unmanaged resources.
Local value type variables are created on the stack. When the method ends, these variables go out of scope and are destroyed.
Objects are references types, and are created on the heap. When you declare an instance of a reference type, you are actually creating a reference to that object’s location in memory. If you declare a reference to an object on the heap within a method, when the method ends, that reference is destroyed. If there are no remaining references to the object on the heap, the object itself is destroyed by the garbage collector.
What is the difference between a class and an object?
Where are reference types created?
Where are value types created?
What does the keyword private
do?
What does the keyword public
do?
What method is called when you create an object?
What is a default constructor?
What types can a constructor return?
How do you initialize the value of a member variable in a class?
What does the keyword this
refer to?
What is the difference between a static method and an instance method?
What does the using
statement do?
Write a program with a Math
class that has four methods:
Add
, Subtract
, Multiply
, and Divide
, each of which takes two
parameters. Call each method from Main( )
.
Modify the program from Exercise 7-1 so that you do not have
to create an instance of Math
to call the four methods.