Classes
Explore the fundamentals of classes in C# including class definition, members like fields, properties, and methods, and how to create and use objects. Understand constructors, object initializers, and best practices to organize and maintain code effectively in modern C#.
We'll cover the following...
C# is an object-oriented language, which means that C# programs are a set of interconnected objects.
A class is an object’s definition, and an object is an instance of a class. We can understand this through an analogy. Consider a blueprint for a car. The class is the blueprint, and the actual object is an instance of the Car class. Another example of a class is an abstract concept of a rectangle. Concrete examples that we draw are instances of this class.
Note: In older versions of C#, the default template explicitly included a Program class with a Main() method. In modern .NET, we use top-level statements, which allow us to write executable code directly in Program.cs without the boilerplate class and method wrapper. The compiler handles creating the entry point for us.
Creating classes
A class represents a user-defined type. A class is created using the class keyword:
class Car{}
A class can be defined inside or outside a namespace, or inside another class. Typically, classes are placed in separate files.
Note: Following C# class structure best practices dictates creating a new file for each class and placing it inside a relevant namespace. This keeps your project organized and maintainable as it grows.
The following example demonstrates how to define a class using a file-scoped namespace, which is the standard in modern C#.
Line 1: Declares the namespace
Classesfor the entire file.Line 5: Defines the
Carclass using theclasskeyword.
Line 1: Imports the
Classesnamespace so we can use theCartype. This is required becauseCaris inside theClassesnamespace, but our top-level program is in the global namespace.Line 3: Declares a variable
familyCarof typeCar. Note that this variable is not yet initialized.
Common mistake: If you forget to add using Classes;, the code will not compile. The compiler will show an error (CS0246) stating: The type or namespace name 'Car' could not be found. This happens because the Program.cs file (in the global namespace) cannot see inside the Classes namespace unless we explicitly import it.
The functionality of a class is provided by its members:
Fields are class variables that can be used to represent the state of the class.
Properties are wrappers around the fields that can control access and determine what kind of values can be assigned to those fields.
Methods perform some actions.
Events can notify class users that the state of the class has changed.
We will now add data members to the Car class.
Line 8: Declares a public field
modeland initializes it to an empty string to ensure safety.Lines 9–10: Declares public fields
yearandprice.Lines 13–16: Defines a method
GetFullInformationthat returns a formatted string using the class's fields.
The declaration in Program.cs remains the same. We still only have a declaration; we have not created an object yet.
Here we have three fields: model, year, and price. A field is a variable defined at the class level. Because it’s a variable, we can choose to initialize it when we declare it:
public int year = 2021;
For fields and other members to be accessible outside of the class, we must denote that with the public modifier.
Because a class is essentially a new user-defined type, we can create variables of this type:
Car familyCar;
Currently, our familyCar variable doesn’t point to a concrete object.
Constructors
Apart from the methods that we’ve been creating, there are also special methods called constructors. A constructor is a method that’s called when we create an instance of a class. In other words, a constructor is used during object initialization.
The familyCar variable is not yet associated with an object. We create an instance of the Car class using the new keyword:
Car familyCar = new Car();
The new keyword allocates the required memory for the object and calls the constructor of the class. As a result, we have an area in computer memory to store the data for the Car object, and familyCar will point to that object.
We can instantiate Car without defining a constructor because C# generates a default one automatically. This default constructor doesn’t accept any parameters and does nothing other than create an empty instance of the class.
After creating an instance of the class, we can manipulate the members of the class through a variable that references an object of that class:
Line 5: Creates a new instance of
Carusing thenewkeyword and the default constructor.Lines 8–10: Assigns values directly to the object's public fields.
Line 13: Calls the
GetFullInformationmethod on the object and prints the result to the console.
Creating custom constructors
If the default constructor doesn’t meet our requirements (for instance, because it doesn’t accept any parameters), we can create our own constructors.
public Car(string carModel){model = carModel;}
When we create a custom constructor, the default one isn’t created. Therefore, if we still need a default constructor, we must create it explicitly:
public Car(){}
The following code playground demonstrates an example of a custom constructor. The comments explain what each part of the code does.
Line 10: Defines a constructor that accepts a
modelargument.Line 19: Explicitly re-defines the parameterless (default) constructor so it remains available.
Line 26: Uses
: this(model)to call the constructor defined on Line 11, avoiding code duplication.Line 31: Uses
: this(model, year)to call the constructor defined on Line 27.
Line 7: Instantiates
Carusing the constructor that accepts three arguments (model,year,price).
Object initializers
Another way to assign values to object fields and properties is to use an initializer:
Car familyCar = new Car { model = "Lexus ES350", year = 2021, price = 60000 };
For this to work, a default empty constructor must be available. Otherwise, we have to call an available constructor and then use an initializer. This is redundant. The initializer runs after object creation, so it may overwrite values set by the constructor.