Le moyen le plus efficace de vérifier DBNull puis de l'affecter à une variable?

151

Cette question revient occasionnellement, mais je n'ai pas vu de réponse satisfaisante.

Un modèle typique est (la ligne est un DataRow ):

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

Ma première question est de savoir lequel est le plus efficace (j'ai inversé la condition):

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

Cela indique que .GetType () devrait être plus rapide, mais peut-être que le compilateur connaît quelques astuces que je ne connais pas?

Deuxième question, vaut-il la peine de mettre en cache la valeur de la ligne ["valeur"] ou le compilateur optimise-t-il l'indexeur de toute façon?

Par exemple:

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

Remarques:

  1. la ligne ["valeur"] existe.
  2. Je ne connais pas l'index de colonne de la colonne (d'où la recherche de nom de colonne).
  3. Je pose des questions spécifiquement sur la vérification de DBNull puis sur l'affectation (pas sur l'optimisation prématurée, etc.).

J'ai comparé quelques scénarios (temps en secondes, 10 000 000 essais):

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 a les mêmes performances que "=="

Le résultat le plus intéressant? Si vous ne correspondez pas au nom de la colonne par cas (par exemple, "Valeur" au lieu de "valeur", cela prend environ dix fois plus de temps (pour une chaîne):

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

La morale de l'histoire semble être que si vous ne pouvez pas rechercher une colonne par son index, assurez-vous que le nom de la colonne que vous fournissez à l'indexeur correspond exactement au nom de DataColumn.

La mise en cache de la valeur semble également être presque deux fois plus rapide:

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

La méthode la plus efficace semble donc être:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }
ilitirit
la source
1
Pouvez-vous préciser si la ligne est un DataRow ou un IDataRecord / IDataReader?
Marc Gravell
7
Nous avons maintenant un bien meilleur .NET Framework et nous pouvons utiliser les méthodes DataRowExtensions .
Pavel Hodek
Si vous ne correspondez pas au nom de la colonne par cas (par exemple, "Valeur" au lieu de "valeur", cela prend environ dix fois plus de temps (pour une chaîne) Cela dépend complètement de l'implémentation. Je me souviens que c'était le cas (changement de cas où le nom de la colonne est beaucoup plus lent) avec le connecteur MySQL ADO.NET, mais pas du tout pour SqlServer ou SQLite (je ne m'en souviens pas). Les choses ont peut-être changé maintenant. Oui, la règle de base est, en cas de doute, d'opter pour les ordinaux.
nawfal
@PavelHodek une telle honte qui n'est que pour DataRow. J'aurais aimé les IDataRecordextensions.
nawfal

Réponses:

72

J'ai dû louper quelque chose. N'est-ce pas vérifier DBNullexactement ce que fait la DataRow.IsNullméthode?

J'ai utilisé les deux méthodes d'extension suivantes:

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

Usage:

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

Si vous ne voulez pas de Nullable<T>valeurs de retour pour GetValue<T>, vous pouvez facilement retourner default(T)ou une autre option à la place.


Sur une note non liée, voici une alternative VB.NET à la suggestion 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
la source
3
Dan, cela risque à nouveau ce qu'OP veut éviter. En écrivant, row.IsNull(columnName)vous le lisez déjà une fois et le relisez. Ne pas dire que cela fera une différence, mais théoriquement, cela peut être moins efficace.
nawfal
2
Est- System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)ce que faire essentiellement la même chose que la première méthode?
Dennis G
35

Vous devez utiliser la méthode:

Convert.IsDBNull()

Étant donné qu'il est intégré au Framework, je m'attendrais à ce que ce soit le plus efficace.

Je suggérerais quelque chose du genre:

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

Et oui, le compilateur doit le mettre en cache pour vous.

Jon Grant
la source
5
Eh bien, toutes les options mentionnées sont intégrées dans le cadre ... En fait, Convert.IsDBNull fait beaucoup de travail supplémentaire concernant IConvertible ...
Marc Gravell
1
Et re le cache - si vous voulez dire avec l'exemple conditionnel, non - cela ne devrait vraiment pas (et ne le fait pas). Il exécutera l'indexeur deux fois.
Marc Gravell
Oh, et ce code ne compile pas - mais ajoutez un (int?) À l'un d'eux, et vous verrez (dans l'IL) 2 de: objet d'instance callvirt [System.Data] System.Data.DataRow :: get_Item (string)
Marc Gravell
20

Le compilateur n'optimisera pas l'indexeur (c'est-à-dire si vous utilisez la ligne ["value"] deux fois), donc oui, c'est un peu plus rapide à faire:

object value = row["value"];

puis utilisez la valeur deux fois; l'utilisation de .GetType () risque de poser des problèmes s'il est nul ...

DBNull.Valueest en fait un singleton, donc pour ajouter une 4ème option - vous pourriez peut-être utiliser ReferenceEquals - mais en réalité, je pense que vous vous inquiétez trop ici ... Je ne pense pas que la vitesse diffère entre "is", "== "etc va être la cause de tout problème de performance que vous rencontrez. Profilez votre code entier et concentrez-vous sur quelque chose qui compte ... ce ne sera pas ça.

Marc Gravell
la source
2
Dans pratiquement tous les cas, == sera équivalent à ReferenceEquals (en particulier à DBNull) et il est beaucoup plus lisible. Utilisez l'optimisation de @Marc Gravell si vous le souhaitez, mais je suis avec lui - cela ne m'aidera probablement pas beaucoup. BTW, l'égalité de référence devrait toujours battre la vérification de type.
tvanfosson
1
Vieux maintenant, mais j'ai récemment vu un certain nombre de cas où c'était exactement ce que le profileur a dit de corriger. Imaginez évaluer de grands ensembles de données, où chaque cellule doit effectuer cette vérification. L'optimisation de cela peut rapporter de grandes récompenses. Mais la partie importante de la réponse est toujours bonne: le profil d' abord, pour savoir où passer le mieux votre temps.
Joel Coehoorn
Je suppose que l'introduction en C # 6 de l'opérateur Elvis permet d'éviter facilement l'exception de référence nulle dans la vérification que vous suggérez. value? .GetType () == typeof (DBNull)
Eniola
Oui je suis d'accord. est généralement une meilleure solution, mais pour ceux qui ne veulent pas utiliser .GetType () dont vous avez signalé les risques, alors?. fournit un moyen de le contourner.
Eniola
9

J'utiliserais le code suivant en C # ( VB.NET n'est pas aussi simple).

Le code affecte la valeur si elle n'est pas nulle / DBNull, sinon il attribue la valeur par défaut qui pourrait être définie sur la valeur LHS permettant au compilateur d'ignorer l'affectation.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
la source
1
La version VB.NET est aussi simple: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@Dan Tao - Je ne pense pas que vous ayez compilé ce code. Regardez une vieille question qui explique pourquoi votre code ne fonctionnera pas. stackoverflow.com/questions/746767/…
stevehipwell
Et encore une fois, commenter une question SO alors que je suis loin de mon propre ordinateur (avec des outils de développement dessus) s'est avéré être une erreur! Vous avez raison; Je suis surpris d'apprendre que TryCastcela ne fournit pas les mêmes fonctionnalités pratiques que l' asopérateur C # pour les Nullable(Of T)types. Le moyen le plus proche d'imiter cela est d'écrire votre propre fonction, comme je l'ai maintenant suggéré dans ma réponse.
Dan Tao
Vous aurez du mal à le refactoriser en une méthode générique, et même si vous le faites, le trop grand casting impliqué le rendra moins efficace.
nawfal
8

Je pense que très peu d'approches ici ne risquent pas le plus d'inquiéter la perspective OP (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) et la plupart sont inutilement complexes. Étant pleinement conscient qu'il s'agit d'une micro-optimisation inutile, laissez-moi vous dire que vous devriez essentiellement les utiliser:

1) Ne lisez pas la valeur de DataReader / DataRow deux fois - alors mettez-la en cache avant les vérifications et les casts / conversions nulles ou mieux encore passez directement votre record[X]objet à une méthode d'extension personnalisée avec la signature appropriée.

2) Pour obéir à ce qui précède, n'utilisez pas la IsDBNullfonction intégrée sur votre DataReader / DataRow car cela appelle le en record[X]interne, donc en fait vous le ferez deux fois.

3) La comparaison de type sera toujours plus lente que la comparaison de valeurs en règle générale. Fais record[X] == DBNull.Valuemieux.

4) Le lancer direct sera plus rapide que d'appeler la Convertclasse pour la conversion, même si je crains que cette dernière ne faiblisse moins.

5) Enfin, accéder à l'enregistrement par index plutôt que par nom de colonne sera à nouveau plus rapide.


Je pense que passer par les approches de Szalay, Neil et Darren Koppand sera mieux. J'aime particulièrement l'approche de la méthode d'extension de Darren Koppand qui prend en compte IDataRecord(bien que je voudrais le restreindre davantage IDataReader) et le nom de l'index / de la colonne.

Prenez soin de l'appeler:

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

et pas

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

au cas où vous auriez besoin de faire la différence entre 0et DBNull. Par exemple, si vous avez des valeurs nulles dans les champs d'énumération, sinon la default(MyEnum)première valeur d'énumération risque d'être renvoyée. Alors mieux vaut appeler record.GetColumnValue<MyEnum?>("Field").

Puisque vous lisez à partir d'un DataRow, je créerais une méthode d'extension pour les deux DataRowet IDataReaderen DRYing le code commun.

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

Alors maintenant, appelez-le comme:

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

Je pense que c'est ainsi que cela aurait dû être dans le cadre (au lieu des méthodes record.GetInt32, record.GetStringetc.) en premier lieu - aucune exception d'exécution et nous donne la flexibilité de gérer les valeurs nulles.

D'après mon expérience, j'ai eu moins de chance avec une méthode générique pour lire à partir de la base de données. J'ai toujours eu à traiter différents types de mesure, donc je devais écrire mes propres GetInt, GetEnum, GetGuid, etc. méthodes à long terme. Que faire si vous souhaitez réduire les espaces blancs lors de la lecture d'une chaîne à partir de db par défaut ou la traiter DBNullcomme une chaîne vide? Ou si votre décimal doit être tronqué de tous les zéros de fin. J'ai eu le plus de problèmes avec le Guidtype où différents pilotes de connecteur se comportaient différemment lorsque les bases de données sous-jacentes peuvent les stocker sous forme de chaîne ou de binaire. J'ai une surcharge comme celle-ci:

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

Avec l'approche de Stevo3000, je trouve l'appel un peu laid et fastidieux, et il sera plus difficile d'en faire une fonction générique.

nawfal
la source
7

Il y a le cas gênant où l'objet pourrait être une chaîne. Le code de méthode d'extension ci-dessous gère tous les cas. Voici comment vous l'utiliseriez:

    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
la source
6

Je préfère personnellement cette syntaxe, qui utilise la méthode explicite IsDbNull exposée par IDataRecord, et met en cache l'index de colonne pour éviter une recherche de chaîne en double.

Développé pour la lisibilité, cela ressemble à:

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

Réécrit pour tenir sur une seule ligne pour la compacité du code DAL - notez que dans cet exemple, nous attribuons int bar = -1if row["Bar"]est nul.

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'affectation en ligne peut être déroutante si vous ne savez pas qu'elle est là, mais elle conserve toute l'opération sur une seule ligne, ce qui, je pense, améliore la lisibilité lorsque vous remplissez les propriétés de plusieurs colonnes dans un bloc de code.

Dylan Beattie
la source
3
DataRow n'implémente cependant pas IDataRecord.
ilitirit
5

Non pas que j'ai fait cela, mais vous pouvez contourner l'appel du double indexeur tout en gardant votre code propre en utilisant une méthode statique / d'extension.

C'est à 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));
}

Ensuite:

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

A également l'avantage de conserver la logique de vérification nulle au même endroit. L'inconvénient est, bien sûr, qu'il s'agit d'un appel de méthode supplémentaire.

Juste une pensée.

Richard Szalay
la source
2
L'ajout d'une méthode d'extension sur un objet est cependant très large. Personnellement, j'aurais pu envisager une méthode d'extension sur DataRow, mais pas un objet.
Marc Gravell
C'est vrai, mais gardez à l'esprit que les méthodes d'extension ne sont disponibles que lorsque l'espace de noms de la classe d'extension est importé.
Richard Szalay
5

J'essaie d'éviter ce contrôle autant que possible.

Évidemment, cela n'a pas besoin d'être fait pour les colonnes qui ne peuvent pas tenir null.

Si vous stockez dans un type valeur Nullable ( int?, etc.), vous pouvez simplement convertir en utilisant as int?.

Si vous n'avez pas besoin de faire la différence entre string.Emptyet null, vous pouvez simplement appeler .ToString(), car DBNull reviendra string.Empty.

bdukes
la source
4

J'utilise toujours:

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

Je l'ai trouvé court et complet.

Patrick Desjardins
la source
4

Voici comment je gère la lecture à partir 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);
    }
}

Exemple d'utilisation:

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

Les accessoires de Monsters Got My .Net pour le code ChageTypeTo.

Chris Marisic
la source
4

J'ai fait quelque chose de similaire avec les méthodes d'extension. Voici mon code:

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

Pour l'utiliser, vous feriez quelque chose comme

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

si dans un DataRow la ligne ["fieldname"] isDbNull remplacez-la par 0 sinon obtenez la valeur décimale:

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

utiliser comme ça

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

J'ai IsDBNull dans un programme qui lit beaucoup de données à partir d'une base de données. Avec IsDBNull, il charge les données en 20 secondes environ. Sans IsDBNull, environ 1 seconde.

Donc je pense qu'il vaut mieux utiliser:

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