一个泛型方法能够同时处理引用类型和可空值类型吗?

15

我有一系列的扩展方法,用于在IDataRecord对象上进行空值检查。目前,我是这样实现的:

public static int? GetNullableInt32(this IDataRecord dr, int ordinal)
{
    int? nullInt = null;
    return dr.IsDBNull(ordinal) ? nullInt : dr.GetInt32(ordinal);
}

public static int? GetNullableInt32(this IDataRecord dr, string fieldname)
{
    int ordinal = dr.GetOrdinal(fieldname);
    return dr.GetNullableInt32(ordinal);
}

对于每种类型,我都需要进行处理等等。

我想将这些重新实现为通用方法,部分原因是为了减少冗余,部分原因是为了学习如何编写通用方法。

我已经写了这个:

public static Nullable<T> GetNullable<T>(this IDataRecord dr, int ordinal)
{
    Nullable<T> nullValue = null;
    return dr.IsDBNull(ordinal) ? nullValue : (Nullable<T>) dr.GetValue(ordinal);
}

只要 T 是值类型,该方法就有效,但如果 T 是引用类型,它就无效。
如果 T 是值类型,此方法需要返回 Nullable 类型,否则默认为 default(T)。如何实现这种行为?
8个回答

11
您可以这样声明您的方法:
public static T GetNullable<T>(this IDataRecord dr, int ordinal)
{
    return dr.IsDBNull(ordinal) ? default(T) : (T) dr.GetValue(ordinal);
}

如果T是可空的int或任何其他可空值类型,则它实际上将返回null。如果它是常规数据类型,则只会返回该类型的默认值。


3
这是一个非常糟糕的解决方案。如果数据记录中有NULL值,您不希望使用int类型的默认值。NULL和0是不同的值。 - TcKs
5
如果数据库中的值可为null,则T将是Nullable<int>,在这种情况下,NULL是默认返回的值。 - BFree
2
如果可以使用简单的 "dr [ordinal] as int?",为什么要使用这个扩展方法呢? - TcKs
1
如果 dr [ordinal] 是 DBNull,它将抛出一个异常。 - Adam Lassek
2
@juan 这似乎是一个非常多余的错误。我不同意 FxCop 的观点 - 明确声明泛型方法的类型并不是一件坏事。 - Adam Lassek
显示剩余3条评论

2
这是可以运行的:
public static T Get<T>( this IDataRecord dr, int ordinal) 
{
    T  nullValue = default(T);
    return dr.IsDBNull(ordinal) ? nullValue : (T) dr.GetValue(ordinal);
}


public void Code(params string[] args)
{
    IDataRecord dr= null;
    int? a = Get<int?>(dr, 1);
    string b = Get<string>(dr, 2);
}

1
这与BFree的答案基本相同。示例用法很有帮助。 - Amy B

1

我认为你不能用单个函数来实现这个。如果C#支持基于返回类型的重载,你可能可以这样做,但即使如此,我也建议不要这样做。

你应该能够通过不使用可空数据类型并返回实际值或null来实现相同的效果,就像BFree所建议的那样。


实际上,你可以拥有两个重载方法,它们的返回类型不同。这是合法的语法:public string Method() { return ""; }public int Method() { return 0; } - BFree
@BFree:类型“xyz”已经使用相同的参数类型定义了一个名为“Method”的成员。 - Amy B
@BFree:我刚刚尝试了你的代码,但是出现了以下错误:类型“Program1”已经定义了一个与参数类型相同的成员“Method”。 - Scott Dorman
参数类型相同,但返回类型不同,这是合法的语法。试一下... - BFree
1
好的,我改口了。出于某种原因,我曾经坚信你可以这样做。我的错... - BFree

1

我不明白为什么需要过度复杂化整个过程。为什么不保持简单,使用以下代码行:

对于允许 null 的值类型,请使用 int? iNullable = dr[ordinal] as int?;

对于不允许 null 的值类型,请使用 int iNonNullable = dr[ordinal] as int? ?? default(int);

对于引用类型,请使用 string sValue = dr[ordinal] as string;

对于任何认为代码无法工作并且 dr[ordinal] 将在此处引发异常的人,这是一个示例方法,一旦给出有效的连接字符串,将证明该概念。

private void Test()
{
  int? iTestA;
  int? iTestB;
  int iTestC;
  string sTestA;
  string sTestB;

  //Create connection
  using (SqlConnection oConnection = new SqlConnection(@""))
  {
    //Open connection
    oConnection.Open();

    //Create command
    using (SqlCommand oCommand = oConnection.CreateCommand())
    {
      //Set command text
      oCommand.CommandText = "SELECT null, 1, null, null, '1'";

      //Create reader
      using (SqlDataReader oReader = oCommand.ExecuteReader())
      {
        //Read the data
        oReader.Read();

        //Set the values
        iTestA = oReader[0] as int?;
        iTestB = oReader[1] as int?;
        iTestC = oReader[2] as int? ?? -1;
        sTestA = oReader[3] as string;
        sTestB = oReader[4] as string;
      }
    }
  }
}

正如我在评论中已经向BFree解释的那样,如果dr[ordinal]为DBNull,它会抛出一个异常。你的例子是行不通的。 - Adam Lassek
@Adam Lassek - 我不知道你为什么认为我的代码不起作用,你试过了吗?我已经在生产系统上运行它,请在投票之前实际检查代码。我认为大多数新手都可以告诉你,如果值为DBNull,则dr [ordinal]不会引发异常。 - stevehipwell
什么框架版本?在我编写这个问题时,我正在使用3.5版本,如果字段为DBNull,则肯定会抛出异常。 - Adam Lassek
https://dev59.com/5krSa4cB1Zd3GeqPYa1Z#1855564 - Adam Lassek
@Adam Lassek - 好的,我已经在.NET 2.0和.NET 3.5上进行了反复测试,它完美无缺地工作。您不会因为从DataReader读取DBNull到兼容的对象类型或使用as关键字转换为类型或设置null而得到异常。我建议您制作一个虚拟应用程序并运行上面的方法来证明这一点。您还可以使用object oObject = oReader [0];来证明您可以直接从读取器访问DBNull对象。 - stevehipwell

0
public static T Get<T>(this IDataRecord rec, Func<int, T> GetValue, int ordinal)
{
    return rec.IsDBNull(ordinal) ? default(T) : GetValue(ordinal);
}

更高效

public static T Get<T>(this IDataRecord rec, Func<IDataRecord, int, T> GetValue, int ordinal)
{
    return rec.IsDBNull(ordinal) ? default(T) : GetValue(rec, ordinal);
}

public static Func<IDataRecord, int, int> GetInt32 = (rec, i) => rec.GetInt32(i);
public static Func<IDataRecord, int, bool> GetBool = (rec, i) => rec.GetBoolean(i);
public static Func<IDataRecord, int, string> GetString = (rec, i) => rec.GetString(i);

并像这样使用它

rec.Get(GetString, index);
rec.Get(GetInt32, index);

你成功地实现了一个通用函数,却没有意识到它的最小好处。泛型的全部意义在于不必为每种类型编写单独的函数,这也是发布这个问题的原因。 - Adam Lassek
也许,但与您的解决方案不同之处在于,您只有一个方法,在其中检查可空性并避免转换,如果您有非常大的数据集,则可能代价高昂。 - SeeR
与BFree解决方案相比,您还可以避免装箱。 - SeeR

0

你不能用一种方法做到这一点,但你可以用三个方法来做到:

public static T GetData<T>(this IDataReader reader, Func<int, T> getFunc, int index)
{
    if (!reader.IsClosed)
    {
        return getFunc(index);
    }
    throw new ArgumentException("Reader is closed.", "reader");
}

public static T GetDataNullableRef<T>(this IDataReader reader, Func<int, T> getFunc, int index) where T : class
{
    if (!reader.IsClosed)
    {
        return reader.IsDBNull(index) ? null : getFunc(index);
    }
    throw new ArgumentException("Reader is closed.", "reader");
}

public static T? GetDataNullableValue<T>(this IDataReader reader, Func<int, T> getFunc, int index) where T : struct
{
    if (!reader.IsClosed)
    {
        return reader.IsDBNull(index) ? (T?)null : getFunc(index);
    }
    throw new ArgumentException("Reader is closed.", "reader");
}

然后使用它,您需要执行以下操作:

private static Whatever CreateObject(IDataReader reader)
{
    Int32? id = reader.GetDataNullableValue<Int32>(reader.GetInt32, 0);
    string name = reader.GetDataNullableRef<string>(reader.GetString, 1);
    Int32 x = reader.GetData<Int32>(reader.GetInt32, 2);
}

-1

Nullable结构只适用于值类型,因为引用类型本身就是可为空的...


3
这就是为什么我无法将其返回为 Nullable<T>,也是我问这个问题的原因。 - Adam Lassek

-2
我是这样做的:
DataRow record = GetSomeRecord();
int? someNumber = record[15] as int?
Guid? someUID = record["MyPrimaryKey"] as Guid?;
string someText = GetSomeText();
record["Description"] = someText.ToDbString();

// ........

public static class StringExtensionHelper {
    public static object ToDbString( this string text ) {
         object ret = null != text ? text : DBNull.Value
         return ret;
    }
}

编辑: 当然,你可以(或者应该)为其他基本类型编写“ToDbInt32、ToDbBool等”扩展方法。

编辑2: 你也可以通过扩展基类“object”的“ToDbValue”方法来实现。

public static class StringExtensionHelper {
    public static object ToDbValue( this object value ) {
         object ret = object.ReferenceEquals( value, null ) ? (object)DBNull.Value : value;
         return ret;
    }
}

我可以这样做,但泛型练习的整个重点是避免为每种数据类型编写单独的方法。而且我不明白你的例子如何相关...我正在扩展IDataRecord以对从数据存储中获取的数据进行空值检查。 - Adam Lassek
如果你想检查记录中的值,可以使用 "as" 关键字,它会做你想要的事情。如果在记录中是 DbNull,则会返回 null。否则,将返回 "int" 作为 "Nullable<int>"。不需要特殊的方法。 - TcKs
不应该使用扩展方法来扩展对象,因为它在所有语言中都无法使用,例如VB.NET。 - Scott Dorman
@Scott Dorman:是的,但只有在需要CLS兼容性时才需要。.NET拥有很多语言,关注所有语言在我看来不是一个好的方式。 - TcKs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接