The Main() method is static. Static methods are said to operate on the class, rather than on an instance of the class. They don’t have athisreference, as there is no instance to point to.
Java programmers take note: in C#, calling static methods through instance variables is not permitted.
Static methods can’t directly access nonstatic members. ForMain()to call a nonstatic method, it must instantiate an object. Consider Example 4-2, shown earlier.
SomeMethod()is a nonstatic method ofMyClass. ForMain()to access this method, it must first instantiate an object of typeMyClass, and then invoke the method through that object.
Using Static Constructors
If your class declares a static constructor, you are guaranteed that the static constructor will run before any instance of your class is created.*
You can’t control exactly when a static constructor will run, but you do know that it will be after the start of your program and before the first instance is created. Because of this, you can’t assume (or determine) whether an instance is being created.
For example, you might add the following static constructor to theTime class from Example 4-4:
static Time() { Name = "Time"; }
Notice that there is no access modifier (e.g.,public) before the static constructor. Access modifiers aren’t allowed on static constructors. In addition, because this is a static member method, you can’t access nonstatic member variables, and soNamemust be declared a static member variable:
private static string Name;
The final change is to add a line toDisplayCurrentTime(), as in the following:
Name: Time 11/27/2007 7:52:54 Name: Time 11/18/2007 11:45:30
(Your output will vary depending on the date and time you run this code.)
Although this code works, it isn’t necessary to create a static constructor to accomplish this goal. You can, instead, use an initializer:
private static string Name = "Time";
which accomplishes the same thing. Static constructors are useful, however, for setup work that can’t be accomplished with an initializer and that needs to be done only once.
Java programmers take note: in C#, a static constructor will serve where a static initializer would be used in Java.
For example, assume you have an unmanaged bit of code in a legacy DLL. You want to provide a class wrapper for this code. You can callLoadLibrary in your static constructor and initialize the jump table in the static constructor. I discuss handling legacy code and interoperating with unmanaged code in Chapter 22.
Static Classes
In C#, there are no global methods or constants. You might find yourself creating small utility classes that exist only to hold static members. Setting aside whether this is a good design, if you create such a class, you won’t want any instances created. Mark your classStaticto ensure that no instance of the class may be created. Static classes are sealed, and thus you may not create derived types of aStaticclass. Note, however, that static classes may not contain nonstatic members or have a constructor.
A common way to demonstrate the use of static member variables is to keep track of the number of instances that currently exist for your class. Example 4-5 illustrates.
Example 4-5. Using static fields for instance counting
#region Using directives
using System; using System.Collections.Generic; using System.Text;
TheCatclass has been stripped to its absolute essentials. A static member variable calledinstancesis created and initialized to 0. Note that the static member is considered part of the class, not a member of an instance, and so it can’t be initialized by the compiler on creation of an instance. Thus, if you want to initialize a static member, you must provide an explicit initializer. When additional instances ofCats are created (in a constructor), the count is incremented.
Static Methods to Access Static Fields
It is undesirable to make member data public. This applies to static member variables as well. One solution is to make the static member private, as we’ve done here withinstances. We have created a public accessor method,HowManyCats(), to provide access to this private member.
Because C# provides garbage collection, you never need to explicitly destroy your objects. However, if your object controls unmanaged resources, you will need to explicitly free those resources when you are done with them. Implicit control over unmanaged resources is provided by a destructor, which will be called by the garbage collector when your object is destroyed.
C and C++ programmers take note: a destructor is not necessarily called when an object goes out of scope, but rather when it is garbage-collected (which may happen much later). This is known as nondeterministic finalization.
The destructor should only release resources that your object holds on to, and should not reference other objects. Note that if you have only managed references, you don’t need to and should not implement a destructor; you want this only for handling unmanaged resources. Because there is some cost to having a destructor, you ought to implement this only on methods that require it (i.e., methods that consume valuable unmanaged resources).
You can’t call an object’s destructor directly. The garbage collector will call it for you.
How Destructors Work
The garbage collector maintains a list of objects that have a destructor. This list is updated every time such an object is created or destroyed.
When an object on this list is first collected, it is placed in a queue with other objects waiting to be destroyed. After the destructor executes, the garbage collector collects the object and updates the queue, as well as its list of destructible objects.
The C# Destructor
C#’s destructor looks, syntactically, much like a C++ destructor, but it behaves quite differently. You declare a C# destructor with a tilde as follows:
~MyClass(){}
In C#, this syntax is simply a shortcut for declaring aFinalize()method that chains up to its base class. Thus, when you write:
~MyClass() { // do work here }
the C# compiler translates it to:
protected override void Finalize() { try { // do work here. } finally { base.Finalize(); } }
It is not legal to call a destructor explicitly. Your destructor will be called by the garbage collector. If you do handle precious unmanaged resources (such as file handles) that you want to close and dispose of as quickly as possible, you ought to implement the IDisposable interface.* (You will learn more about interfaces in Chapter 8.) TheIDisposableinterface requires its implementers to define one method, namedDispose(), to perform whatever cleanup you consider to be crucial. The availability ofDispose()is a way for your clients to say, “Don’t wait for the destructor to be called, do it right now.”
If you provide aDispose()method, you should stop the garbage collector from calling your object’s destructor. To do so, call the static methodGC.SuppressFinalize(), passing in thethispointer for your object. Your destructor can then call yourDispose()method. Thus, you might write:
using System; class Testing : IDisposable { bool is_disposed = false; protected virtual void Dispose(bool disposing) { if (!is_disposed) // only dispose once! { if (disposing) { Console.WriteLine( "Not in destructor, OK to reference other objects"); } // perform cleanup for this object Console.WriteLine("Disposing..."); } this.is_disposed = true; }
public void Dispose() { Dispose(true); // tell the GC not to finalize GC.SuppressFinalize(this); }
For some objects, you may prefer to have your clients call a method namedClose(). (For example,Close()may make more sense thanDispose()for file objects.) You can implement this by creating a privateDispose()method and a publicClose()method, and having yourClose()method invokeDispose().
The using Statement
To make it easier for your clients to properly dispose of your objects, C# provides a using statement that ensures that Dispose()will be called at the earliest possible time. The idiom is to declare the objects you are using and then to create a scope for these objects with curly braces. When the closing brace is reached, theDispose()method will be called on the object automatically, as illustrated in Example 4-6.
Example 4-6. The using statement
#region Using directives
using System; using System.Collections.Generic; using System.Drawing; using System.Text;
#endregion
namespace usingStatement { class Tester { public static void Main() { using ( Font theFont = new Font( "Arial", 10.0f ) ) { // use theFont
} // compiler will call Dispose on theFont
Font anotherFont = new Font( "Courier", 12.0f );
using ( anotherFont ) { // use anotherFont
} // compiler calls Dispose on anotherFont } } }
In the first part of this example, theFontobject is created within theusingstatement. When theusing statement ends,Dispose()is called on theFontobject.
In the second part of the example, aFont object is created outside theusingstatement. When we decide to use that font, we put it inside theusing statement; when that statement ends,Dispose()is called once again.
This second approach is fraught with danger. If an exception is thrown after the object is created, but before theusingblock is begun, the object will not be disposed. Second, the variable remains in scope after theusingblock ends, but if it is accessed, it will fail.
Theusing statement also protects you against unanticipated exceptions. Regardless of how control leaves theusingstatement,Dispose()is called. An implicittry-finallyblock is created for you. (See Chapter 11 for details.)
By default, value types are passed into methods by value. (See the section “Method Arguments,” earlier in this chapter.) This means that when a value object is passed to a method, a temporary copy of the object is created within that method. Once the method completes, the copy is discarded. Although passing by value is the normal case, there are times when you will want to pass value objects by reference. C# provides the ref parameter modifier for passing value objects into a method by reference, and theoutmodifier for those cases in which you want to pass in arefvariable without first initializing it. C# also supports theparamsmodifier, which allows a method to accept a variable number of parameters. I discuss theparamskeyword in Chapter 9.
Passing by Reference
Methods can return only a single value (though that value can be a collection of values). Let’s return to the Time class and add a GetTime()method, which returns the hour, minutes, and seconds.
Java programmers take note: in C#, there’s no need for wrapper classes for basic types such asint(integer). Instead, use reference parameters.
Because you can’t return three values, perhaps you can pass in three parameters, let the method modify the parameters, and examine the result in the calling method. Example 4-7 shows a first attempt at this.
Example 4-7. Returning values in parameters
#region Using directives
using System; using System.Collections.Generic;
using System.Text;
#endregion
namespace ReturningValuesInParams { public class Time { // private member variables private int Year; private int Month; private int Date; private int Hour; private int Minute; private int Second;
// public accessor methods public void DisplayCurrentTime() { System.Console.WriteLine( "{0}/{1}/{2} {3}:{4}:{5}", Month, Date, Year, Hour, Minute, Second ); }
public int GetHour() { return Hour; }
public void GetTime( int h, int m, int s ) { h = Hour; m = Minute; s = Second; }
// constructor public Time( System.DateTime dt ) {
Year = dt.Year; Month = dt.Month; Date = dt.Day; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; } }
public class Tester { static void Main() { System.DateTime currentTime = System.DateTime.Now; Time t = new Time( currentTime ); t.DisplayCurrentTime();
int theHour = 0; int theMinute = 0; int theSecond = 0; t.GetTime( theHour, theMinute, theSecond ); System.Console.WriteLine( "Current time: {0}:{1}:{2}", theHour, theMinute, theSecond ); } } }
Output: 11/17/2007 13:41:18 Current time: 0:0:0
Notice that theCurrent timein the output is0:0:0. Clearly, this first attempt did not work. The problem is with the parameters. You pass in three integer parameters toGetTime(), and you modify the parameters inGetTime(), but when the values are accessed back inMain(), they are unchanged. This is because integers are value types, and so are passed by value; a copy is made inGetTime(). What you need is to pass these values by reference.
Two small changes are required. First, change the parameters of theGetTime()method to indicate that the parameters areref(reference) parameters:
public void GetTime(ref int h, ref int m, ref int s) { h = Hour; m = Minute; s = Second; }
Second, modify the call toGetTime()to pass the arguments as references as well:
If you leave out the second step of marking the arguments with the keywordref, the compiler will complain that the argument can’t be converted from anintto aref int.
The results now show the correct time. By declaring these parameters to berefparameters, you instruct the compiler to pass them by reference. Instead of a copy being made, the parameter inGetTime()is a reference to the same variable (theHour) that is created inMain(). When you change these values inGetTime(), the change is reflected inMain().
Keep in mind thatrefparameters are references to the actual original value: it is as though you said, “Here, work on this one.” Conversely, value parameters are copies: it is as though you said, “Here, work on one just like this."
Please check back tomorrow for the conclusion to this article.