开发者

Generic method, unboxing nullable enum

I've made the following extension method ...

public static class ObjectExtensions
{
    public static T As<T>(this object pObject, T pDefaultValue)
    {
        if (pObject == null || pObject == DBNull.Value)
            return pDefaultValue;
  开发者_StackOverflow      return (T) pObject;
    }
}

... which i use for e.g. reading data like so:

string field = datareader["column"].As("default value when null")

But it doesn't work when i want to cast to a nullable enum from a boxed value. The best i could come up with was this (messy WIP code which doesn't work):

public static class ObjectExtensions
{
    public static T As<T>(this object pObject, T pDefaultValue)
    {
        if (pObject == null || pObject == DBNull.Value)
            return pDefaultValue;

        var lType = typeof (T);

        if (!IsNullableEnum(lType))
            return (T) pObject;

        var lEnumType = Nullable.GetUnderlyingType(lType);
        var lEnumPrimitiveType = lEnumType.GetEnumUnderlyingType();

        if (lEnumPrimitiveType == typeof(int))
        {
            var lObject = (int?) pObject;
            return (T) Convert.ChangeType(lObject, lType);
        }

        throw new InvalidCastException();
    }

    private static bool IsNullableEnum(Type pType)
    {
        Type lUnderlyingType = Nullable.GetUnderlyingType(pType);
        return (lUnderlyingType != null) && lUnderlyingType.IsEnum;
    }
}

Usage:

public enum SomeEnum {Value1, Value2};
object value = 1;
var result = value.As<SomeEnum?>();

The current error is an InvalidCastException when it tries to cast an Int32 to the nullable enum. Which is ok i guess, but i've no idea how else i could do that? I've tried to create an instance of the nullable enum T and assign it a value, but i'm stuck on how exactly this can be done.

Anyone an idea or a better way to solve this? Is it even possible to solve that in a generic way? I've done quite a lot of searching on that, but i've not found anything useful.


You can do it by invoking the constructor for the nullable type you need. Like this:

            Type t = typeof(Nullable<>).MakeGenericType(lEnumType);
            var ctor = t.GetConstructor(new Type[] { lEnumType });
            return (T)ctor.Invoke(new object[] { pObject });


There's a more general problem here which is that you can't unbox and convert a type with a single cast. However the rules around unboxing enums specifically are a bit inconsistent.

(SomeEnum) (object) SomeEnum.Value1; // OK (as expected)
(SomeEnum?) (object) SomeEnum.Value1; // OK (as expected)
(SomeEnum) (object) 1; // OK (this is actually allowed)
(SomeEnum?) (object) 1; // NOPE (but then this one is not)

The reflection snippet in the accepted answer doesn't actually create an instance of Nullable<SomeEnum> because Invoke needs to box its return value and a non null instance of a Nullable type is boxed as if it were an instance of the underlying type. It still works in this case because it converts the int to SomeEnum which can then be unboxed to SomeEnum?.

We can solve the general problem by permitting type conversions in addition to unboxing.

This can be done by unboxing the int first and than casting it, to do that with a generic type parameter as a target you need something like the class CastTo described here.

However after running some experiments I found that just using dynamic has about the same performance:

public static T As<T>(this object pObject, T pDefaultValue = default)
{
    if (pObject == null || pObject == DBNull.Value)
    {
        return pDefaultValue;
    }

    // You can fine tune this for your application,
    // for example by letting through types that have implicit conversions you want to use.
    if (!typeof(T).IsValueType)
    {
        return (T) pObject;
    }

    try
    {
        return (T) (dynamic) pObject;
    }
    // By using dynamic you will get a RuntimeBinderException instead of 
    // an InvalidCastExeption for invalid conversions.
    catch (RuntimeBinderException ex)
    {
        throw new InvalidCastException(ex.Message);
    }
}

These are some benchmarks to get an idea about the performance differences between the different ways to unbox an int to SomeEnum?:

|     Method |      Mean |    Error |   StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------- |----------:|---------:|---------:|-------:|------:|------:|----------:|
|    Casting |  12.07 ns | 0.004 ns | 0.003 ns |      - |     - |     - |         - |
| Reflection | 374.03 ns | 2.009 ns | 1.879 ns | 0.0267 |     - |     - |     112 B |
|     CastTo |  16.16 ns | 0.016 ns | 0.014 ns |      - |     - |     - |         - |
|    Dynamic |  17.45 ns | 0.023 ns | 0.020 ns |      - |     - |     - |         - |

This solution also enables all other conversion that can usually be achieved by casting, e.g.:

var charVal = (object) 'A';
charVal.As<int?>();


With Hans' answer i was able to get it working and if anyone is interested here's the fixed version:

public static class ObjectExtensions
{
    private static Dictionary<Type, ConstructorInfo> _NullableEnumCtor = new Dictionary<Type, ConstructorInfo>();

    public static T As<T>(this object pObject)
    {
        return As(pObject, default(T));
    }

    public static T As<T>(this object pObject, T pDefaultValue)
    {
        if (pObject == null || pObject == DBNull.Value)
            return pDefaultValue;

        var lObjectType = pObject.GetType();
        var lTargetType = typeof(T);

        if (lObjectType == lTargetType)
            return (T) pObject;

        var lCtor = GetNullableEnumCtor(lTargetType);
        if (lCtor == null)
            return (T) pObject;

        return (T)lCtor.Invoke(new[] { pObject });
    }

    private static ConstructorInfo GetNullableEnumCtor(Type pType)
    {
        if (_NullableEnumCtor.ContainsKey(pType))
            return _NullableEnumCtor[pType];

        var lUnderlyingType = Nullable.GetUnderlyingType(pType);
        if (lUnderlyingType == null || !lUnderlyingType.IsEnum)
        {
            lock (_NullableEnumCtor) { _NullableEnumCtor.Add(pType, null); }
            return null;
        }

        var lNullableType = typeof(Nullable<>).MakeGenericType(lUnderlyingType);
        var lCtor = lNullableType.GetConstructor(new[] { lUnderlyingType });

        lock (_NullableEnumCtor) { _NullableEnumCtor.Add(pType, lCtor); }
        return lCtor;
    }
}

But the additional checks/code for the nullable enum hurts performance for all other types. Before the extension method was ~2-3 slower, now it's ~10-15 times. Doing it 1000000 (million) times using the code above:

Unboxing int: 4ms
Unboxing int using extension method: 59ms (before without taking care of nullable enum: 12ms)
Unboxing to nullable enum: 5ms
Unboxing to nullable enum using extension method: 3382ms

So, looking at these numbers these methods shouldn't be the first choice when performance is critical - at least not when using it for nullable enums.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜