Convert.ChangeType()在可空类型上失败

352
我想将一个字符串转换为对象属性值,该属性的名称以字符串形式给出。我尝试按以下方式实现:
string modelProperty = "Some Property Name";
string value = "SomeValue";
var property = entity.GetType().GetProperty(modelProperty);
if (property != null) {
    property.SetValue(entity, 
        Convert.ChangeType(value, property.PropertyType), null);
}

问题出在当属性类型是可空类型时,这个代码会失败并抛出一个无效类型转换异常。这不是数值无法转换的情况 - 如果我手动进行转换它们是有效的(例如:DateTime? d = Convert.ToDateTime(value);)我看到了一些类似的问题,但仍然无法解决。


1
我正在使用PetaPoco 4.0.3的ExecuteScalar<int?>,但由于在第554行的return (T)Convert.ChangeType(val, typeof(T))产生的相同原因而失败。 - Larry
7个回答

482
未经测试,但也许像这样会起作用:
string modelProperty = "Some Property Name";
string value = "Some Value";

var property = entity.GetType().GetProperty(modelProperty);
if (property != null)
{
    Type t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;

    object safeValue = (value == null) ? null : Convert.ChangeType(value, t);

    property.SetValue(entity, safeValue, null);
}

14
我自己需要那段代码。感谢Nullable.GetUnderlyingType!在为需要它的项目构建一个简易版ModelBinder时,这对我帮助很大。我欠你一杯啤酒! - Maxime Rouiller
3
也许可以使用(value == null) ? default(t)代替(value == null) ? null?请注意,我会尽力使翻译通俗易懂,但不改变原意。 - threadster
似乎不能将唯一标识符转化为字符串。 - Anders Lindén
有没有特别的原因来创建safeValue变量而不是只重新赋值给value - jrnxf
2
@threadster,你不能在类型为'Type'的变量上使用默认运算符。请参考https://dev59.com/FnRC5IYBdhLWcg3wW_pk。 - andy250
如果您正在从数据库中获取数据,则在倒数第二行中可能希望将其与 DBNull.Value 进行比较,而不是 null - Ali Umair

82
你需要获取基础类型才能这样做...
尝试使用以下代码,我已经成功地在泛型中使用过:
//Coalesce to get actual property type...
Type t = property.PropertyType();
t = Nullable.GetUnderlyingType(t) ?? t;

//Coalesce to set the safe value using default(t) or the safe type.
safeValue = value == null ? default(t) : Convert.ChangeType(value, t);

我在代码中的许多地方都使用了它,一个例子是我用于以类型安全的方式转换数据库值的辅助方法:

public static T GetValue<T>(this IDataReader dr, string fieldName)
{
    object value = dr[fieldName];

    Type t = typeof(T);
    t = Nullable.GetUnderlyingType(t) ?? t;

    return (value == null || DBNull.Value.Equals(value)) ? 
        default(T) : (T)Convert.ChangeType(value, t);
}

调用方式:

string field1 = dr.GetValue<string>("field1");
int? field2 = dr.GetValue<int?>("field2");
DateTime field3 = dr.GetValue<DateTime>("field3");

我写了一系列博客文章,其中包括此文在内,可以在http://www.endswithsaurus.com/2010_07_01_archive.html查看(向下滚动到“补充说明”,@JohnMacintyre实际上发现了我原始代码中的错误,这使我走上了你现在要走的同样的道路)。自那篇文章以来,我进行了一些小修改,包括枚举类型的转换,因此,如果您的属性是枚举类型,仍然可以使用相同的方法调用。只需添加一行检查枚举类型的代码,您就可以像这样轻松使用:
if (t.IsEnum)
    return (T)Enum.Parse(t, value);

通常你会做一些错误检查或使用TryParse而不是Parse,但你已经有了大致的理解。


谢谢 - 我仍然缺少一步或者没有理解某些东西。我正在尝试设置一个属性值,为什么我得到的是它所在对象的基础类型?我也不确定如何从我的代码转换成像你的扩展方法那样的形式。我不知道类型将会是什么,以便执行类似于 value.Helper<Int32?>() 的操作。 - iboeno
@iboeno - 抱歉,我在开会所以无法帮助你联系到点。不过很高兴你已经找到了解决方案。 - BenAlabaster
1
在第一个代码块中,当t是一个变量(即一个值)时,你如何编写default(t)?你只能使用default()与(编译时)类型(可以是泛型类型)一起使用。 - Jeppe Stig Nielsen

12

即使对于可空类型,这也能完美地工作:

TypeConverter conv = TypeDescriptor.GetConverter(type);
return conv.ConvertFrom(value);

在使用 ConvertFrom() 之前,出于类型安全的考虑,您还应该调用 conv.CanConvertFrom(type) 方法。如果返回 false,则可以回退到使用 ChangeType 或其他方法。


很棒的答案 - 而且非常简单! - Lee Z
请注意,如果value不是底层类型 - 例如尝试将double转换为decimal? - NetMage
@NetMage 不确定我是否理解了你的评论。如果你在问类型是否无法转换,那么你可以通过调用 CanConvertFrom() 方法来验证它。这个方法已经在答案中给出了。 - Major
1
原始问题涉及将类型转换为“可空”类型 - 如果值的类型与“可空”类型的基础类型相同,则“ConvertFrom”才能起作用。因此,例如您可以将其用于decimal=>decimal?但不能用于double=>decimal?。这是否对OP很重要取决于他们的列中的类型与目标类字段类型。 - NetMage
@NetMage 抱歉,TypeDescriptor.GetConverter() 完美地将字符串转换为可空类型,正如 OP 明确要求的那样。了解其他限制是有帮助的,但与实际问题无关。 - undefined

10

这个示例有点长,但是这是一种相对强大的方法,可以将未知值转换为未知类型,并从中分离出了转换的任务。

我有一个TryCast方法,它也类似地考虑了可空类型。

public static bool TryCast<T>(this object value, out T result)
{
    var type = typeof (T);

    // If the type is nullable and the result should be null, set a null value.
    if (type.IsNullable() && (value == null || value == DBNull.Value))
    {
        result = default(T);
        return true;
    }

    // Convert.ChangeType fails on Nullable<T> types.  We want to try to cast to the underlying type anyway.
    var underlyingType = Nullable.GetUnderlyingType(type) ?? type;

    try
    {
        // Just one edge case you might want to handle.
        if (underlyingType == typeof(Guid))
        {
            if (value is string)
            {
                value = new Guid(value as string);
            }
            if (value is byte[])
            {
                value = new Guid(value as byte[]);
            }

            result = (T)Convert.ChangeType(value, underlyingType);
            return true;
        }

        result = (T)Convert.ChangeType(value, underlyingType);
        return true;
    }
    catch (Exception ex)
    {
        result = default(T);
        return false;
    }
}

当然,TryCast是一个带有类型参数的方法,因此要动态调用它,您需要自己构造MethodInfo:
var constructedMethod = typeof (ObjectExtensions)
    .GetMethod("TryCast")
    .MakeGenericMethod(property.PropertyType);

然后设置实际属性值:

public static void SetCastedValue<T>(this PropertyInfo property, T instance, object value)
{
    if (property.DeclaringType != typeof(T))
    {
        throw new ArgumentException("property's declaring type must be equal to typeof(T).");
    }

    var constructedMethod = typeof (ObjectExtensions)
        .GetMethod("TryCast")
        .MakeGenericMethod(property.PropertyType);

    object valueToSet = null;
    var parameters = new[] {value, null};
    var tryCastSucceeded = Convert.ToBoolean(constructedMethod.Invoke(null, parameters));
    if (tryCastSucceeded)
    {
        valueToSet = parameters[1];
    }

    if (!property.CanAssignValue(valueToSet))
    {
        return;
    }
    property.SetValue(instance, valueToSet, null);
}

还有与属性.CanAssignValue处理相关的扩展方法...

public static bool CanAssignValue(this PropertyInfo p, object value)
{
    return value == null ? p.IsNullable() : p.PropertyType.IsInstanceOfType(value);
}

public static bool IsNullable(this PropertyInfo p)
{
    return p.PropertyType.IsNullable();
}

public static bool IsNullable(this Type t)
{
    return !t.IsValueType || Nullable.GetUnderlyingType(t) != null;
}

7

我有类似的需求,而来自LukeH的答案指引了我方向。我设计了这个通用函数,使其易于使用。

    public static Tout CopyValue<Tin, Tout>(Tin from, Tout toPrototype)
    {
        Type underlyingT = Nullable.GetUnderlyingType(typeof(Tout));
        if (underlyingT == null)
        { return (Tout)Convert.ChangeType(from, typeof(Tout)); }
        else
        { return (Tout)Convert.ChangeType(from, underlyingT); }
    }

使用方法如下:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

请注意,第二个参数仅用作原型来展示函数如何转换返回值,因此它实际上不必是目标属性。这意味着您也可以像这样做:

        DateTime? source = new DateTime(2015, 1, 1);
        var dest = CopyValue(source, (string)null);

我这样做是因为使用 out 无法与属性一起使用。当前的代码可以适用于属性和变量。如果需要,你还可以创建重载函数来传递类型参数。

2

我是这样做的

public static List<T> Convert<T>(this ExcelWorksheet worksheet) where T : new()
    {
        var result = new List<T>();
        int colCount = worksheet.Dimension.End.Column;  //get Column Count
        int rowCount = worksheet.Dimension.End.Row;

        for (int row = 2; row <= rowCount; row++)
        {
            var obj = new T();
            for (int col = 1; col <= colCount; col++)
            {

                var value = worksheet.Cells[row, col].Value?.ToString();
                PropertyInfo propertyInfo = obj.GetType().GetProperty(worksheet.Cells[1, col].Text);
                propertyInfo.SetValue(obj, Convert.ChangeType(value, Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType), null);

            }
            result.Add(obj);
        }

        return result;
    }

1

感谢 @LukeH
我做了一点改动:

public static object convertToPropType(PropertyInfo property, object value)
{
    object cstVal = null;
    if (property != null)
    {
        Type propType = Nullable.GetUnderlyingType(property.PropertyType);
        bool isNullable = (propType != null);
        if (!isNullable) { propType = property.PropertyType; }
        bool canAttrib = (value != null || isNullable);
        if (!canAttrib) { throw new Exception("Cant attrib null on non nullable. "); }
        cstVal = (value == null || Convert.IsDBNull(value)) ? null : Convert.ChangeType(value, propType);
    }
    return cstVal;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接