Search Unity

[SOLVED] (C#) Strange reference-like behaviour with dictionaries.

Discussion in 'Scripting' started by Gunging, Jan 2, 2020.

  1. Gunging

    Gunging

    Joined:
    Sep 6, 2016
    Posts:
    139
    You probably know about references, that they stand for a variable somewhere, and when any operations are performed on them, the variable somewhere is affected.

    Well, I have a rather strange problem; Changing a value of an entry in one dictionary is changing the corresponding values of entries in other two dictionaries.
    All dictionaries have the same Key (enum) and Value (int) types. *enum is "GSB_GeneralPart"

    This is how its happening:
    Code (CSharp):
    1.  
    2. // Gets this' class Animation Sector
    3. AnimationSector gAsector = GetComponent<AnimationSector>();
    4.  
    5. // Creates a new test Dictionary, equal to the Global Dictionary
    6. Dictionary<GSB_GeneralPart, int> testFix = GlobalStyleBytes.style_karyotype;
    7.  
    8. // What are the original (default) values of the "Stripes Material" entry of these three dictionaries?
    9. Debug.Log("Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] + ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] + ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]);
    10.  
    11. // Modify only the value of the Dictionary in the 'local' AnimationSector
    12. gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] = (int)GSB_ShipStyle.GrayscaleGradient;
    13.  
    14. // How do all the values look now?
    15. Debug.Log("Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] + ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] + ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]);
    16.  
    17. // Modify only the global dictionary value
    18. GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] = (int)GSB_ShipStyle.TomatoGradient;
    19.  
    20. // How have all the values been affected?
    21. Debug.Log("Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] + ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] + ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]);
    22.  
    23. // Modify only the value of the test Dictionary
    24. testFix[GSB_GeneralPart.StripesMaterial_3DS] = (int)GSB_ShipStyle.CitrusGradient;
    25.  
    26. // How have all the values been affected?
    27. Debug.Log("Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] + ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] + ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]);
    Always logs the following four lines:
    upload_2020-1-1_23-56-2.png
    The values of the three dictionaries are the same always. When I changed the value in one of them, the other two also changed. The reason they are separate is to have different values in all of them...

    It is very dense so here is a
    Code (CSharp):
    1. Debug.Log("Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS] + ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS] + ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]);
    Code (CSharp):
    1. "Global: " + (GSB_ShipStyle)GlobalStyleBytes.style_karyotype[GSB_GeneralPart.StripesMaterial_3DS]
    This is reading the style of the stripes on the exterior of the spaceship, because the values the player chose are stored 'globally' (so they sync across scenes).
    • GlobalStyleBytes is a static class, stores a variety of values relating to the player's customizable spaceship.
    • style_karyotype is a Dictionary<GSB_GeneralPart, int>. Relates a part of the spaceship to the 'style' the player chose for it.
    • GSB_GeneralPart is an enum so that 'numbers have meaning'. For example, "StripesMaterial_3DS" (the material of the 'stripes' on the exterior of the spaceship) stands for "7".
    • GSB_ShipStyle is another enum. For example, "GrayscaleGradient" (which would be the color of a texture) stands for "0".
    Produces the output, "Global: CottonCandyIceCreamGradient" the first time it runs, before any changes are made anywhere. "CottonCandyIceCreamGradient" is the default value (and is from pink to sky blue).

    Code (CSharp):
    1. ", Local: " + (GSB_ShipStyle)gAsector.style_karyotype_override[GSB_GeneralPart.StripesMaterial_3DS]
    This is reading the style of the stripes on the exterior of this very particular spaceship. The values stored in there actually define how this spaceship will look in-game (note that, in AnimationSector.Start(), the line style_karyotype_override = GlobalStyleBytes.style_karyotype; is found)
    • AnimationSector is just a class, when fed values for its own "style_karyotype_override" it actually applies them to the spaceship in-game. That way, I can feed it values other than those chose by the player for enemy spaceships and NPCs (called animation sector because some textures and models are animated).
    • style_karyotype_override is a Dictionary<GSB_GeneralPart, int>. I called it 'override' at 3am that day, but it is not really an override of anything.
    Produces the output, ", Local: CottonCandyIceCreamGradient" the first time it runs, before any changes are made anywhere.


    Code (CSharp):
    1. ", Test: " + (GSB_ShipStyle)testFix[GSB_GeneralPart.StripesMaterial_3DS]
    This is reading the value of a test Dictionary, declared just before the debug lines begin.
    Produces the output, ", Test: CottonCandyIceCreamGradient" the first time it runs, before any changes are made anywhere.

    It seems that making these dictionaries equal the Global (as shown in the following line of code) is linking their values in some way, so that changing the value of one changes the values everywhere else. Which is undesirable, if I wanted this behaviour I would use references or pointers.
    Code (CSharp):
    1. testFix = GlobalStyleBytes.style_karyotype;
    Any idea of why this would be happening?

    EDIT: Indeed, it is caused by making Dictionaries equal to each other using that line above.
    Making testFix have the same initial values as GlobalStyleBytes.style_karyotype the following way causes them the be edited separately as they are supposed to:
    Code (CSharp):
    1.        // Just another dictionary, nothing similar to anything else before
    2.         Dictionary<GSB_GeneralPart, int> testFix = new Dictionary<GSB_GeneralPart, int>();
    3.  
    4.         // Time to copy every entry from the Global Dictionary
    5.         foreach (KeyValuePair<GSB_GeneralPart, int> p in GlobalStyleBytes.style_karyotype) {
    6.             GSB_GeneralPart observedKey;
    7.             int observedInt;
    8.  
    9.             observedKey = p.Key;
    10.             observedInt = p.Value;
    11.  
    12.             testFix.Add(observedKey, observedInt);
    13.         }
    Log Output:

    The third "Test" Dictionary value is independent of the other two.
     
    Last edited: Jan 2, 2020
  2. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    It's nothing special with Dictionary, a Dictionary is a class and in C# all classes (instances) are handled as references.
    (I don't want to sound rude, but I wouldn't call this "advanced C#", it's one of the fundamental understanding how the language works)

    Code (CSharp):
    1. // Creates a new test Dictionary, equal to the Global Dictionary
    2. Dictionary<GSB_GeneralPart, int> testFix = GlobalStyleBytes.style_karyotype;
    Your comment is wrong, you don't create a new Dictionary, you are just assigning the Dictionary (reference) to a new variable, which means that both variables will contain (reference) the same Dictionary instance, which is why all your Dictionary variables return the changed values.

    If you want to create a new Dictionary instance you will have to use the new keyword. Dictionary has a constructor which takes an other Dictionary as argument so you don't have to use a foreach to copy the values, because the constructor will handle this for you.

    If you want to learn more about this topic you can google something like "C# value vs reference type" there should be some good posts which explain it in more detail.
     
    Last edited: Jan 2, 2020
    Gunging likes this.
  3. Gunging

    Gunging

    Joined:
    Sep 6, 2016
    Posts:
    139
    Oh, that does make a lot of sense...

    Pardon my ignorance. Didn't know I could use the constructor, thank you very much.
     
    Last edited: Jan 6, 2020