Chapter 3
discussed
the myriad primitive types built into the C# language, such as
int
, long
, and
char
. The heart and soul of C#, however, is the
ability to create new, complex, programmer-defined types that map
cleanly to the objects that make up the problem you are trying to
solve.
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. You can also define types with interfaces, as you will see in Chapter 8. Instances of a class are called objects. Objects are created in memory when your program executes.
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) has 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 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 they encapsulate the characteristics and capabilities of an entity in a single, self-contained, and self-sustaining unit of code. When you want to sort the contents of an instance of a Windows listbox control, for example, tell the listbox to sort itself. How it does so is of no concern; that it does so is all you need to know. Encapsulation, along with polymorphism and inheritance, is one of three cardinal principles of object-oriented programming.
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. (Alternate answer: none, Microsoft has changed the standard to darkness.)
This chapter explains the C# language features that are used to specify new classes. The elements of a class—its behaviors and properties—are known collectively as its class members. This chapter will show how methods are used to define the behaviors of the class, and how the state of the class is maintained in member variables (often called fields). In addition, this chapter introduces properties, which act like methods to the developer of the class but look like fields to clients of the class.
To
define a new type or class, first declare it, and then define its
methods and fields. Declare a class using the
class
keyword. The complete syntax is as follows:
[attributes
] [access-modifiers
] classidentifier
[:base-class
[,interface(s)
]] {class-body
}
Attributes are covered in Chapter 8; access
modifiers are discussed in the next section. (Typically, your classes
will use the keyword public
as an access
modifier.) The identifier
is the name of the class
that you provide. The optional base-class
is
discussed in Chapter 5. The member definitions
that make up the class-body
are enclosed by open
and closed curly braces ({}
).
Tip
C and C++ programmers take note: a C# class definition doesn’t end with a semicolon, though if you add one, the program will still compile.
In C#, everything happens within a class. So far, however,
we’ve not instantiated any
instances of that class; that is, we haven’t created
any objects. What is the
difference between a class
and an instance of that class? To answer that question, start with
the distinction between the type
int
and a variable of type
int
. Thus, while you would write:
int myInteger = 5;
you wouldn’t write:
int = 5;
You can’t assign a value to a type; instead, you
assign the value to an object of that type (in this case, a variable
of type int
).
When you declare a new class, you define the properties of all objects of that class, as well as their behaviors. For example, if you are creating a windowing environment, you might want to create screen widgets (more commonly known as controls in Windows programming) to simplify user interaction with your application. One control of interest might be a listbox, which 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—for example, height, width, location, and text color. 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. Such a class might have member variables named
height
, width
,
location
, and text_color
, and
member methods named sort( )
,
add()
, remove()
, etc.
You can’t assign data to the
ListBox
type. Instead you must first create an
object of that type, as in the following code snippet:
ListBox myListBox;
Once you create an instance of ListBox
, you can
assign data to its fields.
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. You might implement such a class by defining a single method and six variables, as shown in Example 4-1.
Example 4-1. Simple Time class
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace TimeClass { public class Time { // private variables int Year; int Month; int Date; int Hour; int Minute; int Second; // public methods public void DisplayCurrentTime( ) { Console.WriteLine( "stub for DisplayCurrentTime" ); } } public class Tester { static void Main( ) { Time t = new Time( ); t.DisplayCurrentTime( ); } } }
Tip
You will receive warnings when you compile this class that the member
variables of Time
(Year
,
Month
, etc.) are never used. Please ignore these
warnings for now (though it is generally not a good idea to ignore
warnings unless you are certain you understand what they are and why
they can be ignored). In this case, we are stubbing
out the Time
class and if this were a
real class, we would make use of these members in other methods.
The only method declared within the
Time
class definition is
DisplayCurrentTime()
. The body of the method is
defined within the class definition itself. Unlike other languages
(such as C++), C# doesn’t require that methods be
declared before they are defined, nor does the language support
placing its declarations into one file and code into another. (C# has
no header files.) All C# methods are defined inline as shown in Example 4-1 with DisplayCurrentTime( )
.
The DisplayCurrentTime()
method is defined to
return void
; that is, it will not return a value
to a method that invokes it. For now, the body of this method has
been stubbed out.
The Time
class definition ends with the
declaration of a number of member variables: Year
,
Month
, Date
,
Hour
, Minute
, and
Second
.
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 and its address is
assigned to object t
. Because t
is an instance of Time
, Main( )
can make use of the DisplayCurrentTime()
method
available with objects of that type and call it to display the time:
t.DisplayCurrentTime();
An access modifier determines which class methods of other classes can see and use a member variable or method within this class. Table 4-1 summarizes the C# access modifiers.
Table 4-1. Access modifiers
It is generally desirable to designate the member variables of a
class as private
. This means that only member
methods of that class can access their value. Because
private
is the default accessibility level, you
don’t need to make it explicit, but I recommend that
you do so. Thus, in Example 4-1, the declarations of
member variables should have been written as follows:
// private variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second;
The Tester
class
and
DisplayCurrentTime()
method are both declared
public
so that any other class can make use of
them.
Methods
can take any number of
parameters.[1] The parameter list follows
the method name and is encased in parentheses, with each parameter
preceded by its type. For example, the following declaration defines
a method named MyMethod()
, which returns
void
(that is, which returns no value at all) and
which takes two parameters: an integer 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 4-2
illustrates how you pass values into a method—in this case,
values of type int
and float
.
Example 4-2. Passing values into SomeMethod()
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace PassingValues { public classMyClass { 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 ); } } }
The method SomeMethod( )
takes an
int
and a float
and displays
them using Console.WriteLine( )
. The parameters,
which are named firstParam
and
secondParam
, are treated as local variables within
SomeMethod()
.
Tip
VB6 programmers take note: C# methods don’t allow you to declare optional arguments. Instead, you have to use method overloading to create methods that declare different combinations of arguments. For more information, see the section “Overloading Methods and Constructors” later in this chapter.
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
Chapter 3, a distinction was drawn between
value types and reference types. The primitive C# types
(int
, char
, etc.) are value
types, and are created on the stack. Objects, however, are reference
types, and are created on the heap, using the keyword
new
, as in the following:
Time t = new Time();
t
doesn’t actually contain the
value for the Time
object; it contains the address
of that (unnamed) object that is created on the heap.
t
itself is just a reference to that object.
Tip
VB6 programmers take
note: while there is a performance penalty in using the
VB6 keywords Dim
and New
on
the same line, in C# this penalty has been removed. Thus, in C# there
is no drawback to using the new
keyword when
declaring an object variable.
In
Example 4-1, notice that the statement that creates
the Time
object looks as though it is invoking a
method:
Time t = new Time();
In fact, a method is invoked whenever you
instantiate an object. This method is called a
constructor, and you must either define one as
part of your class definition or let the CLR provide one on your
behalf. The job of a constructor is to create the object specified by
a class and to put it into a valid state. Before
the constructor runs, the object is undifferentiated memory; after
the constructor completes, the memory holds a valid instance of the
class type
.
The Time
class of Example 4-1
doesn’t define a constructor. If a constructor is
not declared, the compiler provides one for you. The default
constructor creates the object but takes no other action.
Member variables are initialized to innocuous values (integers to 0, strings to the empty string, etc.).[2] Table 4-2 lists the default values assigned to primitive types.
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 4-1, assume that you want to pass in the current year, month, date, and so forth, so that the object is created with meaningful data.
To define a
constructor,
declare a method whose name is the same as the class in which it is
declared. Constructors have no return type and are typically declared
public. If there are arguments to pass, define an argument list just
as you would for any other method. Example 4-3
declares a constructor for the Time
class that
accepts a single argument, an object of type
DateTime
.
Example 4-3. Declaring a constructor
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace DeclaringConstructor { public class Time { // private member variables int Year; int Month; int Date; int Hour; int Minute; int Second; // public accessor methods public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); } // constructor public Time( System.DateTime dt ) { Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime( ); } } } Output: 11/16/2005 16:21:40
In this example, the constructor takes a DateTime
object and initializes all the member variables based on values in
that object. 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 the member variable is
initialized by the compiler to 0
. Integer member
variables are set to 0
if you
don’t otherwise assign them. Remember,
value
types (e.g., integers) can’t be
uninitialized; if you don’t
tell the constructor what to do, it will try for something innocuous.
In Example 4-3, the DateTime
object is created in the Main()
method of
Tester
. This object, supplied by the
System
library, offers a number of public
values—Year
, Month
,
Day
, Hour
,
Minute
, and Second
—that
correspond directly to the private member variables of the
Time
object. In addition, the
DateTime
object offers a static member property,
Now
, which is a reference to an instance of a
DateTime
object initialized with the current time.
Examine the highlighted line in Main()
, where the
DateTime
object is created by calling the static
property Now
. Now
creates a
DateTime
value which, in this case, gets copied to
the currentTime
variable on the stack.
The currentTime
variable is passed as a parameter
to the Time
constructor. The
Time
constructor parameter, dt
,
is a copy of the DateTime
object.
It is possible to initialize the values of member variables in an initializer, instead of having to do so in every constructor. Create an initializer by assigning an initial value to a class member:
private int Second = 30; // initializer
Assume that the semantics of our Time
object are
such that no matter what time is set, the seconds are always
initialized to 30
. We might rewrite the
Time
class to use an initializer so that no matter
which constructor is called, the value of Second
is always initialized, either explicitly by the constructor or
implicitly by the initializer. See Example 4-4.
Tip
Example 4-4 uses an overloaded constructor, which means there are two versions of the constructor that differ by the number and type of parameters. Overloading constructors is explained in detail later in this chapter.
Example 4-4. Using an initializer
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace Initializer { public classTime { // private member variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second = 30; // initializer // public accessor methods public void DisplayCurrentTime( ) { System.DateTime now = System.DateTime.Now; System.Console.WriteLine( "\nDebug\t: {0}/{1}/{2} {3}:{4}:{5}", now.Month, now.Day, now.Year, now.Hour, now.Minute, now.Second ); System.Console.WriteLine( "Time\t: {0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); } // constructors public Time( System.DateTime dt ) { Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; //explicit assignment } public Time( int Year, int Month, int Date, int Hour, int Minute ) { this.Year = Year; this.Month = Month; this.Date = Date; this.Hour = Hour; this.Minute = Minute; } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime( ); Time t2 = new Time( 2005, 11, 18, 11, 45 ); t2.DisplayCurrentTime( ); } } } Output: Debug : 11/27/2005 7:52:54 Time : 11/27/2005 7:52:54 Debug : 11/27/2005 7:52:54 Time : 11/18/2005 11:45:30
If you don’t provide a specific initializer, the
constructor will initialize each integer member variable to zero
(0
). In the case shown, however, the
Second
member is initialized to
30
:
private int Second = 30; // initializer
If a value is not passed in for Second
, its value
will be set to 30
when t2
is
created:
Time t2 = new Time(2005,11,18,11,45); t2.DisplayCurrentTime();
However, if a value is assigned to Second
, as is
done in the constructor (which takes a DateTime
object, shown in bold), that value overrides the initialized value.
The first time we invoke DisplayCurrentTime()
, we
call the constructor that takes a DateTime
object,
and the seconds are initialized to 54
. The second
time the method is invoked, we explicitly set the time to
11:45
(not setting the seconds), and the
initializer takes over.
If the program didn’t have an initializer and did
not otherwise assign a value to Second
, the value
would be initialized by the CLR to 0.
The .NET Framework defines an
ICloneable
interface to support the concept of a copy constructor. (Interfaces
are covered in detail in Chapter 8.) This
interface defines a single method:
Clone( )
. Classes that support the idea of a
copy constructor should implement ICloneable
and
then should implement either a shallow copy (calling
MemberwiseClone
) or a deep copy (e.g., by calling the copy
constructor and hand-copying all the members).
class SomeType: ICloneable { public Object Clone() { return MemberwiseClone( ); // shallow copy } }
The keyword
this
refers to the
current instance of an object. The this
reference
(sometimes referred to as a this pointer[3]
) is a hidden reference passed
to every nonstatic method of a class. Each method can refer to the
other methods and variables of that object by way of the
this
reference.
The this
reference is typically used in a number
of ways. The first way is to qualify instance members otherwise
hidden by 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 name ambiguity. While this.hour
refers to the
member variable, hour
refers to the parameter.
The argument in favor of this style is that you pick the right variable name and then use it for both the parameter and the member variable. The counter argument is that using the same name for both the parameter and the member variable can be confusing.
The second use of the this
reference is to pass
the current object as a parameter to another method. For instance:
class myClass { public void Foo(OtherClass otherObject) { otherObject.Bar(this); } }
Let’s unpack this example. Here we have a method
named myClass.Foo
. In the body of this method, you
invoke the Bar
method of the
OtherClass
instance, passing in a reference to the
current instance of myClass
. This allows the
Bar
method to fiddle with the public methods and
members of the current instance of myClass
.
The third use of this
is with indexers, covered in
Chapter 9.
The fourth use of the this
reference is to call
one overloaded constructor from another, for example:
class myClass { public myClass(int i) { //... } public myClass( ) : this(42) { //... } }
In this example, the default constructor invokes the overloaded
constructor that takes an integer, by using the
this
keyword.
The final way that the this
keyword is used is to
explicitly invoke methods and members of a class, as a form of
documentation:
public void MyMethod(int y) { int x = 0; x = 7; // assign to a local variable y = 8; // assign to a parameter this.z = 5; // assign to a member variable this.Draw( ); // invoke member method }
In the cases shown, the use of the this
reference
is superfluous, but may make the intent of the programmer clearer and
does no harm (except, arguably, to clutter the code).
The
members of a class
(variables, methods, events, indexers, etc.) can be either
instance
members or
static
members. Instance
members are associated with instances of a type, while static members are considered to be part
of the class. 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
.[4] Suppose as well
that the Button
class has a static
method
SomeMethod()
. To access the static method, you
write:
Button.SomeMethod();
rather than:
btnUpdate.SomeMethod( );
In C#, it is not legal to access a static method or member variable through an instance, and trying to do so will generate a compiler error (C++ programmers, take note).
Some languages distinguish between class methods and other (global) methods that are available outside the context of any class. In C# there are no global methods, only class methods, but you can achieve an analogous result by defining static methods within your class.
Tip
VB6
programmers
take
note: don’t confuse the
static
keyword in C# with the
Static
keyword in VB6 and VB.NET. In VB, the
Static
keyword declares a variable that is
available only to the method it was declared in. In other words, the
Static
variable is not shared among different
objects of its class (i.e., each Static
variable
instance has its own value). However, this variable exists for the
life of the program, which allows its value to persist from one
method call to another.
In C#, the static
keyword indicates a class
member. In VB, the equivalent keyword is Shared
.
Static methods act more or less like global methods, in that you can invoke them without actually having an instance of the object at hand. The advantage of static methods over global, however, is that the name is scoped to the class in which it occurs, and thus you don’t clutter up the global namespace with myriad function names. This can help manage highly complex programs, and the name of the class acts very much like a namespace for the static methods within it.
In addition, static methods may be passed instance members as parameters (or may create such instances themselves within the static method). Because they are scoped to the class, instead of being scoped globally, they have access to the private members of the instances.
Tip
Resist the temptation to create a single class in your program in which you stash all your miscellaneous methods. It is possible but not desirable and undermines the encapsulation of an object-oriented design.
The
Main( )
method is static. Static methods are
said to operate on the class, rather than on an instance of the
class. They don’t have a this
reference, as there is no instance to point to.
Tip
Java programmers take note: in C#, calling static methods through instance variables is not permitted.
Static methods can’t directly access nonstatic
members. For Main()
to call a nonstatic method, it
must instantiate an object. Consider Example 4-2
shown earlier.
SomeMethod()
is a nonstatic method of
MyClass
. For Main()
to access
this method, it must first instantiate an object of type
MyClass
and then invoke the method through that
object.
If your class declares a static constructor, you are guaranteed that the static constructor will run before any instance of your class is created.[5]
Tip
You can’t control exactly when a static constructor will run, but you do know that it will be after the start of your program and before the first instance is created. Because of this, you can’t assume (or determine) whether an instance is being created.
For example, you might add the following static constructor to the
Time
class from Example 4-4:
static Time() { Name = "Time"; }
Notice that there is no access modifier (e.g.,
public
) before the static constructor. Access
modifiers aren’t allowed on static constructors. In
addition, because this is a static member method, you
can’t access nonstatic member variables, and so
Name
must be declared a static member variable:
private static string Name;
The final change is to add a line to
DisplayCurrentTime( )
, as in the following:
public void DisplayCurrentTime() { System.Console.WriteLine("Name: {0}", Name); System.Console.WriteLine("{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second); }
When all these changes are made, the output is:
Name: Time 11/27/2005 7:52:54 Name: Time 11/18/2005 11:45:30
(Your output will vary depending on the date and time you run this code.)
Although this code works, it isn’t necessary to create a static constructor to accomplish this goal. You can, instead, use an initializer:
private static string Name = "Time";
which accomplishes the same thing. Static constructors are useful, however, for set- up work that can’t be accomplished with an initializer and that needs to be done only once.
Tip
Java programmers take note: in C#, a static constructor will serve where a static initializer would be used in Java.
For example, assume you have an unmanaged bit of code in a legacy
DLL. You want to provide a class wrapper for this code. You can call
LoadLibrary
in your static constructor and
initialize the jump table in the static constructor. Handling legacy
code and interoperating with unmanaged code is discussed in Chapter 22.
In C#,
there are no global methods or
constants. You might find yourself creating small utility classes
that exist only to hold static members. Setting aside whether this is
a good design, if you create such a class you won’t
want any instances created. Mark your class Static
to ensure that no instance of the class may be created. Static
classes are sealed ,
and thus you may not create derived types of a
Static
class. Note, however, that static classes
may not contain nonstatic members or have a constructor.
A common way to demonstrate the use of static member variables is to keep track of the number of instances that currently exist for your class. Example 4-5 illustrates.
Example 4-5. Using static fields for instance counting
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace StaticFields { public classCat { private static int instances = 0; public Cat( ) { instances++; } public static void HowManyCats( ) { Console.WriteLine( "{0} cats adopted", instances ); } } public class Tester { static void Main( ) { Cat.HowManyCats( ); Cat frisky = new Cat( ); Cat.HowManyCats( ); Cat whiskers = new Cat( ); Cat.HowManyCats( ); } } } Output: 0 cats adopted 1 cats adopted 2 cats adopted
The Cat
class has been stripped to its absolute
essentials. A static member variable called
instances
is created and initialized to 0. Note
that the static member is considered part of the class, not a member
of an instance, and so it can’t be initialized by
the compiler on creation of an instance. Thus, if you want to
initialize a static member, you must provide an explicit initializer.
When additional instances of Cat
s are created (in
a constructor), the count is incremented.
Since C# provides garbage collection, you never need to explicitly destroy your objects. However, if your object controls unmanaged resources, you will need to explicitly free those resources when you are done with them. Implicit control over unmanaged resources is provided by a destructor , which will be called by the garbage collector when your object is destroyed.
Tip
C and C++ programmers take note: a destructor is not necessarily called when an object goes out of scope, but rather, when it is garbage-collected (which may happen much later). This is known as nondeterministic finalization.
The destructor should only release resources that your object holds on to, and should not reference other objects. Note that if you have only managed references, you don’t need to and should not implement a destructor; you want this only for handling unmanaged resources. Because there is some cost to having a destructor, you ought to implement this only on methods that require it (that is, methods that consume valuable unmanaged resources).
You can’t call an object’s destructor directly. The garbage collector will call it for you.
C#’s destructor looks, syntactically, much like a C++ destructor, but it behaves quite differently. Declare a C# destructor with a tilde as follows:
~MyClass(){}
In C#, this syntax is simply a shortcut for declaring a
Finalize( )
method that chains up to its base
class. Thus, when you write:
~MyClass() { // do work here }
the C# compiler translates it to:
protected override void Finalize() { try { // do work here. } finally { base.Finalize( ); } }
It is not legal to call a
destructor explicitly. Your
destructor 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.[6] (You will learn
more about interfaces in Chapter 8.) The
IDisposable
interface requires its implementers to
define one method, named
Dispose()
, to perform
whatever cleanup you consider to be crucial. The availability of
Dispose( )
is a way for your clients to say,
“Don’t wait for the destructor to
be called, do it right now.”
If you provide a Dispose()
method, you should stop
the garbage collector from calling your object’s
destructor. To do so, call the static method
GC.SuppressFinalize( )
, passing in the
this
pointer 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 may prefer to have your clients
call a method named
Close( )
. (For example,
Close()
may make 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()
.
To make it easier for your clients to
properly dispose your objects, C# provides a using
statement that ensures that
Dispose( )
will be called at the earliest
possible time. The idiom is to declare the 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 in Example 4-6.
Example 4-6. The using statement
#region Using directives using System; using System.Collections.Generic; using System.Drawing; using System.Text; #endregion namespace usingStatement { classTester { public static void Main( ) { using ( Font theFont = new Font( "Arial", 10.0f ) ) { // use theFont } // compiler will call Dispose on theFont Font anotherFont = new Font( "Courier", 12.0f ); using ( anotherFont ) { // use anotherFont } // compiler calls Dispose on anotherFont } } }
In the first part of this example, the Font
object
is created within the using
statement. When the
using
statement ends, Dispose( )
is called on the Font
object.
In the second part of the example, a Font
object
is created outside of the using
statement. When we
decide to use that font, we put it inside the
using
statement; when that statement ends,
Dispose()
is called once again.
This second approach is fraught with danger. If an exception is
thrown after the object is created but before the
using
block is begun, the object will not be
disposed. Second, the variable remains in scope after the
using
block ends, but if it is accessed it will
fail.
The using
statement also protects you against
unanticipated exceptions. Regardless of how control leaves the
using
statement, Dispose()
is
called. An implicit try-finally
block is created
for you. (See Chapter 11 for details.)
By
default, value types are passed into methods by value. (See the
section entitled “Method
Arguments,” earlier in this chapter.) This means
that when a value object is passed to a method, a temporary copy of
the object is created within that method. Once the method completes,
the copy is discarded. Although passing by value is the normal case,
there are times when you will want to pass value objects by
reference. C# provides the
ref
parameter modifier for passing value
objects into a method by reference, and the
out
modifier for those cases in which you
want to pass in a ref
variable without first
initializing it. C# also supports the params
modifier, which allows a method to accept a variable number of
parameters. The params
keyword is discussed in
Chapter 9.
Methods can return only a single
value (though that value can be a collection of values).
Let’s return to the Time
class
and add a GetTime()
method, which returns the
hour, minutes, and seconds.
Tip
Java programmers take
note: in C#, there’s no need for wrapper
classes for basic types like int
(integer).
Instead, use reference parameters.
Because we can’t return three values, perhaps we can pass in three parameters, let the method modify the parameters, and examine the result in the calling method. Example 4-7 shows a first attempt at this.
Example 4-7. Returning values in parameters
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ReturningValuesInParams { public classTime { // private member variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second; // public accessor methods public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); } public int GetHour( ) { return Hour; } public void GetTime( int h, int m, int s ) { h = Hour; m = Minute; s = Second; } // constructor public Time( System.DateTime dt ) { Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime( ); int theHour = 0; int theMinute = 0; int theSecond = 0; t.GetTime( theHour, theMinute, theSecond ); System.Console.WriteLine( "Current time: {0}:{1}:{2}", theHour, theMinute, theSecond ); } } } Output: 11/17/2005 13:41:18 Current time: 0:0:0
Notice that the Current
time
in
the output is 0:0:0
. Clearly, this first attempt
did not work. The problem is with the parameters. We pass in three
integer parameters to GetTime( )
, and we modify the
parameters in GetTime()
, but when the values are
accessed back in Main( )
, they are unchanged. This
is because integers are value types, and so are passed by value; a
copy is made in GetTime( )
. What we need is to pass
these values by reference.
Two small changes are required. First, change the parameters of the
GetTime()
method to indicate that the parameters
are ref
(reference) parameters:
public void GetTime(ref int h, ref int m, ref int s) { h = Hour; m = Minute; s = Second; }
Second, modify the call to GetTime( )
to pass the
arguments as references as well:
t.GetTime(ref theHour, ref theMinute, ref theSecond);
If you leave out the second step of marking the arguments with the
keyword ref
, the compiler will complain that the
argument can’t be converted from an
int
to a ref
int
.
The results now show the correct time. By declaring these parameters
to be ref
parameters, you instruct the compiler to
pass them by reference. Instead of a copy being made, the parameter
in GetTime( )
is a reference to the same variable
(theHour
) that is created in
Main( )
. When you change these values in
GetTime()
, the change is reflected in
Main( )
.
Keep in mind that
ref
parameters are
references to the actual original value: it is as if you said,
“Here, work on this one.”
Conversely, value parameters are copies: it is as
if you said, “Here, work on one
just
like
this.”
C# imposes
definite
assignment, which requires that all variables be
assigned a value before they are used. In Example 4-7, if you don’t initialize
theHour
, theMinute
, and
theSecond
before you pass them as parameters to
GetTime( )
, the compiler will complain. Yet the
initialization that is done merely sets their values to
0
before they are passed to the method:
int theHour = 0; int theMinute = 0; int theSecond = 0; t.GetTime( ref theHour, ref theMinute, ref theSecond);
It seems silly to initialize these values because you immediately
pass them by reference into GetTime
where
they’ll be changed, but if you
don’t, the following compiler errors are reported:
Use of unassigned local variable 'theHour' Use of unassigned local variable 'theMinute' Use of unassigned local variable 'theSecond'
C# provides the
out
parameter
modifier for this situation. The out
modifier
removes the requirement that a reference parameter be initialized.
The parameters to GetTime( )
, for example, provide
no information to the method; they are simply a mechanism for getting
information out of it. Thus, by marking all three as
out
parameters, you eliminate the need to
initialize them outside the method. Within the called method, the
out
parameters must be assigned a value before the
method returns. The following are the altered parameter declarations
for GetTime( )
.
public void GetTime(out int h, out int m, out int s) { h = Hour; m = Minute; s = Second; }
and here is the new invocation of the method in
Main( )
:
t.GetTime( out theHour, out theMinute, out theSecond);
To summarize, value types are passed into methods by
value. ref
parameters are used to pass value types
into a method by reference. This allows you to retrieve their
modified value in the calling method. out
parameters are used only to return information from a method. Example 4-8 rewrites Example 4-7 to use
all three.
Example 4-8. Using in, out, and ref parameters
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace InOutRef { public classTime { // private member variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second; // public accessor methods public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); } public int GetHour( ) { return Hour; } public void SetTime( int hr, out int min, ref int sec ) { // if the passed in time is >= 30 // increment the minute and set second to 0 // otherwise leave both alone if ( sec >= 30 ) { Minute++; Second = 0; } Hour = hr; // set to value passed in // pass the minute and second back out min = Minute; sec = Second; } // constructor public Time( System.DateTime dt ) { Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime( ); int theHour = 3; int theMinute; int theSecond = 20; t.SetTime( theHour, out theMinute, ref theSecond ); System.Console.WriteLine( "the Minute is now: {0} and {1} seconds", theMinute, theSecond ); theSecond = 40; t.SetTime( theHour, out theMinute, ref theSecond ); System.Console.WriteLine( "the Minute is now: " + "{0} and {1} seconds", theMinute, theSecond ); } } } Output: 11/17/2005 14:6:24 the Minute is now: 6 and 24 seconds the Minute is now: 7 and 0 seconds
SetTime
is a bit contrived, but it illustrates the
three types of parameters. theHour
is passed in as
a value parameter; its entire job is to set the member variable
Hour
, and no value is returned using this
parameter.
The ref
parameter theSecond
is
used to set a value in the method. If theSecond
is
greater than or equal to 30, the member variable
Second
is reset to 0 and the member variable
Minute
is incremented.
Finally, theMinute
is passed into the method only
to return the value of the member variable Minute
,
and thus is marked as an out
parameter.
It makes perfect sense that theHour
and
theSecond
must be initialized; their values are
needed and used. It is not necessary to initialize
theMinute
, as it is an out
parameter that exists only to return a value. What at first appeared
to be arbitrary and capricious rules now make sense; values are
required to be initialized only when their initial value is
meaningful.
Often you’ll
want to have more than one function with the same name. The most
common example of this is to have more than one constructor. In the
examples shown so far, the constructor has taken a single parameter:
a DateTime
object. It would be convenient to be
able to set new Time
objects to an arbitrary time
by passing in year, month, date, hour, minute, and second values. It
would be even more convenient if some clients could use one
constructor, and other clients could use the other constructor.
Function overloading provides for exactly these contingencies.
The signature of a method is defined by its name and its parameter list. Two methods differ in their signatures if they have different names or different parameter lists. Parameter lists can differ by having different numbers or types of parameters. For example, in the following code the first method differs from the second in the number of parameters, and the second differs from the third in the types of parameters:
void myMethod(int p1); void myMethod(int p1, int p2); void myMethod(int p1, string s1);
A class can have any number of methods, as long as each one’s signature differs from that of all the others.
Example 4-9 illustrates our Time
class with two constructors: one that takes a
DateTime
object, and the other that takes six
integers.
Example 4-9. Overloading the constructor
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace OverloadedConstructor { public classTime { // private member variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second; // public accessor methods public void DisplayCurrentTime( ) { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); } // constructors public Time( System.DateTime dt ) { Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } public Time( int Year, int Month, int Date, int Hour, int Minute, int Second ) { this.Year = Year; this.Month = Month; this.Date = Date; this.Hour = Hour; this.Minute = Minute; this.Second = Second; } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t1= new Time( currentTime ); t.DisplayCurrentTime( ); Time t2 = new Time( 2005, 11, 18, 11, 03, 30 ); t2.DisplayCurrentTime( ); } } }
As you can see, the Time
class in Example 4-9 has two constructors. If a
function’s signature consisted only of the function
name, the compiler would not know which constructors to call when
constructing t1
and t2
.
However, because the signature includes the function argument types,
the compiler is able to match the constructor call for
t1
with the constructor whose signature requires a
DateTime
object. Likewise, the compiler is able to
associate the t2
constructor call with the
constructor method whose signature specifies six integer arguments.
When you overload a method, you must change the signature (i.e., the name, number, or type of the parameters). You are free, as well, to change the return type, but this is optional. Changing only the return type doesn’t overload the method, and creating two methods with the same signature but differing return types will generate a compile error (see Example 4-10).
Example 4-10. Varying the return type on overloaded methods
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace VaryingReturnType { public classTester { private int Triple( int val ) { return 3 * val; } private long Triple( long val ) { return 3 * val; } public void Test( ) { int x = 5; int y = Triple( x ); System.Console.WriteLine( "x: {0} y: {1}", x, y ); long lx = 10; long ly = Triple( lx ); System.Console.WriteLine( "lx: {0} ly: {1}", lx, ly ); } static void Main( ) { Tester t = new Tester( ); t.Test( ); } } }
In this example, the Tester
class overloads the
Triple()
method, one to take an integer, the other
to take a long. The return type for the two
Triple( )
methods varies. Although this is not
required, it is very convenient in this case.
Properties allow clients to access class state as if they were accessing member fields directly, while actually implementing that access through a class method.
This is ideal. The client wants direct access to the state of the object and doesn’t want to work with methods. The class designer, however, wants to hide the internal state of his class in class members, and provide indirect access through a method.
By decoupling the class state from the
method that accesses that state, the designer is free to change the
internal state of the object as needed. When the
Time
class is first created, the
Hour
value might be stored as a member variable.
When the class is redesigned, the Hour
value might
be computed or retrieved from a database. If the client had direct
access to the original Hour
member variable, the
change to computing the value would break the client. By decoupling
and forcing the client to go through a method (or property), the
Time
class can change how it manages its internal
state without breaking client code.
Properties meet both goals: they provide a simple interface to the client, appearing to be a member variable. They are implemented as methods, however, providing the data-hiding required by good object-oriented design, as illustrated in Example 4-11.
Example 4-11. Using a property
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace UsingAProperty { public classTime { // private member variables private int year; private int month; private int date; private int hour; private int minute; private int second; // public accessor methods public void DisplayCurrentTime( ) { System.Console.WriteLine( "Time\t: {0}/{1}/{2} {3}:{4}:{5}", month, date, year, hour, minute, second ); } // constructors public Time( System.DateTime dt ) { year = dt.Year; month = dt.Month; date = dt.Day; hour = dt.Hour; minute = dt.Minute; second = dt.Second; } // create a property public int Hour { get { return hour; } set { hour = value; } } } public class Tester { static void Main( ) { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime( ); int theHour = t.Hour; System.Console.WriteLine( "\nRetrieved the hour: {0}\n", theHour ); theHour++; t.Hour = theHour; System.Console.WriteLine( "Updated the hour: {0}\n", theHour ); } } }
To declare a
property,
write the property type and name followed by a pair of braces. Within
the braces you may declare get
and
set
accessors. Neither of these has explicit
parameters, though the set( )
accessor has an
implicit parameter value
, as shown next.
In Example 4-11, Hour
is a
property. Its declaration creates two accessors:
get
and set
:
public int Hour { get { return hour; } set { hour = value; } }
Each accessor has an accessor body that does the work of retrieving and setting the property value. The property value might be stored in a database (in which case the accessor body would do whatever work is needed to interact with the database), or it might just be stored in a private member variable:
private int hour;
The body of the
get
accessor is
similar to a class method that returns an object of the type of the
property. In the example, the accessor for Hour
is
similar to a method that returns an int
. It
returns the value of the private member variable in which the value
of the property has been stored:
get { return hour; }
In this example, a local int
member variable is
returned, but you could just as easily retrieve an integer value from
a database, or compute it on the fly.
Whenever you read the property the get
accessor is
invoked:
Time t = new Time(currentTime); int theHour = t.Hour;
In this example, the value of the Time
object’s Hour
property is
retrieved, invoking the get
accessor to extract
the property, which is then assigned to a local variable.
The
set
accessor sets
the value of a property and is similar to a method that returns
void
. When you define a set
accessor, you must use the value
keyword to
represent the argument whose value is passed to and stored by the
property:
set { hour = value; }
Here, again, a private member variable is used to store the value of
the property, but the set
accessor could write to
a database or update other member variables as needed.
When you assign a value to the property, the set
accessor is automatically invoked, and the implicit parameter
value
is set to the value you assign:
theHour++; t.Hour = theHour;
The two main advantages of this approach are that the client can interact with the properties directly, without sacrificing the data-hiding and encapsulation sacrosanct in good object-oriented design, and that the author of the property can ensure that the data provided is valid.
It is possible to set an access
modifier
(protected
,
internal
, private
) to modify
access to either the get
or set
accessor. To do so, your property must have both a
set
and a get
accessor, and you
may modify only one or the other. Also, the modifier must be more
restrictive than the accessibility level already on the property or
the indexer (thus, you may add protected
to the
get
or set
accessor of a public
property but not to a private property):
public string MyString { protected get { return myString; } set { myString = value; } }
In this example, access to the get
accessor is
restricted to methods of this class and classes derived from this
class, while the set
accessor is publicly visible.
Tip
Note that you may not put an access modifier on an interface (see Chapter 8) nor on explicit interface member implementation. In addition, if you are overriding a virtual property or index (as discussed next), the access modifier must match the base property’s access modifier.
You
might want to create a version of the Time
class
that is responsible for providing public static values representing
the current time and date. Example 4-12 illustrates a
simple approach to this problem.
Example 4-12. Using static public constants
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace StaticPublicConstants { public classRightNow { // public member variables public static int Year; public static int Month; public static int Date; public static int Hour; public static int Minute; public static int Second; static RightNow( ) { System.DateTime dt = System.DateTime.Now; Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } } public class Tester { static void Main( ) { System.Console.WriteLine( "This year: {0}", RightNow.Year.ToString( ) ); RightNow.Year = 2006; System.Console.WriteLine( "This year: {0}", RightNow.Year.ToString( ) ); } } } Output: This year: 2005 This year: 2006
This works well enough, until someone comes along and changes one of
these values. As the example shows, the
RightNow.Year
value can be changed, for example,
to 2006
. This is clearly not what
we’d like.
We’d like to mark the static values as constant, but
that is not possible because we don’t initialize
them until the static constructor is executed. C# provides the
keyword readonly
for exactly this purpose. If you
change the class member variable declarations as follows:
public static readonly int Year; public static readonly int Month; public static readonly int Date; public static readonly int Hour; public static readonly int Minute; public static readonly int Second;
then comment out the reassignment in Main( )
:
// RightNow.Year = 2006; // error!
the program will compile and run as intended.
[1] 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.
[2] When you write your own constructor you’ll find that these values have been initialized before the constructor runs. In a sense, there are two steps to building new objects—some CLR-level magic that zeros out all the fields and does whatever else needs to be done to make the thing a valid object, and then the steps in the constructor you create (if any).
[3] A
pointer is a variable that holds the
address of an object in memory. C# doesn’t use
pointers with managed objects. Some C++ programmers have become so
used to talking about a this
pointer that
they’ve carried the term over (incorrectly) to C#.
We’ll refer to the this
reference, and pay a $0.25 fine to charity each time we
forget.
[4] As noted
earlier, btnUpdate
and
btnDelete
are actually variables that refer to the
unnamed instances on the heap. For simplicity we’ll
refer to these as the names of the objects, keeping in mind that this
is just shorthand for “the name of the variables
that refer to the unnamed instances on the
heap.”
[5] Actually, the CLR guarantees to start running the static constructor before anything else is done with your class. However, it only guarantees to start running the static constructor; it doesn’t actually guarantee to finish running it. It is possible to concoct a pathological case where two classes have a circular dependency on each other. Rather than deadlock, the CLR can run the constructors on different threads so that it meets the minimal guarantee of at least starting to run both constructors in the right order.
[6] Most of the time you will not write
classes that deal with unmanaged resources such as raw handles
directly. You may, however, use wrapper classes like
FileStream
or Socket
, but these
classes do implement IDisposable
, in which case
you ought to have your class implement IDisposable
(but not a finalizer). Your Dispose
method will
call Dispose
on any disposable resources that
you’re using.
Get Programming C#, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.