问题
I have an interface for a creaky property-map:
interface IPropertyMap
{
bool Exists(string key);
int GetInt(string key);
string GetString(string key);
//etc..
}
I want to create an extension method like so:
public static T GetOrDefault<T>(this IPropertyMap map, string key, T defaultValue)
{
if (!map.Exists(key))
return defaultValue;
else
{
if (typeof(T) == typeof(int)) return (T)map.GetInt(key);
//etc..
}
}
But the compiler won't let me cast to T. I tried adding where T : struct but that doesn't seem to help.
What am I missing?
回答1:
I believe this is because the compiler doesn't know what type of operation it needs to perform. IIRC, you can get it to work if you introduce boxing:
if (typeof(T) == typeof(int)) return (T)(object)map.GetInt(key);
but that's not ideal in terms of performance.
I think it's just a limitation of generics, unfortunately.
回答2:
What do GetInt, GetString etc do internally? There may be other options involving Convert.ChangeType(...) or TypeDescriptor.GetConverter(...).ConvertFrom(...), and a single cast, using an "object" indexer:
for example, if the objects are already correctly typed:
public T GetOrDefault<T>(this IPropertyMap map, string key, T defaultValue)
{
return map.Exists(key) ? (T)map[key] : defaultValue;
}
or if they are stored as strings and need conversion, something involving:
T typedVal = (T) TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(map[key]);
回答3:
I suppose it's just a typo, but bool GetInt(string key) seems weird. It should be int GetInt(string key), or better yet int GetInt32(string key).
Next, Jon has already noted that boxing is required for your code to work, so this is what you do.
And finally, add a "catch-all" method to your IPropertyMap interface -- say object GetValue(string key) and then rewrite GetOrDefault<T> to utilize this method instead of endless and error prone Type comparisons:
else
return (T)(object)map.GetValue(key);
回答4:
Just for reference, I discovered another interface which does have GetType() and GetAsObject() methods, which allows me to integrate elements of these answers to do this:
public static T GetOrDefault<T>(this IInfosContainer container, string key, T defaultValue)
{
//I just read p273 of C# in Depth, +1 Jon Skeet :)
if (container == null) throw new ArgumentNullException("container");
if (container.Exist(key))
{
if (container.GetType(key) != typeof(T))
throw new ArgumentOutOfRangeException("key",
"Key exists, but not same type as defaultValue parameter");
else
return (T)container.GetAsObject(key);
}
else
return defaultValue;
}
(Purists will note that I'm not of the 'braces for one statement' school...)
回答5:
I don't think this is a good method. You have no way of controlling what T is. For instance
float value = map.GetOrDefault("blah", 2.0);
won't compile either because Cannot implicitly convert type 'double' to 'float'. An explicit conversion exists (are you missing a cast?) Another failure point is when the desired default value is null. This methods leaves the dev at the mercy of the compiler to resolve what it thinks the dev intends.
If you can change the interface then add a GetObject method. Since you are using an extension method I assume you can't so cast to an object then to int. Either way change the method to look like
public static void GetOrDefault(this IPropertyMap map, string key, ref T value) { if (map.Exists(key)) { if (typeof(T) == typeof(int)) { value = (T)(object)map.GetInt(key); } value = default(T); // this is just a nicety because I am lazy, // add real code here. } } and call like this
PropertyMap map = new PropertyMap();
float value = 2.0f;
map.GetOrDefault("blah", ref value);
I hate ref params but I see the point here. The registry is a classic example of when this kind of method is usefull. The code above forces the dev user to explicity specify the type of the output and preserves the concept default value.
来源:https://stackoverflow.com/questions/856723/casting-value-to-t-in-a-generic-method