Monday, November 8, 2021

Persistent variables for method scope only (III) - Non-Static values

As shown in the previous article, we have possibility to use static values for method scopes only. Now we want to achieve the same for non-static values. We could say for member values, but as member indicates to object's scope, we rather call it here non-static, or later in the code just local.

When we provide values for life time of an object, we need to ensure the provided values dies with the object. So the question is: How to get notified when an object dies? Actually this is impossible, but we can make use of the disposable pattern. When the consumer class can inform about its disposal, we can get rid of the values.

But as you can see, this is now a breaking change: We need to prepare the class where we want to use our feature, which stinks, because the feature is now class depending, not just method depending as before. However, I couldn't find another solution, so lets start with that approach.

First, we need a notification implementation for these consumer classes. A really easy extension of IDisposable will do the job:

public interface INotifyDisposed : IDisposable
{
    event EventHandler Disposed;
}

Then let's extend the Persistent class which will be able now, to keep object-life-time values from method scopes as well:

public static class Persistent<Twhere T : struct
{
    #region StaticValues
 
    static readonly Dictionary<int, T[]> staticValues;
 
    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];
    }
    #endregion
 
    #region NonStaticValues
    static readonly Dictionary<int, Dictionary<INotifyDisposed, T[]>> nonStaticValues;
 
    public static ref T Local(T value, INotifyDisposed owner)
    {
        var key = new StackFrame(1, false).GetNativeOffset();
 
        if (!nonStaticValues.ContainsKey(key))
        {
            nonStaticValues.Add(key, new Dictionary<INotifyDisposed, T[]>());
        }
        if (!nonStaticValues[key].ContainsKey(owner))
        {
            nonStaticValues[key].Add(owner,new T[] { value });
 
            owner.Disposed += (sendere) =>
            {
                if (nonStaticValues[key].Remove(owner))
                {
                    if (nonStaticValues[key].Keys.Count == 0)
                    {
                        nonStaticValues.Remove(key);
                    }
                }
                return;
            };
        }
        return ref nonStaticValues[key][owner][0];
    }
    #endregion
 
 
    static Persistent()
    {
        staticValues = new Dictionary<int, T[]>();
        nonStaticValues = new Dictionary<int, Dictionary<INotifyDisposed, T[]>>();
    }
}

See, that the region NonStaticValues has been added. Basically it works the same as for static values, but you need to provide an owner, type of INotifyDisposed. When the Disposed event is called, the memorized values from it's reference will be destroyed, and with it also the references to the owner object itself. To get rid of the owner's reference is very important, otherwise the garbage collector will never release the consumer object, because we still have references to it.

Let's see how the consumption now looks like:

class Test: INotifyDisposed
{
    public void Method1()
    {
        ref int myInt = ref Persistent<int>.Local(0, this);
        myInt++;
        Console.WriteLine($"myInt is now {myInt}");
    }
 
    #region INotifyDisposed & IDisposable implementation
 
    public event EventHandler Disposed;
 
    private bool disposedValue;
 
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                Disposed?.Invoke(thisnew EventArgs());
            }
            disposedValue = true;
        }
    }
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
 
    #endregion 

} 

The correct implementation of INotifyDisposed and the correct object disposal are elemental. Not doing so will lead to catastrophic memory leakage! Yes, I can read your mind: "Uh - another really ugly pit you need to be aware of!". But let me state a conclusion in further chapter. Let's finish here first.

There's one more thing: Even all works fine with correct implementation and disposing objects, the access of the persistent values is painfully slow. Next chapter will explain why and what we can do here.


No comments:

Post a Comment