6

I just discovered this interesting issue:

int done = 50;
int? total = 100;

var perc = done * 100 / total; // No problem

// Error: Argument 2: cannot convert from 'int?' to 'byte'
// Argument 1 is still int32
var perc2 = Math.Min(100, done * 100 / total);

enter image description here

At first, my code only needed the first perc. Surprisingly there was no error there as I missed a null check that should be there. I didn't see my mistake until later:

Then there are some cases where the estimate total is more less than what it should be but the percent needs to be capped at 100 so I added the Math.Min. Now I discovered I missed a null check and the error message is kind of misleading.

What is happening here? What is C# attempting to do in this case?

Luke Vo
  • 17,859
  • 21
  • 105
  • 181
  • 6
    There is no `Min` overload operating on `int, int?`, so you then get the complex rules C# has for overloading methods to get the "best fit". Undoubtedly by parsing the standard closely enough it then becomes clear why the first argument is resolved to a `byte` and not any other integral type, but this is the most difficult part in the standard, so I just trust the compiler is doing the right thing there. – Jeroen Mostert Apr 17 '23 at 16:09
  • 1
    `perc` is of type `int?`, so that's OK. But the value passed to `Math.Min()` must be `int`, NOT `int?`, so that won't work. – Matthew Watson Apr 17 '23 at 16:09
  • 2
    @MatthewWatson this is the best intepretation IMO. I didn't notice `perc` is `int?` as well (which is `Nullable`, a totally different type of `int`). – Luke Vo Apr 17 '23 at 16:11
  • 2
    And @JeroenMostert is right, with the parameter being `int?`, it can just inteprete however it wants. It's strange that if I hover on the first argument, it says it's `int32`, but it picks the first overload (which is `byte, byte`) instead. – Luke Vo Apr 17 '23 at 16:12
  • 1
    I also wondered why a division by int? is accepted (and should yield null)... I never noticed that, but it seems to me very dangerous. – Mario Vernari Apr 18 '23 at 09:58
  • 1
    @MarioVernari: this is simply following the standard rules for lifting operators defined on types to their nullable equivalents. Just as `1 + default(int?) == null`, `1 / default(int?) == null`. This is analogous to how things work in database systems, if `null` is interpreted as "unknown" (and mapping to database types was one of the motivations for adding nullables). It's not particularly dangerous in that the result type is also nullable, and that's typically not that easy to overlook (since the compiler will complain if you do, as evidenced by this very question). – Jeroen Mostert Apr 18 '23 at 10:07
  • @JeroenMostert fully respect the decisions (also in favor of DB, where I personally am not an expert), but I don't like that a "non number" (better, non object) would pass through my code without an exception or, at least, a warning. For the same reason, I should expect that I can call a nullable reference's method without exception when the ref is actually null. No problem, anyway. I'll bear in mind. – Mario Vernari Apr 18 '23 at 11:55
  • I agree with @MarioVernari as well. "Luckily" my code developed further and I realized `perc` is `int?` but even in Nullable Reference Type context, there is no warning or anything on `perc` being `int?`. The final consumption of the value was in Razor and it would be troublesome if it was `null` (printing empty string) but Razor gladly accept `null` value. I wish there is an opt-in option that this should result in a compile error instead. – Luke Vo Apr 18 '23 at 14:32
  • 1
    `int?` is a nullable *value* type; NRTs are really something quite different yet still comparable enough that `?` is also used to indicate nullability. If you don't want nullability the remedy is very simple -- declare your type explicitly as `int`. No implicit conversion from `int?` to `int` is permitted. Outlawing *any* use of `Nullable` is not possible with a compiler switch and probably quite problematic if you want to consume external APIs, but you *could* write a custom Roslyn analyzer for it (or even a simple linting rule that regexes `\w\?`, which should exclude ternary expressions). – Jeroen Mostert Apr 18 '23 at 14:52

2 Answers2

4

You are using a Nullable type. This is a kind of wrapper over a structure that cannot be null. To get the value, you need to refer to it:

var perc2 = Math.Min(100, done * 100 / total.Value);
MrKekMan04
  • 41
  • 1
  • The question is not how to solve the immediate error (depending on where `total` comes from, `.Value` may throw), but why the compiler behaves the way it does. – CodeCaster Apr 18 '23 at 13:29
3

When you call a method in code, the compiler will try to find that method provided the argument list you pass (if any). Take this method:

public static void M(int i) { }

You can call it using M(42) and all is well. When there are multiple candidates:

public static void M(byte b) { }
public static void M(int i) { }

The compiler will apply "overload resolution" to determine "applicable function members", and when more than one, the compiler will calculate the "betterness" of the overloads to determine the "better function member" that you intend to call.

In the case of the above example, M(42), the int overload is chosen because non-suffixed numeric literals in C# default to int, so M(int) is the "better" overload.

(All parts in "quotes" can be looked up in the C# language specification, 12.6.4 Overload resolution.)

Now onto your code.

Given the literal 100 could be stored in both a byte and an int (and then some), and your second argument doesn't match either overload, the compiler doesn't know which overload you intend to call.

It has to pick any to present you with an error message. In this case it appears to pick the first one in declaration order.

Given this code:

public static void Main()
{
    int? i = null;
    M(100, i);
}

public static void M(int i, int i2) {}
public static void M(byte b, byte b2) {}

It mentions:

cannot convert from 'int?' to 'int'

If we swap the method declarations:

public static void M(byte b, byte b2) {}
public static void M(int i, int i2) {}

It complains:

cannot convert from 'int?' to 'byte'

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • 1
    In this case the compiler seems to just use the "since there is no best match for the overload, I'm going to pick an arbitrary one and you can't complain" school of thought. The spec merely says that "if there is not exactly one function member that is better than all other function members, then the function member invocation is ambiguous and a binding-time error occurs", without putting constraints on what the error should look like. I haven't worked through the rules to see there is indeed no one better match, but I suspect it to be the case. – Jeroen Mostert Apr 18 '23 at 10:14
  • Thanks. This is the answer I was looking for when I asked this question. Just a side question, how are you sure the other (now deleted) answer was ChatGPT. I suspected it due to the "weirdness" in the sentences but can we be so sure? – Luke Vo Apr 18 '23 at 14:29
  • 1
    @Luke I've recognized and flagged over a dozen of them now. It's just a gut feeling, but it's quite a recognizable writing style. It also is perfect English, contrary to their earlier answers. – CodeCaster Apr 18 '23 at 14:31