Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 985
haraldr Avatar asked Nov 06 '22 09:11

haraldr


2 Answers

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 });
like image 64
Hans Passant Avatar answered Nov 12 '22 13:11

Hans Passant


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?>();
like image 43
Roald Avatar answered Nov 12 '22 12:11

Roald