Why does C# attempt to convert int? to byte when using Math.Min?
Asked Answered
M

2

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?

Modesta answered 17/4, 2023 at 16:5 Comment(9)
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.Manaker
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.Caffey
@MatthewWatson this is the best intepretation IMO. I didn't notice perc is int? as well (which is Nullable<int>, a totally different type of int).Modesta
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.Modesta
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.Augustusaugy
@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).Manaker
@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.Augustusaugy
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.Modesta
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).Manaker
G
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'

Gun answered 18/4, 2023 at 10:6 Comment(3)
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.Manaker
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?Modesta
@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.Gun
I
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);
Islington answered 18/4, 2023 at 11:44 Comment(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.Gun
G
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'

Gun answered 18/4, 2023 at 10:6 Comment(3)
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.Manaker
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?Modesta
@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.Gun

© 2022 - 2024 — McMap. All rights reserved.