Store reference to an object in dictionary
Asked Answered
M

4

13

I have been searching for a way to save the references of variables of various types into a dictionary, together with a corresponding key. Then i would like to modify the instance of the variable by accessing its reference through the dictionary by its key. For storing the references, i tried to use <object>, but without success. Neither Dictionaries nor Lists accept anything like Dictionary<string, ref int>. The following code compiles, but seems to update the variables by value only. Any ideas or workarounds?

Here's the (tested) code:

class Test1
{
    IDictionary<string, object> MyDict = new Dictionary<string, object>();

    public void saveVar(string key, ref int v) //storing the ref to an int
    {
        MyDict.Add(key, v);
    }
    public void saveVar(string key, ref string s) //storing the ref to a string
    {
        MyDict.Add(key, s);
    }

    public void changeVar(string key) //changing any of them
    {
        if(MyDict.GetType() == typeof(int))
        {
            MyDict[key] = (int)MyDict[key] * 2;
        }
        if(MyDict.GetType() == typeof(string))
        {
            MyDict[key] = "Hello";
        }
    }
}

And this is how i call the methods of the class

Test1 t1 = new Test1();
int myInt = 3;
string myString = "defaultString";

Console.WriteLine(myInt); //returns "3"
Console.WriteLine(myString); //returns "defaultString"

t1.saveVar("key1", ref myInt);
t1.saveVar("key2", ref myString);

t1.changeVar("key1");
t1.changeVar("key2");

Console.WriteLine(myInt); //should return "6"
Console.WriteLine(myString); //should return "Hello"
Macarthur answered 20/6, 2014 at 13:57 Comment(12)
You want to update the key of the dictionary?Pontiff
String is reference type, so you want to store a reference to a reference?Thea
For your Test class to be able to save a reference to your string/int and update it at a later time, it would have to pin the object in memory and use pointers. Don't do it.Forerun
You are asking to store a pointer. No can do. A pointer is already ably wrapped in C#, it is a reference to an object. You need to store the int or the string as a member of the object.Kindle
yes, i want to update the key. in C++ i would simply store the address of the variable. How about this, when I store the int or string as a member of Test1, can I then notify my calling class when their values change?Macarthur
@Macarthur Then I will recommend you the following url: #1938347Pontiff
@Pontiff sorry, i was wrong. Of course i want to update the Value...Macarthur
@PeterRitchie Yes, exactly. Storing a reference to a reference is a very useful and powerful tool, and that's exactly what it sounds like he wants to be doing here. He just needs to be using a different tool than ref to create the second reference.Donnie
@Macarthur Updating the key is impossible, the Value is possible.Pontiff
Maybe something with WeakReference...Thea
@PeterRitchie No, you're over thinking it. The easiest way to have a reference to a value is to use a class, as it's implementation is a reference to a value.Donnie
The proposed duplicate has nothing to do with this question...Rugose
R
14

The best solution I can think of for this is to store delegates in the dictionary that will allow you to retrieve and modify the variables.

Let’s start by declaring a type that contains a getter and a setter delegate:

sealed class VariableReference
{
    public Func<object> Get { get; private set; }
    public Action<object> Set { get; private set; }
    public VariableReference(Func<object> getter, Action<object> setter)
    {
        Get = getter;
        Set = setter;
    }
}

The dictionary would have the type:

Dictionary<string, VariableReference>

To store a variable, say foo of type string, in the dictionary, you’d write the following:

myDic.Add(key, new VariableReference(
    () => foo,                      // getter
    val => { foo = (string) val; }  // setter
));

To retrieve the value of a variable, you’d write

var value = myDic[key].Get();

To change the value of a variable to newValue, you’d write

myDic[key].Set(newValue);

This way, the variable that you’re changing is genuinely the original variable foo, and foo can be anything (a local variable, a parameter, a field on an object, a static field... even a property).

Putting this all together, this is what the class Test1 would look like:

class Test1
{
    Dictionary<string, VariableReference> MyDict = new Dictionary<string, VariableReference>();

    public void saveVar(string key, Func<object> getter, Action<object> setter)
    {
        MyDict.Add(key, new VariableReference(getter, setter));
    }

    public void changeVar(string key) // changing any of them
    {
        if (MyDict[key].Get() is int)
        {
            MyDict[key].Set((int)MyDict[key].Get() * 2);
        }
        else if (MyDict[key].Get() is string)
        {
            MyDict[key].Set("Hello");
        }
    }
}

// ...

Test1 t1 = new Test1();
int myInt = 3;
string myString = "defaultString";

Console.WriteLine(myInt);    // prints "3"
Console.WriteLine(myString); // prints "defaultString"

t1.saveVar("key1", () => myInt, v => { myInt = (int) v; });
t1.saveVar("key2", () => myString, v => { myString = (string) v; });

t1.changeVar("key1");
t1.changeVar("key2");

Console.WriteLine(myInt);    // actually prints "6"
Console.WriteLine(myString); // actually prints "Hello"
Rugose answered 20/6, 2014 at 14:25 Comment(7)
Thanks Timwi, this solved it. Just compiled your example and it works.Macarthur
While this works, it's a bit... complex. And it can be quite confusing as to how it works to people that don't understand how variable capture works in lambda's. Speaking of which, it seems like this may have problems with variable capture scoping if not used correctly...Dunson
@ErikFunkenbusch: Capturing a variable doesn’t change its scope. I think the beauty in this solution is that you don’t really need to understand lambda variable capturing; it really Just Works...Rugose
@Rugose - For instance, when using it in a loop, the getter may capture the value at the time of the first call, and each loop might use the same value over and over in some circumstances.Dunson
@ErikFunkenbusch: No, that is not correct. Lambda expressions do not capture values. They capture the variable.Rugose
Nice. I like it, but one thing I would do is make the delegate reference class generic with VariableReference<T> replacing all <object>s with <T>s. That way you can restrict the type if you want. Then you can simply do Dictionary<string, VariableReference<object>> later if you want to accept any type.Gherlein
And it looks like someone thought my suggestion too: #2256548Gherlein
D
3

Apart from the problem Kevin points out, you need to wrap your value types in some kind of reference type.

The problem, as you've figured out, is that generic types don't work with the ref keyword, and when you assign a new value type into your dictionary, it's replacing the reference with a different reference, not updating it. There is no way to retain the ref semantics once you assign it to the dictionary.

But, what you could do is something like this, simply wrap the value type in a reference type:

public class MyRef<T> {
    public T Ref {get;set;}
}

public class Test1
{
    Dictionary<string, object> MyDict = new Dictionary<string, object>();

    public void saveVar(string key, object v) 
    {
        MyDict.Add(key, v);
    }

    public void changeVar(string key, object newValue) //changing any of them
    {
        var ref1 = MyDict[key] as MyRef<int>;
        if (ref1 != null) {
            ref1.Ref = (int)newValue;
            return; // no sense in wasting cpu cycles
        }

        var ref2 = MyDict[key] as MyRef<string>;
        if (ref2 != null) {
            ref2.Ref = newValue.ToString();
        }
    }

    public void DoIt()
    {
        var v = new MyRef<int> { Ref = 1 };

        saveVar("First", v);
        changeVar("First", 2);

        Console.WriteLine(v.Ref.ToString()); // Should print 2
        Console.WriteLine(((MyRef<int>)MyDict["First"]).Ref.ToString()); // should also print 2
    }
}
Dunson answered 20/6, 2014 at 14:38 Comment(2)
This won't compile. You can't call Ref on an object, which you're doing on the value of your dictionary.Donnie
Servy - You're right, thanks, this fixes it.. compiled and test, it works.Dunson
D
0

A ref parameter's reference can not leave the scope of the method that calls it. This is because the variable that is reference cannot be guaranteed to be in scope after the method call has finished. You need to use a tool other than ref to create a layer of indirection allowing a variable a caller is using to be mutated.

Doing this is quite easy though. You simply need a class with a mutable member:

public class Pointer
{
    public object Value { get; set; }
}

You can now write:

class Test1
{
    IDictionary<string, Pointer> MyDict = new Dictionary<string, Pointer>();

    public void saveVar(string key, Pointer pointer) //storing the ref to an int
    {
        MyDict.Add(key, pointer);
    }

    public void changeVar(string key) //changing any of them
    {
        if (MyDict[key].Value.GetType() == typeof(int))
        {
            MyDict[key].Value = (int)(MyDict[key].Value) * 2;
        }
        if (MyDict[key].Value.GetType() == typeof(string))
        {
            MyDict[key].Value = "Hello";
        }
    }
}

Since you're now mutating a reference type that the caller also has a reference to, they can observe the change to its value.

Donnie answered 20/6, 2014 at 14:23 Comment(2)
The changevar code will never run because a typeof(Dict) will never equal a typeof(int) or a typeof(string)Kirkpatrick
@KevinCook Yes, you need to get the type of the value.Donnie
R
0

You can wrap a Dictionary<TKey, int> alongside a TValue[] in a class that implements IDictionary<TKey, TValue>. Use the inner dictionary to track where the data is stored in the inner array, and return refs to the inner array data where needed.

Here's an example implementation:

class RefDictionary<TKey, TValue> : IDictionary<TKey, TValue> where TValue : struct
{
    private Dictionary<TKey, int> indices = new();
    private TValue[] values = Array.Empty<TValue>();
    private Stack<int> freeIndices = new();

    public ref TValue this[TKey key]
    {
        get => ref values[indices[key]];
    }

    public Enumerator GetEnumerator() => new(this);
    IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        if (!freeIndices.TryPop(out var index))
        {
            index = indices.Count;
            if (index >= values.Length)
                Array.Resize(ref values, Mathf.Max(2, Mathf.NextPowerOfTwo(index + 1)));
        }
        indices.Add(item.Key, index);
        values[index] = item.Value;
    }

    public void Clear()
    {
        Array.Clear(values, 0, indices.Count + freeIndices.Count);
        indices.Clear();
        freeIndices.Clear();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return indices.TryGetValue(item.Key, out var index) && EqualityComparer<TValue>.Default.Equals(values[index], item.Value);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        foreach (var kv in indices)
            array[arrayIndex++] = new(kv.Key, values[kv.Value]);
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        if (indices.TryGetValue(item.Key, out var index) &&
            EqualityComparer<TValue>.Default.Equals(values[index], item.Value))
        {
            values[index] = default;
            indices.Remove(item.Key);
            freeIndices.Push(index);
            return true;
        }

        return false;
    }

    public int Count => indices.Count;
    public bool IsReadOnly => false;
    public void Add(TKey key, TValue value)
    {
        Add(new (key, value));
    }

    public bool ContainsKey(TKey key)
    {
        return indices.ContainsKey(key);
    }

    public bool Remove(TKey key)
    {
        if (indices.TryGetValue(key, out var index))
        {
            values[index] = default;
            indices.Remove(key);
            freeIndices.Push(index);
            return true;
        }

        return false;
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        if (indices.TryGetValue(key, out var index))
        {
            value = values[index];
            return true;
        }

        value = default;
        return false;
    }

    TValue IDictionary<TKey, TValue>.this[TKey key]
    {
        get => this[key];
        set => this[key] = value;
    }

    public ICollection<TKey> Keys => indices.Keys;
    public ICollection<TValue> Values => values;

    public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
    {
        private readonly RefDictionary<TKey, TValue> self;
        private Dictionary<TKey, int>.Enumerator e;

        public Enumerator(RefDictionary<TKey, TValue> self)
        {
            this.self = self;
            e = self.indices.GetEnumerator();
        }

        public bool MoveNext()
        {
            return e.MoveNext();
        }

        public void Reset()
        {
        }

        public KeyValuePair<TKey, TValue> Current => new(e.Current.Key, self.values[e.Current.Value]);

        object IEnumerator.Current => Current;

        public void Dispose()
        {
            e.Dispose();
        }
    }
}
Rudelson answered 18/6, 2024 at 19:15 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.