检查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. row [“ value”]存在。
  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具有与“ ==”相同的性能

最有趣的结果?如果您按大小写不匹配列名(例如,用“ Value”代替“ value”,则花费的时间大约是字符串的十倍):

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
您能否澄清row是DataRow还是IDataRecord / IDataReader?
马克·格雷韦尔

7
现在我们有了更好的.NET Framework,并且可以使用DataRowExtensions方法
Pavel Hodek

如果您按大小写对列名进行不匹配(例如,“ Value”而不是“ value”,则花费的时间大约是字符串的十倍), 这完全取决于实现。我记得确实是这种情况(更改的情况下,列名的速度要慢得多)(使用MySQL ADO.NET连接器,但对于SqlServer或SQLite则完全没有(记住),现在情况可能已经改变了。
nawfal

@PavelHodek只为DataRow感到遗憾。本来喜欢IDataRecord扩展。
nawfal

Answers:


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)或其他一些选项。


无关紧要的是,这是Stevo3000建议的VB.NET替代方案:

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
Dan再次冒着OP想要避免的风险。通过编写,row.IsNull(columnName)您已经阅读了一次,然后再次阅读。不是说一定会有所作为,但理论上它可以是效率不高..
nawfal

2
System.Data.DataSetExtensions.DataRowExtensions.Field<T>(this System.Data.DataRow, string)基本上与第一种方法没有做相同的事情吗?
丹尼斯·G

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(string)
Marc Gravell

20

编译器不会优化掉索引(也就是说,如果你使用行[“价值”]两次),所以是的,它是稍微快做:

object value = row["value"];

然后使用价值两次;如果为null则使用.GetType()存在风险...

DBNull.Value实际上是一个单例,因此要添加第四个选项-您也许可以使用ReferenceEquals-但实际上,我认为您在这里担心太多...我认为“ is”,“ ==”之间的速度不会有所不同“等将成为您看到的任何性能问题的原因。分析您的整个代码,并专注于重要的事情...并非如此。


2
在几乎所有情况下,==将等同于ReferenceEquals(尤其是DBNull),并且可读性更高。如果需要,可以使用@Marc Gravell的优化,但是我支持他-可能不会有太大帮助。顺便说一句,引用相等性应始终胜过类型检查。
tvanfosson

1
现在已经很老了,但是我最近看到了很多情况,这正是探查器要解决的问题。想象一下评估大型数据集,每个单元都需要进行此检查。进行优化可以收获丰厚的回报。但是答案的重要部分仍然是好的:首先介绍个人资料,以了解最适合在哪里度过的时间。
2014年

我猜想Elvis运算符的C#6引入使您可以轻松避免在建议的检查中出现空引用异常。value?.GetType()== typeof(DBNull)
Eniola

是的我同意。通常是更好的选择,但对于那些不想使用.GetType()的人,您会指出其风险,然后呢?提供了解决方法。
Eniola

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)
丹涛2010年

1
@丹涛-我认为您没有编译该代码。看看我的一个老问题,它解释了为什么您的代码无法正常工作。stackoverflow.com/questions/746767/...
stevehipwell

再一次,在远离我自己的计算机(使用开发工具)的情况下对SO问题进行评论被证明是错误的!你是对的; 令我惊讶的是,TryCast它没有提供与C#的类型as运算符相同的便捷功能Nullable(Of T)。我能想到的最接近这种方式的方法就是编写自己的函数,正如我现在在答案中所建议的那样。
丹涛2010年

您将很难将其重构为通用方法,并且即使您这样做,所涉及的太多转换也会使其效率降低。
nawfal

8

我觉得这里只有极少数的方法可以让潜在的风险最大的担心(Marc Gravell,Stevo3000,Richard Szalay,Neil和Darren Koppand),而且大多数方法都不必要地复杂。充分意识到这是无用的微优化,让我说您应该基本上采用以下这些方法:

1)不要两次从DataReader / DataRow中读取值-因此可以在进行null检查和强制转换/转换之前将其缓存,甚至最好直接传递给您 record[X]对象给具有适当签名的自定义扩展方法。

2)为了遵守上述规定,请勿使用内置 IsDBNull DataReader / DataRow上函数,因为它是在record[X]内部调用的,因此实际上您将执行两次。

3)通常,类型比较总是比值比较慢。只要做到record[X] == DBNull.Value更好。

4)直接投射比调用更快 Convert类进行转换,尽管我担心后者会更少动摇。

5)最后,按索引而不是按列名访问记录将再次更快。


我觉得采用Szalay,Neil和Darren Koppand的方法会更好。我特别喜欢Darren Koppand的扩展方法方法,该方法可以接受IDataRecord(尽管我想将其范围缩小到IDataReader)和索引/列名称。

小心称呼它:

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

并不是

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

如果您需要区分0DBNull。例如,如果您在枚举字段中具有空值,则default(MyEnum)冒着返回第一个枚举值的风险。所以更好的电话record.GetColumnValue<MyEnum?>("Field")

由于您正在从中读取内容DataRow,因此我将为它们创建扩展方法,DataRowIDataReader通过干燥常见代码来创建扩展方法。

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.GetInt32record.GetString等方法)摆在首位-没有运行时异常,使我们能够灵活地处理空值。

根据我的经验,我不太希望使用一种从数据库读取的通用方法。我总是不得不自定义处理各种类型的,所以我不得不写我自己的GetIntGetEnumGetGuid等从长远看方法。如果默认情况下要从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 = -1if设置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));

如果您不知道内联分配,它会令人困惑,但是它将整个操作保持在一行上,当您从一个代码块中的多个列中填充属性时,我认为这可以提高可读性。


3
虽然DataRow没有实现IDataRecord。
ilitirit

5

并不是我已经做到了,但是您可以绕过double indexer调用,并且仍然可以通过使用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.Emptynull,您可以致电.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中将row [“ 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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.