C#构造函数约束到支持比较运算符的类型

时间:2012-01-24 14:44:22

标签: c#

如果类型允许比较运算符确保提供给构造函数的第一个值小于第二个值,我如何创建一个类来存储任何类型的范围?

public class Range<T> where T : IComparable<T>
{
    private readonly T lowerBound;
    private readonly T upperBound;

    /// <summary>
    /// Initializes a new instance of the Range class
    /// </summary>
    /// <param name="lowerBound">The smaller number in the Range tuplet</param>
    /// <param name="upperBound">The larger number in the Range tuplet</param>
    public Range(T lowerBound, T upperBound)
    {
        if (lowerBound > upperBound)
        {
            throw new ArgumentException("lowerBlound must be less than upper bound", lowerBound.ToString());
        }

        this.lowerBound = lowerBound;
        this.upperBound = upperBound;            
    }

我收到错误:

Error   1   Operator '>' cannot be applied to operands of type 'T' and 'T'  C:\Source\MLR_Rebates\DotNet\Load_MLR_REBATE_IBOR_INFO\Load_MLR_REBATE_IBOR_INFO\Range.cs   27  17  Load_MLR_REBATE_IBOR_INFO

4 个答案:

答案 0 :(得分:9)

您可以使用

where T : IComparable<T>

...或者您可以在代码中使用IComparer<T>,默认为Comparer<T>.Default

后一种方法很有用,因为它允许指定范围,即使对于自然彼此可比的类型,也可以以自定义,合理的方式进行比较。

另一方面,它确实意味着您不会在编译时捕获无法比较的类型

(顺便说一下,创建一个范围类型引入了一系列有趣的API决策,围绕你是否允许反转范围,你是如何跨过它们等等。去过那里,做到了,从来没有完全对结果感到满意......)

答案 1 :(得分:5)

您不能限制T来支持给定的一组运算符,但您可以约束到IComparable<T>

where T : IComparable<T>

至少允许您使用first.CompareTo(second)。您的基本数字类型,加上字符串,DateTimes等,实现此接口。

答案 2 :(得分:2)

为了结合已经给出的两个建议,我们将创建范围的能力与手动定义的比较规则结合起来,对实现IComparable<T>的那些类型进行覆盖,并对后者进行编译时安全。

我们采用与静态Tuple类'Create方法相同的方法。这也可以让我们依赖类型推断:

public static class Range // just a class to a hold the factory methods
{
    public static Range<T> Create<T>(T lower, T upper) where T : IComparable<T>
    {
        return new Range<T>(lower, upper, Comparer<T>.Default);
    }
    //We don't need this override, but it adds consistency that we can always
    //use Range.Create to create a range we want.
    public static Range<T> Create<T>(T lower, T upper, IComparer<T> cmp)
    {
        return new Range<T>(lower, upper, cmp);
    }
}
public class Range<T>
{
    private readonly T lowerBound;
    private readonly T upperBound;
    private readonly IComparer<T> _cmp;
    public Range(T lower, T upper, IComparer<T> cmp)
    {
        if(lower == null)
            throw new ArgumentNullException("lower");
        if(upper == null)
            throw new ArgumentNullException("upper");
        if((_cmp = cmp).Compare(lower, upper) > 0)
            throw new ArgumentOutOfRangeException("Argument \"lower\" cannot be greater than \"upper\".");
        lowerBound = lower;
        upperBound = upper;
    }
}

现在我们不能意外地使用默认的比较器来构造一个Range,它不会起作用,但是也可以省略比较器,只有在它能够工作时才进行编译。

编辑:

有两种主要的方法可以在.NET中以订单授予的方式使项目具有可比性,并使用两者。

一种方法是让一个类型定义它与另一个相同类型的对象进行比较*。这是由IComparable<T>(或非泛型IComparable完成的,但是您必须在运行时捕获类型不匹配,因此在.NET1.1之后它不那么有用。

int例如,实现IComparable<int>,这意味着我们可以执行3.CompareTo(5)并收到一个负数,表示当两者按顺序排列时,3位于5之前。

另一种方法是让一个实现IComparer<T>的对象(同样是一个在.NET1.1之后不太有用的非泛型IComparer)。这用于将两个对象(通常为不同的类型)与比较器进行比较。我们明确地使用它,因为我们感兴趣的类型没有实现IComparable<T>,或者因为我们想要覆盖默认的排序顺序。例如,我们可以创建以下类:

public class EvenFirst : IComparer<int>
{
  public int Compare(int x, int y)
  {
    int evenOddCmp = x % 2 - y % 2;
    if(evenOddCmp != 0)
      return evenOddCmp;
    return x.CompareTo(y);
  }
}

如果我们使用它来对整数列表(list.Sort(new EvenFirst()))进行排序,它会将所有偶数放在第一位,所有奇数放在最后,但在它们的块内按正常顺序排列偶数和奇数

好的,现在我们有两种不同的方式来比较给定类型的实例,一种是由类型本身提供的,一般是“最自然的”,这很好,而且它给了我们更多灵活性,这也很棒。但这意味着我们必须编写关注此类比较的任何代码片段的两个版本 - 一个使用IComparable<T>.CompareTo(),另一个使用IComparer<T>.Compare()

如果我们关心两种类型的对象会变得更糟。然后我们需要4种不同的方法!

解决方案由Comparer<T>.Default提供。对于调用IComparer<T>.Compare()的给定T,此静态属性为我们提供了IComparable<T>.CompareTo的实现。

所以,现在我们通常只编写我们的方法来使用IComparer<T>.Compare()。提供使用CompareTo进行最常见比较的版本只是使用默认比较器的覆盖问题。例如。而不是:

public void SortStrings(IComparer<string> cmp)//lets caller decide about case-sensitivity etc.
{
//pretty complicated sorting code that uses cmp.Compare(string1, string2)
}
public void SortStrings()
{
//equally complicated sorting code that uses string.CompareTo()
}

我们有:

public void SortStrings(IComparer<string> cmp)//lets caller decide about case-sensitivity etc.
{
//pretty complicated sorting code that uses cmp.Compare(string1, string2)
}
public void SortStrings()
{
  SortStrings(Comparer<string>.Default);//simple one-line code to re-use all the above.
}

正如您所看到的,我们在这里两全其美。只想要默认行为的人会调用SortStrings(),有人希望使用更具体的比较规则,例如SortStrings(StringComparer.CurrentCultureIgnoreCase),实施只需要做一些工作就可以提供这种选择。

这是Range的建议。构造函数始终使用IComparer<T>并始终使用Compare,但是有一个工厂方法使用Comparer<T>.Default调用它以提供其他行为。

请注意,我们并不严格需要这个工厂方法,我们可以在构造函数上使用重载:

public Range(T lower, T upper)
  :this(lower, upper, Comparer<T>.Default)
{
}

不利的一点是,我们无法在此处添加where子句以将其限制为可行的情况。这意味着如果我们使用未实现IComparer<T>的类型调用它,我们将在运行时获得ArgumentException而不是编译器错误。当Jon说出来时,Jon的意思是:

  

另一方面,它确实意味着你不会在编译时捕获无法比较的类型。

使用工厂方法纯粹是为了确保不会发生这种情况。就个人而言,我可能只是使用构造函数覆盖,并尝试确保不要不恰当地调用它,但我添加了工厂方法的位,因为它确实结合了这个线程上出现的两件事。

*严格来说,没有什么可以阻止,例如A : IComparable<B>,虽然这在一开始就没用,但是大多数用户也不知道使用它的代码是否最终会调用a.CompareTo(b)b.CompareTo(a)所以它不会除非我们在两个班级都这样做,否则不会工作。在排序中,如果它不能被推到一个共同的基类,那么它就会变得很乱。

答案 3 :(得分:1)

您可以使用.NET框架中广泛使用的IComparable接口。