If you want to become a C# programmer, then terms like “C# Class” and “OOP” must be part of your repertoire. You need to understand what they mean, why they exist and when/how to use the concepts they represent.
Object-oriented programming (OOP) is a paradigm (a sort of “style”) of programming that revolves around objects communicating with each other, as opposed to functions operating on data structures. C# is the flagship language of the .NET ecosystem. Despite being a multi-paradigm language, its forte is certainly OOP.
A class is a crucial concept in many object-oriented programming languages, C# being one of them.
In this post, we’ll offer you a guide on OOP concepts using C# as the language for the examples. You’ll learn about C# class, C# objects, their definitions, how they work, best practices and how all of that support the basic pillars of object-oriented programming.
Let’s dig in.
As the name implies, object-oriented programming revolves around the concept of object. So, nothing is more appropriate than to start our guide by defining this concept and giving examples.
In the context of object-oriented programming, an object is a cohesive unit of data and behavior associated with such data.
An object represents a concept or entity in the given business domain the program resides on. The data it carries describe the characteristics or properties of said entity. The object’s behavior represents actions that can be operated on its data, and it’s expressed to external objects via the object’s methods.
How is programming with objects different than the alternatives? Pick procedural programming, for example. In that paradigm, there’s a stark divide between data and behavior. Generally speaking, in this paradigm, you have data structures that carry data around and don’t do anything and functions that operate on said data structures.
Let’s say you have a string and want to split it according to a delimiter—for instance, a comma. In a procedural programming context, you’d probably do something like this:
input = "123,asd,345"
parts = split(input, ",")
The example above isn’t any particular programming language—it’s pseudo-code. As you can see, we called the split function and provided both the complete string and the delimiters as arguments.
In an OOP language, the string itself would be an object containing a split method. Here’s the same code, now in C#:
string input = "123,asd,345";
var parts = input.Split(",");
In simple terms, a class is a blueprint from which you create an object, a process we call instantiation. A class defines both the data and the behavior the objects created from it will have.
Let’s see a basic example of a C# class:
public class Person
{
private readonly string _name;
private readonly int _age;
public Person(string name, int age)
{
_name = name;
_age = age;
}
public string Introduce()
{
return $"My name is {_name} and I'm {_age} years old.";
}
}
In the class above, we define both the data—the name and age fields—and the behavior—the Introduce method—that objects instantiated from the class will have.
And how do we create objects? By instantiating them, using the new keyword. Let’s see a few examples:
Person p = new Person("John Doe", 25);
Person p2 = new Person("Mariah Silva", 32);
Assert.AreEqual("My name is John Doe and I'm 25 years old.", p.Introduce());
Assert.AreEqual("My name is Mariah Silva and I'm 32 years old.", p2.Introduce());
Alternatively, we could’ve omitted the Person type in the examples above when instantiating. That’s because the compiler is smart enough to infer the correct type which to assign to the identifier on the left based on the type on the right. In other words, C# is capable of type inference, and we can do that by replacing the type from the variable declaration with the var keyword:
var p = new Person("John Doe", 25);
var p2 = new Person("Mariah Silva", 32);
Currently, there’s yet another way you can write the two lines above. C# 9 introduced target-typed new expressions, which allows you to write this:
Person p = new ("John Doe", 25);
Person p2 = new ("Mariah Silva", 32);
In the class above, we made use of fields to store the person’s name and age. More specifically, we used private fields, which means other objects can’t access those values. What if we wanted to let other objects know those values? Would it be possible to define the fields as public?
Yes, and that would be terrible because it’d break encapsulation. You’ll read more about encapsulation in a minute, but for now, understand that an object’s internals is no one else’s business. If an object B knows about the internals of object A, then we say that B depends on A.
That means that when A changes, B probably has to change as well. In order to avoid that, we need to make use of encapsulation: the access to an object’s internal has to be protected in such a way that other objects aren’t affected in case a change in implementation occurs.
In C#, the mechanism we use for that is properties. Let’s see a new version of the previous class:
public class Person
{
private readonly string _name;
private readonly int _age;
public Person(string name, int age)
{
_name = name;
_age = age;
}
public string Introduce()
{
return $"My name is {_name} and I'm {_age} years old.";
}
public string Name { get { return _name; } }
public int Age { get { return _age; } }
}
Now, via properties, we define getters that return the value of the private fields in a safe way. As you can see, we simply return the values as they are, so you might wonder why to bother.
Let’s imagine that some changes made it necessary to split the _name field into _firstName and _lastName. If our clients—that is, the objects that use our class—know about the fields, they would have to change. But if they only depend on the property, we can make this instead:
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
private readonly int _age;
public Person(string firstName, string lastName, int age)
{
_firstName = firstName;
_lastName = lastName;
_age = age;
}
public string Introduce()
{
return $"My name is {Name} and I'm {_age} years old.";
}
public string Name { get { return $"{_firstName} {_lastName}"; } }
public int Age { get { return _age; } }
}
The property now hides the fact that, internally, the name is split into two fields. (Of course, the constructor for the class now gets the two parameters, which means clients would still need to change, but you can get the idea.)
Methods are the functions that we define in classes and, generally speaking, operate on the data of the object. They have different visibility options: the main ones are private, internal, and public, but there are several more.
There’s no mystery to creating a C# class. If you’re using the full-fledged Visual Studio, only available on Windows, just right-click a project or folder on Solution Explorer, then go to Add, and finally click on Class. Give you class a name and you’re set.
If you’re using Visual Studio Code, just create a new file with the cs. extension. Within the file, add a namespace definition and a definition for a class. Give it a name that matches the name of the file.
Let’s now leave C#-land for a while and cover the fundamental pillars of OOP itself, which are transferable to other object-oriented languages.
This provides essential features without describing any background details. Abstraction is important because it can hide unnecessary details from reference objects to names. It is also necessary for the construction of programs. Instead of showing how an object is represented or how it works, it focuses on what an object does. Therefore, data abstraction is often used for managing large and complex programs.
Encapsulation binds the member function and data member into a single class. This also allows for abstraction. Within OOP, encapsulation can be achieved by creating classes. Those classes then display public methods and properties. The name encapsulation comes from the fact that this class encapsulates the set of methods, properties, and attributes of its functionalities to other classes.
Polymorphism is the ability of an object to perform in a wide variety of ways. There are two types:
1. Dynamic polymorphism (runtime time). You can obtain this type through executing function overriding.
2. Static polymorphism (compile time). You can achieve static polymorphism through function overloading and operator overloading.
Within OOP, polymorphism can be achieved using many techniques including:
Through inheritance, a class can “inherit” the characteristics of another general class. To illustrate, dogs are believed to be the descendants of wolves. All dogs have four paws and can hunt their prey. This function can be coded into the Wolf class. All of its descendants can use it. Inheritance is also an is-kind-of relationship. For instance, a golden retriever is a kind of animal. Here’s an example:
class BaseClass
{
}
class DerivedClass : BaseClass
{
}
Before wrapping-up, let’s answer some commonly asked questions about classes in C#.
Since a class represents a concept, an entity, something that belongs to the business domain, class names are usually nouns that refer to such concepts. The best practice here is to avoid overly generic names, such as Data, Manager, Service, but prefer names that are specific and clearly descriptive.
If you need to use more than one word, stick to the PascalCasing convention. Examples:
In C#, it is a best practice to have a class name match the file in which it is defined. So, a class called InvoiceRepository should live in a file called InvoiceRepository.cs.
C# makes distinction between two categories of types: value types and reference types. A variable of a value type stores the actual value, while a variable of a reference type stores a reference that points to the actual value, that resides in a different part of memory. Reference types are defined using classes in C#, while value types are defined using structs.
This is something that has profound implications for memory management, garbage collection, copy semantics, and more. A deeper discussion would be out of the scope for this post but, for now, know this: instantiating many objects unnecessarily can lead to needless allocation of memory, causing performance issues.
A great way to prevent such problems is to use an APM platform such as Stackify that can monitor your application searching for performance issues. Give it a try.
In C#, the default accessor for a class is internal. That means that the class is only visible/accessible for types within the same assembly.
Primary constructor is a new feature introduced in C# 12. It’s a new, more concise way to define a constructor for a class. It does so by merging the definition of the class with its constructor. For an example, here’s the Person class we’ve been using throughout this article, rewritten in the primary constructor style:
public class Person(string name, int age)
{
private readonly string _name = name;
private readonly int _age = age;
public string Introduce() => $"My name is {_name} and I'm {_age} years old.";
}
For more information on OOP concepts in C# and how you can best utilize OOP, visit the following resources and tutorials:
In the short and medium terms, we expect C# and .NET to continue being highly influential, so it’s worth learning the fundamentals if you’re not already familiar.
Check out some of our other posts for more basics and advanced concepts in C#, such as throwing C# exceptions, how to handle C# exceptions, catching them and finding application errors, and other tips.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]