Chapter 1
IN THIS CHAPTER
Introducing and using the C# class
Assigning and using object references
Examining classes that contain classes
Identifying static and instance class members
You can freely declare and use all the intrinsic data types — such as int
, double
, and bool
— to store the information necessary to make your program the best it can be. For some programs, these simple variables are enough. However, most programs need a way to bundle related data into a neat package.
As shown in Book 1, C# provides arrays and other collections for gathering into one structure groups of like-typed variables, such as string
or int
. A hypothetical college, for example, might track its students by using an array. But a student is much more than just a name — how should this type of program represent a student?
Some programs need to bundle pieces of data that logically belong together but aren't of the same type. A college enrollment application handles students, each of whom has a name, rank (grade-point average), and serial number. Logically, the student’s name may be a string
; the grade-point average, a double
; and the serial number, a long
. That type of program needs a way to bundle these three different types of variables into a single structure named Student
. Fortunately, C# provides a structure known as the class for accommodating groupings of unlike-typed variables.
There are many ways to write applications, and Object-Oriented Programming (OOP) is one of them. You’ve already seen procedural programming techniques in minibook 1, so minibook 2 is your foray into OOP. The purpose of OOP is to make it easier to model the real world using code. So, when you see a Student
class, what you see is a model of a real-world student in the form of code. Of course, this model is limited to what you need to do with the student in specific circumstances. For example, the model wouldn't include the student’s eating preferences, unless that’s what you’re modeling. The following sections provide you with a quick overview of OOP that the rest of Book 2 develops further.
OOP relies on the class to create a container for the Student
model, so developers call the result the Student
class. When you work with Student
, your focus is on a real-world student, so you don't care about the underlying code details. The first principle of OOP then is abstraction — the ability to focus on what is needed in the real world, rather than what is needed to program the underlying details. By changing the focus of programming, the development process becomes easier and less error prone.
Keeping data and code together so that everything you need is in one place is another principle of OOP that developers call encapsulation. A class is self-contained in that it has everything needed to model a particular kind of object.
Because of the manner in which classes are put together, after you create a class, you can reuse it for every object that fits within that class’ model. Reuse makes it possible to write a class once and then use it everywhere that the class fits. Many people use the phase Don’t Repeat Yourself (DRY) to emphasize this part of OOP.
An extension of DRY is the idea that you can create a class hierarchy. For example, you could create a class called Vehicle
. Vehicles come in many forms, such as Car
, Truck
, and Bus
. Each of these subclasses of Vehicle
would inherit (derive and use) the features that Vehicle
provides and add specifics of their own with regard to that particular kind of vehicle. The Car
class might be further broken down into the Coup
, Sedan
, and Racer
classes. Inheritance makes it possible to start with a general kind of object and then become very specific.
Trying to keep the focus on the object and not the underlying code is one reason that OOP classes employ access modifiers, which are indicators of what is and isn’t accessible to users of a class. The use of access modifiers keeps code private so that users don’t worry about implementation details. It also provides the developer with the flexibility to make code changes that don’t modify the class interface.
public
: The code used to create a particular parent class element is accessible by any other class, even if that class hasn’t inherited from the parent class.protected
: The code is accessible by members of the same class and any child classes. So, this code would be accessible by members of the Vehicle
class and the Car
class, but not accessible by members of the Road
class, which doesn't inherit from the Vehicle
class.private
: The code is accessible only by members of the same class. For example, the code is accessible by any member of the Vehicle
class, but not accessible by members of the Car
class.internal
: This is the super-secret code that is accessible only from within a given assembly, but not within any other assembly. It means that you can create code that is only accessible from your application and not any other application, even if the other application uses classes from your application.Classes represent blueprints for real-world objects. When you create a Student
class, what you really have is a blueprint for creating a student object. To create an object, you instantiate the class. Perhaps you might create the Mike
and Sally
objects, both of which are instances of the Student
class. Both objects would have the same structure, but the details would differ. For example, one object would have a Name
value of "Mike"
and the other a Name
value of "Sally"
.
string
data type for a student's identifier, you think about a Name
property.Student
or changed their grade-point average. Events make it possible to monitor objects.A class is a bundling of unlike data and functions that logically belong together into one tidy package. C# gives you the freedom to foul up your classes any way you want, but good classes are designed to represent concepts.
Computer science models the world via structures that represent concepts or things in the world, such as bank accounts, tic-tac-toe games, customers, game boards, documents, and products. Analysts say that “a class maps concepts from the problem into the program.” For example, your problem might be to build a traffic simulator that models traffic patterns for the purpose of building streets, intersections, and highways.
Any description of a problem concerning traffic would include the term vehicle in its solution. Vehicles have a top speed that must be figured into the equation. They also have a weight, and some of them are clunkers. In addition, vehicles stop and vehicles go. Thus, as a concept, vehicle is part of the problem domain.
A good C# traffic-simulator program would necessarily include the class Vehicle
, which describes the relevant properties of a vehicle. The C# Vehicle
class would have properties such as topSpeed
, weight
, and isClunker
.
Because the class is central to C# programming, the rest of minibook 2 discusses the ins and outs of classes in much more detail. This chapter gets you started.
This section begins with a class declaration for the VehicleData
example. An example of the class Vehicle
may appear this way (you put this class definition before class Program
in the file):
public class Vehicle
{
public string model { get; set; } // Name of the model
public string manufacturer { get; set; } // Name of the manufacturer
public int numOfDoors { get; set; } // Number of vehicle doors
public int numOfWheels { get; set; } // You get the idea.
}
A class definition begins with the words public
(the access modifier) and class
(the kind of structure you're creating), followed by the name of the class — in this case, Vehicle
. Like all names in C#, the name of the class is case sensitive. C# doesn’t enforce any rules concerning class names, but an unofficial rule holds that the name of a class starts with a capital letter.
The class name is followed by a pair of open and closed braces. Within the braces, you have zero or more members. The members of a class are items that make up the parts of the class. In this example, class Vehicle
starts with the member model
of type string
, which contains the name of the model of the vehicle. If the vehicle were a car, its model name could be Trooper II. The second member of this Vehicle
class example is manufacturer
of type string
. The other two properties are the number of doors and the number of wheels on the vehicle, both of which are type int
.
The public
modifier in front of the class name makes the class universally accessible throughout the program. Similarly, the public
modifier in front of the member names makes them accessible to everything else in the program. Other modifiers are possible. (Chapter 4 in this minibook covers the topic of accessibility in more detail and shows how you can hide some members.)
The class definition should describe the properties of the object that are salient to the problem at hand. That's a little hard to do right now because you don’t know what the problem is, but it becomes clearer as you work through the problem.
Defining a Vehicle
design isn’t the same task as building a car. Someone has to cut some sheet metal and turn some bolts before anyone can drive an actual car. A class object is declared in a similar (but not identical) fashion to declaring an intrinsic object such as an int
.
Vehicle myCar;
myCar = new Vehicle();
The first line declares a variable myCar
of type Vehicle
, just as you can declare a somethingOrOther
of class int
. (Yes, a class is a type, and all C# objects are defined as classes.) The new Vehicle()
call instantiates (creates) a specific object of type Vehicle
(the blueprint or class) and stores the location in memory of that object into the variable myCar
(the instance). The new
keyword has nothing to do with the age of myCar
. (My car could qualify for an antique license plate if it weren't so ugly.) The new
operator creates a new block of memory in which your program can store the properties of myCar
.
Each object of class Vehicle
has its own set of members. The following expression stores the number 1 into the numberOfDoors
member of the object referenced by myCar
:
myCar.numberOfDoors = 1;
myCar.manufacturer = "BMW"; // Don't get your hopes up.
myCar.model = "Isetta"; // The Urkel-mobile
The Isetta was a small car built during the 1950s with a single door that opened the entire front of the car. Check it out at https://www.motortrend.com/vehicle-genres/1956-1962-bmw-isetta-300-collectible-classic/
.
The “Defining a class” section of this chapter tells you how to declare a class to use within an application. Starting with C# 9.0, you have a number of ways to interact with this class. The following sections show two of these techniques. The traditional approach in the first section that follows uses the Vehicle
class in a manner that works with older versions of C#, and you should use it for compatibility purposes with existing code that uses the same approach. It requires 52 lines of code to get the job done. The C# 9.0 approach in the second section that follows is easier to read and uses only 47 lines of code. That's not much of a difference until you start looking at the number of lines saved in a larger program.
The simple VehicleData
program performs these tasks:
Vehicle
.myCar
.myCar
.Here's the code for the VehicleData
program:
static void Main(string[] args)
{
// Prompt user to enter a name.
Console.WriteLine("Enter the properties of your vehicle");
// Create an instance of Vehicle.
Vehicle myCar = new Vehicle();
// Populate a data member via a temporary variable.
Console.Write("Model name = ");
string s = Console.ReadLine();
myCar.model = s;
// Or you can populate the data member directly.
Console.Write("Manufacturer name = ");
myCar.manufacturer = Console.ReadLine();
// Enter the remainder of the data.
// A temp variable, s, is useful for reading ints.
Console.Write("Number of doors = ");
s = Console.ReadLine();
myCar.numOfDoors = Convert.ToInt32(s);
Console.Write("Number of wheels = ");
s = Console.ReadLine();
myCar.numOfWheels = Convert.ToInt32(s);
// Now display the results.
Console.WriteLine("
Your vehicle is a ");
Console.WriteLine(myCar.manufacturer + " " + myCar.model);
Console.WriteLine("with " + myCar.numOfDoors + " doors, "
+ "riding on " + myCar.numOfWheels
+ " wheels.");
Console.Read();
}
The program creates an object myCar
of class Vehicle
and then populates each field by reading the appropriate data from the keyboard. (The input data isn't — but should be — checked for legality.) The program then writes myCar
’s properties in a slightly different format. Here’s some example output from executing this program:
Enter the properties of your vehicle
Model name = Metropolitan
Manufacturer name = Nash
Number of doors = 2
Number of wheels = 4
Your vehicle is a
Nash Metropolitan
with 2 doors, riding on 4 wheels
The C# 9.0 approach to this example appears in the VehicleData2
example. The Main()
method is different in arrangement, as shown here.
static void Main(string[] args)
{
// Prompt user to enter a name.
Console.WriteLine("Enter the properties of your vehicle");
// Obtain the data needed to create myCar.
Console.Write("Model name = ");
string s = Console.ReadLine();
Console.Write("Manufacturer name = ");
string mfg = Console.ReadLine();
Console.Write("Number of doors = ");
int doors = Convert.ToInt32(Console.ReadLine());
Console.Write("Number of wheels = ");
int wheels = Convert.ToInt32(Console.ReadLine());
// Create an instance of Vehicle.
Vehicle myCar = new()
{
model = s,
manufacturer = mfg,
numOfDoors = doors,
numOfWheels = wheels
};
// Now display the results.
Console.WriteLine($"
Your vehicle is a {myCar.manufacturer} " +
$"{myCar.model} with {myCar.numOfDoors} doors riding on " +
$"{myCar.numOfWheels} wheels");
Console.Read();
}
The example is much more structured because it collects all of the data needed to create myCar
first. It then instantiates the myCar
object using Vehicle
with two changes. First, you don't declare the object type when calling new()
. The compiler deduces the object type based on the type you provide for the object. Second, you set the properties individually, which means that you set them as part of a list after new()
. Using this approach is significantly clearer because you know precisely where the values in myCar
come from and have to look in only one place to see them. The output of this version is the same as before.
Detroit car manufacturers can track every car they make without getting the cars confused. Similarly, a program can create numerous objects of the same class, as shown in this example:
Vehicle car1 = new() {manufacturer = "Studebaker", model = "Avanti"};
// The following has no effect on car1.
Vehicle car2 = new() {manufacturer = "Hudson", model = "Hornet"};
Creating an object car2
and assigning it the manufacturer name Hudson
has no effect on the car1
object (with the manufacturer name Studebaker
). That's because car1
and car2
appear in totally separate memory locations. In part, the ability to discriminate between objects is the real power of the class construct. The object associated with the Hudson Hornet can be created, manipulated, and dispensed with as a single entity, separate from other objects, including the Avanti. (Both are classic automobiles, especially the latter.)
The dot operator and the assignment operator are the only two operators defined on reference types:
// Create a null reference.
Vehicle yourCar;
// Assign the reference a value.
yourCar = new Vehicle();
// Use dot to access a member.
yourCar.manufacturer = "Rambler";
// Create a new reference and point it to the same object.
Vehicle yourSpousalCar = yourCar;
The first line creates an object yourCar
without assigning it a value. A reference that hasn't been initialized is said to point to the null object. Any attempt to use an uninitialized (null) reference generates an immediate error that terminates the program.
The second statement creates a new Vehicle
object and assigns it to yourCar
. The last statement in this code snippet assigns the reference yourSpousalCar
to the reference yourCar
. This action causes yourSpousalCar
to refer to the same object as yourCar
. This relationship is shown in Figure 1-1.
The following two calls that set the car's model in the following code have the same effect:
// Build your car.
Vehicle yourCar = new Vehicle();
yourCar.model = "Kaiser";
// It also belongs to your spouse.
Vehicle yourSpousalCar = yourCar;
// Changing one changes the other.
yourSpousalCar.model = "Henry J";
Console.WriteLine("Your car is a " + yourCar.model);
Executing this program would output Henry J
and not Kaiser
. Notice that yourSpousalCar
doesn't point to yourCar
; rather, both yourCar
and yourSpousalCar
refer to the same vehicle (the same memory location). In addition, the reference yourSpousalCar
would still be valid, even if the variable yourCar
were somehow “lost” (if it went out of scope, for example), as shown in this chunk of code:
// Build your car.
Vehicle yourCar = new Vehicle();
yourCar.model = "Kaiser";
// It also belongs to your spouse.
Vehicle yourSpousalCar = yourCar;
// When your spouse takes your car away …
yourCar = null; // yourCar now references the "null object."
// …yourSpousalCar still references the same vehicle
Console.WriteLine("your car was a " + yourSpousalCar.model);
Executing this program generates the output Your car was a Kaiser
, even though the reference yourCar
is no longer valid. The object is no longer reachable from the reference yourCar
because yourCar
no longer contains a reference to the required memory location. The object doesn't become completely unreachable until both yourCar
and yourSpousalCar
are “lost” or nulled out.
At some point, the C# garbage collector steps in and returns the space formerly used by that particular Vehicle
object to the pool of space available for allocating more Vehicles
(or Student
s, for that matter). The garbage collector only runs after all the references to an object are lost. (The “Garbage Collection and the C# Destructor” sidebar at the end of Chapter 5 of this minibook says more about garbage collection.)
The members of a class can themselves be references to other classes, as shown in the VehicleData3
example, which uses the VehicleData2
example as a starting point. For example, vehicles have motors, which have power and efficiency factors, including displacement. You could throw these factors directly into the class this way:
public class Vehicle
{
public string model; // Name of the model
public string manufacturer; // Ditto
public int numOfDoors; // The number of doors on the vehicle
public int numOfWheels; // You get the idea.
// New stuff:
public int power; // Power of the motor [horsepower]
public double displacement; // Engine displacement [liter]
}
However, power and engine displacement aren't properties of the car. For example, your friend’s Jeep might be supplied with two different motor options that have drastically different levels of horsepower. The 2.4-liter Jeep is a snail, and the same car outfitted with the 4.0-liter engine is quite peppy. The motor is a concept of its own and deserves its own class:
public class Motor
{
public int power; // Power [horsepower]
public double displacement; // Engine displacement [liter]
}
You can combine this class into the Vehicle
(see boldfaced text):
public class Vehicle
{
public string model { get; set; } // Name of the model
public string manufacturer { get; set; } // Name of the manufacturer
public int numOfDoors { get; set; } // Number of vehicle doors
public int numOfWheels { get; set; } // You get the idea.
public Motor motor { get; set; } // Type of engine.
}
Creating myCar
now appears this way (instead of asking for input, this version simply provides the required values in the interest of space):
static void Main(string[] args)
{
// Create an instance of Vehicle.
Vehicle myCar = new()
{
model = "Cherokee Sport",
manufacturer = "Jeep",
numOfDoors = 2,
numOfWheels = 4,
motor = new()
{
power = 230,
displacement = 4.0
}
};
// Now display the results.
Console.WriteLine($"
Your vehicle is a {myCar.manufacturer} " +
$"{myCar.model} with {myCar.numOfDoors} doors riding on " +
$"{myCar.numOfWheels} wheels using a {myCar.motor.displacement}" +
$" liter engine producing {myCar.motor.power} hp.");
Console.Read();
}
Notice how you can place new()
statements within new()
statements to define an entire hierarchy in a manner that is very easy to follow. Earlier versions of C# would require that you instantiate motor
first and then add it to myCar
. Everything is self-contained within a single hierarchical declaration now so that the opportunities for errors are fewer. Notice that you access the power
and displacement
values using a dot hierarchy as well: myCar.motor.power
and myCar.motor.displacement
.
Most data members of a class are specific to their containing object, not to any other objects. Consider the Car
class:
public class Car
{
public string licensePlate; // The license plate ID
}
Because the license plate ID is an object property, it describes each object of class Car
uniquely. For example, your spouse's car will have a different license plate from your car, as shown here:
Car spouseCar = new Car();
spouseCar.licensePlate = "XYZ123";
Car yourCar = new Car();
yourCar.licensePlate = "ABC789";
However, some properties exist that all cars share. For example, the number of cars built is a property of the class Car
but not of any single object. These class properties are flagged in C# with the keyword static
:
public class Car
{
public static int numberOfCars; // The number of cars built
public string licensePlate; // The license plate ID
}
// Create a new object of class Car.
Car newCar = new Car();
newCar.licensePlate = "ABC123";
// Now increment the count of cars to reflect the new one.
Car.numberOfCars++;
The object member newCar.licensePlate
is accessed through the object newCar
, and the class (static) member Car.numberOfCars
is accessed through the class Car
. All Cars
share the same numberOfCars
member, so each car contains exactly the same value as all other cars.
One special type of static member is the const
data member, which represents a constant. You must establish the value of a const
variable in the declaration, and you cannot change it anywhere within the program, as shown here:
class Program
{
// Number of days in the year (including leap day)
public const int daysInYear = 366; // Must have initializer.
public static void Main(string[] args)
{
// This is an array, covered later in this chapter.
int[] maxTemperatures = new int[daysInYear];
for(int index = 0; index < daysInYear; index++)
{
// …accumulate the maximum temperature for each
// day of the year …
}
}
}
You can use the constant daysInYear
in place of the value 366
anywhere within your program. The const
variable is useful because it can replace a mysterious number such as 366
with the descriptive name daysInYear
to enhance the readability of your program. C# provides another way to declare constants — you can preface a variable declaration with the readonly
modifier, like so:
public readonly int daysInYear = 366; // This could also be static.
As with const
, after you assign the initial value, it can't be changed. Although the reasons are too technical for this book, the readonly
approach to declaring constants is usually preferable to const
.
You can use const
with class data members like those you might have seen in this chapter and inside class methods. But readonly
isn't allowed in a method. Chapter 2 of this minibook dives into methods.
An alternative convention also exists for naming constants. Rather than name them like variables (as in daysInYear
), many programmers prefer to use uppercase letters separated by underscores, as in DAYS_IN_YEAR
. This convention separates constants clearly from ordinary read-write variables.