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

151

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

Un patrón típico es (la fila es una 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 ¿tal vez el compilador conoce algunos trucos que yo no?

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

Por ejemplo:

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

Notas:

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

Comparé algunos escenarios (tiempo en segundos, 10,000,000 pruebas):

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 el 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 ser que si no puede buscar una columna por su índice, asegúrese de que el nombre de la columna que alimenta al indexador coincida exactamente con el nombre de la columna de datos.

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();
 }
ilitirit
fuente
1
¿Puede aclarar si la fila es un DataRow o un IDataRecord / IDataReader?
Marc Gravell
77
Ahora tenemos mucho mejor .NET Framework y podemos usar métodos DataRowExtensions .
Pavel Hodek
Si no coincide el nombre de la columna por caso (por ejemplo, "Valor" en lugar de "valor", se tarda aproximadamente diez veces más (para una cadena) Esto depende completamente de la implementación. Recuerdo que este fue el caso (cambio en caso de que el nombre de la columna sea mucho más lento) con el conector MySQL ADO.NET, pero no para SqlServer o SQLite (no recuerdo). Las cosas podrían haber cambiado ahora. Sí, la directriz básica es, en caso de duda, ir a los ordinales.
nawfal
@PavelHodek es una pena que solo sea para DataRow. Me hubiera encantado las IDataRecordextensiones.
nawfal

Respuestas:

72

Debo estar perdiendo algo. ¿No está comprobando DBNullexactamente qué hace el DataRow.IsNullmétodo?

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>, podría devolver fácilmente default(T)o alguna otra opción en su lugar.


En una nota no relacionada, aquí hay 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
Dan Tao
fuente
3
Dan esto arriesga nuevamente lo que OP quiere evitar. Al escribir, row.IsNull(columnName)ya 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 está System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)haciendo esencialmente lo mismo que el primer método?
Dennis G
35

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 en la línea de:

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

Y sí, el compilador debería almacenarlo en caché.

Jon Grant
fuente
55
Bueno, todas las opciones mencionadas están integradas en el marco ... En realidad, Convert.IsDBNull hace mucho trabajo adicional relacionado con IConvertible ...
Marc Gravell
1
Y vuelva a la caché, si quiere decir con el ejemplo condicional, no, realmente no debería (y no lo hace). Ejecutará el indexador dos veces.
Marc Gravell
Ah, y ese código no se compila, pero agregue un (int?) A uno de ellos, y verá (en la IL) 2 de: objeto de instancia de callvirt [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell
20

El compilador no optimizará el indexador (es decir, si usa la fila ["valor"] dos veces), entonces sí, es un poco más rápido:

object value = row["value"];

y luego usa el valor dos veces; el uso de .GetType () plantea problemas si es nulo ...

DBNull.Valueen realidad es un singleton, así que para agregar una cuarta opción, quizás podría usar ReferenceEquals, pero en realidad, creo que se está 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é viendo. Perfile todo su código y concéntrese en algo importante ... no será esto.

Marc Gravell
fuente
2
En prácticamente todos los casos, == 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 ayudará mucho. Por cierto, la igualdad de referencia siempre debe vencer a la verificación de tipos.
tvanfosson
1
Viejo ahora, pero recientemente he visto una serie de casos en los que esto era exactamente lo que el perfilador dijo que solucionara. Imagine evaluar grandes conjuntos de datos, donde cada celda necesita hacer esta verificación. Optimizar eso puede cosechar grandes recompensas. Pero la parte importante de la respuesta sigue siendo buena: primero el perfil , para saber dónde mejor pasar el tiempo.
Joel Coehoorn
Supongo que la introducción de C # 6 del operador de Elvis hace que sea fácil evitar la excepción de referencia nula en la verificación que sugiera. value? .GetType () == typeof (DBNull)
Eniola
Sí estoy de acuerdo. generalmente es una mejor manera de hacerlo, pero para aquellos que no quieren usar .GetType () cuyos riesgos usted señaló, entonces? proporciona una forma de evitarlo.
Eniola
9

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, asigna el valor predeterminado que podría establecerse al valor LHS permitiendo que el compilador ignore la asignación.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
fuente
1
La versión VB.NET es tan simple: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@ Dan Tao: no creo que hayas compilado ese código. Mire una vieja pregunta mía que explica por qué su código no funcionará. stackoverflow.com/questions/746767/…
stevehipwell
¡Y una vez más, comentar una pregunta SO mientras estoy lejos de mi propia computadora (con herramientas de desarrollo) ha resultado ser un error! Tienes razón; Me sorprende saber que TryCastno proporciona la misma funcionalidad conveniente que el asoperador de C # para los Nullable(Of T)tipos. La forma más cercana en que puedo pensar para imitar esto es escribir su propia función, como ya he sugerido en mi respuesta.
Dan Tao
Te resultará difícil refactorizar esto en un método genérico, e incluso si lo haces, el exceso de casting involucrado lo hará menos eficiente.
nawfal
8

Siento que muy pocos enfoques aquí no arriesgan al prospecto OP la mayor preocupación (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) y la mayoría son innecesariamente complejos. Siendo plenamente consciente de que esta es una microoptimización inútil, déjame decirte que básicamente debes emplear estos:

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

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

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

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


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

Tenga cuidado de llamarlo:

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

y no

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

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

Puesto que usted está leyendo de una DataRow, me gustaría crear método de extensión para ambos DataRowy IDataReaderpor secado 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 llámalo así:

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 así es como debería haber sido en el marco (en lugar de los métodos record.GetInt32, record.GetStringetc.) 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 tenía para manejar varios tipos de encargo, así que tuve que escribir mi propio GetInt, GetEnum, GetGuidmétodos, etc., en el largo plazo. ¿Qué sucede si desea recortar espacios en blanco al leer cadenas de db por defecto, o tratarlas DBNullcomo una cadena vacía? O si su decimal se debe truncar de todos los ceros finales. Tuve más problemas con el Guidtipo donde los diferentes controladores de conector se comportaban de manera diferente cuando las bases de datos subyacentes pueden almacenarlos como cadenas o binarios. Tengo una sobrecarga como esta:

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, encuentro la llamada un poco fea y tediosa, y será más difícil hacer una función genérica.

nawfal
fuente
7

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

    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
fuente
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 de cadena duplicada.

Ampliado para facilitar la lectura, 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 caber en una sola línea para compacidad en el código DAL; tenga en cuenta que en este ejemplo estamos asignando int bar = -1si 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 sabe que está allí, pero mantiene toda la operación en una línea, lo que creo que mejora la legibilidad cuando está completando propiedades de varias columnas en un bloque de código.

Dylan Beattie
fuente
3
Sin embargo, DataRow no implementa IDataRecord.
ilitirit
5

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

Es decir.

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

Luego:

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 adicional.

Solo un pensamiento.

Richard Szalay
fuente
2
Sin embargo, agregar un método de extensión en el objeto es muy amplio. Personalmente, podría haber considerado un método de extensión en DataRow, pero no un objeto.
Marc Gravell
Es cierto, aunque 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.
Richard Szalay
5

Intento evitar este control tanto como sea posible.

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

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

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

bdukes
fuente
4

Yo siempre uso:

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

Lo encontré breve y completo.

Patrick Desjardins
fuente
4

Así es como manejo 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"))
                  };
}

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

Chris Marisic
fuente
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, harías algo como

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

si en una DataRow la fila ["fieldname"] esDbNull, reemplácela por 0; de lo contrario, obtenga el valor decimal:

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

usar así

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

Tengo IsDBNull en un programa que lee muchos datos de una base de datos. Con IsDBNull carga datos en unos 20 segundos. Sin IsDBNull, aproximadamente 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;
}
Mastahh
fuente