This chapter resumes the examination of classes and methods. It begins by explaining how to control access to the members of a class. It then discusses the passing and returning of objects, method overloading, the various forms of Main( ), recursion, and the use of the keyword static.
In its support for encapsulation, the class provides two major benefits. First, it links data with code. You have been taking advantage of this aspect of the class since Chapter 6. Second, it provides the means by which access to members can be controlled. It is this second feature that is examined here.
Although C#’s approach is a bit more sophisticated, in essence, there are two basic types of class members: public and private. A public member can be freely accessed by code defined outside of its class. This is the type of class member that we have been using up to this point. A private member can be accessed only by methods defined by its class. It is through the use of private members that access is controlled.
Restricting access to a class’ members is a fundamental part of object-oriented programming because it helps prevent the misuse of an object. By allowing access to private data only through a well-defined set of methods, you can prevent improper values from being assigned to that data—by performing a range check, for example. It is not possible for code outside the class to set the value of a private member directly. You can also control precisely how and when the data within an object is used. Thus, when correctly implemented, a class creates a “black box” that can be used, but the inner workings of which are not open to tampering.
Member access control is achieved through the use of four access modifiers: public, private, protected, and internal. In this chapter, we will be concerned with public and private. The protected modifier applies only when inheritance is involved and is described in Chapter 11. The internal modifier applies mostly to the use of an assembly, which for C# loosely means a deployable program or library. The internal modifier is examined in Chapter 16.
When a member of a class is modified by the public specifier, that member can be accessed by any other code in your program. This includes methods defined inside other classes.
When a member of a class is specified as private, then that member can be accessed only by other members of its class. Thus, methods in other classes are not able to access a private member of another class. As explained in Chapter 6, if no access specifier is used, a class member is private to its class by default. Thus, the private specifier is optional when creating private class members.
An access specifier precedes the rest of a member’s type specification. That is, it must begin a member’s declaration statement. Here are some examples:
public string errMsg;
private double bal;
private bool isError(byte status) { // ...
To understand the difference between public and private, consider the following program:
// Public vs. private access.
using System;
class MyClass {
private int alpha; // private access explicitly specified
int beta; // private access by default
public int gamma; // public access
// Methods to access alpha and beta. It is OK for a member
// of a class to access a private member of the same class.
public void SetAlpha(int a) {
alpha = a;
}
public int GetAlpha() {
return alpha;
}
public void SetBeta(int a) {
beta = a;
}
public int GetBeta() {
return beta;
}
}
class AccessDemo {
static void Main() {
MyClass ob = new MyClass();
// Access to alpha and beta is allowed only through methods.
ob.SetAlpha(-99);
ob.SetBeta(19);
Console.WriteLine("ob.alpha is " + ob.GetAlpha());
Console.WriteLine("ob.beta is " + ob.GetBeta());
// You cannot access alpha or beta like this:
// ob.alpha = 10; // Wrong! alpha is private!
// ob.beta = 9; // Wrong! beta is private!
// It is OK to directly access gamma because it is public.
ob.gamma = 99;
}
}
As you can see, inside the MyClass class, alpha is specified as private, beta is private by default, and gamma is specified as public. Because alpha and beta are private, they cannot be accessed by code outside of their class. Therefore, inside the AccessDemo class, neither can be used directly. Each must be accessed through public methods, such as SetAlpha( ) and GetAlpha( ). For example, if you were to remove the comment symbol from the beginning of the following line
// ob.alpha = 10; // Wrong! alpha is private!
you would not be able to compile this program because of the access violation. Although access to alpha by code outside of MyClass is not allowed, methods defined within MyClass can freely access it, as the SetAlpha( ) and GetAlpha( ) methods show. The same is true for beta.
The key point is this: A private member can be used freely by other members of its class, but it cannot be accessed by code outside its class.
The proper use of public and private access is a key component of successful object-oriented programming. Although there are no hard and fast rules, here are some general principles that serve as guidelines:
• Members of a class that are used only within the class itself should be private.
• Instance data that must be within a specific range should be private, with access provided through public methods that can perform range checks.
• If changing a member can cause an effect that extends beyond the member itself (that is, affects other aspects of the object), that member should be private, and access to it should be controlled.
• Members that can cause harm to an object when improperly used should be private. Access to these members should be through public methods that prevent improper usage.
• Methods that get and set the values of private data must be public.
• Public instance variables are permissible when there is no reason for them to be private.
Of course, there are many nuances that the preceding rules do not address, and special cases cause one or more rules to be violated. But, in general, if you follow these rules, you will be creating resilient objects that are not easily misused.
To better understand the “how and why” behind access control, a case study is useful. One of the quintessential examples of object-oriented programming is a class that implements a stack. As you probably know, a stack is a data structure that implements a last-in, first-out list. Its name comes from the analogy of a stack of plates on a table. The first plate on the table is the last one to be used.
A stack is a classic example of object-oriented programming because it combines storage for information along with the methods that access that information. Thus, a stack is a data engine that enforces the last-in, first-out usage. Such a combination is an excellent choice for a class in which the members that provide storage for the stack are private, and public methods provide access. By encapsulating the underlying storage, it is not possible for code that uses the stack to access the elements out of order.
A stack defines two basic operations: push and pop. A push puts a value onto the top of the stack. A pop removes a value from the top of the stack. Thus, a pop is consumptive; once a value has been popped off the stack, it has been removed and cannot be accessed again.
The example shown here creates a class called Stack that implements a stack. The underlying storage for the stack is provided by a private array. The push and pop operations are available through the public methods of the Stack class. Thus, the public methods enforce the last-in, first-out mechanism. As shown here, the Stack class stores characters, but the same mechanism could be used to store any type of data:
// A stack class for characters.
using System;
class Stack {
// These members are private.
char[] stck; // holds the stack
int tos; // index of the top of the stack
// Construct an empty Stack given its size.
public Stack(int size) {
stck = new char[size]; // allocate memory for stack
tos = 0;
}
// Push characters onto the stack.
public void Push(char ch) {
if(tos==stck.Length) {
Console.WriteLine(" -- Stack is full.");
return;
}
stck[tos] = ch;
tos++;
}
// Pop a character from the stack.
public char Pop() {
if(tos==0) {
Console.WriteLine(" -- Stack is empty.");
return (char) 0;
}
tos--;
return stck[tos];
}
// Return true if the stack is full.
public bool IsFull() {
return tos==stck.Length;
}
// Return true if the stack is empty.
public bool IsEmpty() {
return tos==0;
}
// Return total capacity of the stack.
public int Capacity() {
return stck.Length;
}
// Return number of objects currently on the stack.
public int GetNum() {
return tos;
}
}
Let’s examine this class closely. The Stack class begins by declaring these two instance variables:
// These members are private.
char[] stck; // holds the stack
int tos; // index of the top of the stack
The stck array provides the underlying storage for the stack, which in this case holds characters. Notice that no array is allocated. The allocation of the actual array is handled by the Stack constructor. The tos member holds the index of the top of the stack.
Both the tos and stck members are private. This enforces the last-in, first-out stack mechanism. If public access to stck were allowed, then the elements on the stack could be accessed out of order. Also, since tos holds the index of the top element in the stack, manipulations of tos by code outside the Stack class must be prevented in order to avoid corruption of the stack. Access to stck and tos is available, indirectly, to the user of Stack through the various public methods described shortly.
The stack constructor is shown next:
// Construct an empty Stack given its size.
public Stack(int size) {
stck = new char[size]; // allocate memory for stack
tos = 0;
}
The constructor is passed the desired size of the stack. It allocates the underlying array and sets tos to zero. Thus, a zero value in tos indicates that the stack is empty.
The public Push( ) method puts an element onto the stack. It is shown here:
// Push characters onto the stack.
public void Push(char ch) {
if(tos==stck.Length) {
Console.WriteLine(" -- Stack is full.");
return;
}
stck[tos] = ch;
tos++;
}
The element to be pushed onto the stack is passed in ch. Before the element is added to the stack, a check is made to ensure that there is still room in the underlying array. This is done by making sure that tos does not exceed the length of stck. If there is still room, the element is stored in stck at the index specified by tos, and then tos is incremented. Thus, tos always contains the index of the next free element in stck.
To remove an element from the stack, call the public method Pop( ). It is shown here:
// Pop a character from the stack.
public char Pop() {
if(tos==0) {
Console.WriteLine(" -- Stack is empty.");
return (char) 0;
}
tos--;
return stck[tos];
}
Here, the value of tos is checked. If it is zero, the stack is empty. Otherwise, tos is decremented, and the element at that index is returned.
Although Push( ) and Pop( ) are the only methods needed to implement a stack, some others are quite useful, and the Stack class defines four more. These are IsFull( ), IsEmpty( ), Capacity( ), and GetNum( ), and they provide information about the state of the stack. They are shown here:
// Return true if the stack is full.
public bool IsFull() {
return tos==stck.Length;
}
// Return true if the stack is empty.
public bool IsEmpty() {
return tos==0;
}
// Return total capacity of the stack.
public int Capacity() {
return stck.Length;
}
// Return number of objects currently on the stack.
public int GetNum() {
return tos;
}
The IsFull( ) method returns true when the stack is full and false otherwise. The IsEmpty( ) method returns true when the stack is empty and false otherwise. To obtain the total capacity of the stack (that is, the total number of elements it can hold), call Capacity( ). To obtain the number of elements currently stored on the stack, call GetNum( ). These methods are useful because the information they provide requires access to tos, which is private. They are also examples of how public methods can provide safe access to private members.
The following program demonstrates the stack:
// Demonstrate the Stack class.
using System;
class StackDemo {
static void Main() {
Stack stk1 = new Stack(10);
Stack stk2 = new Stack(10);
Stack stk3 = new Stack(10);
char ch;
int i;
// Put some characters into stk1.
Console.WriteLine("Push A through J onto stk1.");
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
if(stk1.IsFull()) Console.WriteLine("stk1 is full.");
// Display the contents of stk1.
Console.Write("Contents of stk1: ");
while( !stk1.IsEmpty() ) {
ch = stk1.Pop();
Console.Write(ch);
}
Console.WriteLine();
if(stk1.IsEmpty()) Console.WriteLine("stk1 is empty.\n");
// Put more characters into stk1.
Console.WriteLine("Again push A through J onto stk1.");
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
// Now, pop from stk1 and push the element in stk2.
// This causes stk2 to hold the elements in reverse order.
Console.WriteLine("Now, pop chars from stk1 and push " +
"them onto stk2.");
while( !stk1.IsEmpty() ) {
ch = stk1.Pop();
stk2.Push(ch);
}
Console.Write("Contents of stk2: ");
while( !stk2.IsEmpty() ) {
ch = stk2.Pop();
Console.Write(ch);
}
Console.WriteLine("\n");
// Put 5 characters into stack.
Console.WriteLine("Put 5 characters on stk3.");
for(i=0; i < 5; i++)
stk3.Push((char) ('A' + i));
Console.WriteLine("Capacity of stk3: " + stk3.Capacity());
Console.WriteLine("Number of objects in stk3: " +
stk3.GetNum());
}
}
The output from the program is shown here:
Push A through J onto stk1.
stk1 is full.
Contents of stk1: JIHGFEDCBA
stk1 is empty.
Again push A through J onto stk1.
Now, pop chars from stk1 and push them onto stk2.
Contents of stk2: ABCDEFGHIJ
Put 5 characters on stk3.
Capacity of stk3: 10
Number of objects in stk3: 5
Up to this point, the examples in this book have been using value types, such as int or double, as parameters to methods. However, it is both correct and common to use a reference type as a parameter. Doing so allows an object to be passed to a method. For example, consider the following program:
// References can be passed to methods.
using System;
class MyClass {
int alpha, beta;
public MyClass(int i, int j) {
alpha = i;
beta = j;
}
// Return true if ob contains the same values as the invoking object.
public bool SameAs(MyClass ob) {
if((ob.alpha == alpha) & (ob.beta == beta))
return true;
else return false;
}
// Make a copy of ob.
public void Copy(MyClass ob) {
alpha = ob.alpha;
beta = ob.beta;
}
public void Show() {
Console.WriteLine("alpha: {0}, beta: {1}",
alpha, beta);
}
}
class PassOb {
static void Main() {
MyClass ob1 = new MyClass(4, 5);
MyClass ob2 = new MyClass(6, 7);
Console.Write("ob1: ");
ob1.Show();
Console.Write("ob2: ");
ob2.Show();
if(ob1.SameAs(ob2))
Console.WriteLine("ob1 and ob2 have the same values.");
else
Console.WriteLine("ob1 and ob2 have different values.");
Console.WriteLine();
// Now, make ob1 a copy of ob2.
ob1.Copy(ob2);
Console.Write("ob1 after copy: ");
ob1.Show();
if(ob1.SameAs(ob2))
Console.WriteLine("ob1 and ob2 have the same values.");
else
Console.WriteLine("ob1 and ob2 have different values.");
}
}
This program generates the following output:
ob1: alpha: 4, beta: 5
ob2: alpha: 6, beta: 7
ob1 and ob2 have different values.
ob1 after copy: alpha: 6, beta: 7
ob1 and ob2 have the same values.
The SameAs( ) and Copy( ) methods each take a reference of type MyClass as an argument. The SameAs( ) method compares the values of alpha and beta in the invoking object with the values of alpha and beta in the object passed via ob. The method returns true only if the two objects contain the same values for these instance variables. The Copy( ) method assigns the values of alpha and beta in the object referred to by ob to alpha and beta in the invoking object. As this example shows, syntactically, reference types are passed to methods in the same way as are value types.
As the preceding example demonstrated, passing an object reference to a method is a straightforward task. However, there are some nuances that the example did not show. In certain cases, the effects of passing a reference type will be different than those experienced when passing a value type. To see why, let’s review the two ways in which an argument can be passed to a subroutine.
The first way is call-by-value. This method copies the value of an argument into the formal parameter of the subroutine. Therefore, changes made to the parameter of the subroutine have no effect on the argument used in the call. The second way an argument can be passed is call-by-reference. In this method, a reference to an argument (not the value of the argument) is passed to the parameter. Inside the subroutine, this reference is used to access the actual argument specified in the call. This means that changes made to the parameter will affect the argument used to call the subroutine.
By default, C# uses call-by-value, which means that a copy of the argument is made and given to the receiving parameter. Thus, when you pass a value type, such as int or double, what occurs to the parameter that receives the argument has no effect outside the method. For example, consider the following program:
// Value types are passed by value.
using System;
class Test {
/* This method causes no change to the arguments
used in the call. */
public void NoChange(int i, int j) {
i = i + j;
j = -j;
}
}
class CallByValue {
static void Main() {
Test ob = new Test();
int a = 15, b = 20;
Console.WriteLine("a and b before call: " +
a + " " + b);
ob.NoChange(a, b);
Console.WriteLine("a and b after call: " +
a + " " + b);
}
}
The output from this program is shown here:
a and b before call: 15 20
a and b after call: 15 20
As you can see, the operations that occur inside NoChange( ) have no effect on the values of a and b used in the call. Again, this is because copies of the value of a and b have been given to parameters i and j, but a and b are otherwise completely independent of i and j. Thus, assigning i a new value will not affect a.
When you pass a reference to a method, the situation is a bit more complicated. In this case, the reference, itself, is still passed by value. Thus, a copy of the reference is made and changes to the parameter will not affect the argument. (For example, making the parameter refer to a new object will not change the object to which the argument refers.) However—and this is a big however—changes made to the object being referred to by the parameter will affect the object referred to by the argument. Let’s see why.
Recall that when you create a variable of a class type, you are only creating a reference to an object. Thus, when you pass this reference to a method, the parameter that receives it will refer to the same object as that referred to by the argument. Therefore, the argument and the parameter will both refer to the same object. This means that objects are passed to methods by what is effectively call-by-reference. Thus, changes to the object inside the method do affect the object used as an argument. For example, consider the following program:
// Objects are passed by reference.
using System;
class Test {
public int a, b;
public Test(int i, int j) {
a = i;
b = j;
}
/* Pass an object. Now, ob.a and ob.b in object
used in the call will be changed. */
public void Change(Test ob) {
ob.a = ob.a + ob.b;
ob.b = -ob.b;
}
}
class CallByRef {
static void Main() {
Test ob = new Test(15, 20);
Console.WriteLine("ob.a and ob.b before call: " +
ob.a + " " + ob.b);
ob.Change(ob);
Console.WriteLine("ob.a and ob.b after call: " +
ob.a + " " + ob.b);
}
}
This program generates the following output:
ob.a and ob.b before call: 15 20
ob.a and ob.b after call: 35 -20
As you can see, in this case, the actions inside Change( ) have affected the object used as an argument.
To review: When a reference is passed to a method, the reference itself is passed by use of call-by-value. Thus, a copy of that reference is made. However, the copy of that reference will still refer to the same object as its corresponding argument. This means that objects are implicitly passed using call-by-reference.
As just explained, value types, such as int or char, are passed by value to a method. This means that changes to the parameter that receives a value type will not affect the actual argument used in the call. You can, however, alter this behavior. Through the use of the ref and out keywords, it is possible to pass any of the value types by reference. Doing so allows a method to alter the argument used in the call.
Before going into the mechanics of using ref and out, it is useful to understand why you might want to pass a value type by reference. In general, there are two reasons: to allow a method to alter the contents of its arguments or to allow a method to return more than one value. Let’s look at each reason in detail.
Often you will want a method to be able to operate on the actual arguments that are passed to it. The quintessential example of this is a Swap( ) method that exchanges the values of its two arguments. Since value types are passed by value, it is not possible to write a method that swaps the value of two ints, for example, using C#’s default call-by-value parameter passing mechanism. The ref modifier solves this problem.
As you know, a return statement enables a method to return a value to its caller. However, a method can return only one value each time it is called. What if you need to return two or more pieces of information? For example, what if you want to create a method that decomposes a floating-point number into its integer and fractional parts? To do this requires that two pieces of information be returned: the integer portion and the fractional component. This method cannot be written using only a single return value. The out modifier solves this problem.
The ref parameter modifier causes C# to create a call-by-reference, rather than a call-by-value. The ref modifier is specified when the method is declared and when it is called. Let’s begin with a simple example. The following program creates a method called Sqr( ) that returns in-place the square of its integer argument. Notice the use and placement of ref.
// Use ref to pass a value type by reference.
using System;
class RefTest {
// This method changes its argument. Notice the use of ref.
public void Sqr(ref int i) {
i = i * i;
}
}
class RefDemo {
static void Main() {
RefTest ob = new RefTest();
int a = 10;
Console.WriteLine("a before call: " + a);
ob.Sqr(ref a); // notice the use of ref
Console.WriteLine("a after call: " + a);
}
}
Notice that ref precedes the entire parameter declaration in the method and that it precedes the argument when the method is called. The output from this program, shown here, confirms that the value of the argument, a, was indeed modified by Sqr( ):
a before call: 10
a after call: 100
Using ref, it is now possible to write a method that exchanges the values of its two value-type arguments. For example, here is a program that contains a method called Swap( ) that exchanges the values of the two integer arguments with which it is called:
// Swap two values.
using System;
class ValueSwap {
// This method now changes its arguments.
public void Swap(ref int a, ref int b) {
int t;
t = a;
a = b;
b = t;
}
}
class ValueSwapDemo {
static void Main() {
ValueSwap ob = new ValueSwap();
int x = 10, y = 20;
Console.WriteLine("x and y before call: " + x + " " + y);
ob.Swap(ref x, ref y);
Console.WriteLine("x and y after call: " + x + " " + y);
}
}
The output from this program is shown here:
x and y before call: 10 20
x and y after call: 20 10
Here is one important point to understand about ref: An argument passed by ref must be assigned a value prior to the call. The reason is that the method that receives such an argument assumes that the parameter refers to a valid value. Thus, using ref, you cannot use a method to give an argument an initial value.
Sometimes you will want to use a reference parameter to receive a value from a method, but not pass in a value. For example, you might have a method that performs some function, such as opening a network socket, that returns a success/fail code in a reference parameter. In this case, there is no information to pass into the method, but there is information to pass back out. The problem with this scenario is that a ref parameter must be initialized to a value prior to the call. Thus, to use a ref parameter would require giving the argument a dummy value just to satisfy this constraint. Fortunately, C# provides a better alternative: the out parameter.
An out parameter is similar to a ref parameter with this one exception: It can only be used to pass a value out of a method. It is not necessary (or useful) to give the variable used as an out parameter an initial value prior to calling the method. The method will give the variable a value. Furthermore, inside the method, an out parameter is considered unassigned; that is, it is assumed to have no initial value. This implies that the method must assign the parameter a value prior to the method’s termination. Thus, after the call to the method, an out parameter will contain a value.
Here is an example that uses an out parameter. In the class Decompose, the GetParts( ) method decomposes a floating-point number into its integer and fractional parts. Notice how each component is returned to the caller.
// Use out.
using System;
class Decompose {
/* Decompose a floating-point value into its
integer and fractional parts. */
public int GetParts(double n, out double frac) {
int whole;
whole = (int) n;
frac = n - whole; // pass fractional part back through frac
return whole; // return integer portion
}
}
class UseOut {
static void Main() {
Decompose ob = new Decompose();
int i;
double f;
i = ob.GetParts(10.125, out f);
Console.WriteLine("Integer portion is " + i);
Console.WriteLine("Fractional part is " + f);
}
}
The output from the program is shown here:
Integer portion is 10
Fractional part is 0.125
The GetParts( ) method returns two pieces of information. First, the integer portion of n is returned as GetParts( )’s return value. Second, the fractional portion of n is passed back to the caller through the out parameter frac. As this example shows, by using out, it is possible for one method to return two values.
Of course, you are not limited to only one out parameter. A method can return as many pieces of information as necessary through out parameters. Here is an example that uses two out parameters. The method HasComFactor( ) performs two functions. First, it determines if two integers have a common factor (other than 1). It returns true if they do and false otherwise. Second, if they do have a common factor, HasComFactor( ) returns the least and greatest common factors in out parameters.
// Use two out parameters.
using System;
class Num {
/* Determine if x and v have a common divisor.
If so, return least and greatest common factors in
the out parameters. */
public bool HasComFactor(int x, int y,
out int least, out int greatest) {
int i;
int max = x < y ? x : y;
bool first = true;
least = 1;
greatest = 1;
// Find least and greatest common factors.
for(i=2; i <= max/2 + 1; i++) {
if( ((y%i)==0) & ((x%i)==0) ) {
if(first) {
least = i;
first = false;
}
greatest = i;
}
}
if(least != 1) return true;
else return false;
}
}
class DemoOut {
static void Main() {
Num ob = new Num();
int lcf, gcf;
if(ob.HasComFactor(231, 105, out lcf, out gcf)) {
Console.WriteLine("Lcf of 231 and 105 is " + lcf);
Console.WriteLine("Gcf of 231 and 105 is " + gcf);
}
else
Console.WriteLine("No common factor for 35 and 49.");
if(ob.HasComFactor(35, 51, out lcf, out gcf)) {
Console.WriteLine("Lcf of 35 and 51 " + lcf);
Console.WriteLine("Gcf of 35 and 51 is " + gcf);
}
else
Console.WriteLine("No common factor for 35 and 51.");
}
}
In Main( ), notice that lcf and gcf are not assigned values prior to the call to HasComFactor( ). This would be an error if the parameters had been ref rather than out. The method returns either true or false, depending upon whether the two integers have a common factor. If they do, the least and greatest common factors are returned in the out parameters. The output from this program is shown here:
Lcf of 231 and 105 is 3
Gcf of 231 and 105 is 21
No common factor for 35 and 51.
The use of ref and out is not limited to the passing of value types. They can also be used when a reference is passed. When ref or out modifies a reference, it causes the reference, itself, to be passed by reference. This allows a method to change what object the reference refers to. Consider the following program, which uses ref reference parameters to exchange the objects to which two references are referring:
// Swap two references.
using System;
class RefSwap {
int a, b;
public RefSwap(int i, int j) {
a = i;
b = j;
}
public void Show() {
Console.WriteLine("a: {0}, b: {1}", a, b);
}
// This method changes its arguments.
public void Swap(ref RefSwap ob1, ref RefSwap ob2) {
RefSwap t;
t = ob1;
ob1 = ob2;
ob2 = t;
}
}
class RefSwapDemo {
static void Main() {
RefSwap x = new RefSwap(1, 2);
RefSwap y = new RefSwap(3, 4);
Console.Write("x before call: ");
x.Show();
Console.Write("y before call: ");
y.Show();
Console.WriteLine();
// Exchange the objects to which x and y refer.
x.Swap(ref x, ref y);
Console.Write("x after call: ");
x.Show();
Console.Write("y after call: ");
y.Show();
}
}
The output from this program is shown here:
x before call: a: 1, b: 2
y before call: a: 3, b: 4
x after call: a: 3, b: 4
y after call: a: 1, b: 2
In this example, the method Swap( ) exchanges the objects to which the two arguments to Swap( ) refer. Before calling Swap( ), x refers to an object that contains the values 1 and 2, and y refers to an object that contains the values 3 and 4. After the call to Swap( ), x refers to the object that contains the values 3 and 4, and y refers to the object that contains the values 1 and 2. If ref parameters had not been used, then the exchange inside Swap( ) would have had no effect outside Swap( ). You might want to prove this by removing ref from Swap( ).
When you create a method, you usually know in advance the number of arguments that you will be passing to it, but this is not always the case. Sometimes you will want to create a method that can be passed an arbitrary number of arguments. For example, consider a method that finds the smallest of a set of values. Such a method might be passed as few as two values, or three, or four, and so on. In all cases, you want that method to return the smallest value. Such a method cannot be created using normal parameters. Instead, you must use a special type of parameter that stands for an arbitrary number of parameters. This is done by creating a params parameter.
The params modifier is used to declare an array parameter that will be able to receive zero or more arguments. The number of elements in the array will be equal to the number of arguments passed to the method. Your program then accesses the array to obtain the arguments.
Here is an example that uses params to create a method called MinVal( ), which returns the minimum value from a set of values:
// Demonstrate params.
using System;
class Min {
public int MinVal(params int[] nums) {
int m;
if(nums.Length == 0) {
Console.WriteLine("Error: no arguments.");
return 0;
}
m = nums[0];
for(int i=1; i < nums.Length; i++)
if(nums[i] < m) m = nums[i];
return m;
}
}
class ParamsDemo {
static void Main() {
Min ob = new Min();
int min;
int a = 10, b = 20;
// Call with 2 values.
min = ob.MinVal(a, b);
Console.WriteLine("Minimum is " + min);
// Call with 3 values.
min = ob.MinVal(a, b, -1);
Console.WriteLine("Minimum is " + min);
// Call with 5 values.
min = ob.MinVal(18, 23, 3, 14, 25);
Console.WriteLine("Minimum is " + min);
// Can call with an int array, too.
int[] args = { 45, 67, 34, 9, 112, 8 };
min = ob.MinVal(args);
Console.WriteLine("Minimum is " + min);
}
}
The output from the program is shown here:
Minimum is 10
Minimum is -1
Minimum is 3
Minimum is 8
Each time MinVal( ) is called, the arguments are passed to it via the nums array. The length of the array equals the number of elements. Thus, you can use MinVal( ) to find the minimum of any number of values.
Notice the last call to MinVal( ). Rather than being passed the values individually, it is passed an array containing the values. This is perfectly legal. When a params parameter is created, it will accept either a variable-length list of arguments or an array containing the arguments.
Although you can pass a params parameter any number of arguments, they all must be of a type compatible with the array type specified by the parameter. For example, calling MinVal( ) like this:
min = ob.MinVal(1, 2.2); // Wrong!
is illegal because there is no automatic conversion from double (2.2) to int, which is the type of nums in MinVal( ).
When using params, you need to be careful about boundary conditions because a params parameter can accept any number of arguments—even zero! For example, it is syntactically valid to call MinVal( ) as shown here:
min = ob.MinVal(); // no arguments
min = ob.MinVal(3); // 1 argument
This is why there is a check in MinVal( ) to confirm that at least one element is in the nums array before there is an attempt to access that element. If the check were not there, then a runtime exception would result if MinVal( ) were called with no arguments. (Exceptions are described in Chapter 13.) Furthermore, the code in MinVal( ) was written in such a way as to permit calling MinVal( ) with one argument. In that situation, the lone argument is returned.
A method can have normal parameters and a variable-length parameter. For example, in the following program, the method ShowArgs( ) takes one string parameter and then a params integer array:
// Use regular parameter with a params parameter.
using System;
class MyClass {
public void ShowArgs(string msg, params int[] nums) {
Console.Write(msg + ": ");
foreach(int i in nums)
Console.Write(i + " ");
Console.WriteLine();
}
}
class ParamsDemo2 {
static void Main() {
MyClass ob = new MyClass();
ob.ShowArgs("Here are some integers",
1, 2, 3, 4, 5);
ob.ShowArgs("Here are two more",
17, 20);
}
}
This program displays the following output:
Here are some integers: 1 2 3 4 5
Here are two more: 17 20
In cases where a method has regular parameters and a params parameter, the params parameter must be the last one in the parameter list. Furthermore, in all situations, there must be only one params parameter.
A method can return any type of data, including class types. For example, the following version of the Rect class includes a method called Enlarge( ) that creates a rectangle that is proportionally the same as the invoking rectangle, but larger by a specified factor:
// Return an object.
using System;
class Rect {
int width;
int height;
public Rect(int w, int h) {
width = w;
height = h;
}
public int Area() {
return width * height;
}
public void Show() {
Console.WriteLine(width + " " + height);
}
/* Return a rectangle that is a specified
factor larger than the invoking rectangle. */
public Rect Enlarge(int factor) {
return new Rect(width * factor, height * factor);
}
}
class RetObj {
static void Main() {
Rect r1 = new Rect(4, 5);
Console.Write("Dimensions of r1: ");
r1.Show();
Console.WriteLine("Area of r1: " + r1.Area());
Console.WriteLine();
// Create a rectangle that is twice as big as r1.
Rect r2 = r1.Enlarge(2);
Console.Write("Dimensions of r2: ");
r2.Show();
Console.WriteLine("Area of r2: " + r2.Area());
}
}
The output is shown here:
Dimensions of r1: 4 5
Area of r1: 20
Dimensions of r2: 8 10
Area of r2: 80
When an object is returned by a method, it remains in existence until there are no more references to it. At that point, it is subject to garbage collection. Thus, an object won’t be destroyed just because the method that created it terminates.
One application of object return types is the class factory. A class factory is a method that is used to construct objects of its class. In some situations, you may not want to give users of a class access to the class’ constructor because of security concerns or because object construction depends upon certain external factors. In such cases, a class factory is used to construct objects. Here is a simple example:
// Use a class factory.
using System;
class MyClass {
int a, b; // private
// Create a class factory for MyClass.
public MyClass Factory(int i, int j) {
MyClass t = new MyClass();
t.a = i;
t.b = j;
return t; // return an object
}
public void Show() {
Console.WriteLine("a and b: " + a + " " + b);
}
}
class MakeObjects {
static void Main() {
MyClass ob = new MyClass();
int i, j;
// Generate objects using the factory.
for(i=0, j=10; i < 10; i++, j--) {
MyClass anotherOb = ob.Factory(i, j); // make an object
anotherOb.Show();
}
Console.WriteLine();
}
}
The output is shown here:
a and b: 0 10
a and b: 1 9
a and b: 2 8
a and b: 3 7
a and b: 4 6
a and b: 5 5
a and b: 6 4
a and b: 7 3
a and b: 8 2
a and b: 9 1
Let’s look closely at this example. MyClass does not define a constructor, so only the default constructor is available. Thus, it is not possible to set the values of a and b using a constructor. However, the class factory Factory( ) can create objects in which a and b are given values. Moreover, since a and b are private, using Factory( ) is the only way to set these values.
In Main( ), a MyClass object is instantiated, and its factory method is used inside the for loop to create ten other objects. The line of code that creates objects is shown here:
MyClass anotherOb = ob.Factory(i, j); // get an object
With each iteration, an object reference called anotherOb is created, and it is assigned a reference to the object constructed by the factory. At the end of each iteration of the loop, anotherOb goes out of scope, and the object to which it refers is recycled.
Since in C# arrays are implemented as objects, a method can also return an array. (This differs from C++ in which arrays are not valid as return types.) For example, in the following program, the method FindFactors( ) returns an array that holds the factors of the argument that it is passed:
// Return an array.
using System;
class Factor {
/* Return an array containing the factors of num.
On return, numfactors will contain the number of
factors found. */
public int[] FindFactors(int num, out int numfactors) {
int[] facts = new int[80]; // size of 80 is arbitrary
int i, j;
// Find factors and put them in the facts array.
for(i=2, j=0; i < num/2 + 1; i++)
if( (num%i)==0 ) {
facts[j] = i;
j++;
}
numfactors = j;
return facts;
}
}
class FindFactors {
static void Main() {
Factor f = new Factor();
int numfactors;
int[] factors;
factors = f.FindFactors(1000, out numfactors);
Console.WriteLine("Factors for 1000 are: ");
for(int i=0; i < numfactors; i++)
Console.Write(factors[i] + " ");
Console.WriteLine();
}
}
The output is shown here:
Factors for 1000 are:
2 4 5 8 10 20 25 40 50 100 125 200 250 500
In Factor, FindFactors( ) is declared like this:
public int[] FindFactors(int num, out int numfactors) {
Notice how the int array return type is specified. This syntax can be generalized. Whenever a method returns an array, specify it in a similar fashion, adjusting the type and dimensions as needed. For example, the following declares a method called someMeth( ) that returns a two-dimensional array of double:
public double[,] someMeth() { // ...
In C#, two or more methods within the same class can share the same name, as long as their parameter declarations are different. When this is the case, the methods are said to be overloaded, and the process is referred to as method overloading. Method overloading is one of the ways that C# implements polymorphism.
In general, to overload a method, simply declare different versions of it. The compiler takes care of the rest. You must observe one important restriction: The type and/or number of the parameters of each overloaded method must differ. It is not sufficient for two methods to differ only in their return types. They must differ in the types or number of their parameters. (Return types do not provide sufficient information in all cases for C# to decide which method to use.) Of course, overloaded methods may differ in their return types, too. When an overloaded method is called, the version of the method executed is the one whose parameters match the arguments.
Here is a simple example that illustrates method overloading:
// Demonstrate method overloading.
using System;
class Overload {
public void OvlDemo() {
Console.WriteLine("No parameters");
}
// Overload OvlDemo for one integer parameter.
public void OvlDemo(int a) {
Console.WriteLine("One parameter: " + a);
}
// Overload OvlDemo for two integer parameters.
public int OvlDemo(int a, int b) {
Console.WriteLine("Two parameters: " + a + " " + b);
return a + b;
}
// Overload OvlDemo for two double parameters.
public double OvlDemo(double a, double b) {
Console.WriteLine("Two double parameters: " +
a + " "+ b);
return a + b;
}
}
class OverloadDemo {
static void Main() {
Overload ob = new Overload();
int resI;
double resD;
// Call all versions of OvlDemo().
ob.OvlDemo();
Console.WriteLine();
ob.OvlDemo(2);
Console.WriteLine();
resI = ob.OvlDemo(4, 6);
Console.WriteLine("Result of ob.OvlDemo(4, 6): " + resI);
Console.WriteLine();
resD = ob.OvlDemo(1.1, 2.32);
Console.WriteLine("Result of ob.OvlDemo(1.1, 2.32): " + resD);
}
}
This program generates the following output:
No parameters
One parameter: 2
Two parameters: 4 6
Result of ob.OvlDemo(4, 6): 10
Two double parameters: 1.1 2.32
Result of ob.OvlDemo(1.1, 2.32): 3.42
As you can see, OvlDemo( ) is overloaded four times. The first version takes no parameters; the second takes one integer parameter; the third takes two integer parameters; and the fourth takes two double parameters. Notice that the first two versions of OvlDemo( ) return void and the second two return a value. This is perfectly valid, but as explained, overloading is not affected one way or the other by the return type of a method. Thus, attempting to use these two versions of OvlDemo( ) will cause an error:
// One OvlDemo(int) is OK.
public void OvlDemo(int a) {
Console.WriteLine("One parameter: " + a);
}
/* Error! Two OvlDemo(int)s are not OK even though
return types differ. */
public int OvlDemo(int a) {
Console.WriteLine("One parameter: " + a);
return a * a;
}
As the comments suggest, the difference in their return types is an insufficient difference for the purposes of overloading.
As you will recall from Chapter 3, C# provides certain implicit (i.e., automatic) type conversions. These conversions also apply to parameters of overloaded methods. For example, consider the following:
// Implicit type conversions can affect overloaded method resolution.
using System;
class Overload2 {
public void MyMeth(int x) {
Console.WriteLine("Inside MyMeth(int): " + x);
}
public void MyMeth(double x) {
Console.WriteLine("Inside MyMeth(double): " + x);
}
}
class TypeConv {
static void Main() {
Overload2 ob = new Overload2();
int i = 10;
double d = 10.1;
byte b = 99;
shorts = 10;
float f = 11.5F;
ob.MyMeth(i); // calls ob.MyMeth(int)
ob.MyMeth(d); // calls ob.MyMeth(double)
ob.MyMeth(b); // calls ob.MyMeth(int) -- type conversion
ob.MyMeth(s); // calls ob.MyMeth(int) -- type conversion
ob.MyMeth(f); // calls ob.MyMeth(double) -- type conversion
}
}
The output from the program is shown here:
Inside MyMeth(int): 10
Inside MyMeth(double): 10.1
Inside MyMeth(int): 99
Inside MyMeth(int): 10
Inside MyMeth(double): 11.5
In this example, only two versions of MyMeth( ) are defined: one that has an int parameter and one that has a double parameter. However, it is possible to pass MyMeth( ) a byte, short, or float value. In the case of byte and short, C# automatically converts them to int. Thus, MyMeth(int) is invoked. In the case of float, the value is converted to double and MyMeth(double) is called.
It is important to understand, however, that the implicit conversions apply only if there is no exact type match between a parameter and an argument. For example, here is the preceding program with the addition of a version of MyMeth( ) that specifies a byte parameter:
// Add MyMeth(byte).
using System;
class Overload2 {
public void MyMeth(byte x) {
Console.WriteLine("Inside MyMeth(byte): " + x);
}
public void MyMeth(int x) {
Console.WriteLine("Inside MyMeth(int): " + x);
}
public void MyMeth(double x) {
Console.WriteLine("Inside MyMeth(double): " + x);
}
}
class TypeConv {
static void Main() {
Overload2 ob = new Overload2();
int i = 10;
double d = 10.1;
byte b = 99;
shorts = 10;
float f = 11.5F;
ob.MyMeth(i); // calls ob.MyMeth(int)
ob.MyMeth(d); // calls ob.MyMeth(double)
ob.MyMeth(b); // calls ob.MyMeth(byte) -- now, no type conversion
ob.MyMeth(s); // calls ob.MyMeth(int) -- type conversion
ob.MyMeth(f); // calls ob.MyMeth(double) -- type conversion
}
}
Now when the program is run, the following output is produced:
Inside MyMeth(int): 10
Inside MyMeth(double): 10.1
Inside MyMeth(byte): 99
Inside MyMeth(int): 10
Inside MyMeth(double): 11.5
In this version, since there is a version of MyMeth( ) that takes a byte argument, when MyMeth( ) is called with a byte argument, MyMeth(byte) is invoked and the automatic conversion to int does not occur.
Both ref and out participate in overload resolution. For example, the following defines two distinct and separate methods:
public void MyMeth(int x) {
Console.WriteLine("Inside MyMeth(int): " + x);
}
public void MyMeth(ref int x) {
Console.WriteLine("Inside MyMeth(ref int): " + x);
}
Thus,
ob.MyMeth(i)
invokes MyMeth(int x), but
ob.MyMeth(ref i)
invokes MyMeth(ref int x).
Although ref and out participate in overload resolution, the difference between the two alone is not sufficient. For example, these two versions of MyMeth( ) are invalid:
// Wrong!
public void MyMeth(out int x) { // ...
public void MyMeth(ref int x) { // ...
In this case, the compiler cannot differentiate between the two versions of MyMeth( ) simply because one uses an out int parameter and the other uses a ref int parameter.
Method overloading supports polymorphism because it is one way that C# implements the “one interface, multiple methods” paradigm. To understand how, consider the following. In languages that do not support method overloading, each method must be given a unique name. However, frequently you will want to implement essentially the same method for different types of data. Consider the absolute value function. In languages that do not support overloading, there are usually three or more versions of this function, each with a slightly different name. For instance, in C, the function abs( ) returns the absolute value of an integer, labs( ) returns the absolute value of a long integer, and fabs( ) returns the absolute value of a floating-point value.
Since C does not support overloading, each function must have its own unique name, even though all three functions do essentially the same thing. This makes the situation more complex, conceptually, than it actually is. Although the underlying concept of each function is the same, you still have three names to remember. This situation does not occur in C# because each absolute value method can use the same name. Indeed, the .NET Framework class library includes an absolute value method called Abs( ). This method is overloaded by the System.Math class to handle the numeric types. C# determines which version of Abs( ) to call based upon the type of argument.
A principal value of overloading is that it allows related methods to be accessed by use of a common name. Thus, the name Abs represents the general action that is being performed. It is left to the compiler to choose the right specific version for a particular circumstance. You, the programmer, need only remember the general operation being performed. Through the application of polymorphism, several names have been reduced to one. Although this example is fairly simple, if you expand the concept, you can see how overloading can help manage greater complexity.
When you overload a method, each version of that method can perform any activity you desire. There is no rule stating that overloaded methods must relate to one another. However, from a stylistic point of view, method overloading implies a relationship. Thus, while you can use the same name to overload unrelated methods, you should not. For example, you could use the name Sqr to create methods that return the square of an integer and the square root of a floating-point value. But these two operations are fundamentally different. Applying method overloading in this manner defeats its original purpose. In practice, you should only overload closely related operations.
C# defines the term signature, which includes the name of a method plus its parameter list. Thus, for the purposes of overloading, no two methods within the same class can have the same signature. Notice that a signature does not include the return type since it is not used by C# for overload resolution. Also, the params modifier is not part of the signature.
Like methods, constructors can also be overloaded. Doing so allows you to construct objects in a variety of ways. For example, consider the following program:
// Demonstrate an overloaded constructor.
using System;
class MyClass {
public int x;
public MyClass() {
Console.WriteLine("Inside MyClass().");
x = 0;
}
public MyClass(int i) {
Console.WriteLine("Inside MyClass(int).");
x = i;
}
public MyClass(double d) {
Console.WriteLine("Inside MyClass(double).");
x = (int) d;
}
public MyClass(int i, int j) {
Console.WriteLine("Inside MyClass(int, int).");
x = i * j;
}
}
class OverloadConsDemo {
static void Main() {
MyClass t1 = new MyClass();
MyClass t2 = new MyClass(88);
MyClass t3 = new MyClass(17.23);
MyClass t4 = new MyClass(2, 4);
Console.WriteLine("t1.x: " + t1.x);
Console.WriteLine("t2.x: " + t2.x);
Console.WriteLine("t3.x: " + t3.x);
Console.WriteLine("t4.x: " + t4.x);
}
}
The output from the program is shown here:
Inside MyClass().
Inside MyClass(int).
Inside MyClass(double).
Inside MyClass(int, int).
t1.x: 0
t2.x: 88
t3.x: 17
t4.x: 8
MyClass( ) is overloaded four ways, each constructing an object differently. The proper constructor is called based upon the arguments specified when new is executed. By overloading a class’ constructor, you give the user of your class flexibility in the way objects are constructed.
One of the most common reasons that constructors are overloaded is to allow one object to initialize another. For example, here is an enhanced version of the Stack class developed earlier that allows one stack to be constructed from another:
// A stack class for characters.
using System;
class Stack {
// These members are private.
char[] stck; // holds the stack
int tos; // index of the top of the stack
// Construct an empty Stack given its size.
public Stack(int size) {
stck = new char[size]; // allocate memory for stack
tos = 0;
}
// Construct a Stack from a stack.
public Stack(Stack ob) {
// Allocate memory for stack.
stck = new char[ob.stck.Length];
// Copy elements to new stack.
for(int i=0; i < ob.tos; i++)
stck[i] = ob.stck[i];
// Set tos for new stack.
tos = ob.tos;
}
// Push characters onto the stack.
public void Push(char ch) {
if(tos==stck.Length) {
Console.WriteLine(" -- Stack is full.");
return;
}
stck[tos] = ch;
tos++;
}
// Pop a character from the stack.
public char Pop() {
if(tos==0) {
Console.WriteLine(" -- Stack is empty.");
return (char) 0;
}
tos--;
return stck[tos];
}
// Return true if the stack is full.
public bool IsFull() {
return tos==stck.Length;
}
// Return true if the stack is empty.
public bool IsEmpty() {
return tos==0;
}
// Return total capacity of the stack.
public int Capacity() {
return stck.Length;
}
// Return number of objects currently on the stack.
public int GetNum() {
return tos;
}
}
// Demonstrate the Stack class.
class StackDemo {
static void Main() {
Stack stk1 = new Stack(10);
char ch;
int i;
// Put some characters into stk1.
Console.WriteLine("Push A through J onto stk1.");
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
// Create a copy of stck1.
Stack stk2 = new Stack(stk1);
// Display the contents of stk1.
Console.Write("Contents of stk1: ");
while( !stk1.IsEmpty() ) {
ch = stk1.Pop();
Console.Write(ch);
}
Console.WriteLine();
Console.Write("Contents of stk2: ");
while ( !stk2.IsEmpty() ) {
ch = stk2.Pop();
Console.Write(ch);
}
Console.WriteLine("\n");
}
}
The output is shown here:
Push A through J onto stk1.
Contents of stk1: JIHGFEDCBA
Contents of stk2: JIHGFEDCBA
In StackDemo, the first stack, stk1, is constructed and filled with characters. This stack is then used to construct the second stack, stk2. This causes the following Stack constructor to be executed:
// Construct a Stack from a stack.
public Stack(Stack ob) {
// Allocate memory for stack.
stck = new char[ob.stck.Length];
// Copy elements to new stack.
for(int i=0; i < ob.tos; i++)
stck[i] = ob.stck[i];
// Set tos for new stack.
tos = ob.tos;
}
Inside this constructor, an array is allocated that is long enough to hold the elements contained in the stack passed in ob. Then, the contents of ob’s array are copied to the new array, and tos is set appropriately. After the constructor finishes, the new stack and the original stack are separate, but identical.
When working with overloaded constructors, it is sometimes useful for one constructor to invoke another. In C#, this is accomplished by using another form of the this keyword. The general form is shown here:
constructor-name(parameter-list1): this(parameter-list2) {
// ... body of constructor, which may be empty
}
When the constructor is executed, the overloaded constructor that matches the parameter list specified by parameter-list2 is first executed. Then, if there are any statements inside the original constructor, they are executed. Here is an example:
// Demonstrate invoking a constructor through this.
using System;
class XYCoord {
public int x, y;
public XYCoord() : this(0, 0) {
Console.WriteLine("Inside XYCoord()");
}
public XYCoord(XYCoord obj) : this(obj.x, obj.y) {
Console.WriteLine("Inside XYCoord(obj)");
}
public XYCoord(int i, int j) {
Console.WriteLine("Inside XYCoord(int, int)");
x = i;
y = j;
}
}
class OverloadConsDemo {
static void Main() {
XYCoord t1 = new XYCoord();
XYCoord t2 = new XYCoord(8, 9);
XYCoord t3 = new XYCoord(t2);
Console.WriteLine("t1.x, t1.y: " + t1.x + ", " + t1.y);
Console.WriteLine("t2.x, t2.y: " + t2.x + ", " + t2.y);
Console.WriteLine("t3.x, t3.y: " + t3.x + ", " + t3.y);
}
}
The output from the program is shown here:
Inside XYCoord(int, int)
Inside XYCoord()
Inside XYCoord(int, int)
Inside XYCoord(int, int)
Inside XYCoord(obj)
t1.x, t1.y: 0, 0
t2.x, t2.y: 8, 9
t3.x, t3.y: 8, 9
Here is how the program works. In the XYCoord class, the only constructor that actually initializes the x and y fields is XYCoord(int, int). The other two constructors simply invoke XYCoord(int, int) through this. For example, when object t1 is created, its constructor, XYCoord( ), is called. This causes this(0, 0) to be executed, which in this case translates into a call to XYCoord(0, 0). The creation of t2 works in similar fashion.
One reason why invoking overloaded constructors through this can be useful is that it can prevent the unnecessary duplication of code. In the foregoing example, there is no reason for all three constructors to duplicate the same initialization sequence, which the use of this avoids. Another advantage is that you can create constructors with implied “default arguments” that are used when these arguments are not explicitly specified. For example, you could create another XYCoord constructor as shown here:
public XYCoord(int x) : this(x, x) { }
This constructor automatically defaults the y coordinate to the same value as the x coordinate. Of course, it is wise to use such “default arguments” carefully because their misuse could easily confuse users of your classes.
Object initializers provide another way to create an object and initialize its fields and properties. (See Chapter 10 for a discussion of properties.) Using object initializers, you do not call a class’ constructor in the normal way. Rather, you specify the names of the fields and/or properties to be initialized, giving each an initial value. Thus, the object initializer syntax provides an alternative to explicitly invoking a class’ constructor. The primary use of the object initializer syntax is with anonymous types created in a LINQ expression. (Anonymous types and LINQ are described in Chapter 19.) However, because the object initializers can be used (and occasionally are used) with a named class, the fundamentals of object initialization are introduced here.
Let’s begin with a simple example:
// A simple demonstration that uses object initializers.
using System;
class MyClass {
public int Count;
public string Str;
}
class ObjInitDemo {
static void Main() {
// Construct a MyClass object by using object initializers.
MyClass obj = new MyClass { Count = 100, Str = "Testing" };
Console.WriteLine(obj.Count + " " + obj.Str);
}
}
This produces the following output:
100 Testing
As the output shows, the value of obj.Count has been initialized to 100 and the value of obj.Str has been initialized to “Testing”. Notice, however, that MyClass does not define any explicit constructors, and that the normal constructor syntax has not been used. Rather, obj is created using the following line:
MyClass obj = new MyClass { Count = 100, Str = "Testing" };
Here, the names of the fields are explicitly specified along with their initial values. This results in a default instance of MyClass being constructed (by use of the implicit default constructor) and then Count and Str are given the specified initial values.
It is important to understand that the order of the initializers is not important. For example, obj could have been initialized as shown here:
MyClass obj = new MyClass { Str = "Testing", Count = 100 };
In this statement, the initialization of Str precedes the initialization of Count. In the program, it was the other way around. However, in either case, the end result is the same.
Here is the general form of object initialization syntax:
new class-name { name = expr, name = expr, name = expr, ... }
Here, name specifies the name of a field or property that is an accessible member of class-name. Of course, the type of the initializing expression specified by expr must be compatible with the type of field or property.
Although you can use object initializers with a named class (such as MyClass in the example), you usually won’t. In general, you will use the normal constructor call syntax when working with named classes. As mentioned, object initializers are most applicable to anonymous types generated by a LINQ expression.
C# 4.0 has a new feature that adds flexibility to the way that arguments are specified when a method is called. Called optional arguments, this feature lets you define a default value for a method’s parameter. This default value will be used if an argument that corresponds to that parameter is not specified when the method is called. Thus, specifying an argument for such a parameter is optional. Optional arguments can simplify the calling of methods in which default arguments apply to some of the parameters. They can also be used as a “shorthand” form of method overloading.
An optional argument is enabled by creating an optional parameter. To do this, simply specify a default value for the parameter, using a syntax similar to a variable initialization. The default value must be a constant expression. For example, consider this method declaration:
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
Here, two optional parameters are declared. They are beta and gamma. In this case, beta has a default value of 10, and gamma has a default value of 20. These defaults are used if no arguments are specified for these parameters when the method is called. Notice that alpha is not an optional parameter. Rather, it is a normal parameter, and an argument for it is always required.
Assuming the preceding declaration of OptArgMeth( ), it can be called in the following ways:
// Pass all arguments explicitly.
OptArgMeth(1, 2, 3);
// Let gamma default.
OptArgMeth(1, 2);
// Let both beta and gamma default.
OptArgMeth(1);
The first call passes the value 1 to alpha, 2 to beta, and 3 to gamma. Thus, all three arguments are specified explicitly, and no default values are used. The second call passes the value 1 to alpha and 2 to beta, but lets gamma default to 20. The third call passes 1 to alpha, and lets both beta and gamma default. It is important to understand that at no time can beta default without gamma also defaulting. Once the first argument defaults, all remaining arguments must also default.
The following program shows the entire process just described.:
// Demonstrate optional arguments.
using System;
class OptionArgDemo {
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
Console.WriteLine("Here is alpha, beta, and gamma: " +
alpha + " " + beta + " " + gamma);
}
static void Main() {
// Pass all arguments explicitly.
OptArgMeth(1, 2, 3);
// Let gamma default.
OptArgMeth(1, 2);
// Let both beta and gamma default.
OptArgMeth(1);
}
}
The output shown here confirms the use of the default arguments:
Here is alpha, beta, and gamma: 1 2 3
Here is alpha, beta, and gamma: 1 2 20
Here is alpha, beta, and gamma: 1 10 20
As the output shows, when an argument is not specified, its default value is used.
It is important to understand that all optional parameters must appear to the right of those that are required. For example, the following declaration is invalid:
int Sample(string name = "user", int userId) { // Error!
To fix this declaration, you must declare userId before name. Once you begin declaring optional parameters, you cannot specify a required one. For example, this declaration is also incorrect:
int Sample(int accountId, string name = "user", int userId) { // Error!
Because name is optional, userId must come before name ( or userId must also be optional).
In addition to methods, optional arguments can be also used in a constructor, indexer, or delegate. (Indexers and delegates are described later in this book.)
One benefit of optional arguments is that they enable the programmer to more easily manage complex method calls and constructor invocations. Quite frequently, a method will specify more parameters than are required for its most common usage. In many cases, through the careful use of optional arguments, some of those parameters can be made optional. This means that you need pass only those arguments that are meaningful to your situation, rather than all of them, as would otherwise be required. Such an approach streamlines the method and makes it easier for the programmer.
In some cases, optional arguments can provide an alternative to method overloading. To understand why, again consider the OptArgMeth( ) method just shown. Prior to the addition of optional arguments to C#, you would need to create three different versions of OptArgMeth( ) to achieve the same functionality as the one version shown earlier. These versions would have the following declarations:
static void OptArgMeth(int alpha)
static void OptArgMeth(int alpha, int beta)
static void OptArgMeth(int alpha, int beta, int gamma)
These overloads enable the method to be called with one, two, or three arguments. (The bodies of the methods would have to provide the values of beta and gamma when they are not passed.) While it is certainly not wrong to implement OptArgMeth( )’s functionality using overloading, the use of optional arguments is a better approach. Of course, not all overloading situations lend themselves to such an approach.
One problem that can result when using optional arguments is ambiguity. This can occur when a method that has optional parameters is overloaded. In some cases, the compiler may not be able to determine which version to call when the optional arguments are not specified. For example, consider the following two versions of OptArgMeth( ):
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
Console.WriteLine("Here is alpha, beta, and gamma: " +
alpha + " " + beta + " " + gamma);
}
static void OptArgMeth(int alpha, double beta=10.0, double gamma = 20.0) {
Console.WriteLine("Here is alpha, beta, and gamma: " +
alpha + " " + beta + " " + gamma);
}
Notice that the only difference between the two versions is the types of beta and gamma, which are the optional parameters. In the first version, their type is int. In the second version, it is double. Given these two overloads, the following call to OptArgMeth( ) is ambiguous:
OptArgMeth(1); // Error! Ambiguous
This call is ambiguous because the compiler doesn’t know if it should use the version in which beta and gamma are int, or the version in which they are double. The key point is that even though the overloading of OptArgMeth( ) is not inherently ambiguous, a specific call might be.
In general, since ambiguity may be a factor when overloading methods that allow optional arguments, it is important that you consider the implications of such overloading. In some cases, you may need to avoid the use of an optional parameter in order to avoid ambiguity that prevents your method from being used in the way that you intend.
For a more practical illustration of how an optional argument can simplify calls to some types of methods, consider the following program. It declares a method called Display( ), which displays a string. The string can be displayed in its entirety or only a portion of the string can be displayed.
// Use an optional argument to simplify a call to a method.
using System;
class UseOptArgs {
// Display part or all of string.
static void Display(string str, int start = 0, int stop = -1) {
if(stop < 0)
stop = str.Length;
// Check for out-of-range condition.
if(stop > str.Length | start > stop | start < 0)
return;
for(int i=start; i < stop; i++)
Console.Write(str[i]);
Console.WriteLine();
}
static void Main() {
Display("this is a test");
Display("this is a test", 10);
Display("this is a test", 5, 12);
}
}
The output is shown here:
this is a test
test
is a te
Look carefully at the Display( ) method. The string to be displayed is passed in the first argument. This argument is required. The second and third arguments are optional. The optional arguments specify the starting and stopping indexes of the portion of the string to display. If stop is not passed a value, then it defaults to –1, which indicates that the stopping point is the end of the string. If start is not passed a value, then it defaults to 0. Therefore, if neither optional argument is present, the string is displayed in its entirety. Otherwise, the indicated portion of the string is displayed. This means that if you call Display( ) with one argument (the string to display), the string is shown in its entirety. If you call Display( ) with two arguments, then the characters beginning at start through the end of the string are shown. If all three arguments are passed, then the portion of the string from start to stop is shown.
Although this example is quite simple, it does demonstrate the essential benefit that optional arguments offer. It lets you specify only those arguments that are needed by your usage. Default values don’t need to be explicitly passed.
Before moving on, an important point must be made. Although optional arguments are a powerful tool when used correctly, they can also be misused. The point of optional arguments is to allow a method to perform its job in an efficient, easy-to-use manner while still allowing considerable flexibility. Toward this end, the default values of all optional arguments should facilitate the normal use of a method. When this is not the case, the use of optional arguments can destructure your code and mislead others. Finally, the default value of an optional parameter should cause no harm. In other words, the accidental use of an optional argument should not have irreversible, negative consequences. For example, forgetting to specify an argument should not cause an important data file to be erased!
Another feature related to passing arguments to a method is the named argument. Named arguments were added by C# 4.0. As you know, normally, when you pass arguments to a method, the order of the arguments must match the order of the parameters defined by the method. In other words, an argument’s position in the argument list determines to which parameter it is assigned. Named arguments remove this restriction. With a named argument, you specify the name of the parameter to which an argument applies. Using this approach, the order of the arguments is not important. Thus, named arguments are a bit like object initializers described earlier, although the syntax differs.
To specify an argument by name, use this syntax:
param-name: value
Here, param-name specifies the name of the parameter to which value is passed. Of course, param-name must specify a valid parameter name for the method being called.
Here is a simple example that demonstrates named arguments. It creates a method called IsFactor( ) that returns true if the first parameter can be evenly divided by the second.
// Use named arguments.
using System;
class NamedArgsDemo {
// Determine if one value is evenly divisible by another.
static bool IsFactor(int val, int divisor) {
if((val % divisor) == 0) return true;
return false;
}
static void Main() {
// The following show various ways in which IsFactor() can be called.
// Call by use of positional arguments.
if(IsFactor(10, 2))
Console.WriteLine("2 is factor of 10.");
// Call by use of named arguments.
if(IsFactor(val: 10, divisor: 2))
Console.WriteLine("2 is factor of 10.");
// Order doesn't matter with a named argument.
if(IsFactor(divisor: 2, val: 10))
Console.WriteLine("2 is factor of 10.");
// Use both a positional argument and a named argument.
if(IsFactor(10, divisor: 2))
Console.WriteLine("2 is factor of 10.");
}
}
The output is shown here:
2 is factor of 10.
2 is factor of 10.
2 is factor of 10.
2 is factor of 10.
As the output shows, each method of calling IsFactor( ) produces the same result.
Beyond showing the named argument syntax in action, the program demonstrates two important aspects of named arguments. First, the order of specifying the arguments doesn’t matter. For example, these two calls are equivalent:
IsFactor(val :10, divisor: 2)
IsFactor(divisor: 2, val: 10)
Being order-independent is a primary benefit of named arguments. It means that you don’t need to remember (or even know) the order of the parameters in the method being called. This can be a benefit when working with COM interfaces, for example. Second, notice that you can specify a positional argument and a named argument in the same call, as this call to IsFactor( ) does:
IsFactor(10, divisor: 2)
Be aware, however, that when mixing both named and positional arguments, all positional arguments must come before any named arguments.
Named arguments can also be used in conjunction with optional arguments. For example, assuming the Display( ) method shown in the previous section, here are some ways it can be called with named arguments:
// Specify all arguments by name.
Display(stop: 10, str: "this is a test", start: 0);
// Let start default.
Display(stop: 10, str: "this is a test");
// Specify the string by position, stop by name, and
// let start default.
Display("this is a test", stop: 10);
In general, the combination of named and optional arguments can make it easier to call large, complicated methods that have many parameters.
Because the named argument syntax is more verbose than the normal positional syntax, most of the time you will want to use positional arguments to call a method. However, in those cases in which named arguments are appropriate, they can be used quite effectively.
NOTE In addition to methods, named and optional arguments can be used with constructors, indexers, and delegates. (Indexers and delegates are described later in this book.)
Up to this point, you have been using one form of Main( ). However, it has several overloaded forms. Some can be used to return a value, and some can receive arguments. Each is examined here.
When a program ends, you can return a value to the calling process (often the operating system) by returning a value from Main( ). To do so, you can use this form of Main( ):
static int Main( )
Notice that instead of being declared void, this version of Main( ) has a return type of int.
Usually, the return value from Main( ) indicates whether the program ended normally or due to some abnormal condition. By convention, a return value of zero usually indicates normal termination. All other values indicate some type of error occurred.
Many programs accept what are called command-line arguments. A command-line argument is the information that directly follows the program’s name on the command line when it is executed. For C# programs, these arguments are then passed to the Main( ) method. To receive the arguments, you must use one of these forms of Main( ):
static void Main(string[ ] args)
static int Main(string[ ] args)
The first form returns void; the second can be used to return an integer value, as described in the preceding section. For both, the command-line arguments are stored as strings in the string array passed to Main( ). The length of the args array will be equal to the number of command-line arguments, which might be zero.
For example, the following program displays all of the command-line arguments that it is called with:
// Display all command-line information.
using System;
class CLDemo {
static void Main(string[] args) {
Console.WriteLine("There are " + args.Length +
" command-line arguments.");
Console.WriteLine("They are: ");
for(int i=0; i < args.Length; i++)
Console.WriteLine(args[i]);
}
}
If CLDemo is executed like this:
CLDemo one two three
you will see the following output:
There are 3 command-line arguments.
They are:
one
two
three
To understand the way that command-line arguments can be used, consider the next program. It uses a simple substitution cipher to encode or decode messages. The message to be encoded or decoded is specified on the command line. The cipher is very simple: To encode a word, each letter is incremented by 1. Thus, A becomes B, and so on. To decode, each letter is decremented. Of course, such a cipher is of no practical value, being trivially easy to break. But it does provide an enjoyable pastime for children.
// Encode or decode a message using a simple substitution cipher.
using System;
class Cipher {
static int Main(string[] args) {
// See if arguments are present.
if(args.Length < 2) {
Console.WriteLine("Usage: encode/decode word1 [word2...wordN]");
return 1; // return failure code
}
// If args present, first arg must be encode or decode.
if(args[0] != "encode" & args[0] != "decode") {
Console.WriteLine("First arg must be encode or decode.");
return 1; // return failure code
}
// Encode or decode message.
for(int n=1; n < args.Length; n++) {
for(int i=0; i < args[n].Length; i++) {
if(args[0] == "encode")
Console.Write((char) (args[n][i] + 1) );
else
Console.Write((char) (args[n][i] - 1) );
}
Console.Write(" ");
}
Console.WriteLine();
return 0;
}
}
To use the program, specify either the “encode” or “decode” command followed by the phrase that you want to encrypt or decrypt. Assuming the program is called Cipher, here are two sample runs:
C:>Cipher encode one two
pof uxp
C:>Cipher decode pof uxp
one two
There are two interesting things in this program. First, notice how the program checks that a command-line argument is present before it continues executing. This is very important and can be generalized. When a program relies on there being one or more command-line arguments, it must always confirm that the proper arguments have been supplied. Failure to do this can lead to program malfunctions. Also, since the first command-line argument must be either “encode” or “decode,” the program also checks this before proceeding.
Second, notice how the program returns a termination code. If the required command line is not present, then 1 is returned, indicating abnormal termination. Otherwise, 0 is returned when the program ends.
In C#, a method can call itself. This process is called recursion, and a method that calls itself is said to be recursive. In general, recursion is the process of defining something in terms of itself and is somewhat similar to a circular definition. The key component of a recursive method is that it contains a statement that executes a call to itself. Recursion is a powerful control mechanism.
The classic example of recursion is the computation of the factorial of a number. The factorial of a number N is the product of all the whole numbers between 1 and N. For example, 3 factorial is 1××3, or 6. The following program shows a recursive way to compute the factorial of a number. For comparison purposes, a nonrecursive equivalent is also included.
// A simple example of recursion.
using System;
class Factorial {
// This is a recursive method.
public int FactR(int n) {
int result;
if(n==1) return 1;
result = FactR(n-1) * n;
return result;
}
// This is an iterative equivalent.
public int FactI(int n) {
int t, result;
result = 1;
for(t=1; t <= n; t++) result *= t;
return result;
}
}
class Recursion {
static void Main() {
Factorial f = new Factorial();
Console.WriteLine("Factorials using recursive method.");
Console.WriteLine("Factorial of 3 is " + f.FactR(3));
Console.WriteLine("Factorial of 4 is " + f.FactR(4));
Console.WriteLine("Factorial of 5 is " + f.FactR(5));
Console.WriteLine();
Console.WriteLine("Factorials using iterative method.");
Console.WriteLine("Factorial of 3 is " + f.FactI(3));
Console.WriteLine("Factorial of 4 is " + f.FactI(4));
Console.WriteLine("Factorial of 5 is " + f.FactI(5));
}
}
The output from this program is shown here:
Factorials using recursive method.
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
Factorials using iterative method.
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
The operation of the nonrecursive method FactI( ) should be clear. It uses a loop starting at 1 and progressively multiplies each number by the moving product.
The operation of the recursive FactR( ) is a bit more complex. When FactR( ) is called with an argument of 1, the method returns 1; otherwise, it returns the product of FactR(n–1)*n. To evaluate this expression, FactR( ) is called with n–1. This process repeats until n equals 1 and the calls to the method begin returning. For example, when the factorial of 2 is calculated, the first call to FactR( ) will cause a second call to be made with an argument of 1. This call will return 1, which is then multiplied by 2 (the original value of n). The answer is then 2. You might find it interesting to insert WriteLine( ) statements into FactR( ) that show the level of recursion of each call and what the intermediate results are.
When a method calls itself, new local variables and parameters are allocated storage on the stack, and the method code is executed with these new variables from the start. A recursive call does not make a new copy of the method. Only the arguments are new. As each recursive call returns, the old local variables and parameters are removed from the stack, and execution resumes at the point of the call inside the method. Recursive methods could be said to “telescope” out and back.
Here is another example of recursion. The DisplayRev( ) method uses recursion to display its string argument backward.
// Display a string in reverse by using recursion.
using System;
class RevStr {
// Display a string backward.
public void DisplayRev(string str) {
if(str.Length > 0)
DisplayRev(str.Substring(1, str.Length-1));
else
return;
Console.Write(str[0]);
}
}
class RevStrDemo {
static void Main() {
strings = "this is a test";
RevStr rsOb = new RevStr();
Console.WriteLine("Original string: " + s);
Console.Write("Reversed string: ");
rsOb.DisplayRev(s);
Console.WriteLine();
}
}
Here is the output:
Original string: this is a test
Reversed string: tset a si siht
Each time DisplayRev( ) is called, it first checks to see if str has a length greater than zero. If it does, it recursively calls DisplayRev( ) with a new string that consists of str minus its first character. This process repeats until a zero-length string is passed. This causes the recursive calls to start unraveling. As they do, the first character of str in each call is displayed. This results in the string being displayed in reverse order.
Recursive versions of many routines may execute a bit more slowly than the iterative equivalent because of the added overhead of the additional method calls. Too many recursive calls to a method could cause a stack overrun. Because storage for parameters and local variables is on the stack, and each new call creates a new copy of these variables, it is possible that the stack could be exhausted. If this occurs, the CLR will throw an exception. However, you probably will not have to worry about this unless a recursive routine runs wild.
The main advantage to recursion is that some types of algorithms can be more clearly and simply implemented recursively than iteratively. For example, the quicksort sorting algorithm is quite difficult to implement in an iterative way. Also, some problems, especially AI-related ones, seem to lend themselves to recursive solutions.
When writing recursive methods, you must have a conditional statement, such as an if, somewhere to force the method to return without the recursive call being executed. If you don’t do this, once you call the method, it will never return. This type of error is very common when developing recursive methods. Use WriteLine( ) statements liberally so that you can watch what is going on and abort execution if you see that you have made a mistake.
There will be times when you will want to define a class member that will be used independently of any object of that class. Normally, a class member must be accessed through an object of its class, but it is possible to create a member that can be used by itself, without reference to a specific instance. To create such a member, precede its declaration with the keyword static. When a member is declared static, it can be accessed before any objects of its class are created and without reference to any object. You can declare both methods and variables to be static. The most common example of a static member is Main( ), which is declared static because it must be called by the operating system when your program begins.
Outside the class, to use a static member, you must specify the name of its class followed by the dot operator. No object needs to be created. In fact, a static member cannot be accessed through an object reference. It must be accessed through its class name. For example, if you want to assign the value 10 to a static variable called count that is part of a class called Timer, use this line:
Timer.count = 10;
This format is similar to that used to access normal instance variables through an object, except that the class name is used. A static method can be called in the same way—by use of the dot operator on the name of the class.
Variables declared as static are, essentially, global variables. When objects of its class are declared, no copy of a static variable is made. Instead, all instances of the class share the same static variable. A static variable is initialized before its class is used. If no explicit initializer is specified, it is initialized to zero for numeric types, null in the case of reference types, or false for variables of type bool. Thus, a static variable always has a value.
The difference between a static method and a normal method is that the static method can be called through its class name, without any instance of that class being created. You have seen an example of this already: the Sqrt( ) method, which is a static method within C#’s System.Math class.
Here is an example that declares a static variable and a static method:
// Use static.
using System;
class StaticDemo {
// A static variable.
public static int Val = 100;
// A static method.
public static int ValDiv2() {
return Val/2;
}
}
class SDemo {
static void Main() {
Console.WriteLine("Initial value of StaticDemo.Val is "
+ StaticDemo.Val);
StaticDemo.Val = 8;
Console.WriteLine("StaticDemo.Val is " + StaticDemo.Val);
Console.WriteLine("StaticDemo.ValDiv2(): " +
StaticDemo.ValDiv2());
}
}
The output is shown here:
Initial value of StaticDemo.Val is 100
StaticDemo.Val is 8
StaticDemo.ValDiv2(): 4
As the output shows, a static variable is initialized before any object of its class is created.
There are several restrictions that apply to static methods:
• A static method does not have a this reference. This is because a static method does not execute relative to any object.
• A static method can directly call only other static methods of its class. It cannot directly call an instance method of its class. The reason is that instance methods operate on specific objects, but a static method is not called on an object. Thus, on what object would the instance method operate?
• A similar restriction applies to static data. A static method can directly access only other static data defined by its class. It cannot operate on an instance variable of its class because there is no object to operate on.
For example, in the following class, the static method ValDivDenom( ) is illegal:
class StaticError {
public int Denom = 3; // a normal instance variable
public static int Val = 1024; // a static variable
/* Error! Can't directly access a non-static variable
from within a static method. */
static int ValDivDenom() {
return Val/Denom; // won't compile!
}
}
Here, Denom is a normal instance variable that cannot be accessed within a static method. However, the use of Val is okay since it is a static variable.
The same problem occurs when trying to call a non-static method from within a static method of the same class. For example:
using System;
class AnotherStaticError {
// A non-static method.
void NonStaticMeth() {
Console.WriteLine("Inside NonStaticMeth().");
}
/* Error! Can't directly call a non-static method
from within a static method. */
static void staticMeth() {
NonStaticMeth(); // won't compile
}
}
In this case, the attempt to call a non-static (that is, instance method) from a static method causes a compile-time error.
It is important to understand that a static method can call instance methods and access instance variables of its class if it does so through an object of that class. It is just that it cannot use an instance variable or method without an object qualification. For example, this fragment is perfectly valid:
class MyClass {
// A non-static method.
void NonStaticMeth() {
Console.WriteLine("Inside NonStaticMeth().");
}
/* Can call a non-static method through an
object reference from within a static method. */
public static void staticMeth(MyClass ob) {
ob.NonStaticMeth(); // this is OK
}
}
Here, NonStaticMeth( ) is called by staticMeth( ) through ob, which is an object of type MyClass.
Because static fields are independent of any specific object, they are useful when you need to maintain information that is applicable to an entire class. Here is an example of such a situation. It uses a static field to maintain a count of the number of objects that are in existence.
// Use a static field to count instances.
using System;
class CountInst {
static int count = 0;
// Increment count when object is created.
public CountInst() {
count++;
}
// Decrement count when object is destroyed.
~CountInst() {
count--;
}
public static int GetCount() {
return count;
}
}
class CountDemo {
static void Main() {
CountInst ob;
for(int i=0; i < 10; i++) {
ob = new CountInst();
Console.WriteLine("Current count: " + CountInst.GetCount());
}
}
}
The output is shown here:
Current count: 1
Current count: 2
Current count: 3
Current count: 4
Current count: 5
Current count: 6
Current count: 7
Current count: 8
Current count: 9
Current count: 10
Each time that an object of type CountInst is created, the static field count is incremented. Each time an object is recycled, count is decremented. Thus, count always contains a count of the number of objects currently in existence. This is possible only through the use of a static field. There is no way for an instance variable to maintain the count because the count relates to the class as a whole, not to a specific instance.
Here is one more example that uses static. Earlier in this chapter, you saw how a class factory could be used to create objects. In that example, the class factory was a non-static method, which meant that it could be called only through an object reference. This meant that a default object of the class needed to be created so that the factory method could be called. However, a better way to implement a class factory is as a static method, which allows the class factory to be called without creating an unnecessary object. Here is the class factory example rewritten to reflect this improvement:
// Use a static class factory.
using System;
class MyClass {
int a, b;
// Create a class factory for MyClass.
static public MyClass Factory(int i, int j) {
MyClass t = new MyClass();
t.a = i;
t.b = j;
return t; // return an object
}
public void Show() {
Console.WriteLine("a and b: " + a + " " + b);
}
}
class MakeObjects {
static void Main() {
int i, j;
// Generate objects using the factory.
for(i=0, j=10; i < 10; i++, j--) {
MyClass ob = MyClass.Factory(i, j); // get an object
ob.Show();
}
Console.WriteLine();
}
}
In this version, Factory( ) is invoked through its class name in this line of code:
MyClass ob = MyClass.Factory(i, j); // get an object
There is no need to create a MyClass object prior to using the factory.
A constructor can also be specified as static. A static constructor is typically used to initialize features that apply to a class rather than an instance. Thus, it is used to initialize aspects of a class before any objects of the class are created. Here is a simple example:
// Use a static constructor.
using System;
class Cons {
public static int alpha;
public int beta;
// A static constructor.
static Cons() {
alpha = 99;
Console.WriteLine("Inside static constructor.");
}
// An instance constructor.
public Cons() {
beta = 100;
Console.WriteLine("Inside instance constructor.");
}
}
class ConsDemo {
static void Main() {
Cons ob = new Cons();
Console.WriteLine("Cons.alpha: " + Cons.alpha);
Console.WriteLine("ob.beta: " + ob.beta);
}
}
Here is the output:
Inside static constructor.
Inside instance constructor.
Cons.alpha: 99
ob.beta: 100
Notice that the static constructor is called automatically (when the class is first loaded) and before the instance constructor. This can be generalized. In all cases, the static constructor will be executed before any instance constructor. Furthermore, static constructors cannot have access modifiers (thus, they use default access) and cannot be called by your program.
A class can be declared static. There are two key features of a static class. First, no object of a static class can be created. Second, a static class must contain only static members. A static class is created by modifying a class declaration with the keyword static, shown here:
static class class-name { // ...
Within the class, all members must be explicitly specified as static. Making a class static does not automatically make its members static.
static classes have two primary uses. First, a static class is required when creating an extension method. Extension methods relate mostly to LINQ, and a discussion of extension methods is found in Chapter 19. Second, a static class is used to contain a collection of related static methods. This second use is demonstrated here.
The following example uses a static class called NumericFn to hold a set of static methods that operate on a numeric value. Because all of the members of NumericFn are declared static, the class can also be declared static, which prevents it from being instantiated. Thus, NumericFn serves an organizational role, providing a good way to logically group related methods.
// Demonstrate a static class.
using System;
static class NumericFn {
// Return the reciprocal of a value.
static public double Reciprocal(double num) {
return 1/num;
}
// Return the fractional part of a value.
static public double FracPart(double num) {
return num - (int) num;
}
// Return true if num is even.
static public bool IsEven(double num) {
return (num % 2) == 0 ? true : false;
}
// Return true if num is odd.
static public bool IsOdd(double num) {
return !IsEven(num);
}
}
class StaticClassDemo {
static void Main() {
Console.WriteLine("Reciprocal of 5 is " +
NumericFn.Reciprocal(5.0));
Console.WriteLine("Fractional part of 4.234 is " +
NumericFn.FracPart(4.234));
if(NumericFn.IsEven(10))
Console.WriteLine("10 is even.");
if(NumericFn.IsOdd(5))
Console.WriteLine("5 is odd.");
// The following attempt to create an instance of
// NumericFn will cause an error.
// NumericFn ob = new NumericFn(); // Wrong!
}
}
The output from the program is shown here.
Reciprocal of 5 is 0.2
Fractional part of 4.234 is 0.234
10 is even.
5 is odd.
Notice that the last line in the program is commented-out. Because NumericFn is a static class, any attempt to create an object will result in a compile-time error. It would also be an error to attempt to give NumericFn a non-static member.
One last point: Although a static class cannot have an instance constructor, it can have a static constructor.