检查 DBNull 然后分配给变量的最有效方法是啥?

Posted

技术标签:

【中文标题】检查 DBNull 然后分配给变量的最有效方法是啥?【英文标题】:Most efficient way to check for DBNull and then assign to a variable?检查 DBNull 然后分配给变量的最有效方法是什么? 【发布时间】:2010-09-18 07:21:16 【问题描述】:

这个问题偶尔会出现,但我还没有看到满意的答案。

一个典型的模式是(行是一个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?

This 表示 .GetType() 应该更快,但也许编译器知道一些我不知道的技巧?

第二个问题,值得缓存 row["value"] 的值还是编译器优化索引器?

例如:

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

注意事项:

    行["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 与 "=="

具有相同的性能

最有趣的结果?如果按大小写不匹配列的名称(例如,“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();
 

【问题讨论】:

你能澄清一下 row 是 DataRow 还是 IDataRecord/IDataReader? 现在我们有了更好的.NET Framework,我们可以使用DataRowExtensions Methods。 如果按大小写不匹配列的名称(例如,“Value”而不是“value”,则大约需要十倍的时间(对于字符串)完全取决于实现。我记得 mysql ADO.NET 连接器就是这种情况(列名的变化要慢得多),但对于 SqlServer 或 SQLite 完全没有(不记得了)。现在情况可能已经改变。是的,基本准则是,如有疑问,请使用序数。 @PavelHodek 这样的耻辱仅适用于 DataRow。会喜欢IDataRecord 扩展。 【参考方案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&lt;T&gt; 返回 GetValue&lt;T&gt; 的值,您可以轻松地返回 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

【讨论】:

Dan 这又冒了 OP 想要避免的风险。通过写row.IsNull(columnName),你已经读过一遍,然后再读一遍。并不是说这会有所作为,但理论上它的效率可能会降低.. System.Data.DataSetExtensions.DataRowExtensions.Field&lt;T&gt;(this System.Data.DataRow, string) 做的事情不是和第一种方法基本一样吗?【参考方案2】:

你应该使用方法:

Convert.IsDBNull()

考虑到它内置在框架中,我希望这是最有效的。

我会建议一些类似的东西:

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

是的,编译器应该为你缓存它。

【讨论】:

嗯,所有提到的选项都内置在框架中...实际上,Convert.IsDBNull 做了很多与 IConvertible 相关的额外工作... 再缓存——如果你的意思是有条件的例子,不——它真的不应该(也不应该)。它将执行索引器两次。 哦,那段代码不能编译——但是在其中一个上添加一个(int?),你会看到(在 IL 中)2:callvirt instance object [System.Data ]System.Data.DataRow::get_Item(string)【参考方案3】:

编译器不会优化索引器(即,如果您使用 row["value"] 两次),所以是的,它稍微更快:

object value = row["value"];

然后使用 value 两次;如果 .GetType() 为空,则使用可能会出现问题...

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

【讨论】:

在几乎所有情况下,== 都将等同于 ReferenceEquals(尤其是 DBNull),并且它更具可读性。如果你愿意,可以使用@Marc Gravell 的优化,但我和他在一起——可能不会有太大帮助。顺便说一句,引用相等应该总是优于类型检查。 现在已经过时了,但我最近看到了许多情况,这正是分析器所说的要修复的。想象一下评估大型数据集,其中每个单元都需要进行此检查。优化可以收获丰厚的回报。但答案的重要部分仍然是好的:profile 首先,要知道最好的时间花在哪里。 我猜 C# 6 引入了 Elvis 运算符可以很容易地避免在您建议的检查中出现空引用异常。 value?.GetType() == typeof(DBNull) 是的,我同意。通常是一种更好的方法,但对于那些想要使用 .GetType() 的人来说,你指出了他们的风险,然后呢?提供了一种解决方法。【参考方案4】:

我会在 C# 中使用以下代码(VB.NET 不是那么简单)。

如果该值不是 null/DBNull,则代码分配该值,否则分配默认值,该值可以设置为 LHS 值,允许编译器忽略分配。

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

【讨论】:

VB.NET 版本很简单:oSomeObject.IntMember = If(TryCast(oRow("Value), Integer?), iDefault). @Dan Tao - 我不认为你已经编译了那个代码。看看我的一个老问题,它解释了为什么你的代码不起作用。 ***.com/questions/746767/… 再一次,在远离我自己的计算机(带有开发工具)的情况下评论一个 SO 问题已被证明是一个错误!你说的对;我很惊讶地发现 TryCast 没有为 Nullable(Of T) 类型提供与 C# 的 as 运算符相同的便捷功能。我能想到的最接近的模仿方法是编写自己的函数,正如我现在在回答中所建议的那样。 你将很难将它重构为一个泛型方法,即使你这样做了,过多的转换也会降低它的效率。【参考方案5】:

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

1) 不要从 DataReader/DataRow 读取值两次 - 因此要么在空检查和强制转换/转换之前缓存它,要么更好地直接将您的 record[X] 对象传递给具有适当签名的自定义扩展方法。

2) 要遵守上述规定,请勿在 DataReader/DataRow 上使用内置的 IsDBNull 函数,因为它会在内部调用 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&lt;MyEnum?&gt;("Field")

由于您正在阅读DataRow,我将通过DRYing 通用代码为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 的方法,我觉得调用有点丑陋和乏味,而且很难用它来制作通用函数。

【讨论】:

【参考方案6】:

存在对象可能是字符串的麻烦情况。下面的扩展方法代码处理所有情况。以下是您将如何使用它:

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

【讨论】:

【参考方案7】:

我个人比较喜欢这种语法,它使用IDataRecord公开的显式 IsDbNull 方法,并缓存列索引以避免重复的字符串查找。

为了可读性而扩展,它类似于:

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 代码中的紧凑性而重写以适应单行 - 请注意,在此示例中,如果 row["Bar"] 为空,我们将分配 int bar = -1

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

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

【讨论】:

DataRow 没有实现 IDataRecord。【参考方案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>();

还具有将空检查逻辑保存在一个位置的好处。当然,缺点是它是一个额外的方法调用。

只是一个想法。

【讨论】:

在对象上添加扩展方法非常广泛。就个人而言,我可能考虑过 DataRow 上的扩展方法,但不是对象。 是的,但请记住,扩展方法仅在扩展类的命名空间被导入时可用。【参考方案9】:

我尽量避免这种检查。

对于不能容纳null的列显然不需要这样做。

如果您存储在 Nullable 值类型(int? 等)中,您可以使用 as int? 进行转换。

如果您不需要区分string.Emptynull,您可以直接调用.ToString(),因为DBNull 将返回string.Empty

【讨论】:

【参考方案10】:

我总是用:

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

发现它简短而全面。

【讨论】:

【参考方案11】:

这就是我处理从 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"))
                  ;

Props to Monsters Got My .Net 用于 ChageTypeTo 代码。

【讨论】:

【参考方案12】:

我对扩展方法做了类似的事情。这是我的代码:

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)

【讨论】:

【参考方案13】:

如果在 DataRow 中的 row["fieldname"] isDbNull 用 0 替换它,否则得到十进制值:

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

【讨论】:

【参考方案14】:
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"])

【讨论】:

【参考方案15】:

我在一个从数据库中读取大量数据的程序中有 IsDBNull。使用 IsDBNull,它会在大约 20 秒内加载数据。 如果没有 IsDBNull,大约需要 1 秒。

所以我认为最好使用:

public String TryGetString(SqlDataReader sqlReader, int row)

    String res = "";
    try
    
        res = sqlReader.GetString(row);
    
    catch (Exception)
     
    
    return res;

【讨论】:

以上是关于检查 DBNull 然后分配给变量的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

检查表中是不是存在行的最有效方法是啥?

使用 Perl 检查数据数组中重复项的最有效方法是啥?

Java中循环局部变量初始化的最有效方法是啥? [复制]

定义由类的不同方法使用的变量和类的最有效方法是啥?

检查浮点变量是不是为整数的最可靠方法是啥?

将标准化数据提供给报告软件的最有效方法是啥