Search code examples
c#referenceservice-locator

Trouble understanding reference types / reference copying in Service Locator implementation


In implementing a Service Locator, I've come across something I'm confused about with regards to reference types.

In the code below, I have a static class ServiceLocator which exposes 2 static methods, GetService and ProvideService - get returns the current service, and provide takes a new service as an argument and assigns it to the current service variable. If the provided service is null, it assigns currentService to a static defaultService initialised at the start of the class declaration. Simple stuff:

public static class ServiceLocator {
    private static readonly Service defaultService = new Service();
    private static Service currentService = defaultService;

    public static Service GetService() {
        return currentService;
    }

    public static void ProvideService(Service service) {
        currentService = service ?? defaultService;
    }
}

What i'm confused about is this: I have a separate class which stores a reference to the currentService at the start of its class declaration in the variable named referenceToCurrentServiceAtStart. When I provide the service locator with a new Service instance to update the current service, referenceToCurrentServiceAtStart appears instead to maintain the reference to defaultService:

public class ClassThatUsesService {
    private Service referenceToCurrentServiceAtStart = ServiceLocator.GetService();

    private static ClassThatUsesService() {
        ServiceLocator.ProvideService(new Service());
        // this variable appears to still reference the defaultService 
        referenceToCurrentServiceAtStart != ServiceLocator.GetService()
    }
}

So the references appear to follow this kind of chain:

referenceToCurrentServiceAtStart -> defaultService -> (Service in memory)

Which is understandable, since referenceToCurrentServiceAtStart simply copies the currentService reference. However, the behaviour I'm looking for/would like is for referenceToCurrentServiceAtStart to always reference whatever currentService references, so it's updated by Provide(). Something more akin to:

referenceToCurrentServiceAtStart -> currentService -> (Service> in memory)

So, is this behaviour possible? I'm really unsure of how I'd achieve this kind of reference behaviour. I'm new to C# so it's very possible there's some obvious language feature I'm clueless about. Any help would be greatly appreciated.


Solution

  • is this behaviour possible?

    No, not as you've described it. As you're already aware, all you get is a copy of the original reference. Changing the original reference doesn't change the copy, any more than copying the value of an int variable to another would allow you to later change the original and have the copy change:

    int original = 17;
    int copy = original;
    
    original = 19;
    // "copy" is still 17, of course!
    

    If you want to always have the current value of the reference in ServiceLocator, then you should just always retrieve the value from that class, rather than using a local field. In your above example, you might indirect through a property, e.g.:

    public class ClassThatUsesService {
        private Service referenceToCurrentServiceAtStart => ServiceLocator.GetService();
    }
    

    It's a one character change (the = becomes =>), but don't be fooled. It's a significant change in implementation. What you wind up with instead of a field, is a read-only property (i.e. has only a get method and no set method), where that property's get method calls the ServiceLocator.GetService() method and returns the result.

    Personally, I wouldn't bother. Unless you have some very strong expectation that the implementation of referenceToCurrentServiceAtStart will change in the future, you should just call ServiceLocator.GetService() directly. Don't even have the referenceToCurrentServiceAtStart property. Since the code expects to always get the current value, the best way to ensure that is to just always get the current value, straight from the class where that value is stored.

    Finally, I'll take the opportunity to show a scenario that is similar to what you're asking, but not exactly. In particular, because you're trying to store the reference in a class field, the above is how you need to do it. But, the latest C# has "reference return values", which must be stored in "ref locals". Since you want to reference a static field, which is guaranteed to always exist, you can in fact return a reference to the field, store that in a local, and when you retrieve the local variable's value, it will always have whatever is in the field, because it's a reference to the field, not a copy of it.

    You can see the example in the documentation (see links above), but here's another example that is more similar to what you're doing:

    class Program
    {
        static void Main(string[] args)
        {
            // stores a reference to the value returned by M1(), which is to say,
            // a reference to the B._o field.
            ref A a1 = ref B.M1();
    
            // Keep the original value, and create a new A instance
            A original = a1, a2 = new A();
    
            // Update the B._o field to the new A instance
            B.M2(a2);
    
            // Check the current state
            Console.WriteLine($"original.ID: {original.ID}");
            Console.WriteLine($"a1.ID: {a1.ID}");
            Console.WriteLine($"a2.ID: {a2.ID}");
        }
    }
    
    class A
    {
        private static int _id;
    
        public int ID { get; }
    
        public A()
        {
            ID = ++_id;
        }
    }
    
    class B
    {
        private static A _o = new A();
    
        public static ref A M1()
        {
            // returns a _reference_ to the _o field, rather than a copy of its value
            return ref _o;
        }
    
        public static void M2(A o)
        {
            _o = o;
        }
    }
    

    When you run the above, you'll get this output:

    original.ID: 1
    a1.ID: 2
    a2.ID: 2

    In other words, the variable a1 winds up yielding the same value found in a2, which is the new object passed to the B.M2() method to modify the B._o field, while the original copy of the B._o field value remains a reference to the original object that field referenced.

    This doesn't work in your case, because the ref value that's returned has to be stored in a ref local. You can't put it into a class field. But it's similar enough to your scenario that I wanted to mention it, in case you want to change your design to allow that, or want to use that technique in some other scenario that does work in that way.