15

I'm writing a class that represents an LED. Basically 3 uint values for r, g and b in the range of 0 to 255.

I'm new to C# and started with uint1, which is bigger than 8 bit that I want. Before writing my own Clamp method I looked for one online and found this great looking answer suggesting an extension method. The problem is that it could not infer the type to be uint. Why is this? This code has uint written all over it. I have to explicitly give the type to make it work.

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://stackoverflow.com/a/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 a mistake, using byte is the way to go of course. But I'm still interested in the answer to the question.

Cœur
  • 37,241
  • 25
  • 195
  • 267
null
  • 5,207
  • 1
  • 19
  • 35
  • Just as a note, I'd try to limit this generic a little more. The way its written every type that implements `IComparable` will get the `Clamp` function, which may not be semantically correct. This is one of those situations where its unfortunate we can't limit `T` to specific types, like `where T : isa(byte, sbyte, int, uint)` etc. – Ron Beyer Sep 22 '15 at 12:39
  • 1
    @RonBeyer According to Eric Lippert, the `IComparable` interface should provide a total order. Given that, why is `Clamp` not semantically correct, even on, say, strings? – Rawling Sep 22 '15 at 12:45
  • @Rawling For example, `string` implements `IComparable`, which to me would not be semantically correct for a `Clamp` (char maybe, but not string). – Ron Beyer Sep 22 '15 at 12:49
  • Hungarian is the idea of prefixing a name with its type, so your class `Led` in hungarian becomes `cLed`. I'm hoping it was a bad attempt at a joke on @DStanley part. – Ron Beyer Sep 22 '15 at 12:59
  • 4
    A quick note: it is rare in C# to use `uint` for anything other than interoperating with unmanaged code that uses `uint`. In C# if you wish to represent a reasonably sized integer quantity use `int` regardless of whether there are sensibly negative numbers in the domain. You'll notice for instance that the length of strings and arrays is `int` in C#, even though these are never negative. – Eric Lippert Sep 22 '15 at 14:26
  • @RonBeyer While I can no longer see the comment that raised the topic of Hungarian notation, [I consider this blog post the topic's proper resolution.](http://www.joelonsoftware.com/articles/Wrong.html) – Keen Sep 22 '15 at 15:56
  • 1
    @RonBeyer: For what it’s worth, I can imagine situations where I would want to use `Clamp` on strings. It’s not common, but it’s not a useless thing to want, either, and it is semantically well-defined. – icktoofay Sep 23 '15 at 01:44

4 Answers4

22

The other answers are correct but there is a subtle point here that I thought should be called out specifically.

Normally in C#, the type of an integer literal is int, but it may be converted implicitly to any numeric type in which the constant is in range. So, even though int is not implicitly convertible to uint, the assignment myuint = 123; is legal because the int fits.

From this fact it is easy to fall into the incorrect belief that int literals can be used anywhere that a uint is expected, but you have discovered why that belief is false.

The type inference algorithm goes like this. (This is a massive simplification of course; lambdas make this considerably more complicated.)

  • Compute the types of the arguments
  • Analyze the relationships between arguments and corresponding formal parameters
  • From that analysis, deduce type bounds on generic type parameters
  • Check the bounds for both completeness -- every generic type parameter must have a bound -- and consistency -- bounds must not be contradictory. If inference is incomplete or inconsistent then the method is inapplicable.
  • If the deduced types violate their constraints, the method is inapplicable.
  • Otherwise, the method with the deduced types is added to the set of methods used for overload resolution.

Overload resolution then proceeds to compare methods in the candidate set against each other to find the best.

(Note that the return type is of course nowhere considered; C# checks to see whether the return type may be assigned to whatever it is assigned to after overload resolution has chosen the method, not during overload resolution.)

In your case type inference is failing at the "verify that there are a consistent set of bounds" step. T is bounded to both int and uint. This is a contradiction, so the method is never even added to the set of methods for overload resolution to consider. The fact that the int arguments are convertible to uint is never considered; the type inference engine works solely on types.

The type inference algorithm also does not "backtrack" in any way in your scenario; it does not say "OK, I can't infer a consistent type for T, but perhaps one of the individual types works. What if I tried both bounds int and uint? We can see if either of them actually produce a method that works." (It does do something similar to that when lambdas are involved, which can cause it to try arbitrarily many possible combinations of types in some scenarios.) If the inference algorithm worked that way then you would get the result you desire, but it does not.

Basically the philosophy here is that the type inference algorithm is not seeking to find any way to make the program work, but rather to find a chain of reasoning about types that derives a unique logical conclusion from information derived from the arguments. C# tries to do what the user means it to do, but also tries to avoid guessing; in this case rather than potentially guessing wrong, it requires you to be clear about the types you intend it to infer.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
21

It's because you are using 0 and 255, which are int values, not uint ones. Bare integer numbers in C# are always treated as int values (if they fit within the int range).

You are invoking Clamp with the form uint.Clamp(int, int) => uint. This gets converted by the compiler to Clamp(unit, int, int) => uint. The compiler though is effectively expecting Clamp(T, T, T) => T, so the it reports an error as the mixture of uint and int types prevent it resolving what type T should take.

Change the line:

_r = value.Clamp(0, 255);

to:

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

and the code will compile. The U suffix tells the compiler that the number is a uint value.

David Arno
  • 42,717
  • 16
  • 86
  • 131
  • 2
    However this doesn't explain why it doesn't use `int` automatically, when it is happy with `uint`. This won't compile: `_r = value.Clamp((int)-1, (int)-22);` but why not? The `int` cast tells the compiler that the number is an `int`, so why doesn't it compile? – Matthew Watson Sep 22 '15 at 12:22
  • 2
    @MatthewWatson - because `value` is `uint`? – Corak Sep 22 '15 at 12:24
  • @MatthewWatson because T and the return type must be equal. Since `r` is `uint` and the two parameters are `int`, it can't infer what you want. – Ron Beyer Sep 22 '15 at 12:25
  • @Corak More likely because there is no implicit conversion from int to uint. – DavidG Sep 22 '15 at 12:25
  • @DavidG Yep that's it I guess. – Matthew Watson Sep 22 '15 at 12:26
  • @MatthewWatson - this is inside a property setter. the `value` is of the type of the property. In this case `uint`. – Corak Sep 22 '15 at 12:26
  • 1
    *There is no method with that signature* is kinda misleading - firstly because the return value isn't part of the signature, and secondly because there isn't a method with two parameters involved here. – Rawling Sep 22 '15 at 12:29
  • 1
    @Rawling, does the edit improve things, or have I just muddied the water? – David Arno Sep 22 '15 at 12:35
  • 1
    @Rawling again, am new to all this, but in this case the return type is T, which is the same as the all parameters and the parameters _are_ part of the signature, no? I understood what David tried to say with "signature" even though that may not be the perfectly accurate name for it. Thanks for pointing out the correct use of terminology, which gets confused so quickly too often. – null Sep 22 '15 at 12:36
  • 5
    To clarify: the C# language defines the signature as including the parameter types but not the return type. The CLI specification defines the signature to include the return type. That you may not overload on return type is a rule of C#, not of the underlying runtime. Because of this small difference the term "signature" can be confusing when it is unclear whether the return type is to be considered or not. – Eric Lippert Sep 22 '15 at 14:28
15

You are calling Clamp<T>(T, T, T) with arguments uint, int, int (as 0 and 255 are int literals).

Since there is no implicit conversion from one type to the other, the compiler cannot work out whether to make T int or uint.

Rawling
  • 49,248
  • 7
  • 89
  • 127
  • 1
    You say there is no implicit conversion in either direction, but in this case the last two arguments, `0` and `255`, are compile-time constants of type `int`, and so there does exist an [implicit __constant__ expression conversion](https://msdn.microsoft.com/da-dk/library/aa691286.aspx) _from_ `int` _to_ `uint`. If that were not the case, how would `value.Clamp(0, 255)` work? As soon as you give the `T` of the generic method explicitly, that implicit constant expression conversion is applied. But see Lippert's newer answer for details. – Jeppe Stig Nielsen Sep 22 '15 at 21:22
4

When an integer literal has no suffix, its type is the first of these types in which its value can be represented: int, uint, long, ulong. You are using 0 and 255, which go nicely into int so that one is chosen.

You can tell the compiler to use uint simply by suffixing the literal

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

More information can be found from documentation.

Sami Kuhmonen
  • 30,146
  • 9
  • 61
  • 74