过载选择和类型约束特性

本文关键字:约束 类型 选择 | 更新日期: 2023-09-27 18:32:08

我正在尝试编写一个通用的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可确保类型不仅具有不等式语义,而且可能是适当的分级范围,更强烈地暗示将值/类/结构重置为绑定是一个明智的操作。例如,一系列订单状态可能具有序列,但将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);

我知道这些不是很好的单元测试,重复和所有,但是嘿,它们至少可以证明代码正在做它应该做的事情......

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

附言考虑到可能可为空的第 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"已经定义了一个名为"ConsconstraintWithinBounds"的成员,具有相同的参数类型

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

过载选择和类型约束特性

我建议你"只是"删除第二种方法中的"类"约束。这应该可以解决问题。

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;
}