【发布时间】:2016-01-27 22:52:25
【问题描述】:
我正在尝试编写一个通用的ConstrainWithinBounds 方法,它可以让我将实现IEquatable 和IComparable 的任何值、可为空值或类对象截断到定义的范围内。我还希望该方法允许参数中包含空值,这在语义上将被视为“不绑定范围的这一侧”。
但是,在尝试这样做时,我遇到了一个我不明白的奇怪问题。
我提出的以下两个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;
}
编程说明:同时实现IEquatable 和IComparable 确保该类型不仅具有不等式语义,而且可能是一个适当的毕业范围,更强烈地暗示将值/类/结构重置为界是一个明智的操作。例如,一系列订单状态可能有一个序列,但将OrderPlaced 限制在OrderShipped 和OrderReturned 之间是没有意义的。
除了,编译器将以下调用解析为第二个重载而不是第一个:
DateTime bounded = new DateTime(2016, 1, 1).ConstrainWithinBounds(
new DateTime(2016, 2, 1),
new DateTime(2016, 3, 1)
);
然后它给出了编译错误:
类型“
DateTime”必须是引用类型才能在泛型类型或方法“ConstrainWithinBounds<T>(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);
我知道这些不是很好的单元测试,还有重复和所有内容,但是嘿,它们至少可以证明代码正在做它应该做的事情......
不是真正的问题,而是一些进一步的想法
附:考虑到可能为空的第二个和第三个参数可能会导致问题,我尝试再添加一个重载:
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”的成员,具有相同的参数类型
额外问题:这是否意味着重载决议只针对方法名称和参数而不考虑泛型类型约束,并且仅在编译过程的后期才检查类型约束?
【问题讨论】:
-
执行重载解析时不考虑约束。返回类型也不是。如果我不得不猜测编译器使用的实际推理,对于提供的示例,您的不可为空的重载类型将与提供的参数的类型完全匹配,因此它是最佳匹配。然后,它在确定最佳匹配之前不会检查约束,因此您会收到约束错误。
-
当然当然返回类型不用于重载解析——我只是犯了一个愚蠢的错误。我将从我的问题中删除该部分。
-
您能否澄清一下 ErikE,您的问题是了解泛型的怪癖,还是解决如何编写适用于所有类型的 ConstrainWithinBounds 的问题?正如你的问题所言,这是一个重复。任何解决问题的建议都将被否决,因为没有回答您的具体问题(我认为这是 SO 的失败)。
-
All:既然我已经更新了这个问题来处理我的 real 愿望(不是在全球范围内解决泛型问题,而是要有一个直观地工作的
ConstrainWithinBounds方法),这不再是重复的。 -
如果您在呼叫现场将两个
DateTimes 转换为DateTime?s 会发生什么?
标签: c# generics overload-resolution type-constraints