Método genérico, enum nullável
Pergunta
Eu fiz o seguinte método de extensão ...
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;
}
}
... que eu uso para por exemplo, dados de leitura como assim:
string field = datareader["column"].As("default value when null")
Mas não funciona quando eu quero lançar para uma enumeração anulável a partir de um valor em caixa. O melhor que eu poderia inventar foi esse (código WIP bagunçado que não funciona):
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;
}
}
Uso:
public enum SomeEnum {Value1, Value2};
object value = 1;
var result = value.As<SomeEnum?>();
O erro atual é um invalidcastException quando tenta lançar um int32 no enumeração anulável. O que é ok, eu acho, mas não tenho ideia de que outra forma eu poderia fazer isso? Tentei criar uma instância do enum Nullável e atribuir um valor a ele, mas estou preso sobre como exatamente isso pode ser feito.
Alguém é uma ideia ou uma maneira melhor de resolver isso? É possível resolver isso de maneira genérica? Eu fiz muita pesquisa nisso, mas não achei nada útil.
Solução
Você pode fazê -lo invocando o construtor para o tipo anulado necessário. Assim:
Type t = typeof(Nullable<>).MakeGenericType(lEnumType);
var ctor = t.GetConstructor(new Type[] { lEnumType });
return (T)ctor.Invoke(new object[] { pObject });
Outras dicas
Com a resposta de Hans, consegui fazê -lo funcionar e se alguém estiver interessado, aqui está a versão fixa:
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;
}
}
Mas as verificações/código adicionais para o enum nulo prejudicam o desempenho de todos os outros tipos. Antes do método de extensão ser ~ 2-3 mais lento, agora é ~ 10-15 vezes. Fazendo isso 1000000 (milhões) vezes usando o código acima:
Unboxing Int: 4ms
Unboxing int usando o método de extensão: 59ms (antes sem cuidar de enum Nullable: 12ms)
Unboxing to Nullable Enum: 5ms
Unboxing para enum nullável usando o método de extensão: 3382ms
Portanto, analisar esses números esses métodos não deve ser a primeira opção quando o desempenho é crítico - pelo menos não quando o usá para enumes anuláveis.