Найефективніший спосіб перевірити наявність 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 () повинен бути швидшим, але, можливо, компілятор знає кілька хитрощів, яких я не знаю?

Друге питання, чи варто кешувати значення рядка ["value"] чи компілятор оптимізує індексатор все одно?

Наприклад:

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

Примітки:

  1. рядок ["значення"] існує.
  2. Я не знаю індекс стовпця стовпця (звідси пошук назви стовпця).
  3. Я запитую конкретно про перевірку наявності DBNull, а потім про призначення (а не про передчасну оптимізацію тощо).

Я визначив кілька сценаріїв (час у секундах, 1000000000 випробувань):

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 має таку ж продуктивність, що і "=="

Найцікавіший результат? Якщо ви не збігаєтеся з назвою стовпця за окремими випадками (наприклад, "Значення" замість "Значення", це займає приблизно в десять разів більше (для рядка):

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

Мораль історії виглядає так: якщо ви не можете шукати стовпчик за його індексом, то переконайтеся, що ім'я стовпця, яке ви подаєте до індексатора, точно відповідає імені DataColumn.

Кешування значення також здається майже вдвічі швидшим:

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?
Марк Гравелл

7
Тепер у нас набагато краще .NET Framework і ми можемо використовувати DataRowExtensions методи .
Павло Ходек

Якщо ви невідповідаєте назві стовпця за окремим випадком (наприклад, "Значення" замість "Значення", це займає приблизно в десять разів більше (для рядка). Це повністю залежить від реалізації. Я пам'ятаю, що це було так (зміни в якщо ім'я стовпця відбувається набагато повільніше) з роз'ємом MySQL ADO.NET, але зовсім не для SqlServer або SQLite (не пам’ятаю). Можливо, змінилися зараз речі. Так, основне керівництво, коли сумніваєтесь, йде на ординари.
nawfal

@PavelHodek така ганьба, що тільки для DataRow. Полюбив би IDataRecordрозширення.
nawfal

Відповіді:


72

Мені, мабуть, чогось не вистачає. Чи не перевіряється DBNull, що саме робить DataRow.IsNullметод?

Я використовував наступні два способи розширення:

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)або інший варіант замість цього.


Що стосується спорідненої замітки, ось альтернатива VB.NET для пропозиції 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

3
Дан це знову ризикує тим, чого хоче уникнути ОП. Написавши, row.IsNull(columnName)ви вже читаєте його один раз і читаєте знову. Не кажучи, що це змінить, але теоретично це може бути менш ефективно ..
nawfal

2
Хіба це не System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)те саме, що перший метод?
Денніс Г

35

Вам слід скористатися методом:

Convert.IsDBNull()

Враховуючи, що це вбудована в Раму, я б очікував, що це буде найбільш ефективно.

Я б запропонував щось таке:

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

І так, компілятор повинен кешувати це за вас.


5
Ну, всі згадані варіанти вбудовані в рамки ... Насправді, Convert.IsDBNull робить багато зайвої роботи, що стосується IConvertible ...
Marc Gravell

1
І повторно кеш - якщо ви маєте на увазі умовний приклад, ні - він насправді не повинен (і ні). Він виконає індексатор двічі.
Марк Гравелл

О, і цей код не компілюється - але додайте (int?) До одного з них, і ви побачите (в IL) 2 об'єкта: примірник callvirt [System.Data] System.Data.DataRow :: get_Item (рядок)
Марк Гравелл

20

Компілятор не оптимізує показник індексатора (тобто якщо ви використовуєте рядок ["значення"] двічі), так що так це зробити трохи швидше:

object value = row["value"];

а потім використовувати значення двічі; використання .GetType () ризикує проблемами, якщо він є нульовим ...

DBNull.Valueнасправді синглтон, тому для додання 4-го варіанту - ви можете, можливо, використати ReferenceEquals - але насправді, я думаю, ви тут занадто сильно переживаєте ... Я не думаю, що швидкість відрізняється між "є", "== "тощо стане причиною будь-якої проблеми із продуктивністю, яку ви бачите. Профілюйте весь свій код і зосередьтеся на чомусь важливому ... це буде не так.


2
Практично у всіх випадках == буде еквівалентним ReferenceEquals (особливо в DBNull), і це набагато читабельніше. Використовуйте оптимізацію @Marc Gravell, якщо хочете, але я з ним - напевно, не дуже допоможу. BTW, контрольна рівність завжди повинна бути перевіркою типу перевірки.
tvanfosson

1
Старий зараз, але останнім часом я бачив ряд випадків, коли саме це сказав тефілер. Уявіть оцінку великих наборів даних, де кожна комірка повинна здійснити цю перевірку. Оптимізація, яка може отримати великі винагороди. Але важлива частина відповіді ще добре: профіль першого, щоб знати , де краще провести час.
Joel Coehoorn

Я думаю, що впровадження оператора Elvis на C # 6 дозволяє легко уникнути виключення нульових посилань у запропонованій вами чеку. значення? .GetType () == typeof (DBNull)
Еніола

Так, я згоден. це, як правило, кращий шлях, але для тих, хто хоче використовувати t .GetType (), на ризики якого ви вказали, тоді ?. забезпечує шлях навколо нього.
Еніола

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).
Дан Тао

1
@Dan Tao - я не думаю, що ви склали цей код. Подивіться на старе моє питання, яке пояснює, чому ваш код не працює. stackoverflow.com/questions/746767 / ...
stevehipwell

І ще раз, коментуючи питання ТА, перебуваючи вдалині від власного комп’ютера (із інструментами для розробників), виявилося помилкою! Ти правий; Я здивований, дізнавшись, що TryCastне забезпечує такий же зручний функціонал, як asоператор C # для Nullable(Of T)типів. Найближчий спосіб, на який я можу наслідувати це, - це написати власну функцію, як я зараз запропонував у своїй відповіді.
Дан Тао

Вам буде важко переробляти це на загальний метод, і навіть якщо ви цього зробите, занадто багато кастингу зробить його менш ефективним.
nawfal

8

Я думаю, що тут дуже мало підходів не ризикує, що перспективна ОП найбільше хвилює (Марк Гравелл, Stevo3000, Річард Шалай, Ніл, Даррен Коппанд), і більшість є надмірно складними. Повністю усвідомлюючи, що це марна мікрооптимізація, дозвольте сказати, що в основному ви повинні використовувати такі:

1) Не читайте значення з DataReader / DataRow двічі - тому або кешуйте його перед нульовими перевірками та кастингами / перетвореннями, а ще краще безпосередньо передайте record[X]об'єкт власному методу розширення з відповідною підписом.

2) Щоб дотримуватися вищезазначеного, не використовуйте вбудовану IsDBNullфункцію на вашому DataReader / DataRow, оскільки це викликає record[X]внутрішнє, так що фактично ви будете робити це двічі.

3) Порівняння типів завжди буде повільнішим, ніж порівняння значень, як загальне правило. Просто робіть record[X] == DBNull.Valueкраще.

4) Прямий кастинг буде швидшим, ніж виклик Convertкласу для конвертації, хоча я побоююся, що останній буде менше працювати.

5) Нарешті, доступ до запису за індексом, а не назвою стовпця знову стане швидшим.


Я відчуваю, що йти підходами Шалай, Ніла та Даррена Коппанда буде краще. Мені особливо подобається метод розширення методу розширення Даррена Коппанда, який використовує IDataRecord(хоча я хотів би звузити його далі IDataReader) та ім'я / ім'я / стовпець

Подбайте про те, щоб зателефонувати:

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

і ні

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

у випадку, якщо вам потрібно розмежувати між 0і DBNull. Наприклад, якщо у полях перерахувань є нульові значення, інакше default(MyEnum)ризикує повернути перше значення перерахунку. Тож краще зателефонуйте record.GetColumnValue<MyEnum?>("Field").

Оскільки ви читаєте з DataRow, я б створив метод розширення і для, DataRowі IDataReaderза допомогою DRYing загального коду.

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.GetInt32, і record.GetStringт.д. методи) , в першу чергу - НЕ у час виконання винятків і дає нам гнучкість значень нульових рукояток.

Зі свого досвіду мені було менше шансів із одним загальним методом читати з бази даних. Я завжди був звичай ручки різних типів, так що мені довелося писати свої власні GetInt, GetEnum, GetGuidі т.д. методи в довгостроковій перспективі. Що робити, якщо ви хочете обрізати пробіли під час читання рядка з db за замовчуванням або вважати DBNullпорожнім рядком? Або якщо ваш десятковий знак має бути врізаний з усіх кінцевих нулів. У мене виникли найбільші проблеми з 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 - зауважте, що в цьому прикладі ми призначаємо, int bar = -1якщо row["Bar"]є нульовим.

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

Вбудоване призначення може бути заплутаним, якщо ви не знаєте, що воно є, але воно зберігає всю операцію в одному рядку, що, на мою думку, підвищує читабельність, коли ви заселяєте властивості з декількох стовпців в один блок коду.


3
Проте DataRow не реалізує IDataRecord.
ілітірит

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

Також має перевагу зберігати логіку перевірки нуля в одному місці. Зворотна сторона - це, звичайно, те, що це додатковий виклик методу.

Просто думка.


2
Додавання методу розширення на об'єкт є дуже широким. Особисто я міг би розглянути метод розширення на DataRow, але не заперечувати.
Marc Gravell

Щоправда, хоча майте на увазі, що методи розширення доступні лише тоді, коли імпортується область імен класу розширень.
Річард Шалай

5

Я намагаюся максимально уникати цієї перевірки.

Очевидно, це не потрібно робити для стовпців, які не можуть містити null.

Якщо ви зберігаєте тип нульового значення ( int?тощо), ви можете просто конвертувати за допомогою as int?.

Якщо вам не потрібно розмежовувати string.Emptyі null, ви можете просто зателефонувати .ToString(), оскільки DBNull повернеться 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"))
                  };
}

Реквізит до монстрів Отримав мій .Net для коду ChageTypeTo.


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 рядок ["ім'я поля"] isDbNull замініть його на 0, інакше отримайте десяткове значення:

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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.