为什么无法为此通用的Clamp方法推断类型?

15
我正在编写一个表示LED的类。基本上有3个uint值,分别为r、g和b,范围在0到255之间。
我刚开始学习C#,并使用了uint1,但它比我想要的8位更大。在编写自己的Clamp方法之前,我在网上搜索了一下,并找到了这个看起来不错的答案,其中提供了一个扩展方法。问题是它无法推断类型为uint。为什么会这样?这段代码中到处都是uint。我必须明确给出类型才能使其正常工作。
class Led
{
    private uint _r = 0, _g = 0, _b = 0;

    public uint R
    {
        get
        {
            return _r;
        }
        set
        {
            _r = value.Clamp(0, 255); // nope

            _r = value.Clamp<uint>(0, 255); // works
        }
    }
}

// https://dev59.com/BHE85IYBdhLWcg3wpFEa#2683487
static class Clamp
{
    public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
    {
        if (val.CompareTo(min) < 0) return min;
        else if (val.CompareTo(max) > 0) return max;
        else return val;
    }
}

1一个错误,当然使用 byte 是正确的方式。但我仍然对这个问题的答案感兴趣。


1
根据Eric Lippert的说法,IComparable<T>接口应该提供一个完全的排序。鉴于此,为什么即使在字符串上,Clamp也不是语义上正确的呢?@RonBeyer - Rawling
例如,string 实现了 IComparable<string> 接口,但对于 Clamp 来说,在语义上并不正确(也许是 char,但不是 string)。 - Ron Beyer
匈牙利命名法是在名称前缀中加入其类型的概念,因此您的类 Led 在匈牙利命名法中变成了 cLed。我希望这只是 @DStanley 的一个失败的玩笑尝试。 - Ron Beyer
4
快速说明:在 C# 中,很少使用 uint 来表示除了与使用 uint 的非托管代码进行交互之外的其他内容。 在 C# 中,如果您希望表示一个合理大小的整数量,请使用 int ,无论该域中是否存在合理的负数。 您会注意到,在 C# 中,字符串和数组的长度都是 int,即使这些值永远不会是负数。 - Eric Lippert
1
@RonBeyer:就我个人而言,我可以想象出一些情况下我会想在字符串上使用“Clamp”。虽然不常见,但也不是毫无用处的东西,并且它在语义上是明确定义的。 - icktoofay
显示剩余2条评论
4个回答

22
其他答案是正确的,但这里有一个微妙的点需要特别指出。通常,在C#中,整数文字的类型是int,但它可以隐式转换为constant在范围内的任何数字类型。因此,即使int不能隐式转换为uint,myuint = 123;赋值是合法的,因为int符合范围。从这个事实很容易陷入错误的信念,即int文字可以在期望uint的任何地方使用,但您已经发现了为什么这种信念是错误的。
类型推断算法的步骤如下。(当然,这是大大简化的;lambda会使其变得更加复杂。)
- 计算参数的类型 - 分析参数和相应形式参数之间的关系 - 从该分析中推导出泛型类型参数的类型界限 - 检查界限是否完整(每个泛型类型参数必须具有界限)和一致性(界限不得矛盾)。如果推断不完整或不一致,则该方法无法应用。 - 如果推断的类型违反其约束,则该方法无法应用。 - 否则,带有推断类型的方法将添加到用于重载决策的方法集中。
然后,重载决策继续比较候选集中的方法以找到最佳方法。
(请注意,当然没有考虑返回类型;在重载决策选择方法后,C#检查是否可以将返回类型分配给分配给它的任何内容,而不是在重载决策期间进行。)
在您的情况下,类型推断在“验证是否存在一致的界限集”步骤中失败。T被绑定到int和uint两者。这是一个矛盾,因此该方法甚至没有添加到重载解析要考虑的方法集中。事实上int参数是可转换为uint的是从未考虑过的;类型推断引擎仅基于类型工作。
在您的场景中,类型推断算法也不会以任何方式“回溯”;它不会说“好吧,我无法推断出关于T的一致类型,但是也许其中一个单独的类型可行。如果我尝试int和uint两个界限呢?我们可以看看它们是否都实际产生一个有效的方法。”(当涉及lambda时,它确实会执行类似的操作,这可能导致它在某些情况下尝试任意多种类型组合。)如果推断算法按照这种方式工作,则会获得所需的结果,但是它不会这样做。

基本上,这里的哲学是类型推断算法并不试图找到“让程序正常工作的任何方法”,而是寻找从参数获取的信息中导出唯一逻辑结论的类型推理链。C#尝试做用户想让它做的事情,但也尝试避免猜测;在这种情况下,它要求您清楚地表明您希望它推断的类型,而不是可能猜错。


21
因为您正在使用的是 int 值而不是 uint 值,所以会出现这种情况。在 C# 中,裸整数始终被视为 int 值(如果它们适合 int 范围内)。您正在使用形式为 uint.Clamp(int, int) => uint 的方法进行调用。这会被编译器转换为 Clamp(unit, int, int) => uint。然而,编译器实际上期望的是 Clamp(T, T, T) => T,因此它报告了一个错误,因为 uint 和 int 类型的混合阻止了它解析类型 T 应该采取什么样的类型。
请更改以下行:
_r = value.Clamp(0, 255);

to:

_r = value.Clamp(0U, 255U);

代码将会编译。 U 后缀告诉编译器这个数字是一个 uint 值。


2
然而,这并不能解释为什么它不会自动使用“int”,当它满意于使用“uint”时。下面的代码无法编译: _r = value.Clamp((int)-1, (int)-22); 但是为什么呢?“int”强制转换告诉编译器这个数字是一个“int”,那么为什么它不能编译呢? - Matthew Watson
2
@MatthewWatson - 因为 valueuint 类型? - Corak
1
“没有该签名的方法”有点误导人,首先因为返回值不是签名的一部分,其次因为这里没有涉及到两个参数的方法。 - Rawling
1
@Rawling,这次编辑有所改善吗?还是让事情变得更加混乱了? - David Arno
5
澄清一下:C#语言定义签名包括参数类型但不包括返回类型。CLI规范定义签名包括返回类型。不能基于返回类型进行重载是C#的规则,而不是底层运行时的规则。由于这个小差别,在不确定是否考虑返回类型时,“签名”这个术语可能会引起困惑。 - Eric Lippert
显示剩余5条评论

15
您正在使用参数uint,int,int(因为0255int字面量)调用Clamp<T>(T,T,T)

由于两种类型之间没有隐式转换,编译器无法确定将T设置为int还是uint。请注意保留HTML标签。


1
你说在这种情况下没有隐式转换,但是在这种情况下,最后两个参数 0255 是类型为 int 的编译时常量,因此存在一种intuint 的隐式 constant 表达式转换。如果不是这样,那么 value.Clamp<uint>(0, 255) 如何工作?一旦你明确给出泛型方法的 T,就会应用隐式常量表达式转换。但是请参阅 Lippert 的更新答案以获取详细信息。 - Jeppe Stig Nielsen

4
当整数文字没有后缀时,其类型是以下类型中可以表示其值的第一个类型:int、uint、long、ulong。您正在使用0和255,它们可以很好地放入int中,因此选择了它。
您可以通过给字面量加后缀来告诉编译器使用uint
_r = value.Clamp(0U, 255U);

您可以从文档中了解更多信息。


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