Monday, November 8, 2021

Persistent variables for method scope only (II) - Static values

As mentioned in the previous article, we have to think about how we can address the memory and how we can achieve a good user experience for using these persistent variables.

When we start with static values we're talking about memory existing once each method and thread, application domain or process (depending on which framework you compile against and how you structure it).

So in the end we want to have something like this:

public void Method1()
{
    static int myInt;
    myInt++;
    Console.WriteLine($"myInt is now {myInt}");
}

Problem 1: The addressing

As a first approach we could easily solve this with a static class providing memory for every static variable, when we just give a identifier. It could look like this:

public void Method1()
{
    int myInt = Persistent.Static("Method1");
    myInt++;
    Console.WriteLine($"myInt is now {myInt}");
}

Persistent would represent a static class maintaining the values of the static scope values and the Static method would provide us the value. But you would have to provide a key (here the same as the method's  name "Method1") which is quite ugly because of two reasons:

  • You cannot trust the uniqueness of your key. Maybe your colleague has used the feature elsewhere too and used also the "Method1" key. You can imagine what will happen: Loss of memory isolation and furthermore your application will behave just crazy.
  • On the other hand you want to access your values quick and elegant, to provide a key is not comfy at all.
We are able to solve this by auto generate a key given by the source which requests the value. An accurate method to do this is to use the native offset from the stack frame. Like this:

public static class Persistent<Twhere T : struct
{
    static readonly Dictionary<int, T> staticValues;
    static Persistent()
    {
        staticValues = new Dictionary<int, T>();
    }
 
    public static T Static(T value)
    {
        var key = new StackFrame(1, false).GetNativeOffset();
 
        if (!staticValues.ContainsKey(key))
        {
            staticValues.Add(key, value);
        }
 
        return staticValues[key];
    }
}
And we can consume it now like this:

public void Method1()
{
    int myInt = Persistent<int>.Static(0);
    myInt++;
    Console.WriteLine($"myInt is now {myInt}"); 
} 

You surely have noticed that we have now a generic type parameter on the Persistent class as well as a argument to the Static method.
The generic type parameter is used to ensure type safety and the value argument on Static is used as initial value. Imagine that your variable would start with any value instead of its default value. And for reference types like objects we need a non-null value anyway, because we cannot predict how to construct such a value you want to use as local static value.

Ok, fine job. But if you test it, you will see, that myInt is always 1. Why is this? This is because the myInt is just a copy of the stored value. Any modification on it will not be saved back to the dictionary in Persistent class.

Problem 2: Accessing the value in comfy way

To enforce write-back mechanism when changing myInt, we could use a class with a typed property instead of providing a typed value directly:

public static class Persistent<Twhere T : struct
{
    public class Memory
    {
        public T Value { getset; }
    }
 
    static readonly Dictionary<int, Memory> staticValues;
    static Persistent()
    {
        staticValues = new Dictionary<int, Memory>();
    }
 
    public static Memory Static(T value)
    {
        var key = new StackFrame(1, false).GetNativeOffset();
 
        if (!staticValues.ContainsKey(key))
        {
            staticValues.Add(key, new Memory() { Value = value });
        }
 
        return staticValues[key];
    }
}

Consuming now like this:
 
public void Method1()
{
    var myInt = Persistent<int>.Static(0);
    myInt.Value++;
    Console.WriteLine($"myInt is now {myInt.Value}");
}

Ok, that works now, the increment is remembered. But you would agree, this looks ugly when using a class object as a proxy to access the stored value, right?

There's another approach: instead to use a class object we're gone use the ref feature to ensure write-back functionality:

public static class Persistent<Twhere T : struct
{
    static readonly Dictionary<int, T[]> staticValues;
    static Persistent()
    {
        staticValues = new Dictionary<int, T[]>();
    }
 
    public static ref T Static(T value)
    {
        var key = new StackFrame(1, false).GetNativeOffset();
 
        if (!staticValues.ContainsKey(key))
        {
            staticValues.Add(key, new T[] { value });
        }
 
        return ref staticValues[key][0];
    }
}

See that the Static method has now upgraded with ref keywords. In order to be able to return values by reference we use now an array declaration of the target type instead of the dictionary's value. Why this? Because a dictionary's value property does not gives us the reference to it's memory, but some other, temporary memory area, in other words: another copy. The array object can give us reference of it's stored memory. As we only store one value each dictionary entry, we just populate and maintain the first entry (key 0) from that array. Looks a little bit confusing, right? I agree, but it works quite well.

But let's see how the consumption now looks like:

public void Method1()
{
    ref int myInt = ref Persistent<int>.Static(0);
    myInt++;
    Console.WriteLine($"myInt is now {myInt}");
}

Take notice, that we have here twice the ref keyword as well. This is needed since we really want a reference to the stored values, not just a copy. Just adding one ref is not enough: either the compiler will not swallow it or the result is not what you wanted. But on the other hand you can again operate with just a variable myInt and any modification on it will be stored persistent.

Cool, we have now to ability to keep static values which are only accessible in Method1. Let's see in next chapter how we can extend the functionality to achieve this also for non-static values, meaning, keep values per object instance.

No comments:

Post a Comment