在C#中将通用类型“升级”为可空类型?

8
在C#中,给定一个通用类型T,我想知道如何获取类型Q,它等于非空的T,并且对于已经是可为空的T,则等于T。
这个问题源自真实的代码。我想统一访问通过查询字符串传递的参数,在我的ASP.NET应用程序中指定相同类型的默认值,但确保可以传递null作为默认值。
public static T FetchValue<T>(
   string name, 
   <T? for non-nullable, T otherwise> default_value = null)  // How to write this?
{
  var page = HttpContext.Current.Handler as Page;

  string str = page.Request.QueryString[name];

  if (str == null)
  {
    if (default_value == null)
    {
      throw new HttpRequestValidationException("A " + name + " must be specified.");
    }
    else
    {
      return default_value;
    }
  }

  return (T)Convert.ChangeType(str, typeof(T));
}

目前我被迫使用两个重载版本的FetchValue函数——一个没有默认值,另一个有默认值:

public static T FetchValue<T>(string name);
public static T FetchValue<T>(string name, T default_value);

这段代码运行良好,但我想知道是否有可能将两个函数合并成一个。

在C ++中,我会使用类型特征,例如具有两个专门为可为空和非可为空类型的PromoteNullablePromoteNullable<T>::type。但是在C#中呢?


我个人认为,如果你只有两个重载函数,从调用方的角度来看,代码会更清晰。 - Matthew Watson
@MatthewWatson 调用站点的代码在两个实现中完全相同。 - Mikhail
C# 的泛型是运行时特性,而不像 C++ 的模板那样是编译时特性。因此,编译器必须生成 一个 IL 序列,该序列对于任何提供的类型参数都是有效的。(在 JIT 时间基于值/引用类型将 IL 翻译为机器代码时有一些变化,但 IL 总是相同的) - Damien_The_Unbeliever
另外一点需要注意的是 - 我可能会选择使用两个重载,但是有一个私有内部方法 bool TryFetchValue<T>(string name,ref T value),两个重载都调用它。它完成了脏活儿,然后所有的 FetchValue 方法只需要返回默认值或抛出异常(取决于它们是哪个重载)。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 我不知道,谢谢。关于TryFetchValue - 这就是我实际实现的方式 :) - Mikhail
显示剩余2条评论
6个回答

3

虽然并没有直接回答问题,但我会这样写:

    public static T FetchValue<T>(string name)
    {
        T value;
        if (TryFetchValue(name, out value))
            return value;
        throw new HttpRequestValidationException("A " + name + " must be specified.");
    }

    public static T FetchValue<T>(string name, T default_value)
    {
        T value;
        if (TryFetchValue(name, out value))
            return value;
        return default_value;
    }

    private static bool TryFetchValue<T>(
         string name,
         out T value)
    {
        var page = HttpContext.Current.Handler as Page;

        string str = page.Request.QueryString[name];

        if (str == null)
        {
            value = default(T);
            return false;
        }

        value = (T)Convert.ChangeType(str, typeof(T));
        return true;
    }

因此,大部分代码只存在一次 - 而且现在甚至可以让调用代码自行选择将null作为默认值,如果它愿意的话。
即使您可以创建所需的参数声明,这一行仍然会出现问题:
return default_value;

如果发现default_valueT?而不是T,那么上面的代码就无法正常工作。即使你强制转换:
return (T)default_value;

仍然存在一个问题——将T?转换为T时,编译器实际上必须插入一个调用以获取可空类型的Value属性。但是如果default_value的类型只是T,则该调用将无效。
在C#泛型中,编译器必须为该方法创建一段IL代码。无法插入一个可选的代码片段,可能会访问Value

这几乎就是当前的实现方式 :) - Mikhail
好的,我没有收到我的问题的答案,但我有一个解释为什么没有答案。谢谢,我接受这个解释。 - Mikhail

1

由于您的返回类型是T,而T是值类型,因此它不能为null。因此,您始终需要传递可空类型,因为您想要一个null返回,对吗?

尝试这个,它允许您传递可空值类型(i)和普通引用类型(o):

static void Main(string[] args)
{
    int? i = 5;
    object x = new object();

    object o = FetchValue("x", i);
    o = FetchValue("x", x);
}

private static T? FetchValue<T>(string name, T? p) where T : struct
{
    T? result = (T?)FetchValue(name, (object)p);
    return result;
}

private static T FetchValue<T>(string name,
    T default_value = default (T)) // default(T) where T is a reference type will always be null!
    where T : class
{
    // do whatever you want
    var page = HttpContext.Current.Handler as Page;

    string str = page.Request.QueryString[name];

    if (str == null)
    {
        if (default_value == null)
        {
            throw new HttpRequestValidationException("A " + name + " must be specified.");
        }
        else
        {
            return default_value;
        }
    }

    return (T)Convert.ChangeType(str, typeof(T));
}

请记住这只是语法 foo,因为您最终会得到一个 T 类型的 object。然而,这是必需的,因为只有 object 才能为 null。

在这种情况下,我不能使用FetchValue<int>("x")而不带默认值。我不想得到一个null,我想要一个默认值或异常。 - Mikhail
当使用FetchValue<int>("x")并且返回类型为T时,由于int不是引用类型,因此无法返回null。在谈论default_value时,我认为你总是想要一个引用/可空类型的返回值。(当然这并没有太多意义,因为你会使用object作为返回类型) - GameScripting

1

这是我的例子:

//Page extension
static class PageExtensions
{
    private static T FetchValue<T>(this Page page, string name, object defaultValue)
    {
        string str = page.Request.QueryString[name];
        if (string.IsNullOrEmpty(str))
        {
            if (defaultValue != null)
                return (T)defaultValue;

            throw new HttpRequestValidationException("A " + name + " must be specified.");
        }

        //not the best way
        return (T)Convert.ChangeType(str, typeof(T));
    }

    public static T FetchValueFromCurrentPage<T>(string name, T defaultValue)
    { 
      var page = HttpContext.Current.Handler as Page;
      if(page == null)
        throw new InvalidOperationException("Current handler is not Page");
      return page.FetchValue<T>(name, defaultValue);
    }

    public static T FetchValueFromCurrentPage<T>(string name) where T : class
    {   
        return FetchValueFromCurrentPage(name, (T)null);
    }
}

关于创建扩展函数的想法不错。但是 FetchValue 本身并不是类型安全的,例如可以调用 FetchValue<int>("id", "foo"),而且它会编译通过。 - Mikhail
另外,我不明白为什么FetchValueFetchValueFromCurrentPage没有对称的原型。对于后者而言,如果没有默认值,就无法使用结构体,例如:PageExtensions.FetchValueFromCurrentPage<int>("id") - Mikhail
我认为,如果T是值类型,你需要像“return default(T)”这样的东西。在你的例子中:null值不能作为默认值 - 这不是一个好的做法。例如,我想返回null值作为默认值。 - arkhivania

0

请尝试以下操作:

private static T FetchValue<T>(string name, Nullable<T> default_value = null) where T : struct
{
    var page = HttpContext.Current.Handler as Page;

    string str = page.Request.QueryString[name];

    if (str == null)
    {
        if (default_value == null)
        {
            throw new Exception("A " + name + " must be specified.");
        }
        else
        {
            return (T)Convert.ChangeType(default_value, typeof(T));
        }
    }

    return default(T);
}

可空类型必须是值类型,因此您必须将约束设置为struct。您将类型T声明为Nullable,并在存在值的情况下转换类型,否则返回该类型的default,在这种情况下为null。调用此方法时,您还必须显式设置类型,如下所示:

int? val = FetchValue<int>("name");

返回字符串

由于string是一个可空引用类型,因此您正确地指出它不能与此函数一起使用。在这种情况下,我会创建一个新的struct,并使用它来代替字符串。

struct NullableString
{
    public string Value;
}

像这样调用函数。

var val = FetchValue<NullableString>("blah", new NullableString() { Value = "default" });

我希望这个函数能够处理可空类型,至少包括字符串。 - Mikhail
这会在调用方的代码上进行混淆,所以我认为这不是实际可行的。不过,在struct中包装string的想法很好。 - Mikhail
@Mikhail 我想你可以将对这个函数的调用代理,这样在传递字符串类型时它就会被包装在这个结构体中。但我同意,从那时起它会变得非常混乱。 - Paul Aldred-Bann

0

您可以指定class约束,以确保调用者传递引用类型(可为空):

public static T FetchValue<T>(
   string name, 
   T default_value = null) where T : class //!
{
  var page = HttpContext.Current.Handler as Page;

  string str = page.Request.QueryString[name];

  if (str == null)
  {
    if (default_value == null)
    {
      throw new HttpRequestValidationException("A " + name + " must be specified.");
    }
    else
    {
      return default_value;
    }
  }

  return (T)Convert.ChangeType(str, typeof(T));
}

在这种情况下,您将 T 转换为 T? 的责任委托给调用者,在必要时进行转换,非常好:他可能更了解!

和@Terric一样 - 我真的希望允许可空和非可空(类和结构)类型。至少允许'int'和'string'。 - Mikhail

0

你可以用以下简单的方式做你想做的事情:

public static T FetchValue<T>(string name, T defaultValue = null) where T : class { }
public static T? FetchValue<T>(string name, T? defaultValue = null) where T : struct { }

这需要两个重载,而我已经有了两个重载的解决方案,我觉得更容易。 - Mikhail
@Mikhail 这是最简单和最有效的方法。 - AgentFire

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