65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 2.0 可空数据读取器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (38投票s)

2006年1月20日

7分钟阅读

viewsIcon

184798

downloadIcon

1023

.NET 2.0 可空类型的可空数据读取器文章。

引言

.NET 2.0 在 CLR 中引入了可空类型,首次允许值类型被赋予 null 值。然而,ADO.NET 2.0 没有引入任何专门处理可空类型的新功能。因此,本文类库的主要目标之一是提供一个简单的 API 来处理持久化层中的可空类型。

数据读取器将解决三个主要目标。

  • 目标 #1:为 IDataReaderDataRow 提供一个简单、强类型的 API 来处理非可空类型。
  • 目标 #2:为 IDataReaderDataRow 提供一个简单、强类型的 API 来处理可空类型。
  • 目标 #3:提供一个统一的接口,以便可以使用相同的代码通过多态方式来消费 IDataReaderDataRow

目标 #1

目标 #1 是为 IDataReaderDataRow 提供一个简单、统一、强类型的 API 来处理非可空类型。API 的两个特性是:

  1. 能够通过可读的列名(而不是序数)引用所有内容,并且
  2. 所有内容都具有强类型方法,以避免进行强制类型转换/转换代码。

IDataReader 接口提供了几个方便的强类型 GetXXX() 方法来访问数据。问题是,这些方法需要一个晦涩的序数而不是可读的列名。这导致数据访问代码看起来像这样

person.Age = reader.GetInt32(reader.GetOrdinal("Age"));

将该方法写成这样会更方便

person.Age = reader.GetInt32("Age");

在使用 DataRow 时,返回的值是 System.Object,这意味着您必须通过像这样进行转换来支付拆箱的代价

person.Age = (int)row["Age"];

或者,您必须像这样使用 Convert

person.Age = Convert.ToInt32(row["Age"]);

如果可以通过类似 IDataReader 的强类型方法从 DataRow 访问数据,而无需编写单调的强制类型转换/转换代码,那将更方便。

目标 #2

目标 #2 是提供一个简单、强类型的 API 来处理可空类型。

IDataReader 没有提供处理可空类型的方法。因此,要正确填充可空类型,数据访问代码必须充斥着像这样的过程式、易出错的代码

if (reader.IsDBNull(reader.GetOrdinal("FiredDate")))
    person.FiredDate = null;
else
    person.FiredDate = 
      reader.GetDateTime(reader.GetOrdinal("FiredDate"));

这显然不理想——最好为可空类型提供相同的强类型 GetXXX() 方法。

目标 #3

目标 #3 是提供一个统一的接口,以便可以使用相同的代码通过多态方式来消费 IDataReaderDataRow

IDataReaderDataRow 具有非常不同的 API。在某些情况下,应用程序可能需要通过 IDataReader 检索对象以获得最佳性能。在其他情况下,数据可能作为 DataSet 的一部分检索(如果它是更大查询的一部分)。理想情况下,可以针对相同的接口以多态方式编程。

INullableReader

INullableReader 接口定义了一个类必须实现的契约,才能读取可空和非可空数据。这不仅将在 IDataReaderDataRow 之间提供统一的接口,而且还将允许类以多态方式使用。该接口定义确保 IDataReader 的所有 GetXXX() 方法都有对应的 GetXXX() 方法,这些方法接受字符串(用于列名)而不是序数。此外,每个 GetXXX() 方法都有 GetNullableXXX() 对应项。

接口定义

public interface INullableReader
{
    bool GetBoolean(string name);
    Nullable<bool> GetNullableBoolean(string name);
    byte GetByte(string name);
    Nullable<byte> GetNullableByte(string name);
    char GetChar(string name);
    Nullable<char> GetNullableChar(string name);
    DateTime GetDateTime(string name);
    Nullable<DateTime> GetNullableDateTime(string name);
    decimal GetDecimal(string name);
    Nullable<Decimal> GetNullableDecimal(string name);
    double GetDouble(string name);
    Nullable<double> GetNullableDouble(string name);
    float GetFloat(string name);
    Nullable<float> GetNullableFloat(string name);
    Guid GetGuid(string name);
    Nullable<Guid> GetNullableGuid(string name);
    short GetInt16(string name);
    Nullable<short> GetNullableInt16(string name);
    int GetInt32(string name);
    Nullable<int> GetNullableInt32(string name);
    long GetInt64(string name);
    Nullable<long> GetNullableInt64(string name);
    string GetString(string name);
    string GetNullableString(string name);
    object GetValue(string name);
    bool IsDBNull(string name);
}

尽管此接口提供了 GetValue()IsDBNull() 方法,但这些方法更多是为了完整性,通常不会在代码中使用。

NullableDataReader

NullableDataReader 实现 INullableReader 接口,并为 IDataReader 对象提供包装器。因此,它适用于 SqlDataReaderOracleDataReader 等。ADO.NET 2.0 中甚至还有一个新类,名为 DataTableReader,也可以进行包装。

要实例化 NullableDataReader,只需将 IDataReader 传递给构造函数。使用 Enterprise Library Data Access 块的示例

dr = new NullableDataReader(db.ExecuteReader(cmd));

使用原始 ADO.NET 的示例

dr = new NullableDataReader(cmd.ExecuteReader());

要读取值,只需引用列名即可

person.Age = dr.GetInt32("Age");
person.FiredDate = dr.GetNullableDateTime("FiredDate");

NullableDataReader 还实现 IDataReader。因此,NullableDataReader 可以像任何其他数据读取器一样使用。例如:

try
{
    while (dr.Read())
    {
      Person person = new Person();
      person.Age = dr.GetInt32("Age");
      person.FiredDate = 
        dr.GetNullableDateTime("FiredDate");
      personList.Add(person);
    }
}
finally
{
    dr.Dispose();
}

上面的代码看起来与其他数据读取器没有区别,除了

  1. GetInt32() 方法接受列名而不是序数,并且
  2. 提供了一个 GetNullableDateTime() 方法,该方法在普通数据读取器上不存在。

NullableDataRowReader

NullableDataReader 还实现 INullableReader 接口,并为 DataRow 对象提供包装器。因为它提供了所有强类型方法,所以访问代码无需包含强制类型转换和转换。

通过将 DataRow 传递给构造函数来实例化 NullableDataRow,或者将 DataRow 分配给 Row 属性。

如果读取单行,那么将 DataRow 传递给构造函数是最简单的

NullableDataRowReader dr = new NullableDataRowReader(row);
person.Age = dr.GetInt32("Age");
person.FiredDate = dr.GetNullableDateTime("FiredDate");

请注意,访问方法看起来与 DataReader 相同。

如果读取多行(例如,在迭代循环时),那么将 DataRow 分配给 Row 属性是最简单的

NullableDataRowReader dr = new NullableDataRowReader();
foreach (DataRow row in dataTable.Rows)
{
    dr.Row = row;
    Person person = new Person();
    person.Age = dr.GetInt32("Age");
    person.FiredDate = dr.GetNullableDateTime("FiredDate");
    personList.Add(person);
}

在上面的示例中,我们为了简单起见,只是迭代了 DataTable 的所有行。实际上,在该示例中,您可以像这样使用 NullableDataReader 和新的 DataTableReader

NullableDataReader dr = new 
   NullableDataReader(dataTable.CreateDataReader());

然而,在处理 DataTable 时,我们经常需要进行过滤,并利用 GetChildRows(),这使得 NullableDataRowReader 非常方便。

多态 NullableReader

在某些情况下,我们可能需要使用 DataReader(为了获得最佳性能)填充业务对象,而在其他情况下,则使用 DataRow(例如,如果它在多结果集 DataSet 中检索)填充相同的对象。为了避免编写两个单独的方法(一个用于 NullableDataReader,另一个用于 NullableDataRowReader),可以以多态方式针对 INullableReader 接口进行编程,并只编写一个方法。

public Address BuildItem(INullableReader dr)
{
    Address address = new Address();

    address.ID = dr.GetInt32(Params.AddressID);
    address.StreetAddress1 = dr.GetString(Params.StreetAddress1);
    address.StreetAddress2 = dr.GetString(Params.StreetAddress2);
    address.City = dr.GetString(Params.City);
    address.State = dr.GetString(Params.State);
    address.ZipCode = dr.GetString(Params.ZipCode);

    return address;
}

BuildItem() 方法可以以两种不同的方式调用。首先,使用 NullableDataReader

NullableDataReader dr = new 
       NullableDataReader(db.ExecuteReader(cmd));
person.Address = addressMapper.BuildItem(dr);

其次,使用 NullableDataRowReader

NullableDataRowReader dr = new 
      NullableDataRowReader(addressTable.Rows[0]);
person.Address = addressMapper.BuildItem(dr);

实现细节

内部,NullableDataReaderNullableDataRowReader 使用了许多新的 C# 2.0 语言特性来生成简洁、高性能的代码。具体来说,它们利用了

  1. 泛型,
  2. 委托推断,当然还有
  3. 可空类型。

当然,要消费数据读取器,开发人员不需要了解任何这些实现细节。

为了说明内部实现,我们将检查 NullableDataReader 类中的 GetInt32()GetNullableInt32() 方法。由于 NullableDataReader 通过构造函数将其包装的 IDataReader 作为私有成员,因此 GetInt32() 方法只需将此方法调用委托给包装的读取器即可

public int GetInt32(int i)
{
    return reader.GetInt32(i);
}

为了提供一个接受列名而不是序数的重载方法,使用了标准方法,同时将实现隐藏在使用者之外

public int GetInt32(string name)
{
    return reader.GetInt32(reader.GetOrdinal(name));
}

到目前为止,我们还没有做任何特别有趣的事情(尽管新的重载提供了相当大的便利)。要为 GetNullableInt32() 方法提供两个重载,我们可以这样做:

public Nullable<int> GetNullableInt32(string name)
{
    return this.GetNullableInt32(reader.GetOrdinal(name));
}

public Nullable<int> GetNullableInt32(int index)
{
    Nullable<int> nullable;
    if (reader.IsDBNull(index))
    {
        nullable = null;
    }
    else
    {
        nullable = GetInt32(index);
    }
    return nullable;
}

然而,这里的问题是,尽管不复杂,但在第二个重载中带有 if 语句的可空赋值将本质上必须在每个数据类型的每个 GetNullableXXX() 方法中重复——唯一的区别是

  1. 可空类型,以及
  2. 调用的方法(例如,上面 else 块中的 GetInt32() 方法)。

为了解决这个问题并生成更简洁、更优雅的代码,我们可以利用一个通用方法,该方法包括传递一个委托,而这个委托现在是我们 C# 2.0 匿名方法功能的一部分。因此,我们可以简单地创建一个 **一个** 方法来执行赋值

private Nullable<T> GetNullable<T>(int ordinal, 
            Conversion<T> convert) where T : struct
{
    Nullable<T> nullable;
    if (reader.IsDBNull(ordinal))
    {
        nullable = null;
    }
    else
    {
        nullable = convert(ordinal);
    }
    return nullable;
}

首先,请注意我们使用的是通用类型,并指定了 T 必须是值类型(即 struct)的约束。其次,请注意该方法的第二个参数实际上是我们为此目的定义的自定义私有委托

private delegate T Conversion<T>(int ordinal);

这使得所有 GetNullableXXX() 方法都可以只用一行代码(而不是自己的 if 语句)

public Nullable GetNullableInt32(int index)
{
    return GetNullable(index, GetInt32);
}

请注意,第二个参数实际上使用了 C# 2.0 委托推断,并指定在值不是 DBNull 的情况下应调用正常的 GetInt32() 方法来执行赋值。

能够为每个 GetNullableXXX() 方法使用相同的方法,可以使代码更简洁、更不容易出错,并且更易于维护。

© . All rights reserved.