ShivaFang
ShivaFang

Reputation: 33

Getting a specifically typed Dictionary with a generic method c#

First off - I appologize if this is a duplicate question. I did search for the answer, however I'm a self-taught programmer and I don't understand some of the higher-level terms used and so may have comletely missed the actual answer.

Now, my question; I'm trying to use a Generic method to return a typed dictionary. Here is the code I'm using.

public Dictionary<string, WorkerValues>     workerValues        = new Dictionary<string, WorkerValues>();
public Dictionary<string, ValueRefValues>   valueRefValues      = new Dictionary<string, ValueRefValues>(); 
public Dictionary<string, LevelValues>      levelValues         = new Dictionary<string, LevelValues>();

public static T getGainer<T>(string value) where T : GainerValues, new() {
return current.getGainerSub<T>(value);  // current is a singleton instance of the class.
}

public static void setGainer<T>(string value, T gain) where T : GainerValues,     new() {
    current.setGainerSub<T>(value, gain);
}

public T getGainerSub<T>(string value) where T : GainerValues, new() {
    Dictionary<string, T> table = getTable <T>();
    if (!table.ContainsKey(value)){
        table.Add (value, new T());
        table[value].setID(value);
    }
    return (T) table[value];
}

public Dictionary<string, T> getTable<T>() where T : GainerValues, new(){
    if (typeof(T) == typeof(WorkerValues))      return workerValues;     //  Error Here
    if (typeof(T) == typeof(ValueRefValues))    return valueRefValues;   //  Error Here
    if (typeof(T) == typeof(LevelValues))       return levelValues;      //  Error Here
    return null;
}

If T is WokerValues, the Dictionary Returned is <string, WorkerValues>, However I get an error when compiling;

"c:\Users\Aquamentos Games\Documents\Idle Artificer\Assets\Scripts\Data.cs(51,51): Error CS0029: Cannot implicitly convert type 'System.Collections.Generic.Dictionary<string,IdleArtificer.WorkerValues>' to 'System.Collections.Generic.Dictionary<string,T>' (CS0029) (Assembly-CSharp)"

The compiler doesn't know that 'T' will be 'WokerValues' when I'm trying to return that value.

Originally I had the dictionaries typed as <string, GainerValues>, which worked. However, for reasons specific to my implementation I had to change them (it has to do with the way I am loading and saving data between sessions - the dictionaries need to be the appropriate Type.)

Alternatively, there a better way to do this? (Keeping in mind that I need the Dictionary types to be the same as here, Dictionary<string, GainerValues> works in the code but it doesn't work for the Saving/Loading API I am using.

I think this thread is similar to my question; Get Key and Value types from dictionary in generic method However, he seems to be trying to do something slightly different (or I don't understand his example code or the answers)

I thought about converting the Dictionary using the answer here; Converting a Dictionary from one type to another However if I am unsure if that is the 'same dictionary' or if it is a new dictionary (IE - would I have to convert it back and change the dictionary entirely every time I need to add a new key)

EDIT Here is the current WORKING version of the entire class, with the casting changes suggested in one answer.

using System;
using System.Collections;
using System.Collections.Generic;

namespace IdleArtificer{
    public class Data {
        public static Data current = new Data();    

        public Data(){
            // constructor with no parameters needed for I/O API
            // also why where T : new() in generic methods
        }

        public Dictionary<string, WorkerValues>     workerValues        = new Dictionary<string, WorkerValues>();
        public Dictionary<string, ValueRefValues>   valueRefValues      = new Dictionary<string, ValueRefValues>();
        public Dictionary<string, LevelValues>      levelValues         = new Dictionary<string, LevelValues>();

        public static T getGainer<T>(string value) where T : GainerValues, new() {
            return current.getGainerSub<T>(value);
        }

        public static void setGainer<T>(string value, T gain) where T : GainerValues, new() {
            current.setGainerSub<T>(value, gain);
        }

        public Dictionary<string, T> getTable<T>() where T : GainerValues, new(){
            if (typeof(T) == typeof(WorkerValues))      return workerValues as Dictionary<string, T>;
            if (typeof(T) == typeof(ValueRefValues))    return valueRefValues as Dictionary<string, T>;
            if (typeof(T) == typeof(LevelValues))       return levelValues as Dictionary<string, T>;
            return null;
        }

        public T getGainerSub<T>(string value) where T : GainerValues, new() {
            Dictionary<string, T> table = getTable <T>();
            if (!table.ContainsKey(value)){
                table.Add (value, new T());
                table[value].setID(value);
            }
            return (T) table[value];
        }

        public void setGainerSub<T>(string value, T gain) where T : GainerValues, new() {
            Dictionary<string, T> table = getTable <T>();
            if (table.ContainsKey(value))           {   table[value] = gain; }
            else                                    {   table.Add(value, gain);}
        }
//I can't save enums, so I convert them to/from strings.  Int might have 
//been better, but I've been changing the order of my enums to sort them, so 
//that would mess up any saved data if I make any changes to the order.
        public static T ParseEnum<T>(string value) where T : struct, IConvertible
        {
            if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type");
            return (T) Enum.Parse(typeof(T), value);
        }

        public static WorkerValues getWorkerValues(string s){
            return getGainer<WorkerValues>(s);
        }
        public static ValueRefValues getValueRefValues(string s){
            return getGainer<ValueRefValues>(s);
        }
        public static LevelValues getLevelValues(string s){
            return getGainer<LevelValues>(s);
        }

        public static WorkerValues getWorkerValues(resource res){
            return getWorkerValues(res.ToString());
        }
        public static ValueRefValues getValueRefValues(valueRef res){
            return getValueRefValues(res.ToString());
        }
        public static LevelValues getLevelValues(level res){
            return getLevelValues(res.ToString());
            }


    }
}

Upvotes: 1

Views: 171

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476557

First of all, this code shows all signs of bad code design (bad smells), it is adviseable to refactor it.

So the problem is that C# can't derive at compile time that when you type:

if (typeof(T) == typeof(WorkerValues))      return workerValues;

that a dictionary Dictionary<String,WorkerValues> is actually also a Dictionary<String,T>. This might look weird, but complex reasoning about code is in many cases not what a compiler is supposed to do. Humans reason like "Haa, the if statement succeeds, so now we know T is guaranteed to be a WorkerValues". A compiler only sees "If that statement succeeds - I don't know what it means - the program is supposed to return that.". Compilers (and code contract verifiers exist that can perform a more advanced analysis, but the C# standard is not to do that).

You can't use Linq's ToDictionary method, because that would make a copy of the dictionary. This is expensive and adding elements will not get reflected to the original dictionary. You can however use a cast:

public Dictionary<string, T> getTable<T>() where T : GainerValues, new(){
    if (typeof(T) == typeof(WorkerValues))      return workerValues as Dictionary<string,T>;
    if (typeof(T) == typeof(ValueRefValues))    return valueRefValues as Dictionary<string,T>;
    if (typeof(T) == typeof(LevelValues))       return levelValues as Dictionary<string,T>;
    return null;
}

(add as Dictionary<string,T> after every return statement.)

The as operator will perform a "safe" cast in the sense that if the can't be converted to a Dictionary<string,T> it will return null. But as said before, this kind of mixed compile-time and run-time generics are a source of a lot of trouble.

For this example, it will work, but it is not good design to perform such operations.

Upvotes: 2

Related Questions