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

Второй вопрос: стоит ли кэшировать значение 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();
 }
ilitirit
источник
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
Dan Tao
источник
3
Дэн, это снова рискует тем, чего ОП хочет избежать. Когда row.IsNull(columnName)вы пишете, вы уже читаете это и читаете снова. Не говоря уже о том, что это будет иметь значение, но теоретически это может быть менее эффективно ..
nawfal
2
Разве это не System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)то же самое, что первый метод?
Деннис Г
35

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

Convert.IsDBNull()

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

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

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

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

Джон Грант
источник
5
Ну, все упомянутые опции встроены в фреймворк ... На самом деле, Convert.IsDBNull проделывает много дополнительной работы, связанной с IConvertible ...
Марк Грэвелл
1
И повторно кеш - если вы имеете в виду с условным примером, нет - это действительно не должно (и не делает). Он выполнит индексатор дважды.
Марк Гравелл
Да, и этот код не компилируется - но добавьте (int?) К одному из них, и вы увидите (в IL) 2 из: объект экземпляра callvirt [System.Data] System.Data.DataRow :: get_Item (string)
Марк Грэвелл
20

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

object value = row["value"];

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

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

Марк Гравелл
источник
2
Практически во всех случаях == будет эквивалентно ReferenceEquals (особенно DBNull), и это будет намного более читабельным. Если хотите, используйте оптимизацию @Marc Gravell, но я с ним - вероятно, не очень помогу. Кстати, равенство ссылок должно всегда побеждать проверку типов.
tvanfosson
1
Старый, но недавно я видел несколько случаев, когда это было именно то, что, по словам профилировщика, исправляло. Представьте себе оценку больших наборов данных, где каждая клетка должна выполнить эту проверку. Оптимизация, которая может принести большие плоды. Но важная часть ответа все еще хороша: сначала профиль , чтобы знать, где лучше всего провести время.
Джоэл Коухорн
Я предполагаю, что введение оператора Elvis в C # 6 позволяет легко избежать исключения нулевой ссылки в предложенной вами проверке. значение? .GetType () == typeof (DBNull)
Эниола
Да, я согласен. Как правило, это лучший способ, но для тех, кто хочет использовать .GetType (), на риски которых вы указали, тогда? обеспечивает способ обойти это.
Эниола
9

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

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

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;
Stevo3000
источник
1
Версия VB.NET это так просто: oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault).
Дан Тао
1
@ Дэн Тао - я не думаю, что вы скомпилировали этот код. Посмотрите на мой старый вопрос, который объясняет, почему ваш код не будет работать. stackoverflow.com/questions/746767/…
stevehipwell
И еще раз, комментировать SO вопрос, находясь вдали от моего собственного компьютера (с инструментами разработчика на нем), оказалось ошибкой! Ты прав; Я удивлен, узнав, что 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от сушильного общего кода.

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 я нахожу вызов немного уродливым и утомительным, и сделать из него общую функцию будет сложнее.

nawfal
источник
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 = -1if 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, хотя.
ilitirit
5

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

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

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

Ричард Сзалай
источник
2
Однако добавление метода расширения к объекту очень широко. Лично я мог бы рассмотреть метод расширения на DataRow, но не объект.
Марк Гравелл
Правда, хотя имейте в виду, что методы расширения доступны только при импорте пространства имен класса расширения.
Ричард Сзалай
5

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

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

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

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

bdukes
источник
4

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

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

Нашел это кратко и всесторонне.

Патрик Дежарден
источник
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)
Darren Kopp
источник
4

если в DataRow строка ["fieldname"] 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;
}
Mastahh
источник