8.1 Defining ClassesWhen you define a new class, you define the characteristics of all objects of that class, as well as their behaviors. For example, if you create your own windowing operating system, you might want to create screen widgets, (known as a control in Windows). One control of interest might be a listbox, a control that is very useful for presenting a list of choices to the user and enabling the user to select from the list. Listboxes have a variety of characteristics: height, width, location, and text color, for example. Programmers have also come to expect certain behaviors of listboxes — they can be opened, closed, sorted, and so on. Object-oriented programming allows you to create a new type, ListBox, which encapsulates these characteristics and capabilities. To define a new type or class, you first declare it and then define its methods and fields. You declare a class using the class keyword. The complete syntax is as follows: [attributes] [access-modifiers] class identifier [:base-class] {class-body} Attributes are used to provide special metadata about a class (that is, information about the structure or use of the class). You will not need attributes for routine C# programming. Access modifiers are discussed later in this chapter. (Typically, your classes will use the keyword public as an access modifier.) The identifier is the name of the class that you provide. Typically, C# classes are named with nouns (e.g., Dog, Employee, ListBox). The naming convention (not required, but strongly encouraged) is to use Pascal notation. In Pascal notation, you don't use underbars or hyphens, but if the name has two words (Golden Retriever) you push the two words together, each word beginning with an uppercase letter (GoldenRetriever). As mentioned earlier, inheritance is one of the pillars of object-oriented programming. The optional base-class is explained when inheritance is discussed in Chapter 11. The member definitions that make up the class-body are enclosed by open and closed curly braces ({}). class Dog { int age; // the dog's age int weight; // the dog's weight Bark() { //... } Eat() { // ... } } Methods within the class definition of Dog describe all the things a dog can do. The fields (member variables) such as age and weight describe all the dog's attributes or state. 8.1.1 Instantiating ObjectsTo make an actual instance, or object, of the Dog class, you must declare the object and allocate memory for the object. These two steps combined are necessary to create, or instantiate, the object. Here's how you do it. First, you declare the object by writing the name of the class (Dog) followed by an identifier (name) for the object or instance of that class: Dog milo; // declare milo to be an instance of Dog This is not unlike the way you create a local variable; you declare the type (in this case Dog) followed by the identifier (milo). Notice also that (as with variables) by convention the identifier for the object uses Camel Notation. Camel Notation is just like Pascal Notation except that the very first letter is lowercase. Thus, a variable or object name might be myDog, designatedDriver, or plantManager. The declaration alone doesn't actually create an instance, however. To create an instance of a class you must also allocate memory for the object using the keyword new. milo = new Dog(); // allocate memory for milo You can combine the declaration of the Dog type with the memory allocation into a single line: Dog milo = new Dog(); This code declares milo to be an object of type Dog and also creates a new instance of Dog. You'll see what the parentheses are for later in this chapter in the discussion of the constructor. In C#, everything happens within a class. No methods can run outside of a class, not even Main(). The Main() method is the entry point for your program; it is called by the operating system, and it is where execution of your program begins. Typically, you'll create a small class to house Main(), because like every method, Main() must live within a class. Some of the examples in this book use of a class named Tester to house Main(): public class Tester { public static void Main() { //... } } Even though Tester was created to house the Main() method, you've not yet instantiated any objects of type Tester. To do so you would write: Tester myTester = new Tester(); // instantiate an object of type Tester As you'll see later in this chapter, creating an instance of the Tester class allows you to call other methods on the object you've created (myTester). 8.1.1.1 Classes versus objectsOne way to understand the difference between a class and an instance (object) is to consider the distinction between the type int and a variable of type int. You can't assign a value to a type: int = 5; // error Instead, you assign a value to an object of that type (in this case, a variable of type int): int myInteger; myInteger = 5; // ok Similarly, you can't assign values to fields in a class; you must assign values to fields in an object. Thus, you can't write: Dog.weight = 5; This is not meaningful. It isn't true that every dog's weight is 5 pounds. You must instead write: milo.weight = 5; This says that a particular dog's weight (milo's weight) is 5 pounds. 8.1.2 Memory Allocation: The Stack Versus the HeapObjects created within methods are called local variables. They are local to the method, as opposed to belonging to the object, as member variables do. The object is created within the method, used within the method, and then destroyed when the method ends. Local objects are not part of the object's state — they are temporary value holders, useful only within the particular method. Local variables of intrinsic types such as int are created on a portion of memory known as the stack. The stack is allocated and de-allocated as methods are invoked. When you start a method, all the local variables are created on the stack. When the method ends, local variables are destroyed. These variables are referred to as local because they exist (and are visible) only during the lifetime of the method. They are said to have local scope. When the method ends, the variable goes out of scope and is destroyed. C# divides the world of types into value types and reference types. Value types are created on the stack. All the intrinsic types (int, long, etc.) are value types, and thus are created on the stack. Classes, on the other hand, are reference types. Reference types are created on an undifferentiated block of memory known as the heap. When you declare an instance of a reference type, what you are actually declaring is a reference, which is a variable that refers to another object. The reference acts like an alias for the object. That is, when you write: Dog milo = new Dog(); the new operator creates a Dog object on the heap and returns a reference to it. That reference is assigned to milo. Thus, milo is a reference object that refers to a Dog object on the heap. It is common to say that milo is a reference to a dog, or even that milo is a Dog object, but technically that is incorrect. milo is actually a reference object that refers to an (unnamed) Dog object on the heap. The reference milo acts as an alias for that unnamed object. For all practical purposes, however, you can treat milo as if it were the Dog object itself. The implication of using references is that you can have more than one reference to the same object. To see this difference between creating value types and reference types, examine Example 8-1. A complete analysis follows the output. Example 8-1. Creating value types and reference typesusing System; namespace heap { public class Dog { public int weight; } class Tester { public void Run() { // create an integer int firstInt = 5; // create a second integer int secondInt = firstInt; // display the two integers Console.WriteLine("firstInt: {0} secondInt: {1}", firstInt, secondInt); // modify the second integer secondInt = 7; // display the two integers Console.WriteLine("firstInt: {0} secondInt: {1}", firstInt, secondInt); // create a dog Dog milo = new Dog(); // assign a value to weight milo.weight = 5; // create a second reference to the dog Dog fido = milo; // display their values Console.WriteLine("Milo: {0}, fido: {1}", milo.weight, fido.weight); // assign a new weight to the second reference fido.weight = 7; // display the two values Console.WriteLine("Milo: {0}, fido: {1}", milo.weight, fido.weight); } static void Main() { Tester t = new Tester(); t.Run(); } } } Output: firstInt: 5 secondInt: 5 firstInt: 5 secondInt: 7 Milo: 5, fido: 5 Milo: 7, fido: 7 The program begins by creating an integer, firstInt, and initializing it with the value 5. The second integer, secondInt, is then created and initialized with the value in firstInt. Their values are displayed as output: firstInt: 5 secondInt: 5 These values are identical. Because int is a value type, a copy of the firstInt value is made and assigned to secondInt; secondInt is an independent second variable, as illustrated in Figure 8-1. Figure 8-1. secondInt is a copy of firstIntThen the program assigns a new value to secondInt: secondInt = 7; Because these variables are value types, independent of one another, the first variable is unaffected. Only the copy is changed, as illustrated in Figure 8-2. Figure 8-2. Only the copy is changedWhen the values are displayed, they are different: firstInt: 5 secondInt: 7 Your next step is to create a simple Dog class with only one member variable (field) called weight. Note that this field is given a keyword, public, which specifies that any method of any class can access this field. public is what is known as an access modifier. (Generally you will not make member variables public. The weight field was made public to simplify this example.) Access modifiers are covered in detail later in this chapter. You instantiate a Dog object and save a reference to that dog in the reference milo. Dog milo = new Dog(); You assign the value 5 to milo's weight field: milo.weight = 5; You commonly say that you've set milo's weight to 5, but actually you've set the weight of the unnamed object on the heap to which milo refers, as shown in Figure 8-3. Figure 8-3. milo is a reference to an unnamed Dog objectNext you create a second reference to Dog and initialize it by setting it equal to milo. This creates a new reference to the same object on the heap. Dog fido = milo; Notice that this is syntactically similar to creating a second int variable and initializing it with an existing int, as you did before: int secondInt = firstInt; Dog fido = milo; The difference is that Dog is a reference type, so fido is not a copy of milo — it is a second reference to the same object to which milo refers. That is, you now have an object on the heap with two references to it, as illustrated in Figure 8-4. Figure 8-4. fido is a second reference to the Dog objectWhen you change the weight of that object through the fido reference: fido.weight = 7; you change the weight of the same object to which milo refers. The output reflects this: Milo: 7, fido: 7 It isn't that fido is changing milo, it is that by changing the (unnamed) object on the heap to which fido refers, you simultaneously change the value of milo because they refer to the same unnamed object. 8.1.3 Creating a Time ClassNow consider a class to keep track of and display the time of day. The internal state of the class must be able to represent the current year, month, date, hour, minute, and second. You probably would also like the class to display the time in a variety of formats. You might implement such a class by defining a single method and six variables, as shown in Chapter 8. Example 8-2. The Time classusing System; public class Time { // private variables private int year; private int month; private int date; private int hour; private int minute; private int second; // public methods public void DisplayCurrentTime() { Console.WriteLine( "stub for DisplayCurrentTime"); } } public class Tester { static void Main() { Time timeObject = new Time(); timeObject.DisplayCurrentTime(); } } This code creates a new user-defined type: Time. The Time class definition begins with the declaration of a number of member variables: Year, Month, Date, Hour, Minute, and Second. The keyword private indicates that these values can only be called by methods of this class. The private keyword is an access modifier, explained later in this chapter.
The only method declared within the Time class is the method DisplayCurrentTime(). The DisplayCurrentTime() method is defined to return void; that is, it will not return a value to the method that invokes it. For now, the body of this method has been "stubbed out." Stubbing out a method is a temporary measure you might use when you first write a program to allow you to think about the overall structure without filling in every detail when you create a class. When you stub out a method body you leave out the internal logic and just mark the method, perhaps with a message to the console: public void DisplayCurrentTime() { Console.WriteLine( "stub for DisplayCurrentTime"); } After the closing brace, a second class, Tester, is defined. Tester contains our now familiar Main() method. In Main() an instance of Time is created, named timeObject. Time timeObject = new Time();
Because timeObject is an instance of Time, Main() can make use of the DisplayCurrentTime() method available with objects of that type and call it to display the time: timeObject.DisplayCurrentTime(); You invoke a method on an object by writing the name of the object (timeObject) followed by the dot operator (.), the method name (DisplayCurrentTime), and the parameter list in parentheses (in this case, empty). You'll see how to pass in values to initialize the member variables in the discussion of constructors, later in this chapter. 8.1.4 Access ModifiersAn access modifier determines which class methods — including methods of other classes — can see and use a member variable or method within a class. Table 8-1 summarizes the C# access modifiers.
Public methods are part of the class's public interface: they define how this class behaves. Private methods are "helper methods" used by the public methods to accomplish the work of the class. Because the internal workings of the class are private, helper methods need not (and should not) be exposed to other classes. The Time class and its method DisplayCurrentTime() are both declared public so that any other class can make use of them. If DisplayCurrentTime() had been private, it would not be possible to invoke DisplayCurrentTime from any method of any class other than methods of Time. In Chapter 8, DisplayCurrentTime was invoked from a method of Tester (not Time), and this was legal because both the class (Time) and the method (DisplayCurrentTime) were marked public. |