Il modo più efficiente per verificare DBNull e quindi assegnarlo a una variabile?


151

Questa domanda si presenta occasionalmente, ma non ho visto una risposta soddisfacente.

Un modello tipico è (la riga è un DataRow ):

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

La mia prima domanda è quale sia più efficiente (ho capovolto la condizione):

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

Ciò indica che .GetType () dovrebbe essere più veloce, ma forse il compilatore conosce alcuni trucchi, no?

Seconda domanda, vale la pena memorizzare nella cache il valore di riga ["valore"] o il compilatore ottimizza comunque l'indicizzatore?

Per esempio:

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

Appunti:

  1. riga ["valore"] esiste.
  2. Non conosco l'indice di colonna della colonna (da qui la ricerca del nome della colonna).
  3. Sto chiedendo in particolare il controllo di DBNull e quindi dell'assegnazione (non dell'ottimizzazione prematura, ecc.).

Ho analizzato alcuni scenari (tempo in secondi, 10.000.000 di prove):

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 ha le stesse prestazioni di "=="

Il risultato più interessante? Se non si corrisponde il nome della colonna per caso (ad esempio, "Valore" anziché "valore", impiega circa dieci volte più a lungo (per una stringa):

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

La morale della storia sembra essere che se non riesci a cercare una colonna dal suo indice, assicurati che il nome della colonna che dai all'indicizzatore corrisponda esattamente al nome del DataColumn.

Anche la memorizzazione nella cache del valore sembra essere quasi il doppio più veloce:

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

Quindi il metodo più efficiente sembra essere:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }

1
Potete chiarire se la riga è un DataRow o un IDataRecord / IDataReader?
Marc Gravell

7
Ora abbiamo .NET Framework molto migliore e possiamo usare i metodi DataRowExtensions .
Pavel Hodek,

Se non corrispondi al nome della colonna per caso (ad esempio, "Valore" anziché "valore", ci vuole circa dieci volte più a lungo (per una stringa) Dipende completamente dall'implementazione. Ricordo che era il caso (modifica in il caso in cui il nome della colonna sia molto più lento) con il connettore ADO.NET di MySQL, ma non è affatto per SqlServer o SQLite (non ricordo). Le cose potrebbero essere cambiate ora. Sì, la linea guida di base è, in caso di dubbio, andare per ordinali.
nawfal,

@PavelHodek un peccato che è solo per DataRow. Avrebbe amato le IDataRecordestensioni.
nawfal,

Risposte:


72

Mi manca qualcosa. Non sta verificando DBNullesattamente cosa fa il DataRow.IsNullmetodo?

Ho usato i seguenti due metodi di estensione:

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

Uso:

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

Se non desideri Nullable<T>valori di ritorno per GetValue<T>, puoi invece restituire facilmente default(T)o qualche altra opzione.


Su una nota non correlata, ecco un'alternativa VB.NET al suggerimento di 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

3
Dan questo rischia di nuovo ciò che OP vuole evitare. Scrivendo row.IsNull(columnName)lo leggi già una volta e lo rileggi. Non dire che farà la differenza, ma teoricamente può essere meno efficiente ..
nawfal

2
System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)Fare essenzialmente non è la stessa cosa del primo metodo?
Dennis G,

35

Dovresti usare il metodo:

Convert.IsDBNull()

Considerando che è integrato nel Framework, mi aspetto che questo sia il più efficiente.

Suggerirei qualcosa del genere:

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

E sì, il compilatore dovrebbe memorizzarlo nella cache.


5
Bene, tutte le opzioni menzionate sono integrate nel framework ... In realtà, Convert.IsDBNull fa un sacco di lavoro extra in relazione a IConvertible ...
Marc Gravell

1
E per quanto riguarda la cache - se intendete con l'esempio condizionale, no - in realtà non dovrebbe (e non lo fa). Eseguirà l'indicizzatore due volte.
Marc Gravell

Oh, e quel codice non viene compilato - ma aggiungi un (int?) A uno di essi e vedrai (nell'IL) 2 di: oggetto di istanza callvirt [System.Data] System.Data.DataRow :: get_Item (stringa)
Marc Gravell

20

Il compilatore non ottimizzerà via l'indicizzatore (cioè se usi la riga ["valore"] due volte), quindi sì è leggermente più veloce da fare:

object value = row["value"];

e quindi usa due volte il valore; l'utilizzo di .GetType () comporta problemi se è nullo ...

DBNull.Valueè in realtà un singleton, quindi per aggiungere una quarta opzione - potresti forse usare ReferenceEquals - ma in realtà, penso che ti preoccupi troppo qui ... Non penso che la velocità tra "is", "== "etc sarà la causa di qualsiasi problema di prestazioni che stai riscontrando. Profila il tuo intero codice e concentrati su qualcosa che conta ... non sarà questo.


2
Praticamente in tutti i casi == sarà equivalente a ReferenceEquals (specialmente DBNull) ed è molto più leggibile. Usa l'ottimizzazione di @Marc Gravell se vuoi, ma io sono con lui - probabilmente non aiuterà molto. A proposito, l'uguaglianza di riferimento dovrebbe sempre battere il controllo del tipo.
tvanfosson,

1
Vecchio ora, ma recentemente ho visto un numero di casi in cui questo era esattamente ciò che il profiler ha detto di risolvere. Immagina di valutare set di dati di grandi dimensioni, in cui ogni cella deve effettuare questo controllo. L'ottimizzazione che può ottenere grandi ricompense. Ma la parte importante della risposta è ancora buona: profilo prima, per sapere dove meglio trascorrere il tempo.
Joel Coehoorn,

Immagino che l'introduzione di C # 6 dell'operatore Elvis renda facile evitare l'eccezione di riferimento null nel controllo suggerito. valore? .GetType () == typeof (DBNull)
Eniola

Si, sono d'accordo. è generalmente un modo migliore di andare, ma per coloro che vogliono usare .GetType () di chi hai indicato i rischi, allora? fornisce un modo per aggirarlo.
Eniola,

9

Vorrei usare il seguente codice in C # ( VB.NET non è così semplice).

Il codice assegna il valore se non è null / DBNull, altrimenti assegna il valore predefinito che potrebbe essere impostato sul valore LHS consentendo al compilatore di ignorare l'assegnazione.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;

1
La versione VB.NET è semplice: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao,

1
@ Taan Tao - Non credo che tu abbia compilato quel codice. Guarda una mia vecchia domanda che spiega perché il tuo codice non funzionerà. stackoverflow.com/questions/746767/...
stevehipwell

E ancora una volta, commentare una domanda SO mentre sono lontani dal mio computer (con gli strumenti di sviluppo su di esso) ha dimostrato di essere un errore! Hai ragione; Sono sorpreso di apprendere che TryCastnon offre la stessa comoda funzionalità asdell'operatore di C # per i Nullable(Of T)tipi. Il modo più vicino che mi viene in mente di imitare questo è scrivere la tua funzione, come ho ora suggerito nella mia risposta.
Dan Tao,

Avrai difficoltà a rifattorizzare questo in un metodo generico e, anche se lo fai, il casting eccessivo in questione lo renderà meno efficiente.
nawfal,

8

Sento che solo pochissimi approcci qui non rischiano la prospettiva dell'OP più preoccupante (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) e la maggior parte sono inutilmente complessi. Essendo pienamente consapevole che si tratta di un'inutile micro-ottimizzazione, lasciami dire che dovresti sostanzialmente utilizzare questi:

1) Non leggere il valore da DataReader / DataRow due volte, quindi memorizzalo nella cache prima di controlli e cast / conversioni nulli o anche meglio passare direttamente l' record[X]oggetto a un metodo di estensione personalizzato con la firma appropriata.

2) Per obbedire a quanto sopra, non utilizzare la IsDBNullfunzione integrata su DataReader / DataRow poiché chiama record[X]internamente, quindi in effetti lo farai due volte.

3) Il confronto dei tipi sarà sempre più lento del confronto dei valori come regola generale. Basta fare di record[X] == DBNull.Valuemeglio.

4) Il casting diretto sarà più veloce di chiamare la Convertclasse per la conversione, anche se temo che quest'ultimo vacillerà meno.

5) Infine, l'accesso al record per indice anziché per nome di colonna sarà di nuovo più veloce.


Sento che l'approccio di Szalay, Neil e Darren Koppand sarà migliore. Mi piace in particolare l'approccio del metodo di estensione di Darren Koppand che comprende IDataRecord(anche se vorrei restringerlo ulteriormente IDataReader) e il nome dell'indice / colonna.

Abbi cura di chiamarlo:

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

e non

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

nel caso in cui sia necessario distinguere tra 0e DBNull. Ad esempio, se si hanno valori null nei campi enum, altrimenti si default(MyEnum)rischia di restituire il primo valore enum. Quindi meglio chiamare record.GetColumnValue<MyEnum?>("Field").

Dal momento che stai leggendo da un DataRow, vorrei creare metodo di estensione per entrambi DataRowe IDataReaderper l'essiccazione codice comune.

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

Quindi ora chiamalo come:

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

Credo che sia come dovrebbe essere stato nel framework (invece dei metodi record.GetInt32, record.GetStringecc.) In primo luogo - senza eccezioni di runtime e ci dà la flessibilità di gestire valori null.

Dalla mia esperienza ho avuto meno fortuna con un metodo generico per leggere dal database. Ho sempre dovuto gestire vari tipi personalizzati, così ho dovuto scrivere il mio GetInt, GetEnum, GetGuidmetodi, ecc nel lungo periodo. Cosa succede se si desidera tagliare gli spazi bianchi quando si legge la stringa da db per impostazione predefinita o si tratta DBNulldi una stringa vuota? O se il tuo decimale deve essere troncato di tutti gli zero finali. Ho avuto molti problemi conGuid tipo in cui diversi driver di connettore si sono comportati diversamente anche quando i database sottostanti possono archiviarli come stringa o binari. Ho un sovraccarico come questo:

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

Con l'approccio di Stevo3000, trovo la chiamata un po 'brutta e noiosa, e sarà più difficile farne una funzione generica.


7

C'è il caso problematico in cui l'oggetto potrebbe essere una stringa. Il codice del metodo di estensione seguente gestisce tutti i casi. Ecco come lo useresti:

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

6

Personalmente preferisco questa sintassi, che utilizza il metodo IsDbNull esplicito esposto da IDataRecord, e memorizza nella cache l'indice di colonna per evitare una ricerca di stringhe duplicate.

Espandibile per la leggibilità, è simile a:

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

Riscritto per adattarsi su una sola riga per compattezza nel codice DAL - nota che in questo esempio stiamo assegnando int bar = -1if row["Bar"]è 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));

L'assegnazione in linea può essere fonte di confusione se non sai che è lì, ma mantiene l'intera operazione su una riga, che penso migliora la leggibilità quando stai popolando proprietà da più colonne in un blocco di codice.


3
Tuttavia DataRow non implementa IDataRecord.
ilitirit,

5

Non che l'ho fatto, ma potresti aggirare la chiamata al doppio indicizzatore e mantenere comunque pulito il tuo codice usando un metodo statico / di estensione.

Vale a dire.

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

Poi:

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

Ha anche il vantaggio di mantenere la logica di controllo null in un unico posto. Il rovescio della medaglia è, ovviamente, che si tratta di una chiamata di metodo extra.

Solo un pensiero.


2
L'aggiunta di un metodo di estensione sull'oggetto è tuttavia molto ampia. Personalmente avrei potuto considerare un metodo di estensione su DataRow, ma non oggetto.
Marc Gravell

È vero, tuttavia tieni presente che i metodi di estensione sono disponibili solo quando viene importato lo spazio dei nomi della classe di estensione.
Richard Szalay,

5

Cerco di evitare questo controllo il più possibile.

Ovviamente non è necessario eseguire le colonne che non possono contenere null .

Se stai memorizzando in un tipo di valore Nullable ( int?, ecc.), Puoi semplicemente convertirlo usando as int?.

Se non hai bisogno di distinguere tra string.Emptye null, puoi semplicemente chiamare .ToString(), poiché DBNull tornerà string.Empty.


4

Uso sempre:

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

L'ho trovato breve e completo.


4

Ecco come gestisco la lettura da 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);
    }
}

Esempio di utilizzo:

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

Puntelli ai mostri Ho il mio codice .Net per ChageType Per il codice.


4

Ho fatto qualcosa di simile con i metodi di estensione. Ecco il mio codice:

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

Per usarlo, faresti qualcosa del genere

int number = record.GetColumnValue<int>("Number",0)

4

se in un DataRow la riga ["fieldname"] isDbNull lo sostituisce con 0 altrimenti ottiene il valore decimale:

decimal result = rw["fieldname"] as decimal? ?? 0;

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

usare così

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

3

Ho IsDBNull in un programma che legge molti dati da un database. Con IsDBNull carica i dati in circa 20 secondi. Senza IsDBNull, circa 1 secondo.

Quindi penso che sia meglio usare:

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.