Chapter 18: Constructors - strict Object Initialization
Theoretical Foundations
In the previous chapter, we learned that Classes are blueprints for objects, and Properties allow us to store data about those objects. We saw how we could create an object and then set its properties one by one:
While this works, it introduces a dangerous problem: temporary invalid states.
Imagine you are building a system to control an AI-powered robotic arm. You create an instance of the RoboticArm class, but you forget to set its MaximumWeightCapacity property. If your code tries to lift an object immediately after creating the arm, the arm might break because it was never told its own strength limit.
Strict Object Initialization is the practice of ensuring that an object is never allowed to exist in an incomplete or invalid state. We achieve this using Constructors.
The Real-World Analogy: The Car Registration Office
Think of a class as the blueprint for a car. The properties (Make, Year, VIN) are the details of that car.
If you build a car and drive it off the lot without filling out the registration papers, you have a car that technically exists, but it's not legally valid. You can't drive it on the road.
A Constructor acts like the Registration Officer at the dealership. Before they hand you the keys (create the instance), they force you to provide all the mandatory information (Year, Make, VIN). They ensure the car is fully registered and valid the moment it rolls off the production line. If you don't provide the mandatory info, they refuse to give you the car.
The Problem: The "Empty" Object
Let's look at a class representing a UserAccount for an AI chat system.
public class UserAccount
{
public string Username { get; set; }
public string Email { get; set; }
public bool IsVerified { get; set; }
}
In Chapter 17, we learned about { get; set; }. These are auto-properties. They allow reading and writing.
Now, look at how we might use this in a Main method:
class Program
{
static void Main()
{
// We create the object
UserAccount user = new UserAccount();
// We immediately try to use it
if (user.IsVerified == true)
{
Console.WriteLine($"Welcome back, {user.Username}");
}
}
}
What happens here?
new UserAccount()creates the object on the Heap (Chapter 15).- Because we didn't set any values,
Usernameisnull,Emailisnull, and crucially,IsVerifiedisfalse(the default for bools). - The
ifstatement fails. - The message prints with an empty name: "Welcome back, ".
This is a "zombie" object. It exists, but it's useless because it lacks data.
The Solution: Constructors
A Constructor is a special method that runs automatically when you create an instance of a class. Its job is to set up the object's initial state.
Key Rules of Constructors:
- The name of the constructor must match the class name exactly.
- It has no return type (not even
void). - It runs when you type
new ClassName().
Let's rewrite the UserAccount class to force the user to provide a username immediately.
public class UserAccount
{
public string Username { get; set; }
public string Email { get; set; }
public bool IsVerified { get; set; }
// This is the Constructor
public UserAccount(string initialName)
{
Username = initialName;
Email = "Not Provided";
IsVerified = false;
}
}
Now, look at how the Main method changes:
class Program
{
static void Main()
{
// This line calls the constructor.
// We MUST provide a string for initialName.
UserAccount user = new UserAccount("AlexNet_99");
Console.WriteLine($"User created: {user.Username}");
}
}
If we try to write new UserAccount() without the string, the compiler will give us an error. We have successfully enforced strict initialization.
The init Keyword: Immutable Data
In AI development, we often deal with configuration settings. Once a configuration is loaded, we don't want other parts of the code to accidentally change it. In Chapter 17, we used set to allow changes. But what if we want to set a value only when the object is created, and never change it after?
C# introduced the init keyword for this.
public class AIConfig
{
public string ModelName { get; init; } // Can only be set during creation
public int MaxTokens { get; init; }
}
How init works:
- You can read
ModelNameanytime (using thegetwhich is implied). - You can set
ModelNameonly inside a constructor or using the Object Initializer (explained next). - Once the object is created, trying to change
ModelNameresults in a compiler error.
AIConfig config = new AIConfig();
// config.ModelName = "GPT-4"; // ERROR: The property 'ModelName' cannot be used in this assignment because it is 'init'
This is vital for AI applications where you want to pass configuration objects around safely, knowing they can't be tampered with.
The Object Initializer Pattern
You might remember from Chapter 16 that new creates an instance. We can combine constructors with a special syntax called an Object Initializer. This allows us to set properties at the moment of creation.
This is extremely common in C# because it keeps the code readable.
Scenario: You are defining the architecture of a Neural Network layer. You need to specify the input size, output size, and activation function.
public class NeuralLayer
{
public int InputSize { get; init; }
public int OutputSize { get; init; }
public string Activation { get; init; }
// We can have a constructor that sets defaults
public NeuralLayer()
{
Activation = "ReLU"; // Default value
}
}
Now, look at how we initialize this in Main:
class Program
{
static void Main()
{
// Using an Object Initializer
// This calls the constructor (NeuralLayer()) first,
// then sets the specific properties.
NeuralLayer hiddenLayer = new NeuralLayer
{
InputSize = 784,
OutputSize = 128
// Activation is automatically "ReLU" from the constructor
};
// We can override the constructor default if we want
NeuralLayer outputLayer = new NeuralLayer
{
InputSize = 128,
OutputSize = 10,
Activation = "Softmax"
};
}
}
This pattern is powerful because:
- It ensures the object is valid immediately.
- It groups the setup logic visually.
- It works perfectly with the
initkeyword.
Comparing Approaches: The Evolution of Robust Code
Let's look at the three ways to initialize an object, moving from "Risky" to "Strict".
1. The Old Way (Risky):
- Create empty object (
new Car()) - Set properties one by one (
car.Year = 2022). - Risk: You might forget to set
Year. The car exists but is invalid.
2. The Constructor Way (Strict):
new Car(2022)- Benefit: You cannot create the car without a year.
- Downside: If you have 10 properties, your constructor call looks like
new Car(2022, "Toyota", "Red", ...)which is hard to read.
3. The Modern Way (Strict + Readable):
- Use a constructor for mandatory fields.
- Use
initproperties for optional fields. - Use Object Initializers to set values clearly.
Visualizing the Flow
Here is a diagram showing how the "Strict Initialization" flow works compared to the old way.
Deep Dive: Mandatory vs. Optional Fields
When designing a class for an AI application, you must decide which fields are mandatory and which are optional.
Mandatory Fields (The Constructor's Job): These are things the system cannot function without.
- Example: A
NeuralNetworkclass needs to know theInputDimensions. If it doesn't know this, it cannot allocate memory for the neurons. - Implementation: Put these as parameters in the constructor.
Optional Fields (The init or set Job):
These are things that add functionality but aren't strictly required to start.
- Example: A
NeuralNetworkmight have aName(e.g., "MyModel_v1"). It's nice to have, but the math works without it. - Implementation: Use a property with
initorset.
Handling Complex Initialization (Logic in Constructors)
Constructors aren't just for copying values. They can contain logic (Chapter 6, 7, 8).
Imagine we are building a DataPreprocessor for text data. We want to enforce that the MaxWordCount is never negative.
public class DataPreprocessor
{
public int MaxWordCount { get; init; }
public DataPreprocessor(int count)
{
// Logic to ensure validity
if (count < 0)
{
// For now, we just set it to 0.
// (In later chapters we will learn to throw exceptions)
MaxWordCount = 0;
}
else
{
MaxWordCount = count;
}
}
}
Usage:
DataPreprocessor dp1 = new DataPreprocessor(500); // MaxWordCount becomes 500
DataPreprocessor dp2 = new DataPreprocessor(-10); // MaxWordCount becomes 0 (Safe!)
This protects the object from being created with invalid data.
Summary of Concepts
- Constructors: Special methods that initialize objects. They match the class name and have no return type.
- Strict Initialization: The practice of forcing the developer to provide necessary data at the moment of creation.
initAccessor: Allows a property to be set only during object creation, making the object immutable afterwards.- Object Initializer: The
{ ... }syntax used withnewto set properties clearly and concisely.
By mastering these concepts, you ensure that your C# code is robust. You stop "zombie objects" (objects that exist but are broken) from ever entering your system. This is the foundation of professional software engineering.
Basic Code Example
Problem Context: Managing User Registration
Imagine you are building a simple user registration system for a new application. When a new user signs up, they must provide a username and an email address. These are mandatory pieces of information. Without them, the user account is invalid.
In previous chapters, we might have created a User object and then set its properties one by one. However, this approach has a flaw: it is possible to create a User object and forget to set the username or email, leaving the object in an invalid state.
To solve this, we use Constructors. A constructor is a special method that runs immediately when we create an instance of a class (using the new keyword). Its job is to ensure the object is initialized with all the necessary data right from the start.
Code Example: Strict User Initialization
Here is a complete program that demonstrates how to enforce mandatory fields using a constructor.
using System;
// 1. Class Definition
// This defines the blueprint for a User object.
public class User
{
// 2. Fields (Private)
// These variables store the data. They are private to prevent
// direct modification from outside the class.
private string _username;
private string _email;
// 3. Properties (Public)
// These allow controlled access to the fields.
// We use 'get' so other parts of the program can read the values.
// Notice there is NO 'set' here. We cannot change these after creation.
public string Username
{
get { return _username; }
}
public string Email
{
get { return _email; }
}
// 4. The Constructor
// This special method has the same name as the class and no return type.
// It accepts parameters for the mandatory fields.
public User(string username, string email)
{
// 5. Assignment
// We assign the parameters to the private fields.
// This ensures the object is created in a valid state.
_username = username;
_email = email;
}
// 6. Helper Method
// A simple method to display the user's info.
public void DisplayInfo()
{
Console.WriteLine($"User: {Username}, Email: {Email}");
}
}
// 7. Main Program
public class Program
{
public static void Main()
{
// 8. Creating an Instance
// We use the 'new' keyword followed by the constructor arguments.
// This guarantees that _username and _email are set immediately.
User user1 = new User("AliceDev", "alice@example.com");
// 9. Accessing Properties
// We can read the values, but we cannot change them directly
// because there is no 'set' accessor.
Console.WriteLine($"Retrieved Username: {user1.Username}");
// 10. Using the Method
user1.DisplayInfo();
// 11. Attempting to create an invalid user (This would cause an error)
// If we tried: User user2 = new User();
// The compiler would stop us because the constructor requires arguments.
}
}
Step-by-Step Explanation
- Class Definition: We define a class named
User. This acts as a template for creating user objects. - Private Fields: We declare
_usernameand_emailasprivate stringfields. Making them private ensures that code outside theUserclass cannot modify them directly, protecting the data's integrity. - Public Properties: We create properties
UsernameandEmailwith only agetaccessor. This allows external code to read the values, but because there is nosetaccessor, the data is immutable (unchangeable) after the object is created. - The Constructor: We define
public User(string username, string email). This is the constructor. It forces anyone creating aUserto provide a username and an email. - Initialization Logic: Inside the constructor, we assign the incoming parameters (
usernameandemail) to the private fields (_usernameand_email). This is the "strict initialization" step. - DisplayInfo Method: This is a standard method (introduced in Chapter 13) that uses String Interpolation (Chapter 3) to print the user's details to the console.
- Main Method: The entry point of our program.
- Instantiation: We create
user1usingnew User(...). We pass "AliceDev" and "alice@example.com" as arguments. The constructor runs immediately, setting the private fields. - Accessing Data: We print
user1.Username. The property allows us to retrieve the value stored in the private field. - Method Call: We call
user1.DisplayInfo()to show the formatted output. - Safety Check: We note that trying to create a user without arguments (e.g.,
new User()) would fail. This is the benefit: it prevents us from creating an incomplete object.
Visualizing the Object Creation
The following diagram illustrates how the constructor bridges the gap between the new keyword and the internal state of the object.
Common Pitfalls
Mistake: Forgetting to pass arguments to the constructor.
A common error for beginners is trying to instantiate a class with a constructor defined like this:
Why this fails: When you define a constructor that accepts parameters (likestring username and string email), C# automatically removes the default parameterless constructor. You cannot create an instance without providing the required data.
The Fix: Always ensure you provide the exact arguments defined in the constructor signature:
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.