Самый эффективный способ проверить DBNull и назначить переменную? - PullRequest
149 голосов
/ 21 октября 2008

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

Типичный шаблон (строка 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, а затем о назначении (не о преждевременной оптимизации и т. Д.).

Я протестировал несколько сценариев (время в секундах, 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();
 }

Ответы [ 15 ]

71 голосов
/ 16 июня 2010

Я должен что-то упустить. Не проверяет ли 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
34 голосов
/ 21 октября 2008

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

Convert.IsDBNull()

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

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

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

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

20 голосов
/ 21 октября 2008

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

object value = row["value"];

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

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

9 голосов
/ 15 апреля 2009

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

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

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
8 голосов
/ 06 февраля 2013

Я чувствую, что только очень немногие подходы здесь не рискуют больше всего беспокоить потенциального оператора (Марк Гравелл, 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 и т. Д. Что, если вы хотите обрезать пробелы при чтении строки из БД по умолчанию или рассматривать 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 голосов
/ 16 июня 2010

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

    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 голосов
/ 21 октября 2008

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

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

5 голосов
/ 22 января 2009

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

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

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

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

5 голосов
/ 21 октября 2008

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

Т.е..

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

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

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

4 голосов
/ 25 февраля 2016

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

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