通用方法,取消装箱可以为空的枚举

时间:2010-08-30 15:44:14

标签: c# enums generics nullable unboxing

我做了以下扩展方法...

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

...我用于例如阅读这样的数据:

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

但是当我想从盒装值转换为可以为空的枚举时,它不起作用。我能想到的最好的是(凌乱的WIP代码不起作用):

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;
    }
}

用法:

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

当尝试将Int32转换为可为空的枚举时,当前错误是InvalidCastException。我猜这是好的,但我不知道我怎么能做到这一点?我试图创建一个可以为空的枚举T的实例并为其赋值,但我仍然坚持如何做到这一点。

任何人都有想法或更好的方法来解决这个问题?是否有可能以通用的方式解决这个问题?我已经做了很多搜索,但我没有找到任何有用的东西。

3 个答案:

答案 0 :(得分:3)

您可以通过调用所需的可空类型的构造函数来完成此操作。像这样:

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

答案 1 :(得分:1)

这里存在一个更普遍的问题,那就是您无法通过单次转换取消装箱和转换类型。 但是,关于取消装箱枚举的规则特别不一致。

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

接受的答案中的反射代码片段实际上并未创建Nullable 的实例,因为Invoke需要将其返回值装箱 并且将Nullable类型的非null实例装箱,就像它是基础类型的实例一样。 在这种情况下它仍然有效,因为它将int转换为SomeEnum,然后可以将其拆箱为SomeEnum?。

我们可以通过取消装箱之外的类型转换来解决一般问题。

这可以通过先对int进行拆箱而不是对其进行转换来完成,要使用通用类型参数作为目标来实现,您需要类似Cast here描述的类。

但是,在进行了一些实验之后,我发现仅使用动态广告就具有大致相同的性能:

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);
    }
}

以下是一些基准测试,以了解将int装箱到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 |      - |     - |     - |         - |

此解决方案还实现了通常可以通过转换实现的所有其他转换,例如:

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

答案 2 :(得分:0)

根据汉斯的回答,我能够让它运作起来,如果有人对此感兴趣,那就是固定版本:

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;
    }
}

但是可以为空的枚举的附加检查/代码会损害所有其他类型的性能。在扩展方法慢~~ 2-3之前,现在它是~10-15次。使用上面的代码执行1000000(百万)次:

拆箱:4毫秒
使用扩展方法取消装箱int:59ms(之前没有照顾可空的枚举:12ms)
拆箱到可以为空的枚举:5ms
使用扩展方法取消装箱到可以为空的枚举:3382ms

因此,看看这些数字,当性能至关重要时,这些方法不应该是首选 - 至少在将它用于可以为空的枚举时不是这样。