Der effizienteste Weg, um nach DBNull zu suchen und dann einer Variablen zuzuweisen?

151

Diese Frage taucht gelegentlich auf, aber ich habe keine zufriedenstellende Antwort gesehen.

Ein typisches Muster ist (Zeile ist eine DataRow ):

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

Meine erste Frage ist, welche effizienter ist (ich habe die Bedingung umgedreht):

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

Dies zeigt an, dass .GetType () schneller sein sollte, aber vielleicht kennt der Compiler ein paar Tricks, die ich nicht kenne?

Zweite Frage: Lohnt es sich, den Wert von row ["value"] zwischenzuspeichern, oder optimiert der Compiler den Indexer trotzdem?

Beispielsweise:

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

Anmerkungen:

  1. Zeile ["Wert"] existiert.
  2. Ich kenne den Spaltenindex der Spalte nicht (daher die Suche nach Spaltennamen).
  3. Ich frage speziell nach DBNull und dann nach Zuweisung (nicht nach vorzeitiger Optimierung usw.).

Ich habe einige Szenarien verglichen (Zeit in Sekunden, 10.000.000 Versuche):

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 hat die gleiche Leistung wie "=="

Das interessanteste Ergebnis? Wenn Sie den Namen der Spalte von Fall zu Fall nicht übereinstimmen (z. B. "Wert" anstelle von "Wert"), dauert dies ungefähr zehnmal länger (für eine Zeichenfolge):

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

Die Moral der Geschichte scheint zu sein: Wenn Sie eine Spalte nicht anhand ihres Index nachschlagen können, stellen Sie sicher, dass der Spaltenname, den Sie dem Indexer zuführen, genau mit dem Namen der DataColumn übereinstimmt.

Das Zwischenspeichern des Werts scheint ebenfalls fast doppelt so schnell zu sein:

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

Die effizienteste Methode scheint also zu sein:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }
ilitirit
quelle
1
Können Sie klären, ob die Zeile eine DataRow oder ein IDataRecord / IDataReader ist?
Marc Gravell
7
Jetzt haben wir viel besseres .NET Framework und können DataRowExtensions-Methoden verwenden .
Pavel Hodek
Wenn Sie den Namen der Spalte von Fall zu Fall nicht übereinstimmen (z. B. "Wert" anstelle von "Wert"), dauert dies ungefähr zehnmal länger (für eine Zeichenfolge). Dies hängt vollständig von der Implementierung ab. Ich erinnere mich, dass dies der Fall war (Änderung in Wenn der Spaltenname viel langsamer ist) mit dem MySQL ADO.NET-Connector, aber überhaupt nicht für SqlServer oder SQLite (nicht vergessen). Die Dinge könnten sich jetzt geändert haben. Ja, die grundlegende Richtlinie lautet im Zweifelsfall Ordnungszahlen.
Nawfal
@PavelHodek so eine Schande, dass nur für DataRow. Hätte IDataRecordErweiterungen geliebt .
Nawfal

Antworten:

72

Mir muss etwas fehlen. Prüft man nicht DBNullgenau, was die DataRow.IsNullMethode macht?

Ich habe die folgenden zwei Erweiterungsmethoden verwendet:

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

Verwendung:

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

Wenn Sie keine Nullable<T>Rückgabewerte für möchten GetValue<T>, können Sie auch einfach default(T)eine andere Option zurückgeben.


In einem anderen Zusammenhang ist hier eine VB.NET-Alternative zu Stevo3000s Vorschlag:

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
quelle
3
Dan riskiert dies erneut, was OP vermeiden will. Wenn row.IsNull(columnName)Sie schreiben , lesen Sie es bereits einmal und lesen es erneut. Nicht zu sagen, dass das einen Unterschied machen wird, aber theoretisch kann es weniger effizient sein ..
Nawfal
2
Ist es nicht im System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)Wesentlichen dasselbe wie bei der ersten Methode?
Dennis G
35

Sie sollten die Methode verwenden:

Convert.IsDBNull()

In Anbetracht dessen, dass es in das Framework integriert ist, würde ich erwarten, dass dies am effizientesten ist.

Ich würde etwas vorschlagen in der Art von:

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

Und ja, der Compiler sollte es für Sie zwischenspeichern.

Jon Grant
quelle
5
Nun, alle genannten Optionen sind in das Framework integriert ... Tatsächlich erledigt Convert.IsDBNull eine Menge zusätzlicher Arbeit in Bezug auf IConvertible ...
Marc Gravell
1
Und wieder der Cache - wenn Sie mit dem bedingten Beispiel meinen, nein - sollte er wirklich nicht (und nicht). Der Indexer wird zweimal ausgeführt.
Marc Gravell
Oh, und dieser Code wird nicht kompiliert - aber fügen Sie einem von ihnen ein (int?) Hinzu, und Sie sehen (in der IL) 2 von: callvirt-Instanzobjekt [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell
20

Der Compiler optimiert den Indexer nicht weg (dh wenn Sie Zeile ["Wert"] zweimal verwenden), also ist es etwas schneller:

object value = row["value"];

und dann Wert zweimal verwenden; Die Verwendung von .GetType () birgt Probleme, wenn es null ist ...

DBNull.Valueist eigentlich ein Singleton, also um eine vierte Option hinzuzufügen - Sie könnten vielleicht ReferenceEquals verwenden -, aber in Wirklichkeit machen Sie sich hier zu viele Sorgen ... Ich glaube nicht, dass die Geschwindigkeit zwischen "is", "==" unterschiedlich ist "etc wird die Ursache für jedes Leistungsproblem sein, das Sie sehen. Profilieren Sie Ihren gesamten Code und konzentrieren Sie sich auf etwas, das wichtig ist ... das wird es nicht sein.

Marc Gravell
quelle
2
In praktisch allen Fällen entspricht == ReferenceEquals (insbesondere DBNull) und ist viel besser lesbar. Verwenden Sie die Optimierung von @Marc Gravell, wenn Sie möchten, aber ich bin bei ihm - wahrscheinlich wird es nicht viel helfen. Übrigens sollte die Referenzgleichheit immer die Typprüfung übertreffen.
Tvanfosson
1
Jetzt alt, aber ich habe kürzlich eine Reihe von Fällen gesehen, in denen genau dies vom Profiler behoben werden soll. Stellen Sie sich vor, Sie bewerten große Datenmengen, bei denen jede Zelle diese Prüfung durchführen muss. Eine Optimierung kann große Belohnungen bringen. Aber der wichtige Teil der Antwort ist immer noch gut: Profil zuerst, um zu wissen, wo Sie Ihre Zeit am besten verbringen können.
Joel Coehoorn
Ich denke, die Einführung des Elvis-Operators in C # 6 macht es einfach, die Nullreferenzausnahme in der von Ihnen vorgeschlagenen Prüfung zu vermeiden. Wert? .GetType () == typeof (DBNull)
Eniola
Ja, ich stimme zu. ist im Allgemeinen ein besserer Weg, aber für diejenigen, die .GetType () nicht verwenden möchten, auf deren Risiken Sie dann hingewiesen haben? bietet einen Weg, um es zu umgehen.
Eniola
9

Ich würde den folgenden Code in C # verwenden ( VB.NET ist nicht so einfach).

Der Code weist den Wert zu, wenn er nicht null / DBNull ist. Andernfalls weist er den Standard zu, der auf den LHS-Wert festgelegt werden kann, sodass der Compiler die Zuweisung ignorieren kann.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
quelle
1
Die VB.NET-Version ist so einfach : oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@ Dan Tao - Ich glaube nicht, dass Sie diesen Code kompiliert haben. Schauen Sie sich eine alte Frage von mir an, die erklärt, warum Ihr Code nicht funktioniert. stackoverflow.com/questions/746767/…
stevehipwell
Und wieder einmal hat sich das Kommentieren einer SO-Frage außerhalb meines eigenen Computers (mit darauf befindlichen Entwicklertools) als Fehler erwiesen! Du hast recht; Ich bin überrascht zu erfahren, dass TryCastdies nicht die gleiche praktische Funktionalität bietet wie der asOperator von C # für Nullable(Of T)Typen. Der beste Weg, dies nachzuahmen, besteht darin, eine eigene Funktion zu schreiben, wie ich jetzt in meiner Antwort vorgeschlagen habe.
Dan Tao
Es wird Ihnen schwer fallen, dies in eine generische Methode umzuwandeln, und selbst wenn Sie dies tun, wird es durch zu viel Casting weniger effizient.
Nawfal
8

Ich bin der Meinung, dass nur sehr wenige Ansätze hier die Aussicht auf OP am meisten gefährden (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) und die meisten unnötig komplex sind. Wenn Sie sich bewusst sind, dass dies eine nutzlose Mikrooptimierung ist, sollten Sie diese grundsätzlich anwenden:

1) Lesen Sie den Wert nicht zweimal aus DataReader / DataRow. Zwischenspeichern Sie ihn also entweder vor Nullprüfungen und Casts / Conversions oder übergeben Sie Ihr record[X]Objekt besser direkt an eine benutzerdefinierte Erweiterungsmethode mit der entsprechenden Signatur.

2) Um dies zu befolgen, verwenden Sie keine integrierte IsDBNullFunktion in Ihrem DataReader / DataRow, da dies den record[X]internen Aufruf aufruft. Sie werden dies also tatsächlich zweimal tun.

3) Der Typvergleich ist in der Regel immer langsamer als der Wertevergleich. Mach record[X] == DBNull.Valuees einfach besser.

4) Direktes Casting ist schneller als das Aufrufen der ConvertKlasse zum Konvertieren, obwohl ich befürchte, dass letztere weniger ins Stocken geraten wird.

5) Schließlich ist der Zugriff auf Datensätze über den Index und nicht über den Spaltennamen wieder schneller.


Ich denke, die Ansätze von Szalay, Neil und Darren Koppand werden besser sein. Ich mag besonders den Ansatz der Erweiterungsmethode von Darren Koppand, der den Index- / Spaltennamen berücksichtigt IDataRecord(obwohl ich ihn gerne weiter eingrenzen möchte IDataReader).

Achten Sie darauf, es zu nennen:

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

und nicht

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

falls Sie zwischen 0und unterscheiden müssen DBNull. Wenn Sie beispielsweise Nullwerte in Aufzählungsfeldern haben, besteht andernfalls die default(MyEnum)Gefahr, dass der erste Aufzählungswert zurückgegeben wird. Also besser anrufen record.GetColumnValue<MyEnum?>("Field").

Da Sie aus a lesen DataRow, würde ich eine Erweiterungsmethode für beide DataRowund IDataReaderdurch DRYing von allgemeinem Code erstellen.

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

Nennen wir es jetzt wie folgt:

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

Ich glaube , das ist , wie es im Rahmen hätte sein sollen (statt dem record.GetInt32, record.GetStringetc Methoden) in erster Linie - keine Laufzeitausnahmen und gibt uns die Flexibilität, Griff Nullwerte.

Aus meiner Erfahrung hatte ich weniger Glück mit einer generischen Methode zum Lesen aus der Datenbank. Ich hatte immer zu benutzerdefinierten Griff verschiedene Typen, so dass ich meine eigene schreiben hatte GetInt, GetEnum, GetGuidetc. Methoden auf lange Sicht. Was ist, wenn Sie beim Lesen von Zeichenfolgen aus db standardmäßig Leerzeichen kürzen oder DBNullals leere Zeichenfolge behandeln möchten? Oder wenn Ihre Dezimalstelle von allen nachgestellten Nullen abgeschnitten werden soll. Ich hatte die meisten Probleme mit dem GuidTyp, bei dem sich verschiedene Connector-Treiber unterschiedlich verhalten haben, auch wenn zugrunde liegende Datenbanken sie als Zeichenfolge oder Binärdatei speichern können. Ich habe eine Überlastung wie diese:

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

Mit dem Ansatz von Stevo3000 finde ich das Aufrufen etwas hässlich und langweilig, und es wird schwieriger sein, daraus eine generische Funktion zu machen.

nawfal
quelle
7

Es gibt den problematischen Fall, dass das Objekt eine Zeichenfolge sein könnte. Der folgende Code für die Erweiterungsmethode behandelt alle Fälle. So würden Sie es verwenden:

    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
quelle
6

Ich persönlich bevorzuge diese Syntax, die die explizite IsDbNull-Methode verwendet, die von verfügbar gemacht wird IDataRecord, und den Spaltenindex zwischenspeichert, um eine doppelte Suche nach Zeichenfolgen zu vermeiden.

Aus Gründen der Lesbarkeit erweitert, geht es ungefähr so:

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

Aus Gründen der Kompaktheit des DAL-Codes in eine einzelne Zeile umgeschrieben. Beachten Sie, dass wir in diesem Beispiel int bar = -1if zuweisen, wenn row["Bar"]es null ist.

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

Die Inline-Zuweisung kann verwirrend sein, wenn Sie nicht wissen, dass sie vorhanden ist, aber sie hält den gesamten Vorgang in einer Zeile, was meiner Meinung nach die Lesbarkeit verbessert, wenn Sie Eigenschaften aus mehreren Spalten in einem Codeblock auffüllen.

Dylan Beattie
quelle
3
DataRow implementiert IDataRecord jedoch nicht.
ilitirit
5

Nicht, dass ich das getan hätte, aber Sie könnten den Doppelindexaufruf umgehen und trotzdem Ihren Code mithilfe einer statischen / Erweiterungsmethode sauber halten.

Dh.

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

Dann:

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

Hat auch den Vorteil, dass die Nullprüflogik an einem Ort bleibt. Nachteil ist natürlich, dass es sich um einen zusätzlichen Methodenaufruf handelt.

Nur ein Gedanke.

Richard Szalay
quelle
2
Das Hinzufügen einer Erweiterungsmethode für ein Objekt ist jedoch sehr umfangreich. Persönlich hätte ich vielleicht eine Erweiterungsmethode für DataRow in Betracht gezogen, aber kein Objekt.
Marc Gravell
Beachten Sie jedoch, dass Erweiterungsmethoden nur verfügbar sind, wenn der Namespace der Erweiterungsklasse importiert wird.
Richard Szalay
5

Ich versuche, diese Überprüfung so weit wie möglich zu vermeiden.

Offensichtlich muss dies nicht für Spalten durchgeführt werden, die nicht enthalten sind null.

Wenn Sie in einem Nullable-Werttyp ( int?usw.) speichern , können Sie einfach mit konvertieren as int?.

Wenn Sie nicht zwischen string.Emptyund unterscheiden müssen null, können Sie einfach anrufen .ToString(), da DBNull zurückkehrt string.Empty.

bdukes
quelle
4

Ich benutze immer:

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

Fand es kurz und umfassend.

Patrick Desjardins
quelle
4

So gehe ich mit dem Lesen aus DataRows um

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

Anwendungsbeispiel:

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

Requisiten für Monster haben mein .Net für ChageTypeTo-Code erhalten.

Chris Marisic
quelle
4

Ich habe etwas Ähnliches mit Erweiterungsmethoden gemacht. Hier ist mein Code:

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

Um es zu benutzen, würden Sie so etwas tun

int number = record.GetColumnValue<int>("Number",0)
Darren Kopp
quelle
4

Wenn in einer DataRow die Zeile ["Feldname"] isDbNull ist, ersetzen Sie sie durch 0, andernfalls erhalten Sie den Dezimalwert:

decimal result = rw["fieldname"] as decimal? ?? 0;
Stefan
quelle
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;
    }
}

benutze so

DBH.Get<String>(itemRow["MyField"])
Neil
quelle
3

Ich habe IsDBNull in einem Programm, das viele Daten aus einer Datenbank liest. Mit IsDBNull werden Daten in ca. 20 Sekunden geladen. Ohne IsDBNull ca. 1 Sekunde.

Ich denke, es ist besser zu verwenden:

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
Mastahh
quelle