This page is a work in progress.You can help improve it. →

Object-Oriented Programming in C#

State + Behavior

So far, we have learned about variables that allow us to store information and methods that will enable us to organize our code. In this lesson, we will introduce the idea of a class that combines these two ideas into a single concept.

When discussing this idea, we are going to shift our terminology just a little bit. We are going to refer to the information we keep track of as state and the code that interacts with that information as behavior.

An example

From our work on methods, we had this Employee Database application. As you can see it can prompt a user for information, save that information, and do some basic calculations.

using System;
namespace EmployeeDatabase
{
class Program
{
static void DisplayGreeting()
{
Console.WriteLine("----------------------------------------");
Console.WriteLine(" Welcome to Our Employee Database ");
Console.WriteLine("----------------------------------------");
Console.WriteLine();
Console.WriteLine();
}
static string PromptForString(string prompt)
{
Console.Write(prompt);
var userInput = Console.ReadLine();
return userInput;
}
static int PromptForInteger(string prompt)
{
Console.Write(prompt);
int userInput;
var isThisGoodInput = Int32.TryParse(Console.ReadLine(), out userInput);
if (isThisGoodInput)
{
return userInput;
}
else
{
Console.WriteLine("Sorry, that isn't a valid input, I'm using 0 as your answer.");
return 0;
}
}
static int ComputeMonthlySalaryFromYearly(int yearlySalary)
{
return yearlySalary / 12;
}
static void Main(string[] args)
{
DisplayGreeting();
var name = PromptForString("What is your name? ");
int department = PromptForInteger("What is your department number? ");
int salary = PromptForInteger("What is your yearly salary (in dollars)? ");
int monthlySalary = ComputeMonthlySalaryFromYearly(salary);
Console.WriteLine($"Hello, {name} you make {monthlySalary} dollars per month.");
}
}
}

What if we wanted to add information about a second employee? Certainly one approach would be to add a second set of variables such as:

var name1 = PromptForString("What is your name? ");
int department1 = PromptForInteger("What is your department number? ");
int salary1 = PromptForInteger("What is your yearly salary (in dollars)? ");
int monthlySalary1 = ComputeMonthlySalaryFromYearly(salary1);
var name2 = PromptForString("What is your name? ");
int department2 = PromptForInteger("What is your department number? ");
int salary2 = PromptForInteger("What is your yearly salary (in dollars)? ");
int monthlySalary2 = ComputeMonthlySalaryFromYearly(salary2);

Or perhaps we would have some arrays, e.g. names[] and departments[].

Each of these has some drawbacks. The first is that the information about a single employee is not correlated. There is nothing about name1 that associates it with department1 instead of department2. The only thing that associates them is that they have similar variable names.

To group related data together we consider them all part of the same state. In this case the state is about an Employee who has individual attributes such as their name, their department, their salary and something we compute named monthlySalary.

A diagram of this might look like:

---------------------
| Employee |
---------------------
| name |
| department |
| salary |
| monthlySalary |
---------------------

If we had a few of these Employee things around, we might also see they could have their own specific values for each of these attributes.

----------------------------- --------------------------------
| Employee | | Employee |
----------------------------| -------------------------------|
| ATTRIBUTE | VALUE | | ATTRIBUTE | VALUE |
----------------------------- --------------------------------
| name | Elon Musk | | name | Grace Hopper |
| department | 42 | | department | 100 |
| salary | 120000 | | salary | 240000 |
| monthlySalary | 10000 | | monthlySalary | 20000 |
----------------------------- --------------------------------

So, if we were able to group these attributes (a process we call encapsulation) then we would be able to keep track of them together!

This is one of the benefits of the idea of a class, the ability to group (encapsulate) related data together. Often these attributes represent real-world attributes of the types of things we are modeling in the computer. Here we are encapsulating the various attributes of an employee we might want to track in an employee database.

Class Syntax

Let's take our idea of an employee in this system and write some syntax to help us.

---------------------
| Employee |
---------------------
| name |
| department |
| salary |
| monthlySalary |
---------------------

C# gives us syntax for defining this structure. We call it a class. You have already seen this syntax in our sample applications so far since dotnet generated a Program class for us by default. We've been using classes but we didn't quite know it yet!

Let us make a class for this Employee structure.

class Employee
{
// public means "this can be seen outside of the class
// |
// | Type
// | |
// | | Name of property
// | | |
// | | |
// | | |
// v v v
public string Name;
public int Department;
public int Salary;
public int MonthlySalary;
// This is a *special* method known as a "constructor"
// The constructor is called when we write a line like: `var bob = new Employee(`
// The arguments to the method should line up to those below
//
// This will become the employee's name
// | This will become the employee's department
// | | This will become the employee's salary
// | | | This will become the employee's monthly salary
// | | | |
// v v v v
public Employee(string newName, int newDepartment, int newSalary, int newMonthlySalary)
{
// In the constructor we should setup the values for any of the properties.
// Here we will *copy* the values given by the arguments to the corresponding property.
Name = newName;
Department = newDepartment;
Salary = newSalary;
MonthlySalary = newMonthlySalary;
}
}

You'll notice a few things right away with this syntax. First is the class keyword followed by the name of the class. We will use CamelCase for our classes' names, so we name this Employee. The rest of the definition of this class is implemented within braces { }.

Inside the definition, we have four similar lines of code. For each line, we are defining an attribute of this class, in C#, we call these properties. The first part of the definition is if this property is public, protected, or private. public properties can be accessed by code outside the class (we'll see what this means in a minute), whereas private can only be seen from other code within the class itself. We'll talk about protected later.

Next, we define the type of this property. Unfortunately, we cannot use the var style definition here since we must tell the code what type of information this property stores.

Finally, we name the property. You'll notice here that we switched to CamelCase as well. Class properties by convention are named with CamelCase. You could certainly use other case approaches, but most C# developers will follow this convention.

Next, in the class, you'll notice what appears to be a method but it is a special type of method. This method has the same name as the class itself and is known as the constructor of the method.

The constructor is used when we eventually write new Employee. Since we indicate that this method requires four arguments we must provide those each time we make a new Employee

In this case, we are requiring the new name, department, salary, and monthly salary of our employee.

Inside the constructor's implementation, we copy these values into the properties for this instance of the object we are creating. Copying the data ensures that we correctly set all the properties of the new object.

We could use this new class to create two new employees:

var graceHopper = new Employee("Grace Hopper", 100, 240000, 20000);
Console.WriteLine(graceHopper.Department); // Will show 100
var elonMusk = new Employee("Elon Musk", 42, 120000, 10000);
Console.WriteLine(elonMusk.Department); // Will show 42

You may notice that this syntax is similar to our usage of new List when working with C#s List. We've been using objects for a while!

Since we often create new objects and want to specify their default properties (in this case, Name, Department, Salary, MonthlySalary), C# provides a shorter syntax.

class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary { get; set; }
}

In this syntax, we do not need a constructor because we have the { get; set; } syntax. For now, we won't explore the specifics of how the { get; set; } works other than to mention it allows us to set (or change) the information in that property as well as get (or retrieve) the information stored within.

We will add this class to our program within the namespace EmployeeDatabase { } but as a sibling of the existing class Program as such:

using System;
namespace EmployeeDatabase
{
class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary { get; set; }
}
class Program
{
static void DisplayGreeting()
{
Console.WriteLine("----------------------------------------");
Console.WriteLine(" Welcome to Our Employee Database ");
Console.WriteLine("----------------------------------------");
Console.WriteLine();
Console.WriteLine();
}
static string PromptForString(string prompt)
{
Console.Write(prompt);
var userInput = Console.ReadLine();
return userInput;
}
static int PromptForInteger(string prompt)
{
Console.Write(prompt);
int userInput;
var isThisGoodInput = Int32.TryParse(Console.ReadLine(), out userInput);
if (isThisGoodInput)
{
return userInput;
}
else
{
Console.WriteLine("Sorry, that isn't a valid input, I'm using 0 as your answer.");
return 0;
}
}
static int computeMonthlySalaryFromYearly(int yearlySalary)
{
return yearlySalary / 12;
}
static void Main(string[] args)
{
DisplayGreeting();
var name = PromptForString("What is your name? ");
int department = PromptForInteger("What is your department number? ");
int salary = PromptForInteger("What is your yearly salary (in dollars)? ");
int monthlySalary = computeMonthlySalaryFromYearly(salary);
Console.WriteLine($"Hello, {name} you make {monthlySalary} dollars per month.");
}
}
}

Allowing properties to be null

As is, the properties of our object cannot be null. That is, they must have some value, even if for a string this is an empty string "" and for a number it might be 0.

However, by adding a ? to the end of the data type, we can indicate that the property is allowed to be null.

This is also true of variables we declare outside of a class as well.

For more information and examples, see this article for details.

Creating and using classes: Objects

Now that we have this class definition let us use it inside our application.

We can think of the class we created as a template used to create new instances of itself. That is, it can create new variables whose type is an Employee and it has the properties we defined. We call these instances objects.

Each object knows what its type is and what state it can store. In our case, each Employee object will have a Name, a Department, a Salary and a MonthlySalary. And each Employee object will keep its own copy of that information. Much like in this diagram from before

+---------------------------+ +------------------------------+
| Employee Object | | Employee Object |
+---------------+-----------+ +---------------+--------------+
| name | Elon Musk | | name | Grace Hopper |
| department | 42 | | department | 100 |
| salary | 120000 | | salary | 240000 |
| monthlySalary | 10000 | | monthlySalary | 20000 |
+---------------+-----------+ +---------------+--------------+

If we were going to create these objects, we would use the new keyword.

var firstEmployee = new Employee();
var secondEmployee = new Employee();

When we create these new variables, the various attributes of the object will all be empty. For strings, this means the values will be an empty string "" and for integers, the values will be 0.

Console.WriteLine(firstEmployee.Department); // This would output 0

We can now access the properties of these new objects using the . notation. For instance, you will see above that we write firstEmployee.Department. The text to the left of the . says which variable we are working with and the text to the right of the . says which property we want from that object.

Now that we know how to say which object and which property we want, we can use that syntax on the left side of an assignment to change that object's property.

var firstEmployee = new Employee();
var secondEmployee = new Employee();
firstEmployee.Name = "Elon Musk";
secondEmployee.Name = "Grace Hopper";

If we were to look at our objects in the computer's memory now they would look like this:

+-----------------------------+ +--------------------------------+
| firstEmployee | | secondEmployee |
+---------------+-------------+ +---------------+----------------+
| Name | "Elon Musk" | | Name | "Grace Hopper" |
| Department | 0 | | Department | 0 |
| Salary | 0 | | Salary | 0 |
| MonthlySalary | 0 | | MonthlySalary | 0 |
+---------------+-------------+ +---------------+----------------=

Let us fill in the rest of the properties.

var firstEmployee = new Employee();
var secondEmployee = new Employee();
firstEmployee.Name = "Elon Musk";
firstEmployee.Department = 42;
firstEmployee.Salary = 120000;
firstEmployee.MonthlySalary = 10000;
secondEmployee.Name = "Grace Hopper";
secondEmployee.Department = 100;
secondEmployee.Salary = 240000;
secondEmployee.MonthlySalary = 20000;

These objects would look like

+-----------------------------+ +--------------------------------+
| firstEmployee | | secondEmployee |
+---------------+-------------+ +---------------+----------------+
| Name | "Elon Musk" | | Name | "Grace Hopper" |
| Department | 42 | | Department | 100 |
| Salary | 120000 | | Salary | 240000 |
| MonthlySalary | 10000 | | MonthlySalary | 20000 |
+---------------+-------------+ +---------------+----------------+

When specifying the initial values of a new object, C# gives us a convenient syntax to make our jobs easier:

var firstEmployee = new Employee {
Name = "Elon Musk",
Department = 42,
Salary = 120000,
MonthlySalary = 10000
};
var secondEmployee = new Employee {
Name = "Grace Hopper",
Department = 100,
Salary = 240000,
MonthlySalary = 20000
};

You will see that we replaced the () with a pair of { } and inside we have a syntax of NameOfPropertyHere = ValueHere, and each property is separated by a comma.

This works well if we know the values ahead of time. Let's change our Employee Database code to use our new class.

using System;
namespace EmployeeDatabase
{
class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary { get; set; }
}
class Program
{
static void DisplayGreeting()
{
Console.WriteLine("----------------------------------------");
Console.WriteLine(" Welcome to Our Employee Database ");
Console.WriteLine("----------------------------------------");
Console.WriteLine();
Console.WriteLine();
}
static string PromptForString(string prompt)
{
Console.Write(prompt);
var userInput = Console.ReadLine();
return userInput;
}
static int PromptForInteger(string prompt)
{
Console.Write(prompt);
int userInput;
var isThisGoodInput = Int32.TryParse(Console.ReadLine(), out userInput);
if (isThisGoodInput)
{
return userInput;
}
else
{
Console.WriteLine("Sorry, that isn't a valid input, I'm using 0 as your answer.");
return 0;
}
}
static int computeMonthlySalaryFromYearly(int yearlySalary)
{
return yearlySalary / 12;
}
static void Main(string[] args)
{
var employee = new Employee();
DisplayGreeting();
employee.Name = PromptForString("What is your name? ");
employee.Department = PromptForInteger("What is your department number? ");
employee.Salary = PromptForInteger("What is your yearly salary (in dollars)? ");
employee.MonthlySalary = computeMonthlySalaryFromYearly(employee.Salary);
Console.WriteLine($"Hello, {employee.Name} you make {employee.MonthlySalary} dollars per month.");
}
}
}

We have replaced our many local variables with creating one new object of type Employee and then used our object property accessing . syntax to both set and get those properties when needed.

So far, we have seen how classes:

  • Store data, which we call state, in attributes we call properties.
  • Classes are the template that describes what data is used
  • Objects are instances of a class.
  • Classes are like cookie cutters, where objects are like the cookies

Next, we will introduce some behavior to our classes. If you'll note in the code above, we have to compute the monthly salary ourselves and store it. What if the Employee object could just do that work for us!

Behavior with methods

In our Employee Database we have a method ComputeMonthlySalaryFromYearly that accesses a property of our object and does some math to compute a monthly salary. We also have a property named MonthlySalary. However, any time we change the Salary we would like to be able to ask for the MonthlySalary to get this computed value.

Adding methods that are specific to a class comes in handy. Rather than create a property named MonthlySalary let us make a method instead.

class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary()
{
// Code will go here
}
}

The definition of this method should seem familiar to our previous encounter with methods. However, you will notice we no longer have the parameters to the method as we do for ComputeMonthySalaryFromYearly. When defining a class's methods, we have access to all the other attributes of the specific object!

That is, any method on a class, when called for a specific object, can ask "Who am I, and what properties (state) do I have?"

We can add the code for MonthlySalary by accessing the existing Salary property!

class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary()
{
return Salary / 12;
}
}

If we created the following objects they would look like this:

var firstEmployee = new Employee {
Name = "Elon Musk",
Department = 42,
Salary = 120000,
};
var secondEmployee = new Employee {
Name = "Grace Hopper",
Department = 100,
Salary = 240000,
}
+-----------------------------+ +--------------------------------+
| firstEmployee | | secondEmployee |
+---------------+-------------+ +---------------+----------------+
| Name | "Elon Musk" | | Name | "Grace Hopper" |
| Department | 42 | | Department | 100 |
| Salary | 120000 | | Salary | 240000 |
| MonthlySalary | METHOD | | MonthlySalary | METHOD |
+---------------+-------------+ +---------------+----------------+

So now each instance of this class, our objects, have individual values for their properties, but have access to a MonthlySalary method. If we were to call firstEmployee.MonthlySalary() we would run the code return Salary / 12 but C# knows we are the variable firstEmployee so when it looks up Salary it sees the value 120000 and we get back 10000. However, when we call secondEmployee.MonthlySalary(), C# knows we are the variable secondEmployee so when it looks up Salary it sees the value 240000 and we get back 20000.

By allowing the objects to share the same behavior and names of properties, but have different VALUES for those properties we get a lot of code reusability.

We now have our classes, and objects, enabled with the ability to have both state (the list of which properties are defined, and what the values are for each object we create) and behavior the methods defined for the class. With both state and behavior we have the ability to represent real world ideas. In this case our modeling of an Employee in our Employee Database:

using System;
namespace EmployeeDatabase
{
class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public int Salary { get; set; }
public int MonthlySalary()
{
return Salary / 12;
}
}
class Program
{
static void DisplayGreeting()
{
Console.WriteLine("----------------------------------------");
Console.WriteLine(" Welcome to Our Employee Database ");
Console.WriteLine("----------------------------------------");
Console.WriteLine();
Console.WriteLine();
}
static string PromptForString(string prompt)
{
Console.Write(prompt);
var userInput = Console.ReadLine();
return userInput;
}
static int PromptForInteger(string prompt)
{
Console.Write(prompt);
int userInput;
var isThisGoodInput = Int32.TryParse(Console.ReadLine(), out userInput);
if (isThisGoodInput)
{
return userInput;
}
else
{
Console.WriteLine("Sorry, that isn't a valid input, I'm using 0 as your answer.");
return 0;
}
}
static int ComputeMonthlySalaryFromYearly(int yearlySalary)
{
return yearlySalary / 12;
}
static void Main(string[] args)
{
var employee = new Employee();
DisplayGreeting();
employee.Name = PromptForString("What is your name? ");
employee.Department = PromptForInteger("What is your department number? ");
employee.Salary = PromptForInteger("What is your yearly salary (in dollars)? ");
Console.WriteLine($"Hello, {employee.Name} you make {employee.MonthlySalary()} dollars per month.");
}
}
}

Default values

We can also define a property to have a default value. If we wanted our Employee to track the date their information was created, we could add a CreatedAt property. However, the developer would have to remember to give it a valid value each time we used new Employee

We can address this by defining a default value, in this case the value of DateTime.Now which will be the current time when the object is created.

We could default other properties such as Salary, or Department as long as the default value we give is compatible with the data type.

class Employee
{
public string Name { get; set; }
public int Department { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
public int Salary { get; set; }
public int MonthlySalary()
{
return Salary / 12;
}
}

Constructors

Another way to initialize an instance of an object is using a special method called a constructor. This method has the same name as the class itself and is used any time we call for a new instance of the object.

For instance, to default the CreatedAt property we could have also used this code:

class Employee
{
public Employee()
{
this.CreatedAt = DateTime.Now;
}
public string Name { get; set; }
public int Department { get; set; }
public DateTime CreatedAt { get; set; }
public int Salary { get; set; }
public int MonthlySalary()
{
return Salary / 12;
}
}

Constructors are more flexible than just assigning a default value, we can have any necessary initialization code required.

Inheritance

Often in modeling the real-world we encounter different types of data that are related. Sometimes these are in an is a or is a kind of relationship.

For instance, in our system, perhaps we have active employees and retired employees. In this case, perhaps we want to return a 0 for the MonthlySalary of any retired employees.

We could declare a new class named RetiredEmployee as such:

class RetiredEmployee : Employee
{
public int MonthlySalary()
{
return 0;
}
}

Now if we defined a new employee as such:

var thirdEmployee = new RetiredEmployee {
Name = "Bill Gates",
Department = 100,
Salary = 120,
};
Console.WriteLine(thirdEmployee.MonthlySalary());

This would output 0. Notice we did not have to redeclare Name, or Department, or Salary as those are inherited from the base class of Employee.

Inheriting allows us to both add new state and behavior and override behavior from the base class.

Every class in the system will have a parent. If we keep moving up the class parent lineage, we eventually reach Object, which is the top-most parent of all classes.

We should also note that while inheritance is a powerful technique, it is used less often in favor of other techniques such as extensions and mixins and dependency injection. We will discuss some of these in other lessons.

© 2017 - 2021; Built with ♥ in St. Petersburg, Florida.