2008-10-21 8 views
145

Esta pregunta aparece ocasionalmente, pero no he visto una respuesta satisfactoria.¿La forma más eficiente de verificar DBNull y luego asignarla a una variable?

Un patrón típico es (fila es un DataRow ):

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

Mi primera pregunta es que es más eficiente (he volteado la condición):

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

This indica ese .GetType() debería ser más rápido, pero tal vez el compilador sepa algunos trucos que yo no?

Segunda pregunta, ¿vale la pena almacenar en caché el valor de la fila ["value"] o el compilador optimiza el indexador de todos modos?

Por ejemplo:

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

Notas:

  1. fila [ "valor"] existe.
  2. No conozco el índice de la columna (de ahí la búsqueda del nombre de la columna).
  3. Pregunto específicamente sobre la comprobación de DBNull y su asignación (no sobre la optimización prematura, etc.).

I Benchmarked algunos escenarios (tiempo en segundos, 10,000,000 de ensayos):

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 tiene el mismo rendimiento que "=="

El resultado más interesante? Si falta de coincidencia del nombre de la columna por caso (por ejemplo, "Valor" en lugar de "valor", se tarda aproximadamente diez veces más (para una cadena):

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

La moraleja de la historia parece haber que si no se puede mirar hacia arriba una columna por su índice, y luego asegurarse de que el nombre de la columna que alimenta al controlador paso a paso coincide con el nombre de la DataColumn exactamente

el almacenamiento en caché el valor también parece ser casi dos veces tan rápido:.

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

Así que la mayoría método eficiente parece ser:

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

Puede aclarar si la fila es un DataRow o un IDataRecord/IDataReader? –

+0

Es un DataRow, lo siento. – ilitirit

+6

Ahora tenemos mucho mejor .NET Framework y podemos usar [DataRowExtensions Methods] (http://msdn.microsoft.com/en-us/library/bb359893.aspx). –

Respuesta

33

se debe utilizar el método:

Convert.IsDBNull() 

Teniendo en cuenta que está integrado en el Marco, que sería de esperar que este sea el más eficiente.

me gustaría sugerir algo en la línea de:

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

Y sí, el compilador debe almacenar en caché por usted.

+5

Bueno, * todas * las opciones mencionadas están integradas en el marco ... En realidad, Convert.IsDBNull hace mucho de trabajo adicional relacionado con IConvertible ... –

+0

Y vuelva a la caché - si se refiere al ejemplo condicional, no - en realidad no debería (y no lo hace). Ejecutará el índice er dos veces. –

+0

Ah, y ese código no compila, pero agrega un (int?) A uno de ellos, y verá (en el IL) 2 de: callvirt instance object [System.Data] System.Data.DataRow: : get_Item (string) –

20

El compilador no permitirá optimizar el indexador de distancia (es decir, si utiliza la fila [ "valor"] dos veces), por lo que sí es ligeramente más rápido que hacer:

object value = row["value"]; 

y luego usar el valor de dos veces ; usar .GetType() arriesga problemas si es nulo ...

DBNull.Value es en realidad un singleton, así que para agregar una cuarta opción - quizás podrías usar ReferenceEquals - pero en realidad, creo que te estás preocupando demasiado aquí ... No creo que la velocidad entre "is", "==", etc. sea la causa de cualquier problema de rendimiento que estés viendo. Perfile su código completo y concéntrese en algo que importa ... no será esto.

+2

En prácticamente todos los casos == va a ser equivalente a ReferenceEquals (especialmente a DBNull) y es mucho más legible. Use la optimización de @Marc Gravell si lo desea, pero estoy con él, probablemente no sea de mucha ayuda. Por cierto, la igualdad de referencia siempre debe superar la verificación de tipos. – tvanfosson

+1

Viejo ahora, pero recientemente he visto varios casos en los que esto era exactamente lo que el perfilador dijo que debía arreglar. Imagine la evaluación de grandes conjuntos de datos, donde cada célula necesita hacer esta verificación. Optimizar eso puede generar grandes recompensas. Pero la parte importante de la respuesta sigue siendo buena: _profile_ primero, para saber dónde pasar mejor tu tiempo. –

+0

Supongo que la introducción C# 6 del operador de Elvis hace que sea fácil evitar la excepción de referencia nula en el cheque que sugiere. value? .GetType() == typeof (DBNull) – Eniola

4

siempre uso:

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

encontrado que es corta y completa.

6

Personalmente estoy a favor de esta sintaxis, que utiliza el método explícito IsDbNull expuesto por IDataRecord, y almacena en caché el índice de la columna para evitar una búsqueda duplicada de cadenas.

ampliado para facilitar la lectura, creo que dice así:

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

reescrito para encajar en una sola línea de compacidad en el código DAL - en cuenta que en este ejemplo estamos asignando int bar = -1row["Bar"] si es nulo.

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

La asignación en línea puede ser confuso si usted no sabe que está ahí, pero mantiene toda la operación en una sola línea, que creo que mejora la legibilidad cuando se está inmuebles de varias columnas poblar en un bloque de código.

+3

DataRow no implementa IDataRecord embargo. – ilitirit

5

No es que haya hecho esto, pero podría evitar la llamada del doble indexador y aún así mantener su código limpio usando un método estático/de extensión.

Ie.

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

continuación:

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

también tiene la ventaja de mantener la lógica de comprobación nula en un solo lugar. Lo malo es, por supuesto, que es una llamada de método extra.

Solo un pensamiento.

+2

Agregar un método de extensión en el objeto es muy amplio, sin embargo. Personalmente, podría haber considerado un método de extensión en DataRow, pero no objeto. –

+0

Es cierto, pero tenga en cuenta que los métodos de extensión solo están disponibles cuando se importa el espacio de nombres de la clase de extensión. –

4

Esta es la forma en que manejamos la lectura de 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); 
    } 
} 

Ejemplo de uso:

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

Apoyos a Monsters Got My .Net para el código ChageTypeTo.

4

He hecho algo similar con los métodos de extensión.Aquí está mi código:

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

Para usarlo, usted haría algo como

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

Trato de evitar esta comprobación tanto como sea posible.

Obviamente no es necesario hacerlo para las columnas que no pueden contener null.

Si está almacenando en un tipo de valor Nullable (int?, etc.), puede simplemente convertir usando as int?.

Si no es necesario diferenciar entre string.Empty y null, sólo puede llamar a .ToString(), ya DBNull volverá string.Empty.

9

Utilizaría el siguiente código en C# (VB.NET no es tan simple).

El código asigna el valor si no es nulo/DBNull, de lo contrario asigns el valor por defecto que podría ser establecido en el valor LHS permitiendo que el compilador de ignorar la asignación.

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

La versión de VB.NET * es * como simple: 'oSomeObject.IntMember = If (TryCast (oRow (" Value), Integer?), IDefault) '. –

+1

@Dan Tao - No creo que hayas compilado eso código. Mire una vieja pregunta mía que explica por qué su código no funcionará. http://stackoverflow.com/questions/746767/how-to-acheive-the-c-as-keyword-for-value-types- in-vb-net – stevehipwell

+0

Y una vez más, comentar una pregunta SO mientras estaba fuera de mi propia computadora (con herramientas de desarrollo) ha demostrado ser un error. Tiene razón; me sorprende saber que 'TryCast' no No proporciono la misma funcionalidad conveniente que el operador 'as' de C# para tipos' Nullable (Of T) '. La manera más cercana en que se me ocurre imitar esto es escribir tu propia función, como he sugerido en mi respuesta. –

3

Tengo IsDBNull en un programa que lee muchos datos de una base de datos. Con IsDBNull carga datos en aproximadamente 20 segundos. Sin IsDBNull, aproximadamente 1 segundo.

así que creo que es mejor utilizar:

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

No es el caso problemático en el que el objeto podría ser una cadena. El código del método de extensión a continuación maneja todos los casos. Así es como lo usaría:

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

Me debe estar faltando algo. ¿No está buscando DBNull exactamente lo que hace el método DataRow.IsNull?

He estado usando los siguientes dos métodos de extensión:

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

Si no desea Nullable<T> valores de retorno para GetValue<T>, aquí se puede volver default(T) o alguna otra opción en su lugar.


En una nota relacionada, aquí es una alternativa VB.NET a la sugerencia de 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, esto vuelve a poner en riesgo lo que OP quiere evitar. Al escribir 'row.IsNull (columnName)', lo está leyendo una vez y lo está leyendo nuevamente.No es que que hará una diferencia, pero en teoría puede ser menos eficiente .. – nawfal

+2

no es 'System.Data.DataSetExtensions.DataRowExtensions.Field (esto System.Data.DataRow, cadena)' haciendo esencialmente el mismo que el primer método? –

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

uso como esto

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

me siento muy pocos enfoques aquí doesn Se arriesga a que el prospecto OP sea la mayor preocupación (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) y la mayoría son innecesariamente complejo.Teniendo en cuenta que esto es una micro-optimización inútil, permítanme decirles que básicamente deberían emplearlos:

1) No lea el valor de DataReader/DataRow dos veces, así que o bien, almacénelo antes de las verificaciones y conversiones nulas o incluso mejor pasar directamente su objeto record[X] a un método de extensión personalizado con la firma adecuada.

2) Para obedecer lo anterior, no utilice la función integrada IsDBNull en su DataReader/DataRow ya que llama al record[X] internamente, por lo que en efecto lo hará dos veces.

3) La comparación de tipos siempre será más lenta que la comparación de valores como regla general. Solo haz record[X] == DBNull.Value mejor.

4) El lanzamiento directo será más rápido que llamar a la clase Convert para la conversión, aunque me temo que este último flaqueará menos.

5) Por último, acceder al registro por índice en lugar de por nombre de columna será más rápido nuevamente.


Siento que ir por los enfoques de Szalay, Neil y Darren Koppand será mejor. Particularmente me gusta el método de método de extensión de Darren Koppand que incluye IDataRecord (aunque me gustaría restringirlo más a IDataReader) y el nombre del índice/columna.

Tenga cuidado de llamarlo:

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

y no

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

en caso de tener que diferenciar entre 0 y DBNull. Por ejemplo, si tiene valores nulos en los campos enum, de lo contrario default(MyEnum) corre el riesgo de que se devuelva el primer valor enum. Así que mejor llame al record.GetColumnValue<MyEnum?>("Field").

Puesto que usted está leyendo de un DataRow, crearía método de extensión para ambos DataRow y IDataReader por DRYing código común.

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

Así que ahora lo llaman como:

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 

Creo que esto es lo que debería haber sido en el marco (en lugar de la record.GetInt32, record.GetString métodos etc) en el primer lugar - sin Run- excepciones de tiempo y nos da la flexibilidad para manejar valores nulos.

Desde mi experiencia tuve menos suerte con un método genérico para leer en la base de datos. Siempre tuve que manejar varios tipos de forma personalizada, así que tuve que escribir mis propios métodos GetInt, GetEnum, GetGuid, etc. a largo plazo. ¿Qué sucede si desea recortar espacios en blanco al leer cadena de db por defecto, o tratar DBNull como una cadena vacía? O si su decimal debe ser truncado de todos los ceros finales. Tuve más problemas con el tipo Guid donde los diferentes controladores de conector se comportaban de manera diferente cuando las bases de datos subyacentes podían almacenarlos como cadena o binario.Tengo una sobrecarga de la siguiente manera:

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 el enfoque de Stevo3000, creo que el llamar a un poco feo y aburrido, y será más difícil hacer una función genérica de ella.

4

si en un DataRow la fila [ "nombre de campo"] IsDBNull reemplazarlo con 0 en caso contrario obtener el valor decimal:

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