La forma más eficiente de verificar DBNull y luego asignar a una variable?

Esta pregunta surge ocasionalmente, pero no he visto una respuesta satisfactoria.

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

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

Mi primera pregunta es cuál es más eficiente (he cambiado la condición):

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

Esto indica que .GetType () debería ser más rápido, pero quizás el comstackdor sepa algunos trucos que yo no?

Segunda pregunta, ¿vale la pena almacenar en caché el valor de la fila [“valor”] o el comstackdor optimiza el indexador de todos modos?

Por ejemplo:

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

Notas:

  1. fila [“valor”] existe.
  2. No sé el índice de columna de la columna (de ahí la búsqueda de nombre de columna).
  3. Estoy preguntando específicamente sobre la comprobación de DBNull y luego la asignación (no sobre la optimización prematura, etc.).

Hice una evaluación comparativa de 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 no coincide con el nombre de la columna por caso (por ejemplo, “Valor” en lugar de “valor”, tarda aproximadamente diez veces más (para una cadena):

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

La moraleja de la historia parece ser que si no puede buscar una columna por su índice, asegúrese de que el nombre de la columna que envía al indexador coincida exactamente con el nombre de la DataColumn.

El almacenamiento en caché del valor también parece ser casi el doble de rápido:

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

Entonces, el método más eficiente parece ser:

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

Debo estar perdiendo algo. ¿No se está comprobando si 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(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("Id"); string name = row.GetText("Name"); double? price = row.GetValue("Price"); 

Si no desea que Nullable devuelva valores para GetValue , puede devolver el default(T) o alguna otra opción.


En una nota no relacionada, aquí hay una alternativa de 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 

Deberías usar el método:

 Convert.IsDBNull() 

Teniendo en cuenta que está integrado en el Marco, espero que sea el más eficiente.

Sugeriría algo como:

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

Y sí, el comstackdor debería guardarlo en caché.

El comstackdor no optimizará el indexador (es decir, si utiliza la fila [“value”] dos veces), entonces sí, es un poco más rápido de hacer:

 object value = row["value"]; 

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

DBNull.Value es en realidad un singleton, por lo tanto, para agregar una cuarta opción, quizás puedas usar ReferenceEquals, pero en realidad, creo que te estás preocupando demasiado aquí … No creo que la velocidad sea diferente entre “es”, “==” etc. va a ser 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.

Usarí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 asigne el valor predeterminado que podría establecerse en el valor LHS, lo que permite al comstackdor ignorar la asignación.

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

Siento que muy pocos enfoques aquí no arriesgan la perspectiva del OP más preocupante (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) y la mayoría son innecesariamente complejos. Siendo completamente consciente de que esto es una micro-optimización inútil, permítanme decirles que básicamente deberían emplear estos:

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

2) Para obedecer lo anterior, no use la función incorporada IsDBNull en su DataReader / DataRow ya que eso 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. Simplemente record[X] == DBNull.Value mejor.

4) La conversión directa será más rápida que llamar a la clase Convert para la conversión, aunque me temo que esta última 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 siguiendo los planteamientos de Szalay, Neil y Darren Koppand serán mejores. Particularmente me gusta el método de método de extensión de Darren Koppand que toma IDataRecord (aunque me gustaría restringirlo más a IDataReader ) y el nombre de índice / columna.

Tenga cuidado de llamarlo:

 record.GetColumnValue("field"); 

y no

 record.GetColumnValue("field"); 

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

Como está leyendo desde un DataRow , crearía el método de extensión tanto para DataRow como para IDataReader al DRYing common code.

 public static T Get(this DataRow dr, int index, T defaultValue = default(T)) { return dr[index].Get(defaultValue); } static T Get(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(this T obj) where T : class { return (object)obj == null || obj == DBNull.Value; } public static T Get(this IDataReader dr, int index, T defaultValue = default(T)) { return dr[index].Get(defaultValue); } 

Así que ahora llámalo así:

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

Creo que así es como debería haber estado en el marco (en lugar de los record.GetInt32 , record.GetInt32 , etc.) en primer lugar, sin excepciones en tiempo de ejecución y nos da la flexibilidad para manejar valores nulos.

Según mi experiencia, tuve menos suerte con un método genérico para leer de la base de datos. Siempre tuve que manejar varios tipos de manera personalizada, así que tuve que escribir mis propios GetInt , GetEnum , GetGuid , etc. a largo plazo. ¿Qué pasaría si quisiera 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 de Guid donde los diferentes controladores de conector se comportaron de manera diferente cuando las bases de datos subyacentes pueden almacenarlos como cadena o binario. Tengo una sobrecarga como esta:

 static T Get(this object obj, T defaultValue, Func converter) { if (obj.IsNull()) return defaultValue; return converter == null ? (T)obj : converter(obj); } 

Con el enfoque de Stevo3000, creo que las llamadas son un poco feas y tediosas, y será más difícil hacer una función genérica de ellas.

Existe 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ías:

  static void Main(string[] args) { object number = DBNull.Value; int newNumber = number.SafeDBNull(); Console.WriteLine(newNumber); } public static T SafeDBNull(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(this object value) { return value.SafeDBNull(default(T)); } 

Personalmente, estoy a favor de esta syntax, 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 la legibilidad, es algo así como:

 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 para compacidad en código DAL: tenga en cuenta que en este ejemplo asignamos int bar = -1 si row["Bar"] 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 confusa si no sabes que está allí, pero mantiene toda la operación en una línea, lo que creo mejora la legibilidad cuando estás rellenando propiedades de varias columnas en un bloque de código.

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

Es decir.

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

Entonces:

 IDataRecord record; // Comes from somewhere entity.StringProperty = record["StringProperty"].IsDBNull(null); entity.Int32Property = record["Int32Property"].IsDBNull(50); entity.NoDefaultString = record["NoDefaultString"].IsDBNull(); entity.NoDefaultInt = record["NoDefaultInt"].IsDBNull(); 

También tiene el beneficio de mantener la lógica de verificación nula en un solo lugar. Lo malo es, por supuesto, que es una llamada de método extra.

Solo un pensamiento.

Yo siempre uso:

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

Lo encontré corto y completo.

Así es como manejo la lectura de DataRows

 /// /// Handles operations for Enumerations /// public static class DataRowUserExtensions { ///  /// Gets the specified data row. ///  ///  /// The data row. /// The key. ///  public static T Get(this DataRow dataRow, string key) { return (T) ChangeTypeTo(dataRow[key]); } private static object ChangeTypeTo(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("Type") == 1) { newNode = new TreeViewNode { ToolTip = dbRow.Get("Name"), Text = (dbRow.Get("Name").Length > 25 ? dbRow.Get("Name").Substring(0, 25) + "..." : dbRow.Get("Name")), ImageUrl = "file.gif", ID = dbRow.Get("ReportPath"), Value = dbRow.Get("ReportDescription").Replace("'", "\'"), NavigateUrl = ("?ReportType=" + dbRow.Get("ReportPath")) }; } 

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

Intento evitar este control 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 convertir utilizando as int? .

Si no necesita diferenciar entre string.Empty y null , puede simplemente llamar a .ToString() , ya que DBNull devolverá string.Empty .

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

 public static class DataExtensions { ///  /// Gets the value. ///  /// The type of the data stored in the record /// The record. /// Name of the column. ///  public static T GetColumnValue(this IDataRecord record, string columnName) { return GetColumnValue(record, columnName, default(T)); } ///  /// Gets the value. ///  /// The type of the data stored in the record /// The record. /// Name of the column. /// The value to return if the column contains a DBNull.Value value. ///  public static T GetColumnValue(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, harías algo como

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

si en un DataRow la fila [“fieldname”] es DbNull, reemplácela con 0, de lo contrario, obtenga el valor decimal:

 decimal result = rw["fieldname"] as decimal? ?? 0; 
 public static class DBH { ///  /// Return default(T) if supplied with DBNull.Value ///  ///  ///  ///  public static T Get(object value) { return value == DBNull.Value ? default(T) : (T)value; } } 

usar así

 DBH.Get(itemRow["MyField"]) 

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

Entonces creo que es mejor usar:

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