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.
public static class Persistent<T> where 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]; } }
public void Method1() { int myInt = Persistent<int>.Static(0); myInt++; Console.WriteLine($"myInt is now {myInt}");
public static class Persistent<T> where T : struct { public class Memory { public T Value { get; set; } } 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]; } }
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<T> where 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