Najbardziej efektywny sposób sprawdzenia DBNull, a następnie przypisania do zmiennej?

151

To pytanie pojawia się czasami, ale nie widziałem satysfakcjonującej odpowiedzi.

Typowy wzorzec to (row to DataRow ):

 if (row["value"] != DBNull.Value)
 {
      someObject.Member = row["value"];
 }

Moje pierwsze pytanie brzmi, co jest bardziej wydajne (odwróciłem warunek):

  row["value"] == DBNull.Value; // Or
  row["value"] is DBNull; // Or
  row["value"].GetType() == typeof(DBNull) // Or... any suggestions?

Oznacza to, że .GetType () powinno być szybsze, ale może kompilator zna kilka sztuczek, których ja nie mam?

Drugie pytanie, czy warto buforować wartość wiersza [„wartość”], czy i tak kompilator optymalizuje indeksator?

Na przykład:

  object valueHolder;
  if (DBNull.Value == (valueHolder = row["value"])) {}

Uwagi:

  1. istnieje wiersz [„wartość”].
  2. Nie znam indeksu kolumny (stąd wyszukanie nazwy kolumny).
  3. Pytam konkretnie o sprawdzenie DBNull, a następnie przypisanie (nie o przedwczesną optymalizację itp.).

Porównałem kilka scenariuszy (czas w sekundach, 10 000 000 prób):

row["value"] == DBNull.Value: 00:00:01.5478995
row["value"] is DBNull: 00:00:01.6306578
row["value"].GetType() == typeof(DBNull): 00:00:02.0138757

Object.ReferenceEquals ma taką samą wydajność jak „==”

Najciekawszy wynik? Jeśli nie dopasujesz nazwy kolumny według wielkości liter (na przykład „Wartość” zamiast „wartość”, trwa to około dziesięć razy dłużej (w przypadku ciągu):

row["Value"] == DBNull.Value: 00:00:12.2792374

Morał z tej historii wydaje się być taki, że jeśli nie możesz wyszukać kolumny według jej indeksu, upewnij się, że nazwa kolumny, którą przekazujesz indeksatorowi, dokładnie odpowiada nazwie kolumny DataColumn.

Buforowanie wartości również wydaje się być prawie dwa razy szybsze:

No Caching: 00:00:03.0996622
With Caching: 00:00:01.5659920

Wydaje się więc, że najbardziej wydajną metodą jest:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }
ilitirit
źródło
1
Czy możesz wyjaśnić, czy wiersz jest DataRow czy IDataRecord / IDataReader?
Marc Gravell
7
Teraz mamy znacznie lepsze .NET Framework i możemy używać metod DataRowExtensions .
Pavel Hodek,
Jeśli nie dopasujesz nazwy kolumny według wielkości liter (na przykład „Wartość” zamiast „wartość”, zajmuje to około dziesięć razy dłużej (dla ciągu znaków). Zależy to całkowicie od implementacji. Pamiętam, że tak było (zmiana w w przypadku, gdy nazwa kolumny jest znacznie wolniejsza) z łącznikiem MySQL ADO.NET, ale wcale nie dla SqlServer lub SQLite (nie pamiętam) .Czas może się teraz zmienić.
nawfal
@PavelHodek taka szkoda, że ​​tylko dla DataRow. Chciałbym IDataRecordrozszerzenia.
nawfal

Odpowiedzi:

72

Muszę czegoś przegapić. Czy nie sprawdza DBNulldokładnie, co DataRow.IsNullrobi ta metoda?

Używałem następujących dwóch metod rozszerzających:

public static T? GetValue<T>(this DataRow row, string columnName) where T : struct
{
    if (row.IsNull(columnName))
        return null;

    return row[columnName] as T?;
}

public static string GetText(this DataRow row, string columnName)
{
    if (row.IsNull(columnName))
        return string.Empty;

    return row[columnName] as string ?? string.Empty;
}

Stosowanie:

int? id = row.GetValue<int>("Id");
string name = row.GetText("Name");
double? price = row.GetValue<double>("Price");

Jeśli nie chcesz Nullable<T>zwracać wartości dla GetValue<T>, możesz łatwo zwrócić default(T)lub inną opcję zamiast.


Z drugiej strony, oto alternatywa VB.NET dla sugestii Stevo3000:

oSomeObject.IntMember = If(TryConvert(Of Integer)(oRow("Value")), iDefault)
oSomeObject.StringMember = If(TryCast(oRow("Name"), String), sDefault)

Function TryConvert(Of T As Structure)(ByVal obj As Object) As T?
    If TypeOf obj Is T Then
        Return New T?(DirectCast(obj, T))
    Else
        Return Nothing
    End If
End Function
Dan Tao
źródło
3
Dan to znowu ryzykuje, czego OP chce uniknąć. Pisząc row.IsNull(columnName), czytasz to już raz i czytasz ponownie. Nie mówię, że to zrobi różnicę, ale teoretycznie może być mniej wydajne ..
nawfal
2
Czy System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)zasadniczo nie robi tego samego, co pierwsza metoda?
Dennis G
35

Powinieneś użyć metody:

Convert.IsDBNull()

Biorąc pod uwagę, że jest wbudowany w Framework, spodziewałbym się, że będzie to najbardziej wydajne.

Proponuję coś w rodzaju:

int? myValue = (Convert.IsDBNull(row["column"]) ? null : (int?) Convert.ToInt32(row["column"]));

I tak, kompilator powinien to dla Ciebie buforować.

Jon Grant
źródło
5
Cóż, wszystkie wymienione opcje są wbudowane w ramy ... Właściwie Convert.IsDBNull wykonuje dużo dodatkowej pracy związanej z IConvertible ...
Marc Gravell
1
I ponownie pamięć podręczna - jeśli masz na myśli przykład warunkowy, nie - naprawdę nie powinien (i nie jest). Spowoduje to dwukrotne wykonanie indeksatora.
Marc Gravell
Aha, i ten kod się nie kompiluje - ale dodaj (int?) Do jednego z nich, a zobaczysz (w IL) 2: obiekt instancji callvirt [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell
20

Kompilator nie zoptymalizuje indeksatora (tj. Jeśli użyjesz row ["value"] dwa razy), więc tak, jest to nieco szybsze:

object value = row["value"];

a następnie dwukrotnie użyj wartości; używanie .GetType () stwarza ryzyko problemów, jeśli jest null ...

DBNull.Valuejest w rzeczywistości singletonem, więc aby dodać czwartą opcję - być może możesz użyć ReferenceEquals - ale w rzeczywistości myślę, że martwisz się tutaj zbytnio ... Nie sądzę, że prędkość różni się między "is", "== "itp. będzie przyczyną wszelkich napotkanych problemów z wydajnością. Profiluj cały kod i skup się na czymś, co ma znaczenie ... to nie będzie to.

Marc Gravell
źródło
2
W praktycznie wszystkich przypadkach == będzie równoważne ReferenceEquals (szczególnie DBNull) i jest znacznie bardziej czytelne. Skorzystaj z optymalizacji @Marc Gravell, jeśli chcesz, ale jestem z nim - prawdopodobnie niewiele pomogę. Przy okazji, równość odwołań powinna zawsze przekraczać sprawdzanie typu.
tvanfosson
1
Teraz jest stary, ale ostatnio widziałem wiele przypadków, w których właśnie to zalecił profiler. Wyobraź sobie, że oceniasz duże zbiory danych, w których każda komórka musi to sprawdzić. Optymalizacja, która może przynieść duże korzyści. Ale ważna część odpowiedzi jest nadal dobra: najpierw profiluj , aby wiedzieć, gdzie najlepiej spędzać czas.
Joel Coehoorn
Wydaje mi się, że wprowadzenie operatora Elvisa w języku C # 6 ułatwia uniknięcie wyjątku zerowego odwołania w sugerowanym przez Ciebie czeku. value? .GetType () == typeof (DBNull)
Eniola
Tak, zgadzam się. jest ogólnie lepszym rozwiązaniem, ale dla tych, którzy nie chcą używać .GetType (), na których ryzyko wskazałeś? zapewnia obejście tego problemu.
Eniola
9

Użyłbym następującego kodu w C # ( VB.NET nie jest tak prosty).

Kod przypisuje wartość, jeśli nie jest ona pusta / DBNull, w przeciwnym razie przypisuje wartość domyślną, która może być ustawiona na wartość LHS, umożliwiając kompilatorowi zignorowanie przypisania.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
źródło
1
Wersja VB.NET jest tak proste: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@Dan Tao - Myślę, że nie skompilowałeś tego kodu. Spójrz na moje stare pytanie, które wyjaśnia, dlaczego twój kod nie działa. stackoverflow.com/questions/746767/…
stevehipwell
I jeszcze raz komentowanie pytania SO z dala od własnego komputera (z narzędziami programistycznymi na nim) okazało się błędem! Masz rację; Jestem zaskoczony, gdy dowiedziałem się, że TryCastnie zapewnia takiej samej wygodnej funkcjonalności, jak asoperator C # dla Nullable(Of T)typów. Najbliższym sposobem, jaki przychodzi mi do głowy, aby to naśladować, jest napisanie własnej funkcji, tak jak zasugerowałem w mojej odpowiedzi.
Dan Tao
Będziesz miał trudności z przekształceniem tego w metodę ogólną, a nawet jeśli to zrobisz, zbyt duże rzucanie spowoduje, że będzie mniej wydajna.
nawfal
8

Wydaje mi się, że tylko kilka podejść tutaj nie naraża perspektywy OP jako największego zmartwienia (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand), a większość jest niepotrzebnie skomplikowana. Będąc w pełni świadomym, że jest to bezużyteczna mikro-optymalizacja, powiem, że w zasadzie powinieneś zastosować następujące:

1) Nie czytaj wartości z DataReader / DataRow dwa razy - więc albo buforuj ją przed sprawdzeniami null i rzutami / konwersjami, albo jeszcze lepiej przekaż swój record[X]obiekt bezpośrednio do niestandardowej metody rozszerzenia z odpowiednim podpisem.

2) Aby zastosować się do powyższego, nie używaj wbudowanej IsDBNullfunkcji w swoim DataReader / DataRow, ponieważ wywołuje to record[X]wewnętrznie, więc w efekcie zrobisz to dwukrotnie.

3) Z reguły porównanie typów będzie zawsze wolniejsze niż porównanie wartości. Po prostu zrób record[X] == DBNull.Valuelepiej.

4) Bezpośrednie rzucanie będzie szybsze niż wywołanie Convertklasy do konwersji, chociaż obawiam się, że ta ostatnia będzie słabsza.

5) Wreszcie, dostęp do rekordu według indeksu zamiast nazwy kolumny będzie ponownie szybszy.


Czuję, że podejście Szalaya, Neila i Darrena Koppanda będzie lepsze. Szczególnie podoba mi się podejście metody rozszerzania Darrena Koppanda, które obejmuje IDataRecord(choć chciałbym zawęzić zakres IDataReader) i nazwę indeksu / kolumny.

Uważaj, aby to nazwać:

record.GetColumnValue<int?>("field");

i nie

record.GetColumnValue<int>("field");

w przypadku potrzeby rozróżnienia między 0i DBNull. Na przykład, jeśli masz wartości null w polach wyliczenia, w przeciwnym razie istnieje default(MyEnum)ryzyko zwrócenia pierwszej wartości wyliczenia. Więc lepiej zadzwoń record.GetColumnValue<MyEnum?>("Field").

Ponieważ czytasz z DataRow, stworzyłbym metodę rozszerzenia dla obu DataRowi IDataReaderprzez DRYing wspólny kod.

public static T Get<T>(this DataRow dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

static T Get<T>(this object obj, T defaultValue) //Private method on object.. just to use internally.
{
    if (obj.IsNull())
        return defaultValue;

    return (T)obj;
}

public static bool IsNull<T>(this T obj) where T : class 
{
    return (object)obj == null || obj == DBNull.Value;
} 

public static T Get<T>(this IDataReader dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

Więc teraz nazwij to tak:

record.Get<int>(1); //if DBNull should be treated as 0
record.Get<int?>(1); //if DBNull should be treated as null
record.Get<int>(1, -1); //if DBNull should be treated as a custom value, say -1

Wierzę, że to, jak powinno być w ramach (zamiast record.GetInt32, record.GetStringetc metody) w pierwszej kolejności - bez wyjątków run-time i daje nam elastyczność wartości null uchwytem.

Z mojego doświadczenia wynika, że ​​miałem mniej szczęścia z jedną ogólną metodą odczytu z bazy danych. I zawsze miał zwyczaj obsługi różnych typów, więc musiałem napisać własny GetInt, GetEnum, GetGuid, itd metod w dłuższej perspektywie. Co by było, gdybyś chciał przyciąć białe spacje podczas domyślnego odczytywania ciągu z db lub traktować DBNulljako pusty ciąg? Lub jeśli Twój dziesiętny powinien zostać obcięty ze wszystkich końcowych zer. Najwięcej problemów miałem z Guidtypem, w którym różne sterowniki łączników zachowywały się inaczej niż wtedy, gdy bazowe bazy danych mogą przechowywać je jako ciąg lub plik binarny. Mam takie przeciążenie:

static T Get<T>(this object obj, T defaultValue, Func<object, T> converter)
{
    if (obj.IsNull())
        return defaultValue;

    return converter  == null ? (T)obj : converter(obj);
}

Przy podejściu Stevo3000 uważam, że wywołanie jest trochę brzydkie i nużące i trudniej będzie zrobić z niego ogólną funkcję.

nawfal
źródło
7

Istnieje kłopotliwy przypadek, w którym obiekt może być łańcuchem. Poniższy kod metody rozszerzenia obsługuje wszystkie przypadki. Oto, jak byś tego użył:

    static void Main(string[] args)
    {
        object number = DBNull.Value;

        int newNumber = number.SafeDBNull<int>();

        Console.WriteLine(newNumber);
    }



    public static T SafeDBNull<T>(this object value, T defaultValue) 
    {
        if (value == null)
            return default(T);

        if (value is string)
            return (T) Convert.ChangeType(value, typeof(T));

        return (value == DBNull.Value) ? defaultValue : (T)value;
    } 

    public static T SafeDBNull<T>(this object value) 
    { 
        return value.SafeDBNull(default(T)); 
    } 
Saleh Najar
źródło
6

Osobiście preferuję tę składnię, która wykorzystuje jawną metodę IsDbNull ujawnioną przez IDataRecordi buforuje indeks kolumny, aby uniknąć podwójnego wyszukiwania ciągów.

Rozszerzony dla czytelności wygląda mniej więcej tak:

int columnIndex = row.GetOrdinal("Foo");
string foo; // the variable we're assigning based on the column value.
if (row.IsDBNull(columnIndex)) {
  foo = String.Empty; // or whatever
} else { 
  foo = row.GetString(columnIndex);
}

Przepisano tak, aby zmieścił się w pojedynczym wierszu dla zwartości w kodzie DAL - zwróć uwagę, że w tym przykładzie przypisujemy int bar = -1if row["Bar"]jest null.

int i; // can be reused for every field.
string foo  = (row.IsDBNull(i  = row.GetOrdinal("Foo")) ? null : row.GetString(i));
int bar = (row.IsDbNull(i = row.GetOrdinal("Bar")) ? -1 : row.GetInt32(i));

Przypisanie w wierszu może być mylące, jeśli nie wiesz, że tam jest, ale utrzymuje całą operację w jednym wierszu, co moim zdaniem zwiększa czytelność, gdy wypełniasz właściwości z wielu kolumn w jednym bloku kodu.

Dylan Beattie
źródło
3
DataRow nie implementuje jednak IDataRecord.
ilitirit
5

Nie żebym to zrobił, ale możesz obejść wywołanie podwójnego indeksatora i nadal utrzymywać kod w czystości, używając metody statycznej / rozszerzającej.

To znaczy.

public static IsDBNull<T>(this object value, T default)
{
    return (value == DBNull.Value)
        ? default
        : (T)value;
}

public static IsDBNull<T>(this object value)
{
    return value.IsDBNull(default(T));
}

Następnie:

IDataRecord record; // Comes from somewhere

entity.StringProperty = record["StringProperty"].IsDBNull<string>(null);
entity.Int32Property = record["Int32Property"].IsDBNull<int>(50);

entity.NoDefaultString = record["NoDefaultString"].IsDBNull<string>();
entity.NoDefaultInt = record["NoDefaultInt"].IsDBNull<int>();

Ma również tę zaletę, że utrzymuje logikę sprawdzania wartości zerowej w jednym miejscu. Wadą jest oczywiście to, że jest to dodatkowe wywołanie metody.

Tylko myśl.

Richard Szalay
źródło
2
Dodawanie metody rozszerzenia do obiektu jest jednak bardzo szerokie. Osobiście mogłem rozważyć metodę rozszerzenia w DataRow, ale nie obiekt.
Marc Gravell
To prawda, należy jednak pamiętać, że metody rozszerzające są dostępne tylko wtedy, gdy zaimportowana jest przestrzeń nazw klasy rozszerzenia.
Richard Szalay
5

W miarę możliwości staram się unikać tego sprawdzania.

Oczywiście nie trzeba tego robić w przypadku kolumn, które nie mogą się utrzymać null.

Jeśli przechowujesz w typie wartości Nullable ( int?itp.), Możesz po prostu przekonwertować za pomocą as int?.

Jeśli nie potrzebujesz rozróżniać między string.Emptyi null, możesz po prostu wywołać .ToString(), ponieważ DBNull zwróci string.Empty.

bdukes
źródło
4

Zawsze używam:

if (row["value"] != DBNull.Value)
  someObject.Member = row["value"];

Okazało się, że jest krótkie i wyczerpujące.

Patrick Desjardins
źródło
4

Oto jak radzę sobie z czytaniem z DataRows

///<summary>
/// Handles operations for Enumerations
///</summary>
public static class DataRowUserExtensions
{
    /// <summary>
    /// Gets the specified data row.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataRow">The data row.</param>
    /// <param name="key">The key.</param>
    /// <returns></returns>
    public static T Get<T>(this DataRow dataRow, string key)
    {
        return (T) ChangeTypeTo<T>(dataRow[key]);
    }

    private static object ChangeTypeTo<T>(this object value)
    {
        Type underlyingType = typeof (T);
        if (underlyingType == null)
            throw new ArgumentNullException("value");

        if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition().Equals(typeof (Nullable<>)))
        {
            if (value == null)
                return null;
            var converter = new NullableConverter(underlyingType);
            underlyingType = converter.UnderlyingType;
        }

        // Try changing to Guid  
        if (underlyingType == typeof (Guid))
        {
            try
            {
                return new Guid(value.ToString());
            }
            catch

            {
                return null;
            }
        }
        return Convert.ChangeType(value, underlyingType);
    }
}

Przykład użycia:

if (dbRow.Get<int>("Type") == 1)
{
    newNode = new TreeViewNode
                  {
                      ToolTip = dbRow.Get<string>("Name"),
                      Text = (dbRow.Get<string>("Name").Length > 25 ? dbRow.Get<string>("Name").Substring(0, 25) + "..." : dbRow.Get<string>("Name")),
                      ImageUrl = "file.gif",
                      ID = dbRow.Get<string>("ReportPath"),
                      Value = dbRow.Get<string>("ReportDescription").Replace("'", "\'"),
                      NavigateUrl = ("?ReportType=" + dbRow.Get<string>("ReportPath"))
                  };
}

Props to Monsters Got My .Net for ChageTypeTo code.

Chris Marisic
źródło
4

Zrobiłem coś podobnego z metodami rozszerzającymi. Oto mój kod:

public static class DataExtensions
{
    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName)
    {
        return GetColumnValue<T>(record, columnName, default(T));
    }

    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <param name="defaultValue">The value to return if the column contains a <value>DBNull.Value</value> value.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName, T defaultValue)
    {
        object value = record[columnName];
        if (value == null || value == DBNull.Value)
        {
            return defaultValue;
        }
        else
        {
            return (T)value;
        }
    }
}

Aby go użyć, zrobiłbyś coś takiego

int number = record.GetColumnValue<int>("Number",0)
Darren Kopp
źródło
4

jeśli w DataRow wiersz ["nazwa pola"] isDbNull zamień go na 0, w przeciwnym razie uzyskaj wartość dziesiętną:

decimal result = rw["fieldname"] as decimal? ?? 0;
Stefan
źródło
3
public static class DBH
{
    /// <summary>
    /// Return default(T) if supplied with DBNull.Value
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    public static T Get<T>(object value)
    {   
        return value == DBNull.Value ? default(T) : (T)value;
    }
}

używać w ten sposób

DBH.Get<String>(itemRow["MyField"])
Neil
źródło
3

Mam IsDBNull w programie, który czyta dużo danych z bazy danych. Z IsDBNull ładuje dane w około 20 sekund. Bez IsDBNull około 1 sekundy.

Więc myślę, że lepiej jest użyć:

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
Mastahh
źródło