5

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

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

当前错误是 InvalidCastException,当它尝试将 Int32 强制转换为可为空的枚举时。我猜这没问题,但我不知道我还能怎么做?我试图创建一个可为空的枚举 T 的实例并为其分配一个值,但我对如何做到这一点感到困惑。

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

4

3 回答 3

2

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

            Type t = typeof(Nullable<>).MakeGenericType(lEnumType);
            var ctor = t.GetConstructor(new Type[] { lEnumType });
            return (T)ctor.Invoke(new object[] { pObject });
于 2010-08-30T16:51:44.710 回答
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<SomeEnum> 的实例,因为 Invoke 需要对其返回值进行装箱,并且将 Nullable 类型的非空实例装箱,就好像它是基础类型的实例一样。在这种情况下它仍然有效,因为它将 int 转换为 SomeEnum 然后可以将其拆箱为 SomeEnum?。

除了拆箱之外,我们还可以通过允许类型转换来解决一般问题。

这可以通过首先拆箱 int 然后进行转换来完成,要使用泛型类型参数作为目标,您需要类似于此处描述的类 CastTo 的东西。

然而,在运行了一些实验后,我发现仅使用动态具有大致相同的性能:

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?>();
于 2020-11-13T12:20:17.910 回答
-1

随着汉斯的回答,我能够让它工作,如果有人感兴趣,这里是固定版本:

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(百万)次:

拆箱 int:4 毫秒
使用扩展方法拆箱 int:59 毫秒(在不考虑可空枚举之前:12 毫秒)
拆箱到可空枚举:5 毫秒
使用扩展方法拆箱到可空枚举:3382 毫秒

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

于 2010-08-31T10:38:49.210 回答