tags:

views:

250

answers:

5

(This may be a dupe, I can't imagine someone hasn't already asked this, I probably haven't worked out the keywords I need)

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?

+4  A: 

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.

Jon Skeet
I'm not looping on this one, so I doubt performance will be a problem.
Benjol
I just did a little test of GetOrDefault against GetInt, the difference is fairly dramatic, for a million iterations, but still negligable for my current needs.
Benjol
+1  A: 

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]);
Marc Gravell
Well, I don't know because I don't have access to all implementations of the interface. Is there any difference using ChangeType (which returns object) to just boxing as Jon suggested?
Benjol
See edit - but if you don't control the interface it isn't an option, I expect...
Marc Gravell
+1  A: 

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);
Anton Gogolev
Nice, doesn't work for the interface I mentioned, but I've just discovered another one which *does* have GetAsObject().
Benjol
A: 

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...)

Benjol
A: 

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.

csaam
Yeh, I had spotted the null problem, currently I am only using this for structs. However, I believe that nothing prevents you including the type parameter explicitly to work round this limitation. map.GetOrDefault<double?>("dibble", null);
Benjol