3

Although I do find some (apparently old) posts on the topic on the web, I could not find one here at SE. Thought of raising this here to see if what I read is accurate/is all there is to it.

So basically, in trying to structure and document my application, one of the prominent architecture principles used is Singletons. It's a .NET service (.NET Core Worker Service) using some 20 singletons, which represent "manager objects" or modules, usually implementing a system feature added in a given Sprint.

Immediately - and for the first time, I guess - I asked myself why singletons and not static variables.

From googling, the main difference seems to be Singletons allowing lazy loading whilst static variables are loaded/allocated the moment the app initializes.

Are there other meaningful differences, in general - and in a .NET context, in particular ?

8
  • 3
    The singleton pattern enforces that there can only ever be one instance of the type (without reflection black magic). A global allows other instances to be made.
    – Telastyn
    Commented Mar 25, 2022 at 14:29
  • 2
    Side note: if you are talking about using DI with AddSingleton those are not classic singletons... Commented Mar 25, 2022 at 18:59
  • @Telastyn How do you create another global variable without changing the code (or using really black reflection magic)? I don't see a difference there.
    – Bergi
    Commented Mar 28, 2022 at 17:08
  • @bergi - By just adding a global anywhere else in code? But it's not about globals. If I make a global instance of a class, that doesn't prevent you from instantiating any number of instances of that class. Unlike a singleton, which forces the entire program to only ever instantiate one of the class. That's the entire point of the pattern.
    – Telastyn
    Commented Mar 29, 2022 at 15:09
  • @Telastyn And so you could add another method to your class that returns new instances. Sure, it's no longer following the singleton pattern; but that's my point: a singleton pattern is just a convention that you shouldn't violate, as is the "single global/static variable". To prevent instantiation, make the constructor private, you just have to initialise the static variable in a place where the private constructor is in scope.
    – Bergi
    Commented Mar 29, 2022 at 15:40

5 Answers 5

10

You're pretty much correct - singletons are just a fancy wrapper around a global variable. You can tell this because when you create a singleton, you end up writing code like

class Singleton
{
  private static Singleton _theOneAndOnlyInstance = new Singleton();

  public static Singleton Instance => _theOneAndOnlyInstance;
}

(maybe with some extra boilerplate if you want lazy initialization).

This is the primary reason the singleton pattern is often criticised, as it in many cases doesn't solve any of the issues caused by using a global variable.

3
  • 2
    Agree in principle (+1) but there are some nice features beyond syntatic sugar. A useful one is: you can identify instances that potentially access singletons with a simple breakpoint in the debugger. Their main downsides seem to be that they are way overused ;)
    – tofro
    Commented Mar 25, 2022 at 16:24
  • This code is broken. The destructor for the static variable will be called at an unpredictable time when the program exits.
    – gnasher729
    Commented Mar 26, 2022 at 18:34
  • 3
    @gnasher729 can you please clarify "This code is broken" part of your comment? Indeed it is a fact that finalizer of an object (if one exist) may or may not be called on exist, but I don't see it related to "it is broken" statement. There are no destructors in C#... (which seem to be the choice of language here as OP asked about .Net). Commented Mar 26, 2022 at 21:51
4

Why singletons and not static variables?

It’s a getter.

If you let everything that needs a reference to the ‘manager’ access it directly you wouldn’t be able run code on access without rewriting everything that accesses it.

For example, to change it from greedy loading to lazy loading you now need to run code later, on access, even if only once.

And so, even though you’re happy writing greedy loading code today, you’re stuck writing boiler plate getter code you don’t need. Why? Because later you might want to make it lazy loading. You have to keep accessing code from knowing or caring which you’re doing.

That is, unless you have properties. Like you do in C#. With those you don’t have to write getter code until you need getter code. Something I get jealous of every time I touch Java.

So why singletons and not static when greedy loading is fine? Because it was written in Java. Or by a Java coder that didn’t know any better.

But singletons are evil!

If you’re stuck in some framework with no access to main or any function that’s only called once (hence no composition root) then they are still useful. Even if they are evil globals. Use them and access them sparingly. If anyone gives you crap about them when you’re stuck in this situation ask them for a better idea.

1

tl;dr: A singleton type enforces that only a single instance can exist at any time, usually by handling instantiation itself and preventing anybody else from creating other instances. That’s its defining feature. If you need that particular enforcement, you need a singleton. If you just need a globally accessible object, a “normal” global (a.k.a. static) variable will do.

The only instance of a singleton type acts a lot like a global variable. You might call it through a function instead of directly, but that’s a detail. Essentially it’s still a globally accessible object.

About lazy loading

Singletons can be both lazy or non-lazy (is there a better word for non-lazy?). Since .NET is foreign to me consider this common singleton implementation in C++.

class Singleton {
public:
    static Singleton& get() {
        static Singleton instance;
        return instance;
    }

private:
    // private ctors prevent
    // the rest of the world from instantiating
    Singleton() = default;  // default ctor
    // ... same for copy ctor etc.
};

That’s lazy. instance isn’t constructed until you call Singleton::get() for the first time. Then that object lives until the program terminates. In particular, calling get() again does not create any more instances or replace the existing one.

You can make it non-lazy, too:

// in file: singleton.hpp
class Singleton {
public:
    static Singleton& get() {
        return instance;
    }

private:
    Singleton() = default;
    // ... same for copy ctor etc.

    static Singleton instance;
};

// in file: singleton.cpp
Singleton Singleton::instance;

Now instance is constructed immediately when the program starts and get() returns a reference to the already existing object. Ignore the split into two files. That’s a C++ language detail.

I’d assume that most languages have the features to implement both lazy and non-lazy singletons. In any case singletonness and lazyness are orthogonal concepts.

Instantiation control vs. access scope

When looking at the fundamental nature of singletons and global variables you can say:

  • Singletons are about restricting instantiation.
  • Global variables are about making the same object accessible in a broad-ish scope.

There is a good bit of overlap. A singleton instance that’s not accessible in a relatively broad scope wouldn’t be particularly useful. And a global variable can be a singleton by convention because many different places in the program use that one canonical object.

The similarities continue when considering lifetime. Both singletons and global variables tend to be instantiated early in a program’s life and tend to exist until it terminates.

What I wouldn’t count as a meaningful difference is the usual get() or instance() access function of a singleton. Hide a global variable behind such a function and you have the same thing, including the possibility to do additional work when the function is called.

0

Singletons are supposed to always exist when they are needed. Usually you have a function or getter returning a reference to the singleton, and the first call creates it. From then the singleton exists forever.

Static variables get one thing right: Their constructor is called the first time the variable is accessed. What’s quite bad is that you can assign to a static variable. What’s really, really bad is that when your program exits, destructors for all static variables are called in an unpredictable order. So you can access the static variable after it’s destructor is called. That’s fun. And if you use multi-threading you can use a static variable while it’s destructor is running, for extra fun.

2
  • 3
    In the context of .NET - which the OP explicitely asked for - your otherwise correct argument about doesn't hold. First, becaus of the Garbage Collector you never have control about when a destructor (finalizer, to be precise) is called. Second, the GC makes sure that an object which is finalized can't be accessed. Commented Mar 26, 2022 at 19:10
  • I'm not sure how you'd implement a singleton so that it doesn't have the same destruction problem. Are you saying that a singleton is never destroyed? Isn't that even worse?
    – Bergi
    Commented Mar 28, 2022 at 17:06
-2

Static objects are singletons, but assigning an object into a static variable isn't the only way to deal with singletons.

One of the cons of working with static things is that they are not easy to mock, which means that the class which is using the static object or function cannot be unit tested properly, without depending on the actual implementation of the object / function.

An example worth 1000 words:

public class MyService {
     public string SayHello() {
        string name = NameClient.SingletonInstance.GetName();
        return "Hello " + name;
     }
}
public class NameClient  {
     private static NameClient _instance;
     public static NameClient SingletonInstance {
         get {
             if (_instance == null) _instance = new NameClient();
             return _instance;
         }
     }
     private NameClient() {}
     public string GetName() {
       //do an http request to get a name
     }
  }

In this example we have a Service class (MyClass) which uses a client (NameClient) to get a value, than it does some business logic on it and returns another value. The NameClient class is a singleton exposed through a static property which does a http request to get the name.

Suppose that you need to unit test the business logic of MyService.SayHello: you can't avoid to perform the http request in the unit test, which is pretty bad.

Dependency Injection helps to deal with this

    public class MyService {
         private readonly INameClient _nameClient;
         public MyService(INameClient nameClient) {
           _nameClient = nameClient;
         }
         public string SayHello() {
            string name = _nameClient.GetName();
            return "Hello " + name;
         }
    }
    public interface INameClient {
         string GetName();
    }
    public class NameClient: INameClient  {
         public string GetName() {
           //do an http request to get a name
         }
    }
    public class Startup {
    ....
         public void RegisterServices(IServiceCollection services) {
             services.AddTransient<MyService>();
             services.AddSingleton<INameService, NameService>();
         }
    ....
    }

In this way MyService doesn't depend on the actual implementation of NameService, you can use mocks for unit tests and most important you leave the responsibility of dealing with the lifecycle of the singleton instance to the IOC container, which should be smart and avoid memory leaks

4
  • 1
    I'd argue the value you're getting here is from the INameClient abstraction, not the singleton. Commented Mar 25, 2022 at 14:55
  • @PhilipKendall They go together Commented Mar 25, 2022 at 15:08
  • @PhilipKendall the dependency injection system will take care always serving the same instance of the specified implementation. This makes it a singleton ( services.AddSingleton<INameService, NameService>(); )
    – ddfra
    Commented Mar 28, 2022 at 8:30

Not the answer you're looking for? Browse other questions tagged or ask your own question.