One of the most important topics in C# programming — in fact, the cornerstone of .NET development — is classes and objects.
Classes are essentially templates from which you create objects. In C# .NET programming, everything you deal with involves classes and objects. This chapter assumes that you already have a basic grasp of object-oriented programming. It tackles:
How to define a class
How to create an object from a class
The different types of members in a class
The root of all objects — System.Object
Everything you encounter in .NET in based on classes. For example, you have a Windows Forms application containing a default form called Form1. Form1
itself is a class that inherits from the base class System.Windows.Forms.Form
, which defines the basic behaviors that a Windows Form should exhibit:
using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace Project1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } }
Within the Form1
class, you code in your methods. For example, to display a "Hello World
" message when the form is loaded, add the following statement in the Form1_Load()
method:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } protected override void OnLoad(EventArgs e) { MessageBox.Show("Hello World!"); } }
The following sections walk you through the basics of defining your own class and the various members you can have in the class.
You use the class
keyword to define a class. The following example is the definition of a class called Contact
:
public class Contact { public int ID; public string FirstName; public string LastName; public string Email; }
This Contact
class has four public members — ID, FirstName, LastName
, and Email
. The syntax of a class definition is:
<access_modifiers
>class Class_Name
{ //---Fields, properties, methods, and events
--- }
Instead of defining an entire class by using the class
keyword, you can split the definition into multiple classes by using the partial
keyword. For example, the Contact
class defined in the previous section can be split into two partial classes like this:
public partial class Contact { public int ID; public string Email;
} public partial class Contact { public string FirstName; public string LastName; }
When the application is compiled, the C# compiler will group all the partial classes together and treat them as a single class.
A class works like a template. To do anything useful, you need to use the template to create an actual object so that you can work with it. The process of creating an object from a class is known as instantiation.
To instantiate the Contact
class defined earlier, you first create a variable of type Contact
:
Contact contact1;
At this stage, contact1
is of type Contact
, but it does not actually contain the object data yet. For it to contain the object data, you need to use the new
keyword to create a new instance of the Contact
class, a process is known as object instantiation:
contact1 = new Contact();
Alternatively, you can combine those two steps into one, like this:
Contact contact1 = new Contact();
Once an object is instantiated, you can set the various members of the object. Here's an example:
contact1.ID = 12; contact1.FirstName = "Wei-Meng"; contact1.LastName = "Lee"; contact1.Email = "[email protected]";
You can also assign an object to an object, like the following:
Contact contact1 = new Contact(); Contact contact2 = contact1;
In these statements, contact2
and contact1
are now both pointing to the same object. Any changes made to one object will be reflected in the other object, as the following example shows:
Contact contact1 = new Contact(); Contact contact2 = contact1; contact1.FirstName = "Wei-Meng"; contact2.FirstName = "Jackson"; //---prints out "Jackson"--- Console.WriteLine(contact1.FirstName);
It prints out "Jackson" because both contact1
and contact2
are pointing to the same object, and when you assign "Jackson" to the FirstName
property of contact2, contact1
's FirstName
property also sees "Jackson".
C# 3.0 introduces a new feature known as anonymous types. Anonymous types enable you to define data types without having to formally define a class. Consider the following example:
var book1 = new { ISBN = "978-0-470-17661-0", Title="Professional Windows Vista Gadgets Programming", Author = "Wei-Meng Lee", Publisher="Wrox" };
Chapter 3 discusses the new C# 3.0 keyword var
.
Here, book1
is an object with 4 properties: ISBN, Title, Author
, and Publisher
(see Figure 4-1).
In this example, there's no need for you to define a class containing the four properties. Instead, the object is created and its properties initialized with their respective values.
C# anonymous types are immutable, which means all the properties are read-only — their values cannot be changed once they are initialized.
You can use variable names when assigning values to properties in an anonymous type; for example:
var Title = "Professional Windows Vista Gadgets Programming"; var Author = "Wei-Meng Lee"; var Publisher = "Wrox"; var book1 = new { ISBN = "978-0-470-17661-0", Title, Author, Publisher };
In this case, the names of the properties will assume the names of the variables, as shown in Figure 4-2.
However, you cannot create anonymous types with literals, as the following example demonstrates:
//---error--- var book1 = new { "978-0-470-17661-0", "Professional Windows Vista Gadgets Programming", "Wei-Meng Lee", "Wrox" };
When assigning a literal value to a property in an anonymous type, you must use an identifier, like this:
var book1 = new { ISBN = "978-0-470-17661-0", Title="Professional Windows Vista Gadgets Programming", Author = "Wei-Meng Lee", Publisher="Wrox" };
So, how are anonymous types useful for your application? Well, they enable you to shape your data from one type to another. You will look into more about this in Chapter 14, which tackles LINQ.
Variables and functions defined in a class are known as a class's members. The Contact
class definition, for instance, has four members that you can access once an object is instantiated:
public class Contact { public int ID; public string FirstName; public string LastName; public string Email; }
Members of a class are classified into two types:
Data members can be further grouped into instance members and static members.
By default, all data members are instance members unless they are constants or prefixed with the static
keyword (more on this in the next section). The variables defined in the Contact
class are instance members:
public int ID; public string FirstName; public string LastName; public string Email;
Instance members can be accessed only through an instance of a class and each instance of the class (object) has its own copy of the data. Consider the following example:
Contact contact1 = new Contact(); contact1.ID = 12; contact1.FirstName = "Wei-Meng"; contact1.LastName = "Lee"; contact1.Email = "[email protected]"; Contact contact2 = new Contact(); contact2.ID = 35; contact2.FirstName = "Jason"; contact2.LastName = "Will"; contact2.Email = "[email protected]";
The objects contact1
and contact2
each contain information for a different user. Each object maintains its own copy of the ID, FirstName, LastName
, and Email
data members.
Static data members belong to the class rather than to each instance of the class. You use the static
keyword to define them. For example, here the Contact
class has a static member named count
:
public class Contact { public static int count; public int ID; public string FirstName; public string LastName; public string Email; }
The count
static member can be used to keep track of the total number of Contact
instances, and thus it should not belong to any instances of the Contact
class but to the class itself.
To use the count
static variable, access it through the Contact
class:
Contact.count = 4; Console.WriteLine(Contact.count);
You cannot access it via an instance of the class, such as contact1
:
//---error--- contact1.count = 4;
Constants defined within a class are implicitly static, as the following example shows:
public class Contact { public const ushort MAX_EMAIL = 5; public static int count; public int ID; public string FirstName; public string LastName; public string Email; }
In this case, you can only access the constant through the class name but not set a value to it:
Console.WriteLine(Contact.MAX_EMAIL); Contact.MAX_EMAIL = 4; //---error---
Access modifiers are keywords that you can add to members of a class to restrict their access. Consider the following definition of the Contact
class:
public class Contact { public const ushort MAX_EMAIL = 5; public static int count; public int ID; public string FirstName; public string LastName; private string _Email; }
Unlike the rest of the data members, the _Email
data member has been defined with the private
keyword. The public
keyword indicates that the data member is visible outside the class, while the private
keyword indicates that the data member is only visible within the class.
By convention, you can denote a private variable by beginning its name with the underscore (
_
) character. This is recommended, but not mandatory.
For example, you can access the FirstName
data member through an instance of the Contact
class:
//---this is OK--- contact1.FirstName = "Wei-Meng";
But you cannot access the _Email
data member outside the class, as the following statement demonstrates:
//---error: _Email is inaccessible--- contact1._Email = "[email protected]";
C# has four access modifiers —
private, public, protected
, andinternal
. The last two are discussed with inheritance in the next chapter.
If a data member is declared without the public
keyword, its scope (or access) is private
by default. So, _Email
can also be declared like this:
public class Contact { public const ushort MAX_EMAIL = 5; public static int count; public int ID; public string FirstName; public string LastName; string _Email; }
A function member contains executable code that performs work for the class. The following are examples of function members in C#:
Methods
Properties
Events
Indexers
User-defined operators
Constructors
Destructors
Events and indexers are covered in detail in Chapters 7 and 13.
In C#, every function must be associated with a class. A function defined with a class is known as a method. In C#, a method is defined using the following syntax:
[access_modifiers] return_type method_name(parameters) { //---Method body--- }
Here's an example — the ValidateEmail()
method defined in the Contact
class:
public class Contact { public static ushort MAX_EMAIL; public int ID; public string FirstName; public string LastName; public string Email; public Boolean ValidateEmail() { //---implementation here--- Boolean valid=true; return valid; } }
If the method does not return a value, you need to specify the return type as void
, as the following PrintName()
method shows:
public class Contact { public static ushort MAX_EMAIL; public int ID; public string FirstName; public string LastName; public string Email; public Boolean ValidateEmail() { //---implementation here--- //... Boolean valid=true; return valid; } public void PrintName() { Console.WriteLine("{0} {1}", this.FirstName, this.LastName); } }
You can pass values into a method using arguments. The words parameter and argument are often used interchangeably, but they mean different things. A parameter is what you use to define a method. An argument is what you actually use to call a method.
In the following example, x and y are examples of parameters:
public int AddNumbers(int x, int y) {}
When you call the method, you pass in values/variables. In the following example, num1
and num2
are examples of arguments:
Console.WriteLine(AddNumbers(num1, num2));
Consider the method named AddNumbers()
with two parameters, x
and y
:
public int AddNumbers(int x, int y) { x++; y++; return x + y; }
When you call this method, you also need to pass two integer arguments (num1
and num2
), as the following example shows:
int num1 = 4, num2 = 5; //---prints out 11--- Console.WriteLine(AddNumbers(num1, num2)); Console.WriteLine(num1); //---4--- Console.WriteLine(num2); //---5---
In C#, all arguments are passed by value by default. In other words, the called method gets a copy of the value of the arguments passed into it. In the preceding example, for instance, even though the value of x
and y
are both incremented within the method, this does not affect the values of num1
and num2
.
If you want to pass in arguments to methods by reference, you need to prefix the parameters with the ref
keyword. Values of variables passed in by reference will be modified if there are changes made to them in the method. Consider the following rewrite of the AddNumbers()
function:
Because C# functions can only return single values, passing arguments by reference is useful when you need a method to return multiple values.
public int AddNumbers(ref int x, ref int y) { x++; y++; return x + y; }
In this case, the values of variables passed into this function will be modified, as the following example illustrates:
int num1 = 4, num2 = 5; //---prints out 11--- Console.WriteLine(AddNumbers(ref num1, ref num2)); Console.WriteLine(num1); //---5--- Console.WriteLine(num2); //---6---
After calling the AddNumbers()
function, num1
becomes 5 and num2
becomes 6. Observe that you need to prefix the arguments with the ref
keyword when calling the function. In addition, you cannot pass literal values as arguments into a method that requires parameters to be passed in by reference:
//---invalid arguments--- Console.WriteLine(AddNumbers(4, 5));
Also note that the ref
keyword requires that all the variables be initialized first. Here's an example:
public void GetDate(ref int day, ref int month, ref int year) { day = DateTime.Now.Day; month = DateTime.Now.Month; year = DateTime.Now.Year; }
The GetDate()
method takes in three reference parameters and uses them to return the day, month, and year.
If you pass in the day, month and year reference variables without initializing them, an error will occur:
//---Error: day, month, and year not initialized--- int day, month, year; GetDate(ref day, ref month, ref year);
If your intention is to use the variables solely to obtain some return values from the method, you can use the out
keyword, which is identical to the ref
keyword except that it does not require the variables passed in to be initialized first:
public void GetDate(out int day, out int month, out int year) { day = DateTime.Now.Day; month = DateTime.Now.Month; year = DateTime.Now.Year; }
Also, the out
parameter in a function must be assigned a value before the function returns. If it isn't, a compiler error results.
Like the ref
keyword, you need to prefix the arguments with the out
keyword when calling the function:
int day, month, year; GetDate(out day, out month, out year);
The this
keyword refers to the current instance of an object (in a nonstatic class; discussed later in the section Static Classes). In the earlier section on methods, you saw the use of this
:
Console.WriteLine("{0} {1}", this.FirstName, this.LastName);
While the FirstName
and LastName
variable could be referenced without using the this
keyword, prefixing them with it makes your code more readable, indicating that you are referring to an instance member.
However, if instance members have the same names as your parameters, using this
allows you to resolve the ambiguity:
public void SetName(string FirstName, string LastName) { this.FirstName = FirstName; this.LastName = LastName; }
Another use of the this
keyword is to pass the current object as a parameter to another method. For example:
public class AddressBook { public void AddContact(Contact c) { Console.WriteLine(c.ID); Console.WriteLine(c.FirstName); Console.WriteLine(c.LastName); Console.WriteLine(c.Email); //---other implementations here--- //... } }
The AddContact()
method takes in a Contact
object and prints out the details of the contact. Suppose that the Contact
class has a AddToAddressBook()
method that takes in an AddressBook
object. This method adds the Contact
object into the AddressBook
object:
public class Contact { public int ID; public string FirstName; public string LastName; public string Email; public void AddToAddressBook(AddressBook addBook) { addBook.AddContact(this); } }
In this case, you use the this
keyword to pass in the current instance of the Contact
object into the AddressBook
object. To test out that code, use the following statements:
Contact contact1 = new Contact(); contact1.ID = 12; contact1.FirstName = "Wei-Meng"; contact1.LastName = "Lee"; contact1.Email = "[email protected]"; AddressBook addBook1 = new AddressBook(); contact1.AddToAddressBook(addBook1);
Properties are function members that provide an easy way to read or write the values of private data members. Recall the Contact
class defined earlier:
public class Contact { public int ID; public string FirstName; public string LastName; public string Email; }
You've seen that you can create a Contact
object and set its public data members (ID, FirstName, LastName
, and Email
) directly, like this:
Contact c = new Contact(); c.ID = 1234; c.FirstName = "Wei-Meng"; c.LastName = "Lee"; c.Email = "[email protected]";
However, if the ID
of a person has a valid range of values — such as from 1 to 9999 — the following value of 12345 would still be assigned to the ID data member:
c.ID = 12345;
Technically, the assignment is valid, but logically it should not be allowed — the number assigned is beyond the range of values permitted for ID
. Of course you can perform some checks before assigning a value to the ID member, but doing so violates the spirit of encapsulation in object-oriented programming — the checks should be done within the class.
A solution to this is to use properties.
The Contact
class can be rewritten as follows with its data members converted to properties:
public class Contact { int _ID; string _FirstName, _LastName, _Email; public int ID { get { return _ID; } set { _ID = value; } }
public string FirstName { get { return _FirstName; } set { _FirstName = value; } } public string LastName { get { return _LastName; } set { _LastName = value; } } public string Email { get { return _Email; } set { _Email = value; } } }
Note that the public members (ID, FirstName, LastName
, and Email
) have been replaced by properties with the set
and get
accessors.
The set
accessor sets the value of a property. Using this example, you can instantiate a Contact
class and then set the value of the ID
property, like this:
Contact c = new Contact(); c.ID = 1234;
In this case, the set
accessor is invoked:
public int ID { get { return _ID; }
set { _ID = value; } }
The value
keyword contains the value that is being assigned by the set
accessor. You normally assign the value of a property to a private member so that it is not visible to code outside the class, which in this case is _ID
.
When you retrieve the value of a property, the get
accessor is invoked:
public int ID { get { return _ID; } set { _ID = value; } }
The following statement shows an example of retrieving the value of a property:
Console.WriteLine(c.ID); //---prints out 1234---
The really useful part of properties is the capability for you to perform checking on the value assigned. For example, before the ID
property is set, you want to make sure that the value is between 1 and 9999, so you perform the check at the set
accessor, like this:
public int ID { get { return _ID; } set { if (value > 0 && value <= 9999) { _ID = value; } else { _ID = 0; }; } }
Using properties, you can now prevent users from setting invalid values.
When a property definition contains the get
and set
accessors, that property can be read as well as written. To make a property read-only, you simply leave out the set
accessor, like this:
public int ID { get { return _ID; } }
You can now read but not write values into the ID
property:
Console.WriteLine(c1.ID); //---OK--- c1.ID = 1234; //---Error---
Likewise, to make a property write-only, simply leave out the get
accessor:
public int ID { set { _ID = value; } }
You can now write but not read from the ID
property:
Console.WriteLine(c1.ID); //---Error--- c1.ID = 1234; //---OK---
You can also restrict the visibility of the get
and set
accessors. For example, the set
accessor of a public property could be set to private
to allow only members of the class to call the set
accessor, but any class could call the get
accessor. The following example demonstrates this:
public int ID { get { return _ID; } private set { _ID = value; } }
In this code, the set
accessor of the ID
property is prefixed with the private
keyword to restrict its visibility. That means that you now cannot assign a value to the ID
property but you can access it:
c.ID = 1234; //---error--- Console.WriteLine(c.ID); //---OK---
You can, however, access the ID
property anywhere within the Contact
class itself, such as in the Email
property:
public string Email { get { //... this.ID = 1234; //... } //... }
Earlier on, you saw that a class definition can be split into one or more class definitions. In C# 3.0, this concept is extended to methods — you can now have partial methods. To see how partial methods works, consider the Contact
partial class:
public partial class Contact { //... private string _Email; public string Email { get { return _Email; } set { _Email = value; } } }
Suppose you that want to allow users of this partial class to optionally log the email address of each contact when its Email
property is set. In that case, you can define a partial method — LogEmail()
in this example — like this:
public partial class Contact { //... } public partial class Contact { //...
private string _Email; public string Email { get { return _Email; } set { _Email = value; LogEmail(); } } //---partial methods are private--- partial void LogEmail(); }
The partial method LogEmail()
is called when a contact's email is set via the Email
property. Note that this method has no implementation. Where is the implementation? It can optionally be implemented in another partial class. For example, if another developer decides to use the Contact
partial class, he or she can define another partial class containing the implementation for the LogEmail()
method:
public partial class Contact { partial void LogEmail() { //---code to send email to contact--- Console.WriteLine("Email set: {0}", _Email); } }
So when you now instantiate an instance of the Contact
class, you can set its Email
property as follows and a line will be printed in the output window:
Contact contact1 = new Contact(); contact1.Email = "[email protected]";
What if there is no implementation of the LogEmail()
method? Well, in that case the compiler simply removes the call to this method, and there is no change to your code.
Partial methods are useful when you are dealing with generated code. For example, suppose that the Contact
class is generated by a code generator. The signature of the partial method is defined in the class, but it is totally up to you to decide if you need to implement it.
A partial method must be declared within a partial class or partial struct.
Partial methods must adhere to the following rules:
Must begin with the partial
keyword and the method must return void
Can have ref
but not out
parameters
They are implicitly private, and therefore they cannot be virtual (virtual methods are discussed in the next chapter)
Parameter and type parameter names do not have to be the same in the implementing and defining declarations
In the Contact
class defined in the previous section, apart from the ID
property, the properties are actually not doing much except assigning their values to private members:
public string FirstName { get { return _FirstName; } set { _FirstName = value; } } public string LastName { get { return _LastName; } set { _LastName = value; } } public string Email { get { return _Email; } set { _Email = value; } }
In other words, you are not actually doing any checking before the values are assigned. In C# 3.0, you can shorten those properties that have no filtering (checking) rules by using a feature known as automatic properties. The Contact
class can be rewritten as:
public class Contact { int _ID; public int ID { get { return _ID; } set { if (value > 0 && value <= 9999) { _ID = value; } else { _ID = 0; }; } } public string FirstName {get; set;} public string LastName {get; set;} public string Email {get; set;} }
Now there's no need for you to define private members to store the values of the properties. Instead, you just need to use the get
and set
keywords, and the compiler will automatically create the private members in which to store the properties values. If you decide to add filtering rules to the properties later, you can simply implement the set
and get
accessor of each property.
To restrict the visibility of the get
and set
accessor when using the automatic properties feature, you simply prefix the get
or set
accessor with the private
keyword, like this:
public string FirstName {get; private set;}
This statement sets the FirstName
property as read-only.
You might be tempted to directly convert these properties (FirstName, LastName
, and Email
) into public data members. But if you did that and then later decided to convert these public members into properties, you would need to recompile all of the assemblies that were compiled against the old class.
Instead of initializing the individual properties of an object after it has been instantiated, it is sometimes useful to initialize them at the time of instantiation. Constructors are class methods that are executed when an object is instantiated.
Using the Contact
class as the example, the following constructor initializes the ID
property to 9999 every time an object is instantiated:
public class Contact { int _ID; public int ID { get { return _ID; } set { if (value > 0 && value <= 9999) { _ID = value; } else { _ID = 0; }; } } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public Contact() { this.ID = 9999; } }
The following statement proves that the constructor is called:
Contact c = new Contact(); //---prints out 9999--- Console.WriteLine(c.ID);
Constructors have the same name as the class and they do not return any values. In this example, the constructor is defined without any parameters. A constructor that takes in no parameters is called a default constructor. It is invoked when you instantiate an object without any arguments, like this:
Contact c = new Contact();
If you do not define a default constructor in your class, an implicit default constructor is automatically created by the compiler.
You can have as many constructors as you need to, as long as each constructor's signature (parameters) is different. Let's now add two more constructors to the Contact
class:
public class Contact { //... public Contact() { this.ID = 9999; } public Contact(int ID) { this.ID = ID; } public Contact(int ID, string FirstName, string LastName, string Email) { this.ID = ID; this.FirstName = FirstName; this.LastName = LastName; this.Email = Email; } }
When you have multiple methods (constructors in this case) with the same name but different signatures, the methods are known as overloaded. IntelliSense will show the different signatures available when you try to instantiate a Contact
object (see Figure 4-3).
You can create instances of the Contact
class using the different constructors:
//---first constructor is called--- Contact c1 = new Contact(); //---second constructor is called--- Contact c2 = new Contact(1234); //---third constructor is called--- Contact c3 = new Contact(1234, "Wei-Meng", "Lee", "[email protected]");
Suppose that the Contact
class has the following four constructors:
public class Contact { //... public Contact() { this.ID = 9999; } public Contact(int ID) { this.ID = ID; } public Contact(int ID, string FirstName, string LastName) { this.ID = ID; this.FirstName = FirstName; this.LastName = LastName; } public Contact(int ID, string FirstName, string LastName, string Email) { this.ID = ID; this.FirstName = FirstName; this.LastName = LastName; this.Email = Email; } }
Instead of setting the properties individually in each constructor, each constructor itself sets some of the properties for other constructors. A more efficient way would be for some constructors to call the other constructors to set some of the properties. That would prevent a duplication of code that does the same thing. The Contact
class could be rewritten like this:
public class Contact { //... //---first constructor--- public Contact() { this.ID = 9999; } //---second constructor--- public Contact(int ID) {
this.ID = ID; } //---third constructor--- public Contact(int ID, string FirstName, string LastName) : this(ID) { this.FirstName = FirstName; this.LastName = LastName; } //---fourth constructor--- public Contact(int ID, string FirstName, string LastName, string Email) : this(ID,FirstName, LastName) { this.Email = Email; } }
In this case, the fourth constructor is calling the third constructor using the this
keyword. In addition, it is also passing in the arguments required by the third constructor. The third constructor in turn calls the second constructor. This process of one constructor calling another is call constructor chaining.
To prove that constructor chaining works, use the following statements:
Contact c1 = new Contact(1234, "Wei-Meng", "Lee", "[email protected]"); Console.WriteLine(c1.ID); //---1234--- Console.WriteLine(c1.FirstName); //----Wei-Meng--- Console.WriteLine(c1.LastName); //---Lee--- Console.WriteLine(c1.Email); //--- [email protected]
To understand the sequence of the constructors that are called, insert the following highlighted statements:
class Contact { //... //---first constructor--- public Contact() { this.ID = 9999; Console.WriteLine("First constructor"); } //---second constructor--- public Contact(int ID) { this.ID = ID;
Console.WriteLine("Second constructor"); } //---third constructor--- public Contact(int ID, string FirstName, string LastName) : this(ID) { this.FirstName = FirstName; this.LastName = LastName; Console.WriteLine("Third constructor"); } //---fourth constructor--- public Contact(int ID, string FirstName, string LastName, string Email) : this(ID, FirstName, LastName) { this.Email = Email; Console.WriteLine("Fourth constructor"); } }
Contact c1 = new Contact(1234, "Wei-Meng", "Lee", "[email protected]");
prints the following output:
Second constructor Third constructor Fourth constructor
If your class has static members, it is only sometimes necessary to initialize them before an object is created and used. In that case, you can add static constructors to the class. For example, suppose that the Contact
class has a public static
member count
to record the number of the Contact
object created. You can add a static constructor to initialize the static member, like this:
public class Contact { //... public static int count; static Contact() { count = 0; Console.WriteLine("Static constructor"); } //---first constructor---
public Contact() { count++; Console.WriteLine("First constructor"); } //... }
When you now create instances of the Contact
class, like this:
Contact c1 = new Contact(); Contact c2 = new Contact(); Console.WriteLine(Contact.count);
the static constructor is only called once, evident in the following output:
Static constructor First constructor First constructor 2
Note the behavior of static constructors:
A static constructor does not take access modifiers or have parameters.
A static constructor is called automatically to initialize the class before the first instance is created or any static members are referenced.
A static constructor cannot be called directly.
he user has no control on when the static constructor is executed in the program.
The C# language does not provide a copy constructor that allows you to copy the value of an existing object into a new object when it is created. Instead, you have to write your own.
The following copy constructor in the Contact
class copies the values of the properties of an existing object (through the otherContact
parameter) into the new object:
class Contact { //... //---a copy constructor--- public Contact(Contact otherContact) { this.ID = otherContact.ID; this.FirstName = otherContact.FirstName;
this.LastName = otherContact.LastName; this.Email = otherContact.Email; } //... }
To use the copy constructor, first create a Contact
object:
Contact c1 = new Contact(1234, "Wei-Meng", "Lee", "[email protected]");
Then, instantiate another Contact
object and pass in the first object as the argument:
Contact c2 = new Contact(c1); Console.WriteLine(c2.ID); //---1234--- Console.WriteLine(c2.FirstName); //----Wei-Meng--- Console.WriteLine(c2.LastName); //---Lee--- Console.WriteLine(c2.Email); //--- [email protected]
Generally, there are two ways in which you can initialize an object — through its constructor(s) during instantiation or by setting its properties individually after instantiation. Using the Contact
class defined in the previous section, here is one example of how to initialize a Contact
object using its constructor:
Contact c1 = new Contact(1234, "Wei-Meng", "Lee", "[email protected]");
You can also set an object's properties explicitly:
Contact c1 = new Contact(); c1.ID = 1234; c1.FirstName = "Wei-Meng"; c1.LastName = "Lee"; c1.Email = "[email protected]";
In C# 3.0, you have a third way of initializing objects — when they are instantiated. This feature is known as the object initializers. The following statement shows an example:
Contact c1 = new Contact() { ID = 1234, FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" };
Here, when instantiating a Contact
class, you are also setting its properties directly using the {}
block. To use the object initializers, you instantiate an object using the new
keyword and then enclose the properties that you want to initialize within the {}
block. You separate the properties using commas.
Do not confuse the object initializer with a class's constructor(s). You should continue to use the constructor (if it has one) to initialize an object. The following example shows that you use the Contact
's constructor to initialize the ID
property and then the object initializers to initialize the rest of the properties:
Contact c2 = new Contact(1234) { FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" };
In C#, a constructor is called automatically when an object is instantiated. When you are done with the object, the Common Language Runtime (CLR) will destroy them automatically, so you do not have to worry about cleaning them up. If you are using unmanaged resources, however, you need to free them up manually.
When objects are destroyed and cleaned up by the CLR, the object's destructor is called. A C# destructor is declared by using a tilde (~
) followed by the class name:
class Contact : Object { //---constructor--- public Contact() { //... } //---destructor--- ~Contact() { //---release unmanaged resources here--- } //... }
The destructor is a good place for you to place code that frees up unmanaged resources, such as COM objects or database handles. One important point is that you cannot call the destructor explicitly — it will be called automatically by the garbage collector.
To manually dispose of your unmanaged resources without waiting for the garbage collector, you can implement the IDisposable
interface and the Dispose()
method.
Chapter 5 discusses the concept of interfaces in more detail.
The following shows the Contact
class implementing the IDisposable
class and implementing the Dispose()
method:
class Contact : IDisposable { //... ~Contact() { //---call the Dispose() method--- Dispose(); } public void Dispose() { //---release unmanaged resources here--- } }
You can now manually dispose of unmanaged resources by calling the Dispose()
method directly:
Contact c1 = new Contact(); //... //---done with c1 and want to dispose it--- c1.Dispose();
There is now a call to the Dispose()
method within the destructor, so you must make sure that the code in that method is safe to be called multiple times — manually by the user and also automatically by the garbage collector.
You can also apply the static
keyword to class definitions. Consider the following FilesUtil
class definition:
public class FilesUtil { public static string ReadFile(string Filename) { //---implementation--- return "file content..."; } public static void WriteFile(string Filename, string content) { //---implementation--- } }
Within this class are two static methods — ReadFile()
and WriteFile()
. Because this class contains only static methods, creating an instance of this class is not very useful, as Figure 4-4 shows.
As shown in Figure 4-4, an instance of the FilesUtil
class does not expose any of the static methods defined within it. Hence, if a class contains nothing except static methods and properties, you can simply declare the class as static, like this:
public static class FilesUtil { public static string ReadFile(string Filename) { //---implementation--- return "file content..."; } public static void WriteFile(string Filename, string content) { //---implementation--- } }
The following statements show how to use the static class: //---this is not allowed for static classes--- FilesUtil f = new FilesUtil(); //---these are OK--- Console.WriteLine(FilesUtil.ReadFile(@"C:TextFile.txt")); FilesUtil.WriteFile(@"C:TextFile.txt", "Some text content to be written");
Use static classes when the methods in a class are not associated with a particular object. You need not create an instance of the static class before you can use it.
In C#, all classes inherit from the System.Object
base class (inheritance is discussed in the next chapter). This means that all classes contain the methods defined in the System.Object
class.
All class definitions that do not inherit from other classes by default inherit directly from the System.Object
class. The earlier Contact
class definition:
public class Contact
for example, is equivalent to:
public class Contact: Object
You can create an instance of the System.Object
class if you want, but it is by itself not terribly useful:
Object o = new object();
The System.Object
class exposes four instance methods (see Figure 4-5):
Equals()
— Checks whether the value of the current object is equal to that of another object. By default, the Equals()
method checks for reference equality (that is, if two objects are pointing to the same object). You should override this method for your class.
GetHashCode()
— Returns a hash code for the class. The GetHashCode()
method is suitable for use in hashing algorithms and data structures, such as a hash table. There will be more about hashing in Chapter 11
GetType()
— Returns the type of the current object
ToString()
— Returns the string representation of an object
In addition, the System.Object
class also has two static methods (see Figure 4-6):
Equals()
— Returns true if the two objects are equal (see next section for more details)
ReferenceEquals()
— Returns true if two objects are from the same instance
All classes that inherit from System.Object
also inherit all the four instance methods, a couple of which you will learn in more details in the following sections.
Consider the following three instances of the Contact
class, which implicitly inherits from the System.Object
class:
Contact c1 = new Contact() { ID = 1234, FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" }; Contact c2 = new Contact() { ID = 1234, FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" }; Contact c3 = new Contact() { ID = 4321, FirstName = "Lee", LastName = "Wei-Meng", Email = "[email protected]" };
As you can see, c1
and c2
are identical in data member values, while c3
is different. Now, let's use the following statements to see how the Equals()
and ReferenceEquals()
methods work:
Console.WriteLine(c1.Equals(c2)); //---False--- Console.WriteLine(c1.Equals(c3)); //---False--- c3 = c1; Console.WriteLine(c1.Equals(c3)); //---True--- Console.WriteLine(Object.ReferenceEquals(c1, c2)); //---False--- Console.WriteLine(Object.ReferenceEquals(c1, c3)); //---True---
The first statement might be a little surprising to you; did I not just mention that you can use the Equals()
method to test for value equality?
Console.WriteLine(c1.Equals(c2)); //---False---
In this case, c1
and c2
have the exact same values for the members, so why does the Equals()
method return False
in this case? It turns out that the Equals()
method must be overridden in the Contact
class definition. This is because by itself, the System.Object
class does not know how to test for the equality of your custom class; the Equals()
method is a virtual method and needs to be overridden in derived classes. By default, the Equals()
method tests for reference equality.
The second statement is straightforward, as c1
and c3
are two different objects:
Console.WriteLine(c1.Equals(c3)); //---False---
The third and fourth statements assign c1
to c3
, which means that c1
and c3
are now two different variables pointing to the same object. Hence, Equals()
returns True
:
c3 = c1; Console.WriteLine(c1.Equals(c3)); //---True---
The fifth and sixth statements test the reference equality of c1
against c2
and then c1
against c3
:
Console.WriteLine(Object.ReferenceEquals(c1, c2)); //---False--- Console.WriteLine(Object.ReferenceEquals(c1, c3)); //---True---
If two objects have reference equality, they also have value equality, but the reverse is not necessarily true.
By default the Equals()
method tests for reference equality. To ensure that it tests for value equality rather than reference equality, you need to override the Equals()
virtual method.
Using the same Contact
class used in the previous section, add the methods highlighted in the following code:
public class Contact { public int ID; public string FirstName; public string LastName; public string Email; public override bool Equals(object obj) { //---check for null obj--- if (obj == null) return false; //---see if obj can be cast to Contact--- Contact c = obj as Contact; if ((System.Object)c == null) return false; //---check individual fields--- return (ID == c.ID) && (FirstName == c.FirstName) && (LastName == c.LastName) && (Email == c.Email); } public bool Equals(Contact c) { //---check for null obj--- if (c == null) return false; //---check individual fields--- return (ID == c.ID) && (FirstName == c.FirstName) && (LastName == c.LastName) && (Email == c.Email); } public override int GetHashCode() { return ID; } }
Essentially, you're adding the following:
The Equals(object obj)
method to override the Equals()
virtual method in the System.Object
class. This method takes in a generic object (System.Object
) as argument.
The Equals(Contact c)
method to test for value equality. This method is similar to the first method, but it takes in a Contact
object as argument.
The GetHashCode()
method to override the GetHashCode()
virtual method in the System.Object
class.
Notice that the Equals()
methods essentially performs the following to determine if two objects are equal in value:
It checks whether the object passed is in null
. If it is, it returns false
.
It checks whether the object passed is a Contact
object (the second Equals()
method need not check for this). If it isn't, it returns false
.
Last, it checks to see whether the individual members of the passed-in Contact
object are of the same value as the members of the current object. Only when all the members have the same values (which members to test are determined by you) does the Equals()
method return true
. In this case, all the four members' values must be equal to the passed-in Contact
object.
The following statement will now print out True
:
Console.WriteLine(c1.Equals(c2)); //---True---
All objects in C# inherits the ToString()
method, which returns a string representation of the object. For example, the DateTime
class's ToString()
method returns a string containing the date and time, as the following shows:
DateTime dt = new DateTime(2008, 2, 29); //---returns 2/29/2008 12:00:00 AM--- Console.WriteLine(dt.ToString());
For custom classes, you need to override the ToString()
method to return the appropriate string. Using the example of the Contact
class, an instance of the Contact
class's ToString()
method simply returns the string "Contact"
:
Contact c1 = new Contact() { ID = 1234, FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" }; //---returns "Contact"--- Console.WriteLine(c1.ToString());
This is because the ToString()
method from the Contact
class inherits from the System.Object
class, which simply returns the name of the class.
To ensure that the ToString()
method returns something appropriate, you need to override it:
class Contact { public int ID; public string FirstName; public string LastName; public string Email; public override string ToString() { return ID + "," + FirstName + "," + LastName + "," + Email; } //... }
In this implementation of the ToString()
method, you return the concatenation of the various data members, as evident in the output of the following code:
Contact c1 = new Contact() { ID = 1234, FirstName = "Wei-Meng", LastName = "Lee", Email = "[email protected]" }; //---returns "1234,Wei-Meng,Lee,[email protected]"--- Console.WriteLine(c1.ToString());
Attributes are descriptive tags that can be used to provide additional information about types (classes), members, and properties. Attributes can be used by .NET to decide how to handle objects while an application is running.
There are two types of attributes:
Attributes that are defined in the CLR.
Custom attributes that you can define in your code.
Consider the following Contact
class definition:
class Contact { public string FirstName; public string LastName; public void PrintName() { Console.WriteLine("{0} {1}", this.FirstName, this.LastName); } [Obsolete("This method is obsolete. Please use PrintName()")] public void PrintName(string FirstName, string LastName) { Console.WriteLine("{0} {1}", FirstName, LastName); } }
Here, the PrintName()
method is overloaded — once with no parameter and again with two input parameters. Notice that the second PrintName()
method is prefixed with the Obsolete
attribute:
[Obsolete("This method is obsolete. Please use PrintName()")]
That basically marks the method as one that is not recommended for use. The class will still compile, but when you try to use this method, a warning will appear (see Figure 4-7).
The Obsolete
attribute is overloaded — if you pass in true
for the second parameter, the message set in the first parameter will be displayed as an error (by default the message is displayed as a warning):
[Obsolete("This method is obsolete. Please use PrintName()", true)]
Figure 4-8 shows the error message displayed when you use the PrintName()
method marked with the Obsolete
attribute with the second parameter set to true
.
Attributes can also be applied to a class. In the following example, the Obsolete
attribute is applied to the Contact
class:
[Obsolete("This class is obsolete. Please use NewContact")] class Contact { //... }
You can also define your own custom attributes. To do so, you just need to define a class that inherits directly from System.Attribute
. The following Programmer
class is one example of a custom attribute:
public class Programmer : System.Attribute { private string _Name; public double Version; public string Dept { get; set; } public Programmer(string Name) { this._Name = Name; } }
In this attribute, there are:
One private member (_Name
)
One public member (Version
)
One constructor, which takes in one string argument
Here's how to apply the Programmer
attribute to a class:
[Programmer("Wei-Meng Lee", Dept="IT", Version=1.5)] class Contact { //... }
You can also apply the Programmer
attribute to methods (as the following code shows), properties, structure, and so on:
[Programmer("Wei-Meng Lee", Dept="IT", Version=1.5)] class Contact { [Programmer("Jason", Dept = "CS", Version = 1.6)] public void PrintName() { Console.WriteLine("{0} {1}", this.FirstName, this.LastName); } //... }
Use the AttributeUsage
attribute to restrict the use of any attribute to certain types:
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method | System.AttributeTargets.Property)] public class Programmer : System.Attribute { private string _Name; public double Version; public string Dept { get; set; } public Programmer(string Name) { this._Name = Name; } }
In this example, the Programmer
attribute can only be used on class definitions, methods, and properties.
An alternative to using classes is to use a struct (for structure). A struct is a lightweight user-defined type that is very similar to a class, but with some exceptions:
Structs do not support inheritance or destructors.
A struct is a value type (class is a reference type).
A struct cannot declare a default constructor.
Structs implicitly derive from object
and unlike classes, a struct is a value type. This means that when an object is created from a struct and assigned to another variable, the variable will contain a copy of the struct object.
Like classes, structs support constructor, properties, and methods. The following code shows the definition for the Coordinate
struct:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } } public struct Coordinate { public double latitude { get; set; } public double longitude { get; set; } }
The Coordinate
struct contains two properties (defined using the automatic properties feature). You can add a constructor to the struct if you want:
public struct Coordinate { public double latitude { get; set; } public double longitude { get; set; } public Coordinate(double lat, double lng) { latitude = lat; longitude = lng; } }
Remember, a struct cannot have a default constructor.
Note that the compiler will complain with the message "Backing field for automatically implemented property 'Coordinate.latitude' must be fully assigned before control is returned to the caller" when you try to compile this application. This restriction applies only to structs (classes won't have this problem). To resolve this, you need to call the default constructor of the struct, like this:
public struct Coordinate { public double latitude { get; set; } public double longitude { get; set; } public Coordinate(double lat, double lng) : this() { latitude = lat; longitude = lng; } } You can also add methods to a struct. The following shows the ToString() method defined in the Coordinate struct: public struct Coordinate { public double latitude { get; set; } public double longitude { get; set; } public Coordinate(double lat, double lng) : this() { latitude = lat; longitude = lng; } public override string ToString() { return latitude + "," + longitude; } }
To use the Coordinate
struct, create a new instance using the new
keyword and then initialize its individual properties:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e)
{ Coordinate pt1 = new Coordinate(); pt1.latitude = 1.33463167; pt1.longitude = 103.74697; } }
Or you can use the object initializer feature:
private void Form1_Load(object sender, EventArgs e) { //... Coordinate pt2 = new Coordinate() { latitude = 1.33463167, longitude = 103.74697 }; }
Because structs are value types, assigning one struct to another makes a copy of its value, as the following code sample shows:
private void Form1_Load(object sender, EventArgs e) { //... Coordinate pt2 = new Coordinate() { latitude = 1.33463167, longitude = 103.74697 }; Coordinate pt3; pt3 = pt2; Console.WriteLine("After assigning pt2 to pt3"); Console.WriteLine("pt2: {0}", pt2.ToString()); Console.WriteLine("pt3: {0}", pt3.ToString()); pt3.latitude = 1.45631234; pt3.longitude = 101.32355; Console.WriteLine("After changing pt3"); Console.WriteLine("pt2: {0}", pt2.ToString()); Console.WriteLine("pt3: {0}", pt3.ToString()); }
Here's the program's output:
After assigning pt2 to pt3 pt2: 1.33463167,103.74697 pt3: 1.33463167,103.74697 After changing pt3 pt2: 1.33463167,103.74697 pt3: 1.45631234,101.32355
Notice that after changing the properties of pt3
, the latitude
and longitude
properties of pt2
and pt3
are different.
This chapter explained how to define a class and the various components that make up a class — properties, methods, constructors, and destructors. In addition, it explored the new features in C# 3.0 — object initializers, anonymous types, and automatic properties. While you need to use the new
keyword to instantiate a new object, you can also create static classes that can be used without instantiation. Finally, you saw how to use structs, the lightweight alternative to classes, that behave much like classes but are value types.
18.117.159.116