C#中的有条件泛型类型构造函数?

4
假设您有一个通用类Foo:
public class Foo<T> {

    public T Data {
        get;
        protected set;
    }

}

是否有可能定义一个仅适用于T继承(或是)特定类型的构造函数。

例如,假设T是一个int

    public Foo () {
        this.Data = 42;
    }

类型约束应该在编译时检查。这对于优化可能很有用。例如,如果您有一个 IEnumerable<T> 并且希望创建一个“缓存”(因为 LINQ 查询可能非常昂贵),那么现在如果 IEnumerable<T> 已经是一个 IList<T>,不复制数据是有用的。另一方面,如果它真的是一个 LINQ 查询,另一个构造函数可以将数据存储在数组中。


作为解决方法,当然可以继承 Foo(例如 IntFoo)并在那里定义一个构造函数:

public class IntFoo : Foo<int> {

    public IntFoo () {
        this.Data = 42;
    }

}

这种方法的问题在于,private 数据不可访问(或者必须将其设置为 protected)。还有其他缺点吗?或者说这样建模类型特定的构造函数是正确的方式吗?

你是指 类型约束 吗?类型约束的唯一问题是,令人沮丧的是,数值原语没有可用于限制的有用基础。 - Matt Burland
真的吗?为什么你不能在构造函数之后定义一个 where ... 子句,在这种情况下,只有当类型匹配时才能访问构造函数。 - Willem Van Onsem
2
关于您的编辑:我相当确定您无法在编译时检查 IEnumerable<T> 是否为 IList<T>。毕竟,您总是可以在运行时将 IList<T> 分配给 IEnumerable<T> - HugoRune
1
对于HugoRune的评论点赞,你的编辑完全改变了问题的意思,从“在编译时很可能做得很好”变成了“没有办法在编译时完成”。请考虑将编辑更改回看起来像“根据编译时泛型类型指定特殊逻辑/默认值”,并提出关于“LINQ如何优化查询,其中IEnumerable<T>实际上是IList<T>,是否可能在编译时完成”的单独问题。 - Alexei Levenkov
4个回答

11

这里有一个技巧可以应用。它适用于许多场景。

internal static class FooHelper
{
    private static class DefaultData<T>
    {
        public static T Value = default(T);
    }

    static FooHelper()
    {
        DefaultData<int>.Value = 42;
        DefaultData<string>.Value = "Hello World";
    }

    // From @JeffreyZhao:
    //
    // Use a static method to trigger the static constructor automatically,
    // or we need to use RuntimeHelpers.RunClassConstructor to make sure
    // DefaultData is corrected initialized.
    //
    // The usage of RuntimeHelpers.RunClassConstructor is kept but commented.
    // Using GetDefault<T>() is a better approach since static Foo() would be
    // called multiple times for different generic arguments (although there's 
    // no side affect in this case).
    //
    // Thanks to @mikez for the suggestion.
    public static T GetDefault<T>()
    {
        return DefaultData<T>.Value;
    }
}

public class Foo<T>
{
    /* See the comments above.
    static Foo()
    {
        RuntimeHelpers.RunClassConstructor(typeof(FooHelper).TypeHandle);
    }
     */

    public T Data { get; protected set }

    public Foo()
    {
        Data = FooHelper.GetDefault<T>();
    }
}

你可以指定有限类型的默认值,它们的结果将保持为默认值。
这种技巧在实践中有几种变化。在我的项目中,我们使用通用的 ITypeConverter<T> 而不是内置的 TypeConverter 来避免不必要的装箱:
public interface ITypeConverter<T>
{
    bool CanConvertTo<TTarget>();
    TTarget ConvertTo(T value);
}

同样的技巧也可以应用于以下情况:
public class LongConverter : ITypeConverter<long>
{
    private static class Op<TTarget>
    {
        public static Func<long, TTarget> ConvertTo;
    }

    static LongConverter()
    {
        Op<string>.ConvertTo = v => v.ToString();
        Op<DateTime>.ConvertTo = v => new DateTime(v);
        Op<int>.ConvertTo = v => (int)v;
    }

    public TTarget ConvertTo<TTarget>(T value)
    {
        return Op<TTarget>.ConvertTo(value);
    }
}

优雅、快速、简洁。

如所写,第一个示例不会按预期工作。因为没有创建FooHelper的实例,也没有访问任何静态成员,所以FooHelper的静态构造函数永远不会被调用。虽然访问了嵌套类FooHelper.DefaultData<T>,但这并不会导致外部类的静态构造函数运行。 - Mike Zboray
@mikez:你说得对。我是在网页上即兴编写代码而没有测试它。 :) 这里的想法是使用一个分离的、非静态类来保存通用的默认值,所以我创建了 FooHelper。我加入了一个 RuntimeHelpers.RunClassConstructor 语句,以确保我们第一次使用 Foo 时会执行 FooHelper 的静态构造函数。谢谢。 - Jeffrey Zhao
1
绝对是一个好的技巧。另一个选项是在 FooHelper 上创建一个通用方法,比如说 GetDefault<T>() - Mike Zboray
@mikez:你的方法更好。Foo 的静态构造函数将会以不同的泛型参数执行多次(尽管在这种情况下没有副作用)。我已经按照你的建议编辑了答案。 - Jeffrey Zhao

4
  public class Foo<T>
  {
      public T Data
      {
          get;
          protected set;
      }

            public Foo()
            {
                switch (Type.GetTypeCode(Data.GetType()))
                {
                    case TypeCode.Int16:
                    case TypeCode.Int32:
                    case TypeCode.Int64:
                        Data = (T)Convert.ChangeType(42, typeof(T));
                        break;
                    default:
                        break;
                }

            }

        }

这样您就可以为很多类型创建一个构造函数。

public class Foo<T>
{

    public T Data
    {
        get;
        protected set;
    }

    public Foo()
    {
        switch (Type.GetTypeCode(Data.GetType()))
        {
            case TypeCode.Boolean:
                Data = ConvertValue<T>(true); 
                break;
            case TypeCode.DateTime:
                Data = ConvertValue<T>("01/01/2014"); 
                break;
            case TypeCode.Double:
                Data = ConvertValue<T>(0.5); 
                break;
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
                Data = ConvertValue<T>(32); 
                break;
            case TypeCode.String:
                Data = ConvertValue<T>("Test");
                break;
            default:
                break;
        }

    }
    private static T ConvertValue<T>(object value)
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }
}

3

虽然可以这样做,但我几乎看不到使用。

public class Foo<T>
{

    public T Data
    {
        get;
        protected set;
    }

    public Foo()
    {
        if (Data is int)
            Data = (T)(object)42;
    }
}

使用方法:

Console.WriteLine("int = {0}", new Foo<int>().Data);
Console.WriteLine("double = {0}", new Foo<double>().Data);
Console.WriteLine("string = {0}", new Foo<string>().Data);

输出:

int = 42
double = 0
string =

在这种情况下,类型约束在运行时进行检查。 - Willem Van Onsem
@CommuSoft:泛型不是C++模板,它们并非100%编译时。 - zerkms
我知道,但这是一些人可以在编译时检查的东西,类本身的类型约束(如果可能)在编译时进行检查。 - Willem Van Onsem
@CommuSoft:但这不是一个约束条件,而是纯粹的应用逻辑。 - zerkms
约束条件是只有在类型匹配的情况下才能应用特定的构造函数,就像只有在根据“where ...”子句指定了T的情况下才能使用类Foo<T>一样。 - Willem Van Onsem

3

如果您不介意使用创建函数而不是构造函数,并将该特定类型的参数作为参数传递(以进行重载分辨率):

public class Foo<T> {

  public T Data { get; private set; }

  public static Foo<T> Create(T value)
  {
     return new Foo<T> { Data = value };
  }

  public static Foo<int> From(int value)
  {
     return new Foo<int> { Data = 42 * value };
  }
}

使用方法:

  void Main()
  {
    var v = Foo<int>.Create(1); 
    var s = Foo<string>.Create("test");
  }

当存在多个可能解析的重载时,非泛型函数是更可取的选择(例如,在Foo<int>的情况下,你将得到特定的覆盖,即Create<int>(int value)Create(int value))。

请注意,你只能针对特定类型来执行此操作,无法使用类型限制,因为在同一个类中无法同时存在两个同名的通用方法(例如Create<U>()),即使类型约束显然是不同的(例如where U:classwhere U:struct)。


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