Cara paling efisien untuk memeriksa DBNull dan kemudian menetapkan ke variabel?

151

Pertanyaan ini muncul sesekali, tetapi saya belum melihat jawaban yang memuaskan.

Pola khas adalah (baris adalah DataRow ):

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

Pertanyaan pertama saya adalah mana yang lebih efisien (saya telah membalik kondisinya):

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

Ini menunjukkan bahwa .GetType () harus lebih cepat, tetapi mungkin kompiler tahu beberapa trik yang tidak saya lakukan?

Pertanyaan kedua, apakah layak untuk men-cache nilai baris ["value"] atau apakah kompiler mengoptimalkan pengindeks?

Sebagai contoh:

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

Catatan:

  1. baris ["value"] ada.
  2. Saya tidak tahu indeks kolom dari kolom (karenanya pencarian nama kolom).
  3. Saya bertanya secara khusus tentang memeriksa DBNull dan kemudian tugas (bukan tentang optimasi prematur, dll.).

Saya membandingkan beberapa skenario (waktu dalam detik, 10.000.000 uji coba):

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 memiliki kinerja yang sama dengan "=="

Hasil paling menarik? Jika Anda tidak cocok dengan nama kolom berdasarkan huruf (misalnya, "Nilai" dan bukan "nilai", dibutuhkan kira-kira sepuluh kali lebih lama (untuk string):

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

Moral dari cerita tersebut adalah bahwa jika Anda tidak dapat mencari kolom berdasarkan indeksnya, maka pastikan bahwa nama kolom yang Anda masukkan ke pengindeks sama persis dengan nama DataColumn.

Caching nilainya juga tampaknya hampir dua kali lebih cepat:

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

Jadi metode yang paling efisien tampaknya menjadi:

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }
ilitirit
sumber
1
Bisakah Anda memperjelas apakah baris adalah DataRow atau IDataRecord / IDataReader?
Marc Gravell
7
Sekarang kami memiliki .NET Framework yang lebih baik dan kami dapat menggunakan Metode DataRowExtensions .
Pavel Hodek
Jika Anda tidak cocok dengan nama kolom berdasarkan kasus (misalnya, "Nilai" dan bukan "nilai", dibutuhkan kira-kira sepuluh kali lebih lama (untuk sebuah string). Ini sepenuhnya tergantung pada implementasi. Saya ingat ini adalah kasusnya (perubahan dalam kasus nama kolom menjadi jauh lebih lambat) dengan konektor MySQL ADO.NET, tetapi tidak sama sekali untuk SqlServer atau SQLite (jangan ingat) .Hal-hal mungkin telah berubah sekarang. Ya, pedoman dasar adalah, ketika ragu-ragu, pilihlah ordinals.
nawfal
@ PavelHodek memalukan yang hanya untuk DataRow. Akan menyukai IDataRecordekstensi.
nawfal

Jawaban:

72

Saya pasti melewatkan sesuatu. Tidak memeriksa DBNullpersis apa yang dilakukan DataRow.IsNullmetode ini?

Saya telah menggunakan dua metode ekstensi berikut:

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

Pemakaian:

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

Jika Anda tidak ingin Nullable<T>mengembalikan nilai GetValue<T>, Anda dapat dengan mudah mengembalikan default(T)atau opsi lain.


Pada catatan yang tidak terkait, inilah alternatif VB.NET untuk saran 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
sumber
3
Dan ini mempertaruhkan lagi apa yang ingin dihindari OP. Dengan menulis row.IsNull(columnName)Anda sudah membacanya sekali dan membacanya lagi. Tidak mengatakan itu akan membuat perbedaan, tetapi secara teoritis itu bisa kurang efisien ..
nawfal
2
Bukankah System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)pada dasarnya melakukan hal yang sama dengan metode pertama?
Dennis G
35

Anda harus menggunakan metode ini:

Convert.IsDBNull()

Mempertimbangkan itu sudah ada di dalam Kerangka, saya berharap ini menjadi yang paling efisien.

Saya akan menyarankan sesuatu di sepanjang baris:

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

Dan ya, kompiler harus menyimpannya untuk Anda.

Jon Grant
sumber
5
Nah, semua opsi yang disebutkan dibangun ke dalam kerangka kerja ... Sebenarnya, Convert.IsDBNull melakukan banyak pekerjaan ekstra yang berkaitan dengan IConvertible ...
Marc Gravell
1
Dan ulang cache - jika Anda maksud dengan contoh kondisional, tidak - itu benar-benar tidak boleh (dan tidak). Ini akan mengeksekusi pengindeks dua kali.
Marc Gravell
Oh, dan kode itu tidak dikompilasi - tetapi tambahkan (int?) Ke salah satunya, dan Anda akan melihat (dalam IL) 2 dari: objek instance callvirt [System.Data] System.Data.Data.DataRow :: get_Item (string)
Marc Gravell
20

Kompiler tidak akan mengoptimalkan pengindeks jauh (yaitu jika Anda menggunakan baris ["nilai"] dua kali), jadi ya itu sedikit lebih cepat untuk dilakukan:

object value = row["value"];

dan kemudian gunakan nilai dua kali; menggunakan .GetType () berisiko menimbulkan masalah jika itu nol ...

DBNull.Valuesebenarnya adalah singleton, jadi untuk menambahkan opsi ke-4 - Anda mungkin bisa menggunakan ReferenceEquals - tetapi dalam kenyataannya, saya pikir Anda terlalu khawatir di sini ... Saya tidak berpikir kecepatan berbeda antara "is", "== "dll akan menjadi penyebab masalah kinerja yang Anda lihat. Profil seluruh kode Anda dan fokus pada sesuatu yang penting ... tidak akan seperti ini.

Marc Gravell
sumber
2
Di hampir semua kasus == akan setara dengan ReferenceEquals (khususnya untuk DBNull) dan itu jauh lebih mudah dibaca. Gunakan pengoptimalan @Marc Gravell jika Anda mau, tapi saya bersamanya - mungkin tidak akan banyak membantu. BTW, kesetaraan referensi harus selalu mengalahkan pengecekan tipe.
tvanfosson
1
Tua sekarang, tapi saya baru saja melihat sejumlah kasus di mana ini persis apa yang dikatakan profiler untuk memperbaikinya. Bayangkan mengevaluasi kumpulan data besar, di mana setiap sel perlu melakukan pemeriksaan ini. Mengoptimalkan yang dapat dapat menuai hasil besar. Tetapi bagian penting dari jawabannya masih bagus: profil terlebih dahulu, untuk mengetahui di mana cara terbaik untuk menghabiskan waktu Anda.
Joel Coehoorn
Saya kira pengenalan C # 6 dari operator Elvis memudahkan untuk menghindari pengecualian referensi nol dalam cek yang Anda sarankan. value? .GetType () == typeof (DBNull)
Eniola
Ya saya setuju. umumnya merupakan cara yang lebih baik untuk digunakan tetapi bagi mereka yang ingin menggunakan .GetType () yang risikonya Anda tunjukkan? menyediakan jalan keluar.
Eniola
9

Saya akan menggunakan kode berikut dalam C # ( VB.NET tidak sesederhana).

Kode memberikan nilai jika bukan null / DBNull, jika tidak, menugaskan default yang dapat diatur ke nilai LHS yang memungkinkan kompiler mengabaikan penetapan.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
sumber
1
Versi VB.NET adalah yang sederhana: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Dan Tao
1
@Dan Tao - Saya tidak berpikir Anda telah mengkompilasi kode itu. Lihatlah pertanyaan lama saya yang menjelaskan mengapa kode Anda tidak akan berfungsi. stackoverflow.com/questions/746767/…
stevehipwell
Dan sekali lagi, mengomentari pertanyaan SO sementara jauh dari komputer saya sendiri (dengan alat dev di dalamnya) telah terbukti menjadi kesalahan! Kamu benar; Saya terkejut mengetahui bahwa TryCasttidak menyediakan fungsionalitas nyaman yang sama dengan asoperator C # untuk Nullable(Of T)tipe. Cara terdekat yang bisa saya pikirkan untuk meniru ini adalah dengan menulis fungsi Anda sendiri, seperti yang sekarang saya sarankan dalam jawaban saya.
Dan Tao
Anda akan kesulitan mengembalikannya menjadi metode generik, dan bahkan jika Anda melakukannya, terlalu banyak casting yang terlibat akan membuatnya kurang efisien.
nawfal
8

Saya merasa hanya sedikit pendekatan di sini yang tidak mengkhawatirkan prospek OP yang paling khawatir (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) dan sebagian besar tidak perlu rumit. Menyadari sepenuhnya ini adalah optimasi mikro yang tidak berguna, saya katakan Anda pada dasarnya harus menggunakan ini:

1) Jangan membaca nilai dari DataReader / DataRow dua kali - jadi temboloklah sebelum cek nol dan gips / konversi atau bahkan lebih baik langsung meneruskan record[X]objek Anda ke metode ekstensi khusus dengan tanda tangan yang sesuai.

2) Untuk mematuhi hal di atas, jangan gunakan IsDBNullfungsi bawaan pada DataReader / DataRow Anda karena itu memanggil record[X]internal, jadi Anda akan melakukannya dua kali.

3) Jenis perbandingan akan selalu lebih lambat daripada perbandingan nilai sebagai aturan umum. Lakukan record[X] == DBNull.Valuelebih baik.

4) casting langsung akan lebih cepat daripada memanggil Convertkelas untuk mengkonversi, meskipun saya khawatir yang terakhir akan lebih sedikit goyah.

5) Terakhir, mengakses catatan dengan indeks daripada nama kolom akan lebih cepat lagi.


Saya merasa berjalan dengan pendekatan Szalay, Neil dan Darren Koppand akan lebih baik. Saya terutama menyukai pendekatan metode ekstensi Darren Koppand yang menerima IDataRecord(meskipun saya ingin mempersempitnya lebih jauh ke IDataReader) dan indeks / nama kolom.

Berhati-hatilah untuk menyebutnya:

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

dan tidak

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

jika Anda perlu membedakan antara 0dan DBNull. Misalnya, jika Anda memiliki nilai nol di bidang enum, jika tidak maka default(MyEnum)risiko nilai enum pertama akan dikembalikan. Panggilan yang lebih baik record.GetColumnValue<MyEnum?>("Field").

Karena Anda membaca dari a DataRow, saya akan membuat metode ekstensi untuk keduanya DataRowdan IDataReaderdengan MENGURANGI kode umum.

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

Jadi sekarang menyebutnya seperti:

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

Saya percaya ini adalah bagaimana seharusnya dalam kerangka (bukan metode record.GetInt32, record.GetStringdll) di tempat pertama - tidak ada pengecualian run-time dan memberi kita fleksibilitas untuk menangani nilai nol.

Dari pengalaman saya, saya kurang beruntung dengan satu metode umum untuk membaca dari database. Saya selalu harus menangani kustom berbagai jenis, jadi saya harus menulis sendiri saya GetInt, GetEnum, GetGuid, dll metode dalam jangka panjang. Bagaimana jika Anda ingin memotong spasi putih saat membaca string dari db secara default, atau memperlakukan DBNullsebagai string kosong? Atau jika desimal Anda harus dipotong dari semua nol yang tertinggal. Saya memiliki banyak masalah dengan Guidtipe di mana driver konektor yang berbeda berperilaku berbeda juga ketika database yang mendasarinya dapat menyimpannya sebagai string atau biner. Saya memiliki kelebihan seperti ini:

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

Dengan pendekatan Stevo3000, saya merasa panggilannya agak jelek dan membosankan, dan akan lebih sulit untuk membuat fungsi generik darinya.

nawfal
sumber
7

Ada kasus yang merepotkan di mana objek bisa menjadi string. Kode metode ekstensi di bawah ini menangani semua kasus. Begini cara Anda menggunakannya:

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

Saya pribadi menyukai sintaks ini, yang menggunakan metode IsDbNull eksplisit diekspos oleh IDataRecord, dan cache indeks kolom untuk menghindari pencarian string duplikat.

Diperluas agar mudah dibaca, seperti:

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

Ditulis ulang agar sesuai pada satu baris untuk kekompakan dalam kode DAL - perhatikan bahwa dalam contoh ini kami menetapkan int bar = -1jika row["Bar"]nol.

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

Tugas sebaris dapat membingungkan jika Anda tidak tahu itu ada di sana, tetapi itu membuat seluruh operasi pada satu baris, yang saya pikir meningkatkan keterbacaan ketika Anda mempopulasikan properti dari beberapa kolom dalam satu blok kode.

Dylan Beattie
sumber
3
DataRow tidak mengimplementasikan IDataRecord.
ilitirit
5

Bukannya saya sudah melakukan ini, tetapi Anda bisa mengatasi panggilan indekser ganda dan masih menjaga kode Anda bersih dengan menggunakan metode statis / ekstensi.

Yaitu.

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

Kemudian:

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

Juga memiliki manfaat menjaga logika pemeriksaan nol di satu tempat. Kelemahannya, tentu saja, ini adalah panggilan metode tambahan.

Hanya pemikiran saja.

Richard Szalay
sumber
2
Menambahkan metode ekstensi pada objek sangat luas. Secara pribadi saya mungkin telah mempertimbangkan metode ekstensi pada DataRow, tetapi tidak keberatan.
Marc Gravell
Benar, meskipun perlu diingat bahwa metode ekstensi hanya tersedia ketika namespace kelas ekstensi diimpor.
Richard Szalay
5

Saya berusaha menghindari cek ini sebanyak mungkin.

Jelas tidak perlu dilakukan untuk kolom yang tidak bisa menampung null.

Jika Anda menyimpan dalam tipe nilai Nullable ( int?, dll.), Anda bisa mengonversi menggunakan as int?.

Jika Anda tidak perlu membedakan antara string.Emptydan null, Anda cukup menelepon .ToString(), karena DBNull akan kembali string.Empty.

bdukes
sumber
4

Saya selalu menggunakan:

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

Menemukannya singkat dan komprehensif.

Patrick Desjardins
sumber
4

Ini adalah bagaimana saya menangani membaca dari 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);
    }
}

Contoh penggunaan:

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

Alat Peraga untuk Monster Mendapat .Net untuk kode ChageTypeTo Saya.

Chris Marisic
sumber
4

Saya telah melakukan sesuatu yang mirip dengan metode ekstensi. Ini kode saya:

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

Untuk menggunakannya, Anda akan melakukan sesuatu seperti

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

jika dalam DataRow baris ["fieldname"] isDbNull ganti dengan 0 jika tidak, dapatkan nilai desimal:

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

gunakan seperti ini

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

Saya memiliki IsDBNull dalam program yang membaca banyak data dari database. Dengan IsDBNull memuat data dalam waktu sekitar 20 detik. Tanpa IsDBNull, sekitar 1 detik.

Jadi saya pikir lebih baik menggunakan:

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