C# 2.0 可空数据读取器






4.90/5 (38投票s)
2006年1月20日
7分钟阅读

184798

1023
.NET 2.0 可空类型的可空数据读取器文章。
引言
.NET 2.0 在 CLR 中引入了可空类型,首次允许值类型被赋予 null 值。然而,ADO.NET 2.0 没有引入任何专门处理可空类型的新功能。因此,本文类库的主要目标之一是提供一个简单的 API 来处理持久化层中的可空类型。
数据读取器将解决三个主要目标。
- 目标 #1:为
IDataReader
和DataRow
提供一个简单、强类型的 API 来处理非可空类型。 - 目标 #2:为
IDataReader
和DataRow
提供一个简单、强类型的 API 来处理可空类型。 - 目标 #3:提供一个统一的接口,以便可以使用相同的代码通过多态方式来消费
IDataReader
或DataRow
。
目标 #1
目标 #1 是为 IDataReader
和 DataRow
提供一个简单、统一、强类型的 API 来处理非可空类型。API 的两个特性是:
- 能够通过可读的列名(而不是序数)引用所有内容,并且
- 所有内容都具有强类型方法,以避免进行强制类型转换/转换代码。
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 是提供一个统一的接口,以便可以使用相同的代码通过多态方式来消费 IDataReader
或 DataRow
。
IDataReader
和 DataRow
具有非常不同的 API。在某些情况下,应用程序可能需要通过 IDataReader
检索对象以获得最佳性能。在其他情况下,数据可能作为 DataSet
的一部分检索(如果它是更大查询的一部分)。理想情况下,可以针对相同的接口以多态方式编程。
INullableReader
INullableReader
接口定义了一个类必须实现的契约,才能读取可空和非可空数据。这不仅将在 IDataReader
和 DataRow
之间提供统一的接口,而且还将允许类以多态方式使用。该接口定义确保 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
对象提供包装器。因此,它适用于 SqlDataReader
、OracleDataReader
等。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();
}
上面的代码看起来与其他数据读取器没有区别,除了
GetInt32()
方法接受列名而不是序数,并且- 提供了一个
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);
实现细节
内部,NullableDataReader
和 NullableDataRowReader
使用了许多新的 C# 2.0 语言特性来生成简洁、高性能的代码。具体来说,它们利用了
- 泛型,
- 委托推断,当然还有
- 可空类型。
当然,要消费数据读取器,开发人员不需要了解任何这些实现细节。
为了说明内部实现,我们将检查 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()
方法中重复——唯一的区别是
- 可空类型,以及
- 调用的方法(例如,上面
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()
方法使用相同的方法,可以使代码更简洁、更不容易出错,并且更易于维护。