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

Во-вторых, стоит ли кэшировать значение строки [ "value" ] или компилятор вообще оптимизирует индексатор?

Например:

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

Примечания:

  • строка [ "значение" ] существует.
  • Я не знаю индекс столбца столбца (следовательно, поиск имени столбца).
  • Я спрашиваю конкретно о проверке 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();
 }

Ответы

Ответ 1

Мне что-то не хватает. Не проверяет на 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

Ответ 2

Вы должны использовать метод:

Convert.IsDBNull()

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

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

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

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

Ответ 3

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

object value = row["value"];

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

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

Ответ 4

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

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

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

Ответ 5

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

    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

Я чувствую, что здесь очень мало подходов не рискует перспективой OP больше всего беспокоиться (Марк Гравелл, Stevo3000, Richard Szalay, Neil, Darren Koppand), и большинство из них излишне сложны. Понимая, что это бесполезная микро-оптимизация, позвольте мне сказать, что вы должны в основном использовать их:

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. Например, если у вас есть нулевые значения в полях enum, в противном случае default(MyEnum) рискует возвратить первое значение перечисления. Так что лучше позвонить record.GetColumnValue<MyEnum?>("Field").

Поскольку вы читаете с DataRow, я бы создал метод расширения для DataRow и IDataReader DRYing common код.

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

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

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

Ответ 8

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

Т.е.

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

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

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

Ответ 9

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

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

Обнаружено, что он короткий и всеобъемлющий.

Ответ 10

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

Ответ 11

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

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)

Ответ 12

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

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

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

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

Ответ 13

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

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

Ответ 14

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

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

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

Ответ 15

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"])