Reputation: 33
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
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