Convert.ChangeType () завершается неудачно для типов Nullable

301

Я хочу преобразовать строку в значение свойства объекта, имя которого у меня есть в виде строки. Я пытаюсь сделать это так:

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

Проблема заключается в том, что происходит сбой и генерируется исключение недопустимого приведения, когда тип свойства имеет тип NULL. Это не тот случай, когда значения невозможно преобразовать - они будут работать, если я сделаю это вручную (например, DateTime? d = Convert.ToDateTime(value);я видел несколько похожих вопросов, но все еще не могу заставить их работать).

iboeno
источник
1
Я использую ExecuteScalar <int?> С PetaPoco 4.0.3, и он не работает по той же причине: return (T) Convert.ChangeType (val, typeof (T)) в строке 554
Ларри

Ответы:

409

Не проверено, но, возможно, что-то вроде этого будет работать:

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);
}
LukeH
источник
12
Мне просто нужен был этот кусок кода. Спасибо за Nullable.GetUnderlyingType! Очень помог мне, когда я построил ModelBinder для бедного человека для проекта, который нуждался в этом. Я должен тебе пива!
Максим Роиллер
3
Может вместо (value == null) ? nullиспользования (value == null) ? default(t)?
Автор темы
Кажется, не работает для uniqueidentifier к строке.
Андерс Линден
Есть ли конкретная причина для создания safeValueпеременной, а не просто для переназначения value?
Колорадоколби,
@threadster Нельзя использовать оператор по умолчанию для переменной типа «Тип». См stackoverflow.com/questions/325426/...
andy250
75

Вы должны получить базовый тип, чтобы сделать это ...

Попробуйте это, я успешно использовал это с дженериками:

//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 фактически обнаружил ошибку в моем исходном коде, которая привела меня по тому же пути, по которому вы сейчас). С тех пор у меня есть несколько небольших модификаций, которые включают преобразование типов enum, поэтому, если ваше свойство является Enum, вы все равно можете использовать тот же вызов метода. Просто добавьте строку, чтобы проверить типы перечислений, и вы приступите к гонкам, используя что-то вроде:

if (t.IsEnum)
    return (T)Enum.Parse(t, value);

Обычно у вас есть некоторая проверка ошибок или использование TryParse вместо Parse, но вы получите картину.

BenAlabaster
источник
Спасибо - я все еще пропускаю шаг или не понимаю что-то. Я пытаюсь установить значение свойства, почему я получаю объект, который находится в базовом типе? Я также не уверен, как перейти от моего кода к методу расширения, подобному вашему. Я не буду знать, какой будет тип, чтобы сделать что-то вроде value.Helper <Int32?> ().
Ибоено
@iboeno - Извините, я был на совещании, поэтому я не мог помочь вам соединить точки. Рад, что у вас есть решение, хотя.
BenAlabaster
9

Это немного длинновато для примера, но это относительно надежный подход, который отделяет задачу приведения от неизвестного значения к неизвестному типу.

У меня есть метод 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;
}
bopapa_1979
источник
6

У меня была похожая потребность, и ответ от 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, потому что вы не можете использовать со свойствами. Как есть, он может работать со свойствами и переменными. Вы также можете создать перегрузку для передачи типа, если хотите.

Стив в Колорадо
источник
0

Спасибо @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;
}
hs586sd46s
источник
0

Я сделал это таким образом

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;
    }
AnishJain87
источник