Convert.ChangeType () kończy się niepowodzeniem w przypadku typów dopuszczających wartości zerowe


301

Chcę przekonwertować ciąg na wartość właściwości obiektu, którego nazwę mam jako ciąg. Próbuję to zrobić w ten sposób:

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

Problem polega na tym, że zawodzi i generuje niepoprawny wyjątek rzutowania, gdy typ właściwości jest typem zerowym. Nie dzieje się tak w przypadku, gdy wartości nie można przekonwertować - zadziałają, jeśli zrobię to ręcznie (np. DateTime? d = Convert.ToDateTime(value);) Widziałem podobne pytania, ale nadal nie mogę ich uruchomić.


1
Używam ExecuteScalar <int?> Z PetaPoco 4.0.3 i nie działa z tego samego powodu: return (T) Convert.ChangeType (val, typeof (T)) w linii 554
Larry

Odpowiedzi:


409

Nie przetestowano, ale może coś takiego będzie działać:

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

12
Sam potrzebowałem tego fragmentu kodu. Dzięki za Nullable.GetUnderlyingType! Bardzo mi pomógł, kiedy zbudowałem ModelBinder biednego człowieka dla projektu, który go potrzebował. Jestem ci winien piwo!
Maxime Rouiller,

3
Może zamiast (value == null) ? nullużywać (value == null) ? default(t)?
Threadster

Wydaje się, że nie działa na unikatowy identyfikator.
Anders Lindén,

Czy istnieje jakiś szczególny powód, aby utworzyć safeValuezmienną, a nie tylko zmienić przypisanie do value?
coloradocolby

@threadster Nie można użyć domyślnego operatora dla zmiennej typu „Type”. Zobacz stackoverflow.com/questions/325426/…
andy250

75

Musisz uzyskać typ podstawowy, aby to zrobić ...

Wypróbuj to, z powodzeniem użyłem go z lekami generycznymi:

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

Używam go w wielu miejscach w kodzie, jednym z przykładów jest metoda pomocnicza, której używam do konwertowania wartości bazy danych w bezpieczny sposób:

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

Wywoływany za pomocą:

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

Napisałem serię postów na blogu, w tym ten na http://www.endswithsaurus.com/2010_07_01_archive.html (Przewiń w dół do aneksu , @JohnMacintyre rzeczywiście zauważył błąd w moim oryginalnym kodzie, który poprowadził mnie tą samą ścieżką teraz). Mam kilka małych modyfikacji, ponieważ ten post obejmuje konwersję typów wyliczeń, więc jeśli twoja właściwość jest wyliczeniem, możesz nadal używać tego samego wywołania metody. Po prostu dodaj wiersz, aby sprawdzić typy wyliczeń, a będziesz w wyścigach, używając czegoś takiego:

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

Zwykle masz trochę sprawdzania błędów lub używasz TryParse zamiast Parse, ale dostajesz obraz.


Dzięki - wciąż brakuje mi kroku lub czegoś nie rozumiem. Usiłuję ustawić wartość właściwości, dlaczego otrzymuję obiekt, który jest w typie bazowym? Nie jestem również pewien, jak przejść z mojego kodu do metody rozszerzenia takiej jak Twoja. Nie będę wiedział, jaki będzie typ, aby zrobić coś takiego jak value.Helper <Int32?> ().
iboeno,

@iboeno - Niestety, byłam na spotkaniu, więc nie mogłem pomóc ci połączyć kropek. Cieszę się, że masz rozwiązanie.
BenAlabaster,

9

Na przykład jest to trochę długie, ale jest to stosunkowo solidne podejście, które oddziela zadanie rzutowania z nieznanej wartości na nieznany typ

Mam metodę TryCast, która robi coś podobnego i bierze pod uwagę typy zerowalne.

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

Oczywiście TryCast to metoda z parametrem typu, więc aby wywołać ją dynamicznie, musisz samodzielnie zbudować metodę MethodInfo:

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

Następnie, aby ustawić rzeczywistą wartość właściwości:

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

I metody rozszerzenia do obsługi property.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;
}

6

Miałem podobną potrzebę, a odpowiedź Łukasza wskazała mi kierunek. Wymyśliłem tę ogólną funkcję, aby ułatwić.

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

Użycie jest takie:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

Zauważ, że drugi parametr służy tylko jako prototyp, aby pokazać funkcji rzutowania wartości zwracanej, więc tak naprawdę nie musi to być właściwość docelowa. Oznacza to, że możesz zrobić coś takiego:

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

Zrobiłem to w ten sposób, zamiast używać out, ponieważ nie można używać z właściwościami. W tej chwili może pracować z właściwościami i zmiennymi. Możesz również utworzyć przeciążenie, aby przekazać typ zamiast tego, jeśli chcesz.


0

Dzięki @LukeH
trochę się zmieniłem:

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

0

Zrobiłem to w ten sposób

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;
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.