C# Programming · Lesson 11

Showing Some Class

Model real-world concepts with C# classes, objects, properties, references, nested classes, static members, const, and readonly.

  • csharp
  • classes
  • objects
  • oop
  • references
  • static
  • properties

Lesson purpose

Lesson 11 opens Part 2 of the course, object-oriented programming. Intrinsic types such as int, double, and bool hold single values, and arrays and collections group values of one type. Real programs also need to bundle unlike data that belongs together: a student has a string name, a double grade-point average, and a long serial number. In C# the class is the structure for that bundle. Students get a quick overview of OOP principles and access modifiers, then define a class, create objects with new, access members with the dot operator, use the C# 9.0 target-typed new() with object initializers, keep objects separate, reason about references and null, build classes that contain classes, and distinguish static members, instance members, const, and readonly.

Learning objectives

  • Explain why programs need a class to bundle related data of different types.
  • Define abstraction, encapsulation, reuse (DRY), inheritance, and polymorphism in plain language.
  • Describe the public, protected, private, and internal access modifiers.
  • Identify the methods, properties, and events that make up a class.
  • Define a class with auto-implemented properties and explain the difference between properties and fields.
  • Declare a reference variable and create an object with new.
  • Access and assign members with the dot operator.
  • Create objects with the traditional approach and with the C# 9.0 target-typed new() and object initializers.
  • Explain why separate objects keep separate data, and why two references can share one object.
  • Describe null references and garbage collection.
  • Build a class that contains another class and access nested members.
  • Distinguish static (class) members from instance members.
  • Compare const and readonly data members.

1. Why classes

Arrays and collections gather like-typed values, such as a string[] of student names. But a student is more than a name. A college enrollment application tracks each student’s name (string), grade-point average (double), and serial number (long). Those three different types logically belong together in one structure named Student. C# provides the class for groupings of unlike-typed variables.

2. A quick overview of object-oriented programming

Part 1 used procedural techniques. Object-oriented programming (OOP) is another way to write applications whose purpose is to model the real world in code. A Student class is a code model of a real student, limited to what the program needs: it would not include eating preferences unless that is what the program models.

Core principles

Principle Meaning
Abstraction Focus on what is needed in the real world rather than on the underlying programming details. Working with a Student means thinking about a student, not the code inside.
Encapsulation Keep data and code together so everything needed is in one self-contained place.
Reuse (DRY) Write a class once and use it for every object that fits its model: Don’t Repeat Yourself.
Inheritance Build a class hierarchy. Car, Truck, and Bus derive from Vehicle and inherit its features, and Car can be broken down further into Coupe, Sedan, and Racer.
Polymorphism Further define inherited features in a subclass. Every vehicle has a roof, so Vehicle defines a Roof property, Car defines it further, and each car type adds specifics: “one thing is like another thing, but with these changes.”

Inheritance and polymorphism are developed in Lessons 15 and 16.

Keeping objects safe: access modifiers

Access modifiers state what users of a class can and cannot reach. They keep implementation details private and let the developer change the code without changing the class’s interface.

Modifier Accessible from
public Any other class
protected The same class and its child classes (a Car can use Vehicle’s protected members; a Road class cannot)
private Only the same class
internal Anywhere within the same assembly (the application), but not from other assemblies

Combinations such as protected internal exist but are beyond this course. Lesson 14 covers access control in detail.

Classes and objects

A class is a blueprint; an object is one thing built from it. Creating an object from a class is called instantiating it. Mike and Sally can both be instances of Student: the same structure, different details (Name is “Mike” in one and “Sally” in the other).

A class contains:

  • Methods: what you can do with the object, such as changing a name or GPA. Methods give a consistent way to interact with objects (Lesson 12).
  • Properties: the object’s characteristics, such as name or GPA. Every object has the same properties with its own values, and a property lets you think “Name” instead of “a string.”
  • Events: alerts to anyone listening that something changed in the object, such as a new GPA (Lesson 18).

Quick check: which principle does a subclass use when it redefines an inherited Roof property?

3. Defining a class

A class bundles unlike data and functions that logically belong together into one tidy package. Good classes represent concepts from the problem: analysts say a class “maps concepts from the problem into the program.” A traffic simulator will certainly involve vehicles, which have a top speed, a weight, and perhaps a clunker status, so the program needs a Vehicle class.

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; }       // Number of wheels
}
  • public: the access modifier; the class is available throughout the program.
  • class: the kind of structure being created.
  • Vehicle: the class name. Names are case-sensitive, and by convention a class name starts with a capital letter.
  • Members: the items inside the braces that make up the class. A class can have zero or more members.
  • { get; set; }: makes each member an auto-implemented property that can be read and written.
  • Give members descriptive names, and add comments when they clarify purpose.

In a traditional program, place the class definition before class Program in the file.

Naming convention: the book uses lowercase property names such as model. Standard C# style capitalizes properties (Model, Manufacturer, NumOfDoors); the classroom convention is PascalCase for new code, while the examples here keep the book’s names so they match the source files.

Properties versus fields

Many online examples call these properties when they are really fields:

public class Vehicle
{
    public string model;          // a field, not a property
    public string manufacturer;
    public int numOfDoors;
    public int numOfWheels;
}
Field Property
Declaration public string model; public string model { get; set; }
Exposes implementation Yes; outside code touches the variable directly No; access goes through accessors
Validation and formatting Not possible Accessors can validate input and format output
Visual Studio reference count Not shown Shown above the declaration

Public fields break abstraction because they expose the implementation. Properties cost only a few extra keystrokes and give far more control, so the classroom convention is properties for public data.

4. Creating an object

Designing a vehicle is not the same as building a car. A class declaration describes; an object exists.

Vehicle myCar;
myCar = new Vehicle();
  • The first line declares a variable myCar of type Vehicle, just as int num; declares an int. A class is a type.
  • new Vehicle() instantiates a new Vehicle object: it allocates a block of memory for the object’s properties.
  • The assignment stores the object’s memory location in myCar. new has nothing to do with the car’s age.

The term object means a “thing”: an int variable is an int object, and a car is a Vehicle object.

Memory model: a value-type variable such as int num = 1; holds the value itself, and local value types live on the stack. new Vehicle() allocates the object on the heap, and myCar holds a reference to that memory, not the data itself.

5. Accessing members with the dot operator

Each object has its own set of members. The dot operator selects a member of a particular object:

myCar.numOfDoors = 1;
myCar.manufacturer = "BMW";
myCar.model = "Isetta";

Every C# operation is checked by type as well as value. myCar.numOfDoors is an int and the constant 1 is an int, so the assignment matches. (The 1950s Isetta had a single door that opened the whole front of the car.) The book’s text calls this member numberOfDoors in places; the class defines it as numOfDoors.

6. Working with object-based code

The traditional approach

The VehicleData program defines the class, creates an object, assigns its properties from input, and displays them:

Console.WriteLine("Enter the properties of your vehicle");

Vehicle myCar = new Vehicle();

Console.Write("Model name = ");
string s = Console.ReadLine() ?? "";
myCar.model = s;                                  // through a temporary variable

Console.Write("Manufacturer name = ");
myCar.manufacturer = Console.ReadLine() ?? "";    // directly

Console.Write("Number of doors = ");
myCar.numOfDoors = Convert.ToInt32(Console.ReadLine());

Console.Write("Number of wheels = ");
myCar.numOfWheels = Convert.ToInt32(Console.ReadLine());

Console.WriteLine("\nYour vehicle is a ");
Console.WriteLine(myCar.manufacturer + " " + myCar.model);
Console.WriteLine("with " + myCar.numOfDoors + " doors, riding on "
                  + myCar.numOfWheels + " wheels.");

Sample: Metropolitan, Nash, 2, 4 prints “Nash Metropolitan with 2 doors, riding on 4 wheels.” Write keeps the user’s input on the prompt line, and \n inside the string produces a blank line. The input is not validated here but should be (Lessons 3 and 9).

The C# 9.0 approach

VehicleData2 gathers all input first, then creates the object in one expression:

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());

Vehicle myCar = new()
{
    model = s,
    manufacturer = mfg,
    numOfDoors = doors,
    numOfWheels = wheels
};

Console.WriteLine($"\nYour vehicle is a {myCar.manufacturer} {myCar.model} " +
    $"with {myCar.numOfDoors} doors riding on {myCar.numOfWheels} wheels");
  • Target-typed new(): the type is omitted after new because the compiler infers it from the declared type Vehicle.
  • Object initializer: the braces after new() set properties in a list as part of creation.
  • One place shows exactly where every value comes from, which is clearer and slightly shorter. Keep the traditional form for compatibility with older code.

7. Discriminating between objects

A program can create many objects of the same class, each with its own data:

Vehicle car1 = new() { manufacturer = "Studebaker", model = "Avanti" };
Vehicle car2 = new() { manufacturer = "Hudson", model = "Hornet" };
// Creating car2 has no effect on car1.

car1 and car2 occupy separate memory locations. Each can be created, changed, and discarded as a single entity without affecting the other. This ability to keep objects distinct is much of the class construct’s power.

8. References

The dot operator and assignment are the basic operations on reference types:

Vehicle yourCar;                   // declared, but refers to no object yet
yourCar = new Vehicle();           // now refers to a new object
yourCar.manufacturer = "Rambler";  // dot accesses a member
Vehicle yourSpousalCar = yourCar;  // a second reference to the SAME object
  • A reference that refers to no object is null. Using a null reference to reach a member throws an error that terminates the program. The compiler catches most uses of an unassigned local variable, but a reference set to null can still fail at run time.
  • Assigning one reference to another copies the reference, not the object. Both variables now refer to the same memory.
Vehicle yourCar = new Vehicle();
yourCar.model = "Kaiser";

Vehicle yourSpousalCar = yourCar;
yourSpousalCar.model = "Henry J";

Console.WriteLine("Your car is a " + yourCar.model);   // Henry J

Changing the object through either reference changes what both see. yourSpousalCar does not point to yourCar; both point to the same vehicle.

Vehicle yourCar = new Vehicle();
yourCar.model = "Kaiser";
Vehicle yourSpousalCar = yourCar;

yourCar = null;                                        // yourCar no longer refers to it
Console.WriteLine("Your car was a " + yourSpousalCar.model);   // Kaiser

The object stays reachable as long as any reference to it remains. When no references remain, the garbage collector eventually reclaims its memory for new objects (more in Lesson 15).

Efficiency point: references have a fixed size, so arrays and collections of objects store references. Swapping two array elements moves references, not whole objects (the swap in Lesson 6).

Quick check: after b = a; and b.model = "X";, what is a.model? Contrast this with int y = x; y = 5;.

9. Classes that contain classes

Members can themselves be references to other classes. Adding motor details directly to Vehicle is tempting:

public int power;            // horsepower
public double displacement;  // liters

But power and displacement describe the motor, not the car. The same Jeep can come with a slow 2.4-liter engine or a peppy 4.0-liter one. A motor is a concept of its own and deserves its own class:

public class Motor
{
    public int power { get; set; }            // horsepower
    public double displacement { get; set; }  // liters
}

public class Vehicle
{
    public string model { get; set; }
    public string manufacturer { get; set; }
    public int numOfDoors { get; set; }
    public int numOfWheels { get; set; }
    public Motor motor { get; set; }          // a Vehicle HAS a Motor
}

Nested initializers build the whole hierarchy in one readable declaration:

Vehicle myCar = new()
{
    model = "Cherokee Sport",
    manufacturer = "Jeep",
    numOfDoors = 2,
    numOfWheels = 4,
    motor = new()
    {
        power = 230,
        displacement = 4.0
    }
};

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.");
  • A new() inside a new() creates the contained object as part of the outer one. Older C# required creating the Motor first and then assigning it.
  • Nested members are reached with a chain of dots: myCar.motor.power.
  • If motor is never assigned, it is null, and myCar.motor.power fails at run time.

The book shows Motor with fields; the classroom version uses properties to match the rest of the lesson.

10. Static members

Most data members describe one object. Each car has its own license plate:

public class Car
{
    public static int numberOfCars;   // shared by the class
    public string licensePlate;       // one per object
}

Car spouseCar = new Car();
spouseCar.licensePlate = "XYZ123";

Car yourCar = new Car();
yourCar.licensePlate = "ABC789";

Car.numberOfCars++;                   // accessed through the class name
  • The number of cars built is a property of the class, not of any single car, so it is marked static.
  • Static members are accessed through the class (Car.numberOfCars), not through an object. There is exactly one copy, shared by every Car.
  • Class members are static members. Non-static members, which belong to each individual object, are instance members.

Quick check: given Car newCar = new Car();, is the count reached with newCar.numberOfCars or Car.numberOfCars?

11. const and readonly data members

A const member is a special kind of static member representing a constant. Its value must be set in the declaration and can never change:

class Program
{
    public const int daysInYear = 366;   // includes leap day; initializer required

    public static void Main(string[] args)
    {
        int[] maxTemperatures = new int[daysInYear];
        for (int index = 0; index < daysInYear; index++)
        {
            // … record the maximum temperature for each day …
        }
    }
}

A descriptive name such as daysInYear replaces a mysterious number such as 366 and makes the program easier to read and change.

A readonly member also cannot change after it receives its value:

public readonly int daysInYear = 366;   // could also be static
const readonly
When the value is set At compile time, in the declaration In the declaration or in a constructor, at run time
Static? Always, automatically Instance by default; can be marked static
Allowed types Built-in types known at compile time (numbers, char, bool, string) Any type
Inside a method Allowed as a local constant Not allowed

The book says readonly is usually preferable to const; the practical guideline is to use const for values that truly never change (such as the number of months in a year) and readonly for values fixed when an object is created or that might change in a future version.

Naming: some programmers write constants as DAYS_IN_YEAR to set them apart from ordinary variables. Current C# style uses PascalCase (DaysInYear) for constants.

Classroom application

  1. Design a Student class with properties for name (string), GPA (double), and serial number (long).
  2. Create two students with target-typed new() and object initializers, change one, and print both to show they are separate.
  3. Assign one student reference to another variable, change the name through the second variable, and predict the output of the first.
  4. Set a reference to null, try to print a property, and read the resulting exception.
  5. Add an Address class with street, city, and state properties and give Student an Address property; create a student with nested initializers and print student.Address.City.
  6. Add public static int studentCount; and increment it after each student is created. Add a const double MaxGpa = 4.0;.

Common misconceptions

  • Defining a class does not create an object; new does.
  • A class variable holds a reference to an object, not the object itself.
  • Assigning one reference to another shares the object; it does not copy it.
  • Separate new calls create separate objects that do not affect each other.
  • Using a null reference throws an error at run time.
  • Public fields and properties are not the same; properties control access.
  • Target-typed new() works only when the declared type is known.
  • Static members belong to the class and are reached through the class name.
  • const must be initialized in its declaration and is automatically static; readonly can be set in a constructor.
  • An object is garbage-collected only after every reference to it is gone.

Lesson summary

A class bundles unlike data and the code that works on it into one type that models a real-world concept. OOP rests on abstraction, encapsulation, reuse, inheritance, and polymorphism, and access modifiers protect a class’s internals. A class declares properties (preferred over public fields), methods, and events; new instantiates an object, and the dot operator reaches its members. Object initializers with target-typed new() make creation clear, even for classes that contain other classes. Class variables hold references, so several variables can share one object, and null means no object. Static members belong to the class, instance members to each object, and const and readonly protect values from change. Lesson 12 focuses on methods.