A maneira mais eficiente de verificar o DBNull e depois atribuir a uma variável?

151

Esta pergunta surge ocasionalmente, mas não vi uma resposta satisfatória.

Um padrão típico é (a linha é um DataRow ):

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

Minha primeira pergunta é qual é mais eficiente (eu inverti a condição):

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

este indica que .GetType () deve ser mais rápido, mas talvez o compilador conheça alguns truques que eu não conheço?

Segunda pergunta, vale a pena armazenar em cache o valor da linha ["value"] ou o compilador otimiza o indexador de qualquer maneira?

Por exemplo:

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

Notas:

  1. a linha ["valor"] existe.
  2. Eu não sei o índice da coluna (daí a pesquisa do nome da coluna).
  3. Estou perguntando especificamente sobre a verificação de DBNull e depois a atribuição (não sobre otimização prematura etc.).

Comparei alguns cenários (tempo em segundos, 10.000.000 de tentativas):

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 tem o mesmo desempenho que "=="

O resultado mais interessante? Se você não corresponder o nome da coluna por maiúsculas e minúsculas (por exemplo, "Valor" em vez de "valor", levará aproximadamente dez vezes mais (para uma sequência de caracteres):

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

A moral da história parece ser que, se você não puder procurar uma coluna por seu índice, verifique se o nome da coluna que você alimenta no indexador corresponde exatamente ao nome da DataColumn.

O armazenamento em cache do valor também parece ser quase duas vezes mais rápido:

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

Portanto, o método mais eficiente parece ser:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }
ilitirit
fonte
1
Você pode esclarecer se a linha é um DataRow ou um IDataRecord / IDataReader?
Marc Gravell
7
Agora temos o .NET Framework muito melhor e podemos usar os Métodos DataRowExtensions .
Pavel Hodek
Se você não corresponder o nome da coluna por caso (por exemplo, "Valor" em vez de "valor", leva aproximadamente dez vezes mais (para uma sequência de caracteres) Isso depende completamente da implementação. Lembro que esse era o caso (alteração no caso o nome da coluna seja muito mais lento) com o conector MySQL ADO.NET, mas não para o SqlServer ou o SQLite (não lembre-se) .As coisas podem ter mudado agora.
Nawfal #
@PavelHodek uma vergonha que é apenas para DataRow. Teria adorado IDataRecordextensões.
Nawfal #

Respostas:

72

Eu devo estar esquecendo alguma coisa. Não está verificando DBNullexatamente o que o DataRow.IsNullmétodo faz?

Eu tenho usado os dois métodos de extensão a seguir:

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 você não deseja Nullable<T>retornar valores GetValue<T>, poderá retornar facilmente default(T)ou alguma outra opção.


Em uma nota não relacionada, aqui está uma alternativa do VB.NET à sugestão do 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
fonte
3
Dan isso arrisca novamente o que o OP quer evitar. Ao escrever, row.IsNull(columnName)você já está lendo uma vez e novamente. Não estou dizendo que vai fazer a diferença, mas teoricamente pode ser menos eficiente ..
Nawfal
2
Não está System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)fazendo essencialmente a mesma coisa que o primeiro método?
Dennis L
35

Você deve usar o método:

Convert.IsDBNull()

Considerando que está embutido no Framework, eu esperaria que este fosse o mais eficiente.

Eu sugeriria algo como:

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

E sim, o compilador deve armazená-lo em cache para você.

Jon Grant
fonte
5
Bem, todas as opções mencionadas são incorporadas ao quadro ... Na verdade, Convert.IsDBNull faz um monte de trabalho extra relacionada com IConvertible ...
Marc Gravell
1
E re o cache - se você quer dizer com o exemplo condicional, não - ele realmente não deveria (e não faz). Ele executará o indexador duas vezes.
Marc Gravell
Ah, e esse código não é compilado - mas adicione um (int?) A um deles e você verá (no IL) 2 de: objeto de instância callvirt [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell
20

O compilador não otimizará o indexador (ou seja, se você usar a linha ["value"] duas vezes), então sim, será um pouco mais rápido:

object value = row["value"];

e depois use o valor duas vezes; usando .GetType () corre o risco de problemas se for nulo ...

DBNull.Valueé realmente um singleton, então, para adicionar uma quarta opção - talvez você possa usar o ReferenceEquals -, mas, na realidade, acho que você está se preocupando demais aqui ... Não acho que a velocidade seja diferente entre "is", "== "etc será a causa de qualquer problema de desempenho que você estiver vendo. Perfile seu código inteiro e concentre-se em algo que importa ... não será isso.

Marc Gravell
fonte
2
Em praticamente todos os casos, o == será equivalente ao ReferenceEquals (especialmente ao DBNull) e é muito mais legível. Use a otimização de @Marc Gravell, se quiser, mas eu estou com ele - provavelmente não vai ajudar muito. BTW, a igualdade de referência deve sempre superar a verificação de tipo.
Tbf #
1
Antigo agora, mas recentemente vi vários casos em que isso era exatamente o que o criador de perfil disse para corrigir. Imagine avaliar grandes conjuntos de dados, onde todas as células precisam fazer essa verificação. A otimização que pode pode colher grandes recompensas. Mas a parte importante da resposta ainda é boa: primeiro perfil , para saber onde melhor gastar seu tempo.
Joel Coehoorn
Eu acho que a introdução em C # 6 do operador Elvis facilita a exceção de referência nula na verificação que você sugere. valor .GetType () == typeof (DBNull)?
Eniola
Sim eu concordo. geralmente é o melhor caminho a percorrer, mas para quem não deseja usar. GetType () cujos riscos você apontou, então? fornece uma maneira de contornar isso.
Eniola
9

Eu usaria o seguinte código em C # (o VB.NET não é tão simples).

O código atribui o valor se não for nulo / DBNull, caso contrário, atribui o padrão que pode ser definido como o valor LHS, permitindo que o compilador ignore a atribuição.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
fonte
1
A versão VB.NET é tão simples: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@ Dan Tao - Eu não acho que você tenha compilado esse código. Veja uma pergunta antiga minha que explica por que seu código não funciona. stackoverflow.com/questions/746767/…
stevehipwell 16/06
E mais uma vez, comentar sobre uma pergunta SO enquanto estiver fora do meu computador (com ferramentas de desenvolvimento) provou ser um erro! Você está certo; Estou surpreso ao saber que TryCastnão fornece a mesma funcionalidade conveniente que o asoperador de C # para Nullable(Of T)tipos. A maneira mais próxima de imitar isso é escrever sua própria função, como sugeri agora na minha resposta.
Dan Tao
Você terá dificuldade em refatorá-lo para um método genérico e, mesmo se o fizer, o excesso de conversão envolvido o tornará menos eficiente.
Nawfal
8

Sinto que apenas poucas abordagens aqui não arriscam a perspectiva do OP mais preocupante (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) e a maioria é desnecessariamente complexa. Consciente de que isso é uma micro-otimização inútil, deixe-me dizer que você deve basicamente empregar estes:

1) Não leia o valor do DataReader / DataRow duas vezes; portanto, armazene-o em cache antes de verificações e lançamentos / conversões nulos ou, melhor ainda, passe diretamente seu record[X] objeto para um método de extensão personalizado com a assinatura apropriada.

2) Para obedecer ao acima, não use a IsDBNullfunção incorporada no seu DataReader / DataRow, pois isso chama orecord[X] internamente; portanto, você fará isso duas vezes.

3) A comparação de tipos sempre será mais lenta que a comparação de valores como regra geral. Apenas façarecord[X] == DBNull.Value melhor.

4) A transmissão direta será mais rápida do que chamar Convert classe pela conversão, embora eu tenha medo de que a segunda vacile menos.

5) Por fim, acessar o registro pelo índice em vez do nome da coluna será mais rápido novamente.


Eu sinto que seguir as abordagens de Szalay, Neil e Darren Koppand será melhor. Eu particularmente gosto da abordagem do método de extensão de Darren Koppand, que inclui IDataRecord(embora eu queira restringir ainda mais IDataReader) o nome do índice / coluna.

Tome cuidado para chamá-lo:

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

e não

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

caso você precise diferenciar entre 0e DBNull. Por exemplo, se você tiver valores nulos nos campos de enumeração, o default(MyEnum)risco será que o primeiro valor de enumeração seja retornado. Então é melhor ligar record.GetColumnValue<MyEnum?>("Field").

Desde que você está lendo a partir de um DataRow, eu iria criar método de extensão para ambos DataRowe IDataReaderpor DRY código comum.

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

Então agora chame assim:

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

Eu acredito que é assim que deveria ter sido na estrutura (em vez dos métodos record.GetInt32, record.GetStringetc) em primeiro lugar - sem exceções em tempo de execução e nos dá a flexibilidade de lidar com valores nulos.

Pela minha experiência, tive menos sorte com um método genérico para ler no banco de dados. Eu sempre tive de lidar com personalizado vários tipos, então eu tive que escrever meu próprio GetInt, GetEnum, GetGuid, etc métodos a longo prazo. E se você quisesse aparar espaços em branco ao ler a string do db por padrão ou tratar DBNullcomo uma string vazia? Ou se o seu decimal deve ser truncado de todos os zeros à direita. Eu tive mais problemas com o Guidtipo em que diferentes drivers de conector se comportavam de maneira diferente quando os bancos de dados subjacentes podem armazená-los como string ou binários. Eu tenho uma 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);
}

Com a abordagem do Stevo3000, considero a chamada um pouco feia e entediante, e será mais difícil criar uma função genérica com ela.

nawfal
fonte
7

Há um caso problemático em que o objeto pode ser uma string. O código do método de extensão abaixo lida com todos os casos. Aqui está como você o usaria:

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

Pessoalmente, sou a favor dessa sintaxe, que usa o método IsDbNull explícito exposto por IDataRecorde armazena em cache o índice da coluna para evitar uma pesquisa de string duplicada.

Expandido para facilitar a leitura, é algo 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 em uma única linha para compactação no código DAL - observe que neste exemplo estamos atribuindo int bar = -1se row["Bar"]for 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));

A atribuição em linha pode ser confusa se você não souber que está lá, mas mantém toda a operação em uma linha, o que eu acho que melhora a legibilidade quando você está preenchendo propriedades de várias colunas em um bloco de código.

Dylan Beattie
fonte
3
O DataRow não implementa o IDataRecord.
Ilitirit 21/10/08
5

Não que eu tenha feito isso, mas você pode contornar a chamada do indexador duplo e ainda manter seu código limpo usando um método estático / extensão.

Ou seja.

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

Então:

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

Também tem o benefício de manter a lógica de verificação nula em um único local. A desvantagem é, obviamente, que é uma chamada de método extra.

Apenas um pensamento.

Richard Szalay
fonte
2
Adicionar um método de extensão no objeto é muito amplo, no entanto. Pessoalmente, eu poderia ter considerado um método de extensão no DataRow, mas não um objeto.
Marc Gravell
É verdade, mas lembre-se de que os métodos de extensão só estão disponíveis quando o namespace da classe de extensão é importado.
Richard Szalay
5

Eu tento evitar essa verificação o máximo possível.

Obviamente, não precisa ser feito para colunas que não podem conter null .

Se você estiver armazenando em um tipo de valor Nullable ( int?, etc.), poderá apenas converter usando as int?.

Se você não precisar diferenciar entre string.Emptye null, basta ligar .ToString(), pois o DBNull retornará string.Empty.

bdukes
fonte
4

Eu sempre uso:

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

Achei curto e abrangente.

Patrick Desjardins
fonte
4

É assim que eu lido com a leitura do 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);
    }
}

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

Adereços para Monsters Got My .Net para código ChageTypeTo.

Chris Marisic
fonte
4

Eu fiz algo semelhante com métodos de extensão. Aqui está o meu 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 usá-lo, você faria algo como

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

se em um DataRow a linha ["fieldname"] isDbNull a substitua por 0, caso contrário, obtenha o valor decimal:

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

use assim

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

Eu tenho o IsDBNull em um programa que lê muitos dados de um banco de dados. Com o IsDBNull, ele carrega dados em cerca de 20 segundos. Sem IsDBNull, cerca de 1 segundo.

Então eu acho que é melhor usar:

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