重载选择和类型约束特性

时间:2016-01-27 22:52:26

标签: c# generics overload-resolution type-constraints

我正在尝试编写一个通用的ConstrainWithinBounds方法,它允许我将在实现IEquatableIComparable的任何值,可空值或类对象截断到定义的范围内。我也喜欢在参数中允许空值的方法,这些方法在语义上被视为“不绑定范围的这一边”。

然而,在我尝试这样做时,我遇到了一个我不明白的奇怪问题。

我提出的以下两个ConstrainWithinBounds方法重载具有基本相同的逻辑,但上面的一个具有泛型类型约束struct而下面的class。第一个允许第二个和第三个参数的可空值类型:

public static T ConstrainWithinBounds<T>(this T value, T? lowerBound, T? upperBound)
   where T : struct, IEquatable<T>, IComparable<T>
{
   return lowerBound.HasValue && value.CompareTo(lowerBound.Value) < 0
      ? lowerBound.Value
      : upperBound.HasValue && value.CompareTo(upperBound.Value) > 0
         ? upperBound.Value
         : value;
}

public static T ConstrainWithinBounds<T>(this T value, T lowerBound, T upperBound)
   where T : class, IEquatable<T>, IComparable<T>
{
   return value == null
      ? null
      : lowerBound != null && value.CompareTo(lowerBound) < 0
         ? lowerBound
         : upperBound != null && value.CompareTo(upperBound) > 0
            ? upperBound
            : value;
}

编程注释:同时实现IEquatableIComparable确保类型不仅具有不等式语义,而且可能是适当的渐变范围,更强烈地暗示重置值/ class / struct到bound是一个明智的操作。例如,一系列订单状态可能会有一个序列,但将OrderPlaced限制在OrderShippedOrderReturned之间是没有意义的。

除此之外,编译器将以下调用解析为第二个重载而不是第一个:

DateTime bounded = new DateTime(2016, 1, 1).ConstrainWithinBounds(
   new DateTime(2016, 2, 1),
   new DateTime(2016, 3, 1)
);

但是它会产生编译错误:

  

DateTime”类型必须是引用类型才能在通用类型或方法“T

中将其用作参数“ConstrainWithinBounds<T>(T, T, T)

正确,DateTime是一个不可为空的结构,为什么过载选择选错了?如果我以不同的方式命名重载,则对第一个重载的完全相同的调用将正确编译(并运行)。

我真正想要的是知道如何使用单个方法组ConstraintWithinBounds来完成工作。

现在,也许我要求的是不可能的,或者不可能优雅,但我真的很想知道!

测试平台,任何建议的答案需要涵盖

这是我用于类类型(C#6.0)的测试类:

public sealed class MyClass : IEquatable<MyClass>, IComparable<MyClass> {
   public MyClass(int value) { Value = value; }
   public int Value { get; }
   public bool Equals(MyClass other) => other != null && Value == other.Value;
   public int CompareTo(MyClass other) => other == null ? 1 : Value.CompareTo(other.Value);
   public override bool Equals(object obj) => obj != null && Value == (obj as MyClass)?.Value;
   public override int GetHashCode() => Value.GetHashCode();
   public static bool operator ==(MyClass a, MyClass b) => ReferenceEquals(a, b) || (object) a != null && (object) b != null && a.Value == b.Value;
   public static bool operator !=(MyClass a, MyClass b) => !(a == b);
   public override string ToString() => Value.ToString();
}

进行一些单元测试以运行重载决策并返回值:

Console.WriteLine(0.ConstrainWithinBounds(5, 10) == 5);
Console.WriteLine(7.ConstrainWithinBounds(5, 10) == 7);
Console.WriteLine(15.ConstrainWithinBounds(5, 10) == 10);

Console.WriteLine(testDate.ConstrainWithinBounds(low, high) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(low, highN) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(low, null) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(lowN, high) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(lowN, highN) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(lowN, null) == low);
Console.WriteLine(testDate.ConstrainWithinBounds(null, high) == testDate);
Console.WriteLine(testDate.ConstrainWithinBounds(null, high) == testDate);
Console.WriteLine(testDate.ConstrainWithinBounds(null, highN) == testDate);
Console.WriteLine(testDate.ConstrainWithinBounds(null, null) == testDate);

Console.WriteLine(testDateN.ConstrainWithinBounds(low, high) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(low, highN) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(low, null) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(lowN, high) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(lowN, highN) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(lowN, null) == lowN);
Console.WriteLine(testDateN.ConstrainWithinBounds(null, high) == testDateN);
Console.WriteLine(testDateN.ConstrainWithinBounds(null, highN) == testDateN);
Console.WriteLine(testDateN.ConstrainWithinBounds(null, null) == testDateN);

Console.WriteLine(testDate2.ConstrainWithinBounds(low, high) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(low, highN) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(low, null) == testDate2);
Console.WriteLine(testDate2.ConstrainWithinBounds(lowN, high) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(lowN, highN) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(lowN, null) == testDate2);
Console.WriteLine(testDate2.ConstrainWithinBounds(null, high) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(null, highN) == high);
Console.WriteLine(testDate2.ConstrainWithinBounds(null, null) == testDate2);

Console.WriteLine(testDate2N.ConstrainWithinBounds(low, high) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(low, highN) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(low, null) == testDate2N);
Console.WriteLine(testDate2N.ConstrainWithinBounds(lowN, high) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(lowN, highN) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(lowN, null) == testDate2N);
Console.WriteLine(testDate2N.ConstrainWithinBounds(null, high) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(null, highN) == highN);
Console.WriteLine(testDate2N.ConstrainWithinBounds(null, null) == testDate2N);

Console.WriteLine(my0.ConstrainWithinBounds(my5, my10).Value == 5);
Console.WriteLine(my0.ConstrainWithinBounds(null, my10).Value == 0);
Console.WriteLine(my0.ConstrainWithinBounds(my5, null).Value == 5);
Console.WriteLine(my0.ConstrainWithinBounds(null, null).Value == 0);
Console.WriteLine(myNull.ConstrainWithinBounds(null, null) == null);
Console.WriteLine(myNull.ConstrainWithinBounds(my5, null) == null);
Console.WriteLine(myNull.ConstrainWithinBounds(null, my10) == null);
Console.WriteLine(myNull.ConstrainWithinBounds(my5, my10) == null);

Console.WriteLine(nullDt.ConstrainWithinBounds(low, high) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(low, highN) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(low, null) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(lowN, high) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(lowN, highN) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(lowN, null) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(null, high) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(null, highN) == null);
Console.WriteLine(nullDt.ConstrainWithinBounds(null, null) == null);

Console.WriteLine(my7.ConstrainWithinBounds(my5, my10).Value == 7);
Console.WriteLine(my7.ConstrainWithinBounds(null, my10).Value == 7);
Console.WriteLine(my7.ConstrainWithinBounds(my5, null).Value == 7);
Console.WriteLine(my7.ConstrainWithinBounds(null, null).Value == 7);
Console.WriteLine(my15.ConstrainWithinBounds(null, null).Value == 15);
Console.WriteLine(my15.ConstrainWithinBounds(my5, null).Value == 15);
Console.WriteLine(my15.ConstrainWithinBounds(null, my10).Value == 10);
Console.WriteLine(my15.ConstrainWithinBounds(my5, my10).Value == 10);

我知道这些不是很好的单元测试,重复的是什么,但是嘿,他们至少会努力证明代码正在做它应该做的......

不是真正的问题,而是一些进一步的想法

P.S。考虑到可能存在可空的第2和第3个参数导致问题,我尝试再添加一个重载:

public static T ConstrainWithinBounds<T>(T value, T lowerBound, T upperBound)
   where T : struct, IEquatable<T>, IComparable<T>
{
   return value.CompareTo(lowerBound) < 0
      ? lowerBound
      : value.CompareTo(upperBound) > 0
         ? upperBound
         : value;
}

但这只会导致第二次重载与这一新重载之间发生冲突:

  

类型'LogicHelper'已经定义了一个名为'ConstrainWithinBounds'的成员,其参数类型相同

Bonus问题:这是否意味着只对方法名称和参数进行重载解析而不考虑泛型类型约束,并且只有在编译过程的后期才会检查类型约束?

1 个答案:

答案 0 :(得分:0)

我建议您“仅”在第二种方法中删除“类”约束。这应该可以解决问题。

public static T ConstrainWithinBounds<T>(this T value, T? lowerBound, T? upperBound)
   where T : struct, IEquatable<T>, IComparable<T>
{
   return lowerBound.HasValue && value.CompareTo(lowerBound.Value) < 0
      ? lowerBound.Value
      : upperBound.HasValue && value.CompareTo(upperBound.Value) > 0
         ? upperBound.Value
         : value;
}

public static T ConstrainWithinBounds<T>(this T value, T lowerBound, T upperBound)
   where T : IEquatable<T>, IComparable<T>   // remove class constraint
{
   return value == null
      ? default(T) // also change this
      : lowerBound != null && value.CompareTo(lowerBound) < 0
         ? lowerBound
         : upperBound != null && value.CompareTo(upperBound) > 0
            ? upperBound
            : value;
}