C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
103: The Singleton Pattern
You'll hear a lot of debate about the Singleton pattern. Some developers call it an "anti-pattern" because it can lead to hidden dependencies and makes unit testing a headache if you aren't careful. But in the real world, there are just some things that should only exist once. A configuration manager, a cache, or in our case today, a game settings manager.
Imagine we're building a small indie game. We need a way to track things like volume and screen resolution. We don't want to pass a settings object into every single class in our game—that's a nightmare. We want one central source of truth that any class can talk to.
Starting with a global settings hub
My first instinct is to create a class that holds our data and a static property to access it. Let's keep it simple: just a volume level and a resolution string.
public class GameSettings
{
public float Volume { get; set; } = 0.75f;
public string Resolution { get; set; } = "1920x1080";
public static GameSettings Instance { get; private set; } = new GameSettings();
}
At first glance, this looks great. I can just call GameSettings.Instance.Volume from anywhere in my project. It's convenient, and it gets the job done for a quick prototype.
Oops, I left the door open
Here is where I messed up. I was testing my code and realized that while I provided a convenient Instance, I didn't actually stop anyone from creating another instance of the settings. Look at this:
var settings1 = GameSettings.Instance;
var settings2 = new GameSettings(); // Wait, I can still do this?
settings1.Volume = 0.1f;
settings2.Volume = 1.0f;
Console.WriteLine(settings1.Volume); // 0.1
Console.WriteLine(settings2.Volume); // 1.0
This defeats the entire purpose of a Singleton. If one part of my game is updating the volume on the singleton instance and another part is accidentally using a new GameSettings() object, my game state is now fragmented. I've created two "sources of truth," which is exactly what we're trying to avoid.
Locking it down and making it thread-safe
To fix this, I need to make the constructor private. If the constructor is private, the new keyword won't work outside of the class itself. But there's another issue: thread safety. If two different threads try to initialize the instance at the exact same time, we could still end up with two objects.
In modern C#, the cleanest way to handle this is using the Lazy<T> type. It handles the thread safety for us and ensures the object is only created the very first time it's actually accessed (lazy initialization).
public sealed class GameSettings
{
// Lazy ensures thread-safety and delayed initialization
private static readonly Lazy<GameSettings> _lazy =
new Lazy<GameSettings>(() => new GameSettings());
public static GameSettings Instance => _lazy.Value;
// The magic happens here: private constructor
private GameSettings()
{
Console.WriteLine("Settings initialized!");
}
public float Volume { get; set; } = 0.75f;
public string Resolution { get; set; } = "1920x1080";
}
I also added the sealed keyword to the class. This prevents other classes from inheriting from it, which could potentially create multiple instances of a derived class. Now, the only way into this class is through GameSettings.Instance. It's locked down, efficient, and safe.
📋 Practical Task
Implementing a Thread-Safe DatabaseConnectionPool Singleton
In a real application, opening a new database connection for every single query is incredibly expensive. To solve this, we often use a connection pool that stays open for the duration of the app's life.
Your task is to create a class called DatabaseConnectionPool that implements the Singleton pattern using the Lazy<T> approach we discussed. It should meet the following requirements:
- The class must be
sealed. - The constructor must be
privateto prevent external instantiation. - It must provide a public static property called
Instance. - Add a simple method called
GetConnection()that returns a string (e.g.,"Connection-12345") to simulate retrieving a connection from the pool.
Test your implementation by trying to call new DatabaseConnectionPool() in your Main method; your code should fail to compile, proving that the Singleton is properly locked down.
There are no comments for now.