Smart Start: Constructors & Initializers in C#

You have a solid grasp of how to define classes and interfaces to create strong contracts for your code. Now, let's refine a critical moment in an object's life: the moment it's created. We know a constructor's job is to initialize an object into a valid state.

But what if you want to provide more than one way to create an object? Perhaps a "default" version, and another more detailed version? And what if there's a more convenient way to set properties right after creation? C# provides several elegant features to handle these scenarios, making your classes more flexible and your code more readable.

Let's explore these advanced techniques for object creation and initialization!

Tommy and Gina looking at cute little robots carried by a conveyor belt

Constructor Overloading Explained

In C#, you are not limited to a single constructor per class. You can define multiple constructors, a feature known as constructor overloading. The key rule is that each constructor must have a unique signature, which means it must have a different number of parameters or different types of parameters.

This is incredibly useful because it gives consumers of your class (including your future self!) flexible ways to create objects based on the information they have available.

Let's create a TestConfiguration class that can be created in a few different ways:

public class TestConfiguration
{
    public string Browser { get; set; }
    public string EnvironmentUrl { get; set; }
    public int TimeoutSeconds { get; set; }
 
    // Constructor 1: Default values
    public TestConfiguration()
    {
        Browser = "Chrome";
        EnvironmentUrl = "https://staging.test-automation.space";
        TimeoutSeconds = 30;
        Console.WriteLine("Created config with default values.");
    }
 
    // Constructor 2: Specify only the browser
    public TestConfiguration(string browser)
    {
        Browser = browser;
        EnvironmentUrl = "https://staging.test-automation.space";
        TimeoutSeconds = 30;
        Console.WriteLine($"Created config for browser: {browser}.");
    }
 
    // Constructor 3: Specify browser and URL
    public TestConfiguration(string browser, string environmentUrl)
    {
        Browser = browser;
        EnvironmentUrl = environmentUrl;
        TimeoutSeconds = 30; // Still using a default for timeout
        Console.WriteLine($"Created config for {browser} on {environmentUrl}.");
    }
}   

While this works, you can probably see the repetition. The TimeoutSeconds = 30; line is in every constructor. Let's clean that up.

Staying DRY with Constructor Chaining

To avoid duplicating initialization logic across multiple constructors, you can use a technique called constructor chaining. This allows one constructor to call another constructor in the same class before its own body runs. You achieve this with the : this() syntax placed after the constructor's parameter list.

The goal is usually to have one "master" constructor that does all the core initialization, and have the other, simpler constructors call it with default values.

Let's refactor our TestConfiguration class:

public class TestConfiguration
{
    public string Browser { get; set; }
    public string EnvironmentUrl { get; set; }
    public int TimeoutSeconds { get; set; }
 
    // Constructor 1: Chains to Constructor 2 with a default browser
    public TestConfiguration() : this("Chrome")
    {
        // This body runs AFTER the constructor it calls.
        Console.WriteLine("(Called via default constructor)");
    }
 
    // Constructor 2: Chains to Constructor 3 (the master) with a default URL
    public TestConfiguration(string browser) : this(browser, "https://staging.test-automation.space")
    {
        // This body is also empty.
    }
 
    // Constructor 3 (The "Master" Constructor): Contains all the logic.
    public TestConfiguration(string browser, string environmentUrl)
    {
        Console.WriteLine("Master constructor running...");
        Browser = browser;
        EnvironmentUrl = environmentUrl;
        TimeoutSeconds = 30; // All initialization logic is now in one place!
    }
}   

Now, when you call new TestConfiguration("Edge"), the C# runtime sees the : this("Edge", 30) and calls the master constructor first. The initialization logic is now centralized, making the code much cleaner and easier to maintain. This is a very common and powerful professional practice.

The Logic of Chaining

When you use : this(...), the constructor you are "chaining" to runs before the body of the constructor that is making the call. This ensures that the object is fully initialized by the more detailed "master" constructor before any additional, specific logic in the simpler constructor's body is applied.

Constructor chaining is a professional technique that makes your code cleaner, more maintainable, and adheres to the DRY (Don't Repeat Yourself) principle.

Setting Properties with Object Initializers

Constructors are great for setting the required state of an object. But what about optional properties? C# provides a very clean and readable syntax for setting any accessible public properties right at the moment you create an object. This is called an object initializer.

You use it by adding curly braces {} after the constructor call, where you can list properties and assign them values.

// Assume TestConfiguration class has a parameterless constructor
 
// Create an object and set its properties in one go
TestConfiguration myConfig = new TestConfiguration()
{
    Browser = "Firefox",
    TimeoutSeconds = 60
    // EnvironmentUrl will use the default value from the constructor
};
 
Console.WriteLine($"Browser: {myConfig.Browser}"); // Output: Firefox
Console.WriteLine($"Timeout: {myConfig.TimeoutSeconds}"); // Output: 60    

It's important to understand the order of operations: the object's constructor always runs first, and then the property assignments in the initializer are executed. This means you can use them together!

// Call the constructor that requires a browser, then set an optional property
TestConfiguration anotherConfig = new TestConfiguration("Edge")
{
    TimeoutSeconds = 90 // Override the default timeout set by the constructor
};
 
Console.WriteLine($"Browser: {anotherConfig.Browser}"); // Output: Edge
Console.WriteLine($"URL: {anotherConfig.EnvironmentUrl}"); // Output: https://staging...
Console.WriteLine($"Timeout: {anotherConfig.TimeoutSeconds}"); // Output: 90    

Object initializers lead to highly readable code, especially when creating objects with many optional properties.

Effortless Collection Setup with Initializers

In a similar spirit of convenience, C# provides a shorthand syntax for creating a collection and adding elements to it all in one statement. This is called a collection initializer.

You've actually seen this before when we initialized arrays:

int[] numbers = { 10, 20, 30, 40 }; // This is array initializer syntax

You can use the same curly brace {} syntax for any collection that has an Add method, like our trusty List<T>.

using System.Collections.Generic;
 
// Without collection initializer
List<string> browsersOldWay = new List<string>();
browsersOldWay.Add("Chrome");
browsersOldWay.Add("Firefox");
browsersOldWay.Add("Edge");
 
// With collection initializer - much cleaner!
List<string> browsersNewWay = new List<string>
{
    "Chrome",
    "Firefox",
    "Edge"
};  

This syntax is purely for convenience. Behind the scenes, C# is creating a new list and then calling the Add() method for each element you provided inside the curly braces. It just makes your code more concise and readable.

The Spread Operator

For those working with modern C# (specifically C# 12 and .NET 8 or later), there's an even more powerful way to create new collections by combining existing ones. It's called the spread operator (..).

This operator allows you to "spread out" the elements of one collection directly inside the initializer of another. It's incredibly expressive.

// Imagine you have two lists of test users
List<string> smokeTestUsers = new() { "user1", "user2" };
List<string> regressionUsers = new() { "user3", "user4" };
 
// Using the spread operator .. to combine them into a new list
// with another element in the middle.
List<string> allTestUsers = [..smokeTestUsers, "special_user", ..regressionUsers];
 
// The "allTestUsers" list now contains:
// "user1", "user2", "special_user", "user3", "user4"      

It's a fantastic feature for writing concise code when initializing new collections from existing ones. While older methods like AddRange() still work perfectly, you will see this new syntax more and more in modern C# code.

A Look Ahead at More Specialized Constructors

As you continue your C# journey, you'll find that constructors have even more specialized forms for specific design patterns and scenarios. We won't dive deep into them now, but it's good to be aware that they exist so you recognize them when you see them in the wild.

Private Constructors

You can declare a constructor as private. This prevents external code from creating instances of the class directly with new. This is a key technique for implementing certain advanced design patterns, most famously the Singleton Pattern, where you want to ensure that only one single instance of a class ever exists in your entire application.

Static Constructors

A static constructor is a very special type. It doesn't run for each object; it runs only once for the entire class, the very first time that class is accessed in any way. Its purpose is to initialize any static data or perform a one-time setup for the class itself.

Primary Constructors (Modern C#)

C# 12 introduced primary constructors, a very concise syntax for defining constructors whose parameters are available throughout the class body. It's a fantastic feature for reducing boilerplate code, which we can explore in a future lesson on "Modern C# Features."

For now, mastering constructor overloading and chaining, along with object and collection initializers, gives you a powerful and flexible toolkit for object creation.

Key Takeaways

  • Constructor Overloading allows you to define multiple constructors in a class, each with a unique parameter signature, providing flexible ways to create objects.
  • You can use Constructor Chaining (: this(...)) to have one constructor call another in the same class, reducing code duplication and centralizing initialization logic.
  • Object Initializers (using curly braces {}) provide a readable syntax to set public properties on an object right after its constructor runs.
  • Collection Initializers offer a concise way to add multiple elements to a collection (like a List<T>) at the moment of its creation.
  • These techniques work together to provide powerful and clean ways to instantiate and initialize objects in C#.

Advanced Object Creation

What's Next?

You've now added some sophisticated object creation techniques to your C# arsenal! So far, nearly every property and method we've created has belonged to an instance of a class (you had to write new UserAccount() to use them). But you've also used methods like Console.WriteLine() without ever creating a "Console" object. This leads to a critical distinction. Next, we'll explore the difference between Static vs. Instance Members to understand how some members belong to the class type itself, while others belong to each individual object.