In C#, classes are fundamental building blocks between which all the rest of the language is built, adhering to the principles of object-oriented programming (OOP). They are actually blueprints that developers use to model real world entities and relationships inside their code. Classes not only promote modularity, reusability and maintainability in software development by encapsulating data (fields) and behavior (methods) but they essentially enforce object oriented thinking of your application.
Designed to be a comprehensive guide, this walks through the concept of classes in C# starting from the basic class declaration to their advanced features such as inheritance, polymorphism and generics. The chapters describe each section in detail with practical examples and explanations to clear up your understanding and contingency to use classes appropriately.
What’s a Class?
A class in C# is a user defined type which is used as a blue print to create objects. Encapsulation encapsulates the object’s (fields) data together with methods to manipulate this data, clearly presenting the idea of encapsulation in OOP. Classes let you create complex data structures that can model the shapes of real world things so they can organize your code into a more sophisticated system of functionality.
Objects are created classes define which properties and behaviors they will have. Assuming a Car class it has properties such as Color, Make and Model and Accelerate(), Brake() methods. When you define these elements as a class, you know that all Car objects are all going to behave the same — as you structured them.
In addition to that, classes support other OOP principles such as inheritance and polymorphism to create hierarchical relationships and dynamic nature of our application. But to maximize the power of C#, and to create the robust, scalable software you want, you must understand classes.
Declaring a Class
In C# declaring a class involves specifying the class’s name and defining its members with curly braces { }. The general syntax is:
[access modifier] class ClassName
{
// Class members go here
}
Access modifier determines how the class visibility to the other parts of the program. Access modifiers that we keep in common include public, private, protected internal, internal, and protected.
public class Person
{
// Fields
private string name;
private int age;
// Methods
public void SetName(string name)
{
this.name = name;
}
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
In this example:
- Class Declaration: We define a public class Person.
- Fields: name is a private field and hence access it within the Person class only.
- Methods: There are two public methods SetName and DisplayInfo which can be called from outside of the class.
It is a structure which encapsulates data (name and age) and has methods which confine interacting with that data and follow the principles of encapsulation and data hiding.
Creating Objects
Making an object (instance) from a class involves using the new keyword calling the class’s constructor (same name as class). The syntax is:
ClassName objectName = new ClassName();
This statement allocates memory for a new object of a specified class and insutes it.
public class Program
{
public static void Main(string[] args)
{
// Creating an object of the Person class
Person person = new Person();
// Setting the name using the method
person.SetName("Alice");
// Displaying the person's information
person.DisplayInfo();
}
}
In this example:
- Object Creation: Person person = new Person();
- Method Invocation: To set the name field we invoke person.SetName(“Alice”);.
- Output: person.DisplayInfo(); will display the information, resulting Name: Alice, Age: 0.
Constructors and Initialization
Special methods of construction are used to construct objects when they are created. They have no return type, just the same name as the class. Overloading constructors allows us to accept different number of parameters in creating instances.
Constructor Declaration
If class has a constructor then it is declared inside the class itself and then invoked automatically when an object is instantiated. It is used to set defaults and other initialization or setup.
public class Person
{
private string name;
private int age;
// Parameterless constructor
public Person()
{
name = "Unknown";
age = 0;
Console.WriteLine("Person object created with default values.");
}
}
In this example:
- Constructor: A parameterless constructor public Person() is a parameterized constructor.
- Initialization: Names it as “Unknown” and 0 in age.
- Message: When an object is created, it will print a message.
Constructors also give you a way to ensure that an object begins its life in a valid state, to reduce the possibility of error from uninitialized fields.
Default Constructors
By default, C# provides a parameterless constructor if the class has no explicit constructor defined – in which case the fields will be set to their default values (i.e. null in custom reference type or 0 if numeric value type), if required, by the expression body, as the instance.
public class Person
{
private string name;
private int age;
// No constructors defined
}
In this case:
- Default Initialization: name is null, and age is 0.
- Object Creation: The implicit default constructor is still valid for Person person = new Person();.
For simple classes, default constructors are acceptable, but explicit constructors give a better grip on injecting objects into an object.
Static Constructors
A static constructor is used to initialize a static member (or members) of a class. They are called automatically so long as the first instance is not created, or before any static member is referenced. Static constructors have access modifiers or parameters.
public class ConfigurationManager
{
public static string ConfigValue;
// Static constructor
static ConfigurationManager()
{
ConfigValue = "Default Configuration";
Console.WriteLine("Static constructor called.");
}
}
In this example:
- Static Field: It’s a static field ConfigValue which is shared across all instances.
- Static Constructor: ConfigValue is initialized by static ConfigurationManager().
- Usage: The static constructor fires when ConfigurationManager.ConfigValue is called.
We can use static constructors to initialize any static data or to do things once.
Private Constructors
It creates a private constructor and restricts the instantiation of the class by outside and no class can instantiate it. This is usually used in the patterns of singleton or just these classes which have only statics.
public class Singleton
{
private static Singleton instance = null;
// Private constructor
private Singleton()
{
Console.WriteLine("Singleton instance created.");
}
public static Singleton GetInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
In this example:
- Private Constructor: External instantiation is prevented.
- Singleton Pattern: GetInstance() gives us the controlled access to the one instance.
- Lazy Initialization: It is created only when needed.
Private constructors are necessary to create particular patterns around control over object creation.
Constructors with Parameters
This means constructors can take parameters, and provide default values for initializing their object fields. This allows for more flexibility, and better guarantees that objects have meaningful data to begin with.
public class Person
{
private string name;
private int age;
// Parameterized constructor
public Person(string name, int age)
{
this.name = name;
this.age = age;
Console.WriteLine($"Person created: {name}, Age: {age}");
}
}
usage:
Person person = new Person("Bob", 25);
In this example:
- Parameters: name and age are passed in the constructor.
- Initialization: The values are provided into fields set.
- Message: Ensures specified data object is created.
Parameterized constructor improves the readablity of the code and ensures that required data is being used in object creation time.
Constructor is calling Constructor
Constructor chaining is the ability to call a constructor, in the same class, with the this keyword. Code reuse and initialization logic is simplified by this.
public class Person
{
private string name;
private int age;
// Default constructor
public Person() : this("Unknown", 0)
{
Console.WriteLine("Default constructor called.");
}
// Parameterized constructor
public Person(string name, int age)
{
this.name = name;
this.age = age;
Console.WriteLine($"Person created: {name}, Age: {age}");
}
}
In this example:
- Chaining: The parameterized one calls the default one.
- this Keyword: Invoked to invoke another constructor of the same class.
- Output: The two constructors print messages to show the call sequence.
Constructor chaining helps to reduce code duplication and put it all in a separate piece.
Copy Constructor, How to do it?
When creating a copy constructor it will create a new object from the existing object using its fields. When you need a duplicate of an object in the same state this is useful.
public class Person
{
private string name;
private int age;
// Existing constructor
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
// Copy constructor
public Person(Person other)
{
this.name = other.name;
this.age = other.age;
Console.WriteLine("Copy constructor called.");
}
}
usage:
Person original = new Person("Charlie", 28);
Person copy = new Person(original);
In this example:
- Copy Constructor: Allows for an object of that class.
- Field Copying: It takes copies the name and age from existing object.
- Message: It means that the copy constructor was just called.
Given mutable objects, copy constructors become a must for creating new objects that are exactly independent copies of their existing counterparts.
Finalizers (Destructors)
They are called finalizers or destructors, as they run when an object is being garbage collected. They give us a way to release unmanaged resources which the garbage collector doesn’t.
As a finalizer in C#, our finalizer is declared by putting a tilde ~ in front of the class name in the first place.
public class ResourceHolder
{
// Constructor
public ResourceHolder()
{
// Allocate unmanaged resources
Console.WriteLine("Resources allocated.");
}
// Finalizer
~ResourceHolder()
{
// Release unmanaged resources
Console.WriteLine("Resources released.");
}
}
In this example:
- Resource Allocation: In the constructor, we are simulating the allocation of the resources.
- Finalizer: They will release the resources in the destructor.
- Garbage Collection: When the object is no longer used finalizer is called.
Important considerations:
- Unpredictable Timing: We can’t guarantee when the finalizer will run.
- Performance Impact: Garbage collection performance suffers from finalizers.
- Best Practices: It’s highly recommended and strongly advisable to implement IDisposable interface and dispose pattern for the unmanaged resources.
Finalizers are generally used only where necessary and should be avoided where possible, adding complexity and potentially performance expense.
Class Inheritance
That’s a great core OOP concept known as inheritance where a derived class inherits members (fields, methods, properties) from the base class. It encourages use of code reuse and defines an ‘is a’ relationship between classes.
// Base class
public class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
}
// Derived class
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}
In this example:
- Inheritance Declaration: This just means that Dog inherits from Animal.
- Inherited Members: Eat() is accessible by Dog.
- Additional Members: A new method Bark() is introduced by Dog.
usage:
Dog dog = new Dog();
dog.Eat(); // Inherited method
dog.Bark(); // Own method
Key points:
- Code Reuse: Dog uses the Eat() method of Animal, in which he does not need to write the duplication .
- Hierarchical Relationships: Relationships that are natural inherit from the OnUp syntax and modeling (like dog is an animal).
- Polymorphism: If we combine Inheritance with virtual methods and overridding we can do polymorphic behavior.
The general idea is to use inheritance judiciously, using derived class when derived class is a specialization (a specialized form) of base class.
Static Classes
A static class is a class that cannot be instantiated and which has only static members. They are good to be used to create utility or helper classes that just does some work but do not keep state.
public static class MathUtilities
{
public static int Square(int number)
{
return number * number;
}
public static double CircleArea(double radius)
{
return Math.PI * radius * radius;
}
}
In this example:
- Static Declaration: This indicates that the class is static — public static — and that it can be accessed from no matter where.
- Static Methods: They’re all static and can be called without instantiating.
- Utility Functions: It is provided with mathematical calculations.
usage:
int squared = MathUtilities.Square(5);
double area = MathUtilities.CircleArea(3.0);
Key characteristics:
- No Instances: You cannot instantiate a static class (e.g. new MathUtilities()); something such as this is invalid.
- Access: We access the members from directly the class name.
- Use Cases: Group utility methods related to one another.
Static classes allow us to group our code and avoid unnecesssary instantiation if we only need the static members on our class.
Partial Classes
Partial classes allow a class to be split over multiple files. It also comes handy in a larger project or when working with auto generated code similar to windows forms or wfp designer files.
public partial class Person
{
public void Walk()
{
Console.WriteLine("Person is walking.");
}
}
public partial class Person
{
public void Talk()
{
Console.WriteLine("Person is talking.");
}
}
In this example:
- Partial Declaration: The first file declares a public partial class Person and the second one does the same.
- Combined Class: All together the parts are combined at compile time into an object called Person containing both Walk() and Talk() methods.
- Organization: Files can be organized logically.
Restrictions
Partial classes must adhere to certain rules:
- Same Accessibility: All parts have to use the same access modifier (public, internal, etc.).
- Same Namespace: They all have to be in the same namespace.
- Unique Members: The signatures of members must not conflict with each other.
Incorrect usage:
// File 1
public partial class Person
{
public void Display()
{
Console.WriteLine("Display from File 1.");
}
}
// File 2
public partial class Person
{
public void Display()
{
Console.WriteLine("Display from File 2.");
}
}
Because of duplicate method definitions we will get a compilation error.
Partial Members
Partial methods are methods declared in partial classes or partial structs, which lets you define a method in one part and define the implementation in another. In the case that the method isn’t implemented, then the compiler will omit the declaration and will also omit any calls to the method.
// File 1
public partial class Person
{
partial void OnNameChanged(string oldName, string newName);
public void ChangeName(string newName)
{
string oldName = name;
name = newName;
OnNameChanged(oldName, newName);
}
private string name;
}
// File 2
public partial class Person
{
partial void OnNameChanged(string oldName, string newName)
{
Console.WriteLine($"Name changed from {oldName} to {newName}");
}
}
In this example:
- Partial Method Declaration: One part with partial void declared.
- Implementation: Provided in another part.
- Optional Implementation: Then calls to OnNameChanged are removed by the compiler if the implementation is ignored.
Partial methods are useful to extend auto generated code without modifying the auto generated files.
Abstract Classes and Class Members
Class that cannot be instantiated and may consists of abstract methods having no implementation is called abstract class. An abstract class is meant to be a base class with the definition of a base class common to many derived classes.
public abstract class Shape
{
public abstract double GetArea();
public void DisplayArea()
{
Console.WriteLine($"The area is {GetArea()}");
}
}
public class Rectangle : Shape
{
private double width;
private double height;
public Rectangle(double width, double height)
{
this.width = width;
this.height = height;
}
public override double GetArea()
{
return width * height;
}
}
In this example:
- Abstract Class: From here, we can no longer instantiate shape directly.
- Abstract Method: The function GetArea() declared but not implemented.
- Concrete Class: Shape is inherited by Rectangle and Rectangle provides the implementation of GetArea().
usage:
Shape rect = new Rectangle(5.0, 4.0);
rect.DisplayArea(); // Outputs: The area is 20
Key points:
- Polymorphism: This allows for treating different shapes uniformly while executing their specified implementations.
- Design: Abstract classes are a way to enforce contract to a subset of methods.
When you want to define a common functionality that you wish to enforce with a common set of methods on different derived classes, abstract classes come in handy.
Sealed Classes and Class Members
A sealed class is a class that can not be inherited. Sometimes it is useful to seal a class; you want to prevent further derivation, not for security reasons, but because you don’t want derivation to occur.
public sealed class Logger
{
public void Log(string message)
{
Console.WriteLine($"Log entry: {message}");
}
}
Attempting to inherit from Logger
will result in a compilation error:
public class FileLogger : Logger // Error: Cannot inherit from sealed class 'Logger'
{
// Additional code
}
Sealing class members:
- Sealed Methods: An override can be prevented in derived classes.
- Usage: Only used when it is used to override a virtual method.
public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("Base display.");
}
}
public class DerivedClass : BaseClass
{
public sealed override void Display()
{
Console.WriteLine("Derived display.");
}
}
public class FurtherDerivedClass : DerivedClass
{
public override void Display() // Error: 'FurtherDerivedClass.Display()' cannot override sealed member 'DerivedClass.Display()'
{
Console.WriteLine("Further derived display.");
}
}
Key points:
- Control: Sealing classes or methods control inheritance as well as method overriding.
- Performance: Becoming optimistic, the compiler can optimize sealed methods.
Use of sealing should be done carefully in mind of the future extensibility need.
Access Modifiers
Classes and members of classes are access modifiable. If they are to be encapsulated ( protected ) then we need to understand them.
- public: It can be accessed from any other code.
- private: Available only within the containing class or struct.
- protected: Available in the containing class and the derived classes.
- internal: Usable within the same assembly.
- protected internal: Contained within the same assembly or from derived classes.
- private protected (C# 7.2 and later): The containing class or derived classes in the same assembly.
Access modifiers let you expose what you need, not what you want. By understanding access modifiers we know how to expose only what is necessary, and that keeps the code more encapsulated which also reduces the chance of the code acting erratically.
Properties
They also offer a way to read, write or compute the value of private fields. They wrap data and allow a public interface to be used to access the fields.
Declaring Properties
We can declare properties with a get and a set accessor. It can be auto implemented or use a backing field for more control.
public class Person
{
// Auto-implemented property
public string Name { get; set; }
// Property with backing field
private int age;
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
else
Console.WriteLine("Age cannot be negative.");
}
}
// Read-only property
public string Info
{
get { return $"Name: {Name}, Age: {Age}"; }
}
}
In this example:
- Auto-Implemented Property: A get and a set have default accessors for name.
- Custom Property: The set accessor includes validation of age.
- Read-Only Property: Computed data without a set accessor is provided by Info.
usage:
Person person = new Person();
person.Name = "Diana";
person.Age = 30;
Console.WriteLine(person.Info); // Outputs: Name: Diana, Age: 30
Encapsulation can be enhanced by properties, which give you controlled access to a private field.
Structs
Value types, structs are mostly used to encapsulate small number of related variables in a single structure. For small data structures, they can save in the stack performance.
Declaring a Struct
You declare structs with struct at the top, and you can declare constructors, methods, fields, properties and indexers inside struct.
public struct Point
{
// Fields
public int X { get; set; }
public int Y { get; set; }
// Constructor
public Point(int x, int y)
{
X = x;
Y = y;
}
// Method
public void Display()
{
Console.WriteLine($"Point coordinates: ({X}, {Y})");
}
}
usage:
Point p1 = new Point(10, 20);
p1.Display(); // Outputs: Point coordinates: (10, 20)
Structs vs. Classes
- Value Type vs. Reference Type: Classes are referred types and structs are value types.
- Memory Allocation: The stack allocates structs, heap classes.
- Copy Behavior: One struct would copy the value in it and one class would copy the reference.
- Inheritance: Structs can implement interfaces but structs cannot inherit from other structs or from classes (with the exception of System.ValueType).
When to Use Structs:
- For a small, lightweight thing.
- This is when you want value type semantics.
- When you don’t want inheritance.
K inds of structing help in making informed decisions as to whether or not to use structs or classes.
Method Overloading in Classes
We allow method overloading in a class which has the same name but different parameters (type, number, or order). Methods can take in different types of input, yet through the same logical operation.
public class Calculator
{
// Overloaded methods
public int Add(int a, int b)
{
Console.WriteLine("Adding integers.");
return a + b;
}
public double Add(double a, double b)
{
Console.WriteLine("Adding doubles.");
return a + b;
}
public int Add(int a, int b, int c)
{
Console.WriteLine("Adding three integers.");
return a + b + c;
}
}
usage:
Calculator calc = new Calculator();
int sum1 = calc.Add(2, 3); // Outputs: Adding integers.
double sum2 = calc.Add(2.5, 3.5); // Outputs: Adding doubles.
int sum3 = calc.Add(1, 2, 3); // Outputs: Adding three integers.
Key points:
- Signature Differentiation: There must be a difference between the parameter types, number or order of methods.
- Return Type: Methods cannot be overloaded only based on the return type.
Method overloading makes code readable and is flexible for using method in place of each other.
Virtual Methods in Classes
In virtual methods, the derived classes have a right to override routines generated from base classes. This makes it polymorphic because it is against the runtime type of the object which method will be called.
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal sound.");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow.");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof.");
}
}
usage:
Animal myAnimal = new Animal();
Animal myCat = new Cat();
Animal myDog = new Dog();
myAnimal.Speak(); // Outputs: Animal sound.
myCat.Speak(); // Outputs: Meow.
myDog.Speak(); // Outputs: Woof.
Key concepts:
- Virtual Keyword: It means that a method can be overridden.
- Override Keyword: To override the base class method, used in derived class.
- Polymorphism: For example, when we use the same method call (Speak()), the result will be different based on the object type.
Dynamic behavior and an extensible system needs to be implemented using virtual methods.
Generic Class
Generics enable class, methods, and interface definitions with type placeholder specified on what data they will store or what they will use. This is type safe without knowing what data type in advance.
public class Storage<T>
{
private T data;
public void SetData(T value)
{
data = value;
Console.WriteLine($"Data stored: {data}");
}
public T GetData()
{
return data;
}
}
usage:
Storage<int> intStorage = new Storage<int>();
intStorage.SetData(42);
Storage<string> stringStorage = new Storage<string>();
stringStorage.SetData("Hello Generics");
In this example:
- Generic Type Parameter: Storage can store any data type<T>.
- Type Safety: It forces you to only use the specific type.
- Reusability: It has multiple data types for one class.
Using Generics in your code reduces duplication and is more type safe making your code better maintained.
this Keyword and Extension Methods
The this Keyword
As the word says, this keyword is talking about the class object of the instance that is running in the method or in the constructor.
public class Person
{
private string name;
public void SetName(string name)
{
// Distinguish between the field and the parameter
this.name = name;
}
public void Display()
{
Console.WriteLine($"Name: {this.name}");
}
}
In this example:
- Field Assignment: The parameter (name) refers to the field of class name, so this.name.
- Clarity: This helps us understand that we are talking about the instance member.
Extension Methods
Extension methods enable you to extend types without changing their definition. You do this by creating extension methods on the fly. static classes and static methods which are declared using static methods in static classes are declared with the this keyword before the first parameter.
public static class StringExtensions
{
public static bool IsCapitalized(this string s)
{
if (string.IsNullOrEmpty(s))
return false;
return char.IsUpper(s[0]);
}
}
string text = "Hello";
bool isCap = text.IsCapitalized(); // Returns true
Key points:
- Static Class and Method: Every extension method has to be in a static class.
- this Parameter: It indicates the type being extended.
- Usage: And their methods are called as if they were instance methods on the extended type.
Extension methods let you extend existing types without creating and useing new types at all.
Fields
Variables declared within a class or struct are called fields which store data. They can be instance fields, unique to each instance, or static fields shared by all of the instances.
public class Counter
{
// Static field
public static int GlobalCount = 0;
// Instance field
public int InstanceCount = 0;
public Counter()
{
GlobalCount++;
InstanceCount++;
}
}
Counter c1 = new Counter();
Counter c2 = new Counter();
Console.WriteLine(Counter.GlobalCount); // Outputs: 2
Console.WriteLine(c1.InstanceCount); // Outputs: 1
Console.WriteLine(c2.InstanceCount); // Outputs: 1
Key concepts:
- Instance Fields: Each object has its own copy.
- Static Fields: Shared across all instances.
- Initialization: In constructors, or at declaration, fields can be initialized.
Although classes are the abstract expression of the fields and methods an object provides, fields are the key part of class design.
Constants
Immutability implies, for example, that constants are immutable values — known at compile time — which will not change across a program’s life time. const keywords are used to declare them and they should be initialized at their declaration.
public class Circle
{
public const double Pi = 3.14159265359;
public double Radius { get; set; }
public double GetCircumference()
{
return 2 * Pi * Radius;
}
}
Circle circle = new Circle { Radius = 5.0 };
double circumference = circle.GetCircumference();
Console.WriteLine($"Circumference: {circumference}");
Key points:
- Compile-Time Constants: At compile time, values are substituted. Static by
- Default: Constants are static members.
- Type Safety: They are of a built in data type.
Defining fixed values and using them throughout your program is particularly useful whenever you are using constants.
Nested Types
Types declared with the other class or struct are known as nested types. They are useful for encapsulating helper types which are only useful inside the containing type.
public class OuterClass
{
public class NestedClass
{
public void NestedMethod()
{
Console.WriteLine("Nested class method.");
}
}
public void OuterMethod()
{
Console.WriteLine("Outer class method.");
}
}
OuterClass outer = new OuterClass();
outer.OuterMethod();
OuterClass.NestedClass nested = new OuterClass.NestedClass();
nested.NestedMethod();
Key concepts:
- Access Modifiers: The access modifiers for the nested types are in addition to those listed above.
- Organization: It groups related types together.
- Encapsulation: Private members of the containing type can be used by nested types.
Nested types provide an excellent way to help organize and encapsulate code in large classes with an uneven level of customer interest.
Summary
Once you learn these, you will have a thorough grasp of classes in C#, and be capable of writing good, efficient, well organised, and scalable code. But classes aren’t just a feature and without powerful paradigm to back them up, they can’t add much to the quality and maintainability of your software projects.