Самый эффективный способ проверить DBNull, а затем присвоить переменной?

Этот вопрос возникает время от времени, но я не нашел удовлетворительного ответа.

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

Второй вопрос: стоит ли кэшировать значение row ["value"] или компилятор все равно оптимизирует индексатор?

Например:

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

Примечания:

  1. строка ["значение"] существует.
  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 имеет ту же производительность, что и "=="

Самый интересный результат? Если имя столбца не соответствует регистру (например, «Значение» вместо «значение», это займет примерно в десять раз больше времени (для строки):

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

person Community    schedule 21.10.2008    source источник
comment
Можете ли вы уточнить, является ли строка DataRow или IDataRecord / IDataReader?   -  person Marc Gravell    schedule 21.10.2008
comment
Теперь у нас намного лучше .NET Framework, и мы можем использовать методы DataRowExtensions. .   -  person Pavel Hodek    schedule 20.12.2012
comment
Если вы не соответствуете имени столбца по регистру (например, Value вместо value, это занимает примерно в десять раз больше (для строки) Это полностью зависит от реализации. Я помню, что это было именно так (изменение в случае, если имя столбца намного медленнее) с соединителем MySQL ADO.NET, но совсем не для SqlServer или SQLite (не помню). Теперь все могло измениться. Да, в случае сомнений основной рекомендацией является для ординалов.   -  person nawfal    schedule 01.12.2017
comment
@PavelHodek такая досада, что только для DataRow. Любил бы IDataRecord расширения.   -  person nawfal    schedule 01.12.2017


Ответы (15)


Я, должно быть, что-то упускаю. Разве не проверяет 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
person Community    schedule 16.06.2010
comment
Дэн, это снова рискует тем, чего OP хочет избежать. Написав row.IsNull(columnName), вы уже читаете его один раз и читаете снова. Я не говорю, что это будет иметь значение, но теоретически это может быть менее эффективным. - person nawfal; 06.02.2013
comment
Разве System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string) не делает по сути то же самое, что и первый метод? - person Dennis G; 02.04.2015

Вам следует использовать метод:

Convert.IsDBNull()

Учитывая, что он встроен в Framework, я ожидал, что он будет наиболее эффективным.

Я бы предложил что-то вроде:

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

И да, компилятор должен кэшировать это за вас.

person Jon Grant    schedule 21.10.2008
comment
Что ж, все упомянутые параметры встроены в структуру ... На самом деле Convert.IsDBNull выполняет много дополнительной работы, связанной с IConvertible ... - person Marc Gravell; 21.10.2008
comment
И снова кеш - если вы имеете в виду условный пример, нет - он действительно не должен (и не делает). Индексатор выполнится дважды. - person Marc Gravell; 21.10.2008
comment
Да, и этот код не компилируется - но добавьте (int?) К одному из них, и вы увидите (в IL) 2 объекта экземпляра: callvirt [System.Data] System.Data.DataRow :: get_Item (строка) - person Marc Gravell; 21.10.2008

Компилятор не будет оптимизировать индексатор (т.е. если вы дважды используете row ["value"]), поэтому да, это немного быстрее:

object value = row["value"];

а затем используйте значение дважды; использование .GetType () рискует проблемами, если оно равно нулю ...

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

person Marc Gravell    schedule 21.10.2008
comment
Практически во всех случаях == будет эквивалентен ReferenceEquals (особенно DBNull) и будет намного более читаемым. Используйте оптимизацию @Marc Gravell, если хотите, но я с ним - вероятно, не сильно поможет. Кстати, равенство ссылок всегда должно превосходить проверку типов. - person tvanfosson; 21.10.2008
comment
Сейчас старая, но недавно я видел несколько случаев, когда профилировщик сказал исправить именно это. Представьте себе оценку больших наборов данных, где каждая ячейка должна выполнить эту проверку. Оптимизация этого может принести большие плоды. Но важная часть ответа по-прежнему хороша: сначала профиль, чтобы знать, где лучше всего проводить время. - person Joel Coehoorn; 29.08.2014
comment
Я полагаю, что введение оператора Элвиса в C # 6 позволяет легко избежать исключения нулевой ссылки в предлагаемой вами проверке. значение? .GetType () == typeof (DBNull) - person Eniola; 10.03.2016
comment
Да, я согласен. как правило, лучший способ, но для тех, кто не хочет использовать .GetType (), чьи риски вы указали, тогда?. предоставляет способ обойти это. - person Eniola; 10.03.2016

Я бы использовал следующий код на C # (VB.NET не так прост).

Код присваивает значение, если оно не равно null / DBNull, в противном случае он присваивает значение по умолчанию, которое может быть установлено равным значению LHS, позволяя компилятору игнорировать присвоение.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
person Community    schedule 14.04.2009
comment
Версия VB.NET очень проста: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault). - person Dan Tao; 16.06.2010
comment
@ Дэн Тао - я не думаю, что вы скомпилировали этот код. Посмотрите на мой старый вопрос, который объясняет, почему ваш код не работает. stackoverflow.com/questions/746767/ - person stevehipwell; 16.06.2010
comment
И еще раз, комментирование вопроса SO вдали от моего собственного компьютера (с инструментами разработчика на нем) оказалось ошибкой! Ты прав; Я удивлен, узнав, что TryCast не обеспечивает такой же удобной функциональности, как оператор as в C # для типов Nullable(Of T). Самый близкий способ, который я могу придумать, чтобы имитировать это, - написать свою собственную функцию, как я теперь предложил в своем ответе. - person Dan Tao; 16.06.2010
comment
Вам будет сложно преобразовать это в универсальный метод, и даже если вы это сделаете, слишком много задействованных приведений сделает его менее эффективным. - person nawfal; 08.02.2013

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

1) Не считывайте значение из DataReader / DataRow дважды - поэтому либо кешируйте его перед проверками на null и преобразованиями / преобразованиями, либо, что еще лучше, напрямую передавайте свой объект 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 и т. Д. Что, если вы хотите обрезать пробелы при чтении строки из базы данных по умолчанию или рассматривать 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 я нахожу вызов немного уродливым и утомительным, и будет сложнее сделать из него универсальную функцию.

person Community    schedule 06.02.2013

Есть неприятный случай, когда объект может быть строкой. Приведенный ниже код метода расширения обрабатывает все случаи. Вот как вы бы это использовали:

    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)); 
    } 
person Community    schedule 16.06.2010

Я лично предпочитаю этот синтаксис, который использует явный метод 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));

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

person Dylan Beattie    schedule 21.10.2008
comment
Однако DataRow не реализует IDataRecord. - person ilitirit; 21.10.2008

Не то чтобы я сделал это, но вы можете обойти вызов двойного индексатора и при этом сохранить свой код чистым, используя статический метод / метод расширения.

Ie.

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

Также имеет преимущество хранения логики нулевой проверки в одном месте. Обратной стороной, конечно же, является то, что это дополнительный вызов метода.

Просто мысль.

person Richard Szalay    schedule 21.10.2008
comment
Однако добавление метода расширения к объекту очень широко. Лично я мог бы рассматривать метод расширения для DataRow, но не объект. - person Marc Gravell; 21.10.2008
comment
Верно, но имейте в виду, что методы расширения доступны только при импорте пространства имен класса расширения. - person Richard Szalay; 17.12.2008

Я стараюсь по возможности избегать этой проверки.

Очевидно, это не нужно делать для столбцов, которые не могут содержать null.

Если вы сохраняете значение типа Nullable (int? и т. Д.), Вы можете просто преобразовать с помощью as int?.

Если вам не нужно различать string.Empty и null, вы можете просто вызвать .ToString(), поскольку DBNull вернет string.Empty.

person Community    schedule 22.01.2009

Я всегда использую:

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

Нашел краткое и исчерпывающее.

person Patrick Desjardins    schedule 21.10.2008

Вот как я обрабатываю чтение из 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"))
                  };
}

Реквизит для Monsters Got My .Net для кода ChageTypeTo .

person Community    schedule 25.11.2008

Я сделал нечто подобное с методами расширения. Вот мой код:

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)
person Community    schedule 26.11.2008

если в DataRow строка ["fieldname"] isDbNull замените ее на 0, иначе получите десятичное значение:

decimal result = rw["fieldname"] as decimal? ?? 0;
person Community    schedule 25.02.2016

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"])
person Community    schedule 05.05.2011

У меня есть IsDBNull в программе, которая читает много данных из базы данных. С IsDBNull он загружает данные примерно за 20 секунд. Без IsDBNull около 1 секунды.

Поэтому я считаю, что лучше использовать:

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}
person Community    schedule 20.05.2010