DBNullをチェックして変数に割り当てる最も効率的な方法は?


151

この質問はときどき出てきますが、満足のいく答えは見当たりません。

典型的なパターンは(行はDataRowです):

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

私の最初の質問は、どちらがより効率的であるかです(私は条件を反転しました):

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

これは、.GetType()の方が高速であるべきであることを示していますが、おそらくコンパイラが知っていないいくつかのトリックを知っていますか?

2番目の質問、row ["value"]の値をキャッシュする価値はありますか、それともコンパイラーはとにかくインデクサーを最適化しますか?

例えば:

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

ノート:

  1. row ["value"]が存在します。
  2. 列の列インデックスがわからない(したがって、列名の検索)。
  3. DBNullのチェックと割り当てについて(時期尚早の最適化などについてではなく)具体的に質問しています。

私はいくつかのシナリオをベンチマークしました(秒単位の時間、10,000,000試行):

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のパフォーマンスは "=="と同じです

最も興味深い結果は?列の名前を大文字と小文字で一致させない場合(たとえば、「値」ではなく「値」の場合)(文字列の場合)はおよそ10倍長くかかります。

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

この話の教訓は、インデックスで列を検索できない場合は、インデクサーにフィードする列名がDataColumnの名前と正確に一致するようにすることです。

値のキャッシュもほぼ2倍の速さで表示されます。

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

したがって、最も効率的な方法は次のようになります。

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

1
行がDataRowかIDataRecord / IDataReaderかを明確にできますか?
Marc Gravell

7
これで.NET Frameworkが大幅に改善され、DataRowExtensionsメソッドを使用できるようになりました
Pavel Hodek

列の名前を大文字と小文字で一致させない場合(たとえば、「値」ではなく「値」の場合、(文字列の場合)およそ10倍かかり ます。これは実装に完全に依存します。これは事実でした(変更列名がはるかに遅い場合)MySQL ADO.NETコネクタを使用するが、SqlServerまたはSQLiteの場合はまったく変更されない(覚えていない)。今は変更されている可能性がある。
nawfal 2017

@PavelHodekは、DataRow専用のそのような恥です。IDataRecord拡張機能を気に入っていただろう。
nawfal 2017

回答:


72

私は何かを逃しているに違いない。メソッドがDBNull何をするかを正確にチェックしてDataRow.IsNullいませんか?

私は次の2つの拡張メソッドを使用しています。

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

使用法:

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

Nullable<T>戻り値が必要ない場合はGetValue<T>default(T)代わりに簡単に返すか、他のオプションを使用できます。


無関係なメモとして、Stevo3000の提案に代わるVB.NETを以下に示します。

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は、OPが避けたいものを再び危険にさらします。書くrow.IsNull(columnName)ことで、あなたはそれをすでに一度読んで、それをもう一度読んでいるのです。それが違いを生むだろうと言って、しかし、理論的にはそれはあまり効率的ではできない...
nawfal

2
System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)基本的に最初の方法と同じことをしていませんか?
Dennis G

35

あなたはメソッドを使うべきです:

Convert.IsDBNull()

フレームワークに組み込まれていることを考えると、これが最も効率的であると思います。

私は以下に沿って何かを提案します:

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

そして、はい、コンパイラはそれをキャッシュするはずです。


5
さて、言及されたすべてのオプションはフレームワークに組み込まれています...実際、Convert.IsDBNullはIConvertibleに関連する多くの追加作業を行います...
Marc Gravell

1
そして、キャッシュを再設定します-条件付きの例を意味する場合、いいえ-それは実際にすべきではありません(そしてしません)。インデクサーを2回実行します。
Marc Gravell

ああ、そのコードはコンパイルされませんが、それらの1つに(int?)を追加すると、(ILで)2が表示されます:callvirtインスタンスオブジェクト[System.Data] System.Data.DataRow :: get_Item(string)
Marc Gravell

20

コンパイラーはインデクサーを最適化しません(つまり、row ["value"]を2回使用する場合)ので、そうするほうが少し高速です。

object value = row["value"];

そして、値を2回使用します。.GetType()を使用すると、nullの場合に問題が発生する可能性があります...

DBNull.Valueは実際にはシングルトンなので、4番目のオプションを追加するには、おそらくReferenceEqualsを使用できますが、実際には、ここではあまり心配しすぎていると思います...「is」と「==」の速度の違いはないと思います「などが、表示されているパフォーマンスの問題の原因になります。コード全体をプロファイリングし、重要なことに焦点を当ててください ...これではありません。


2
事実上すべての場合において、==はReferenceEquals(特にDBNull)と同等になり、はるかに読みやすくなります。必要に応じて@Marc Gravellの最適化を使用しますが、私は彼と一緒にいます-おそらくあまり役​​に立ちません。ところで、参照の等価性は常に型チェックに勝るはずです。
tvanfosson 2008年

1
今は古いですが、最近、プロファイラーが修正するように言ったケースがいくつかあります。すべてのセルがこのチェックを行う必要がある大きなデータセットを評価することを想像してください。それを最適化することは大きな報酬を得ることができます。しかし、答えの重要な部分は依然として適切です。最初にプロファイルを作成し、時間を過ごすのに最適な場所を知ることです。
Joel Coehoorn、2014

ElvisオペレーターのC#6の導入により、提案するチェックでnull参照の例外を簡単に回避できると思います。value?.GetType()== typeof(DBNull)
Eniola

はい私は同意する。一般的にはより良い方法ですが、リスクを指摘した.GetType()を使用したくない場合は?それを回避する方法を提供します。
Eniola 2016年

9

次のコードをC#で使用します(VB.NETはそれほど単純ではありません)。

null / DBNullでない場合、コードは値を割り当てます。それ以外の場合は、LHS値に設定できるデフォルトを割り当て、コンパイラが割り当てを無視できるようにします。

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

1
VB.NETバージョンは次のように単純ですoSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault)
Dan Tao

1
@Dan Tao-そのコードをコンパイルしたとは思わない。あなたのコードが機能しない理由を説明する私の古い質問を見てください。stackoverflow.com/questions/746767/...
stevehipwell

また、自分のコンピューターから離れているときに(開発ツールがインストールされている)SOの質問にコメントするのは間違いです。あなたが正しいです; TryCastC#の型のas演算子と同じ便利な機能が提供されていないことを知って驚いていNullable(Of T)ます。これを模倣するために私が考えることができる最も近い方法は、私の答えで提案したように、独自の関数を書くことです。
Dan Tao

これをジェネリックメソッドにリファクタリングするのに苦労するでしょうし、そうしたとしても、キャストが多すぎると効率が低下します。
nawfal 2013

8

私はここでごく少数のアプローチのみが見込み客OPを最も心配する危険を冒さず(マークグラベル、Stevo3000、リチャードシャレー、ニール、ダレンコパンド)、ほとんどが不必要に複雑であると感じています。これは役に立たないマイクロ最適化であることを十分に認識しているので、基本的にこれらを使用する必要があるとしましょう。

1)DataReader / DataRowから値を2回読み取らないでください。つまり、nullチェックとキャスト/変換の前に値をキャッシュするかrecord[X]、適切なシグネチャを使用してオブジェクトをカスタム拡張メソッドに直接渡します。

2)上記に従うにはIsDBNull、DataReader / DataRowで組み込み関数を使用しないでくださいrecord[X]。これは内部で呼び出されるため、実際には2回実行します。

3)原則として、型の比較は常に値の比較より遅くなります。ただrecord[X] == DBNull.Value上手に。

4)直接キャストは呼び出しよりも速くなります Convert、変換のためにクラスを、後者の方が変更が少なくなるのではないかと心配しています。

5)最後に、列名ではなくインデックスでレコードにアクセスする方が速くなります。


Szalay、Neil、Darren Koppandのアプローチがうまくいくと思います。私は特に、Darren Koppandの拡張メソッドアプローチIDataRecordが好きです(これをさらにに絞りたいのですがIDataReader)とインデックス/列名。

それを呼び出すように注意してください:

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

ではなく

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

場合にはあなたは区別する必要がある0DBNull。たとえば、列挙フィールドにnull値がある場合、それ以外のdefault(MyEnum)場合、最初の列挙値が返されるリスクがあります。だから、より良い電話record.GetColumnValue<MyEnum?>("Field")

あなたはから読んでいるのでDataRow、私は両方のための拡張メソッドを作成しますDataRowIDataReaderにより、乾燥、共通のコード。

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

したがって、次のように呼び出します。

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

私は、これはそれが(代わりの枠組みの中でされている必要がありますどのようにあると信じてrecord.GetInt32record.GetStringノー実行時例外と私たちにはnull値を処理するための柔軟性を提供します-最初の場所でなどのメソッド)。

私の経験から、データベースから読み取るための1つの一般的な方法ではあまり運が悪かったです。私は自分の書かなければならなかったので、私はいつも、カスタムハンドル、様々なタイプに持っていたGetIntGetEnumGetGuid長期的には、などの方法を。デフォルトでdbから文字列を読み取るときに空白を削除する場合、またはDBNull空の文字列として扱う場合はどうなりますか?または、10進数の末尾のゼロをすべて切り捨てる必要がある場合。Guidさまざまなコネクタドライバーの動作が異なるタイプで最も問題がありました。これは、基盤となるデータベースが文字列またはバイナリとしてそれらを格納できる場合にも同じです。私はこのようなオーバーロードがあります:

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

Stevo3000のアプローチでは、呼び出しが少し見苦しく退屈で、汎用的な関数を作成するのが難しくなります。


7

オブジェクトが文字列であるという厄介なケースがあります。以下の拡張メソッドコードは、すべてのケースを処理します。使用方法は次のとおりです。

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

6

私は個人的には、この構文を使用しています。これは、によって公開される明示的なIsDbNullメソッドを使用しIDataRecord、列インデックスをキャッシュして、文字列の重複検索を回避します。

読みやすくするために拡張すると、次のようになります。

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

DALコードを簡潔にするために1行に収まるように書き直されました。この例ではint bar = -1、if row["Bar"]がnullであることに注意してください。

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

インライン割り当てがあるかどうかわからない場合は混乱する可能性がありますが、操作全体が1行に収まるため、コードの1つのブロックで複数の列からプロパティを設定するときに読みやすくなります。


3
ただし、DataRowはIDataRecordを実装していません。
非合法08年

5

私がこれを行ったわけではありませんが、静的/拡張メソッドを使用することで、二重インデクサー呼び出しを回避し、コードをクリーンに保つことができます。

すなわち。

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

次に:

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

また、ヌルチェックロジックを1か所に保持できるという利点もあります。もちろん欠点は、それが追加のメソッド呼び出しであることです。

ちょっとした考え。


2
ただし、オブジェクトに拡張メソッドを追加することは非常に広範囲です。個人的には、DataRowの拡張メソッドを検討した可能性がありますが、オブジェクトは検討していません。
マークグラベル

True、ただし、拡張メソッドは、拡張クラスの名前空間がインポートされている場合にのみ使用できることに注意してください。
Richard Szalay

5

私はこのチェックをできるだけ避けるようにしています。

保持できない列については明らかにする必要はありません null

Nullable値タイプ(int?など)で保存している場合は、を使用して変換できますas int?

string.Emptyとを区別する必要がない場合は、DBNullが返されるため、をnull呼び出すだけです。.ToString()string.Empty



4

これは、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);
    }
}

使用例:

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

モンスターへの小道具は、ChageTypeToコードのために私の.Net得ました


4

拡張メソッドでも同様のことを行いました。これが私のコードです:

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

それを使用するには、次のようなことをします

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

4

DataRowの場合、row ["fieldname"] isDbNullを0に置き換え、それ以外の場合は10進数の値を取得します。

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

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

このように使う

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

3

データベースから大量のデータを読み取るプログラムにIsDBNullがあります。IsDBNullを使用すると、約20秒でデータがロードされます。IsDBNullがない場合、約1秒。

だから私はそれを使う方が良いと思います:

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.