Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UInt32 and UInt64 types cannot be inferred from the usage when used along with Int32 type in generic method

Initially I faced this issue when I was testing my code with UnitTest framework using Assert.AreEqual methods. I noticed that for UInt32 and UInt64 types different overload of AreEqual was selected (AreEqual(object, object) instead of AreEqual<T>(T, T)). I did some research and got the following simple code:

public struct MyInteger
{
    public SByte SByte { get; set; }
    public Byte Byte { get; set; }
    public UInt16 UInt16 { get; set; }
    public UInt32 UInt32 { get; set; }
    public UInt64 UInt64 { get; set; }
    public Int16 Int16 { get; set; }
    public Int32 Int32 { get; set; }
    public Int64 Int64 { get; set; }
}

public class MyGenericClass
{
    public static void DoNothing<T>(T expected, T actual)
    {
    }
}

public class IntegerTest
{
    public void TestIntegers()
    {
        var integer = new MyInteger
        {
            SByte = 42,
            Byte = 42,
            Int16 = 42,
            Int32 = 42,
            Int64 = 42,
            UInt16 = 42,
            UInt32 = 42,
            UInt64 = 42
        };
        MyGenericClass.DoNothing(42, integer.SByte); // T is Int32
        MyGenericClass.DoNothing(42, integer.Byte); // T is Int32
        MyGenericClass.DoNothing(42, integer.Int16); // T is Int32
        MyGenericClass.DoNothing(42, integer.Int32); // T is Int32
        MyGenericClass.DoNothing(42, integer.Int64); // T is Int64
        MyGenericClass.DoNothing(42, integer.UInt16); // T is Int32
        MyGenericClass.DoNothing(42, integer.UInt32); // Error
        MyGenericClass.DoNothing(42, integer.UInt64); // Error
        MyGenericClass.DoNothing((UInt32)42, integer.UInt32); // T is UInt32
        MyGenericClass.DoNothing((UInt64)42, integer.UInt64); // T is UInt64
    }
}   

The Error message I get is "The type arguments for method 'MyGenericClass.DoNothing<T>(T, T)' cannot be inferred from the usage. Try specifying the type arguments explicitly.". The workaround is relatively easy (use explicit cast), so I just want to know, what is so special about UInt32 and UInt64, what other types don't have (or have), and why UInt16 is not behaving the same way?
P.S. Oh, I almost forgot - I've found this table of type conversions, but first of all - it is for new "Roslyn" compiler, and second of all - I don't see the answer there anyway, maybe someone will point it out?

like image 212
Ilya Luzyanin Avatar asked Jul 18 '14 10:07

Ilya Luzyanin


2 Answers

Because T is shared for both arguments to DoNothing, the compiler can only use it if there is a common base class or implicit operator to convert to a common class for both inputs.

The two errors come because there is no implicit conversion for UInt32 or UInt64 to/from Int32 (your 42 literal).

The reason there is no implicit conversion is because there can an information loss between the two. All the other conversions share a common range of values. For example, UInt16 has a range of 0 to 65535, which is within the range of a normal int. Same goes for Byte which can be represented as 0-7. C# considers these kinds of conversions "safe" and so can be performed implicitly. But UInt32 can go from 0 to 4,294,967,295 which is twice as high as a normal int can possibly go. C# considers these kinds of conversions not safe and requires you to perform an explicit cast conversion. The semantics of which denote that you expect that this conversion might fail under certain circumstances (values being outside a compatible range).

DoNothing(42, integer.SByte); // Converts SByte to Int32
DoNothing(42, integer.Byte); // Converts Byte to Int32
DoNothing(42, integer.Int16); // Converts Int16 to Int32
DoNothing(42, integer.Int32); // Already the same
DoNothing(42, integer.Int64); // Converts Int32 to Int64
DoNothing(42, integer.UInt16); // Converts UInt16 to Int32
DoNothing(42, integer.UInt32); // Error - no implicit conversion
DoNothing(42, integer.UInt64); // Error - no implicit conversion
DoNothing((UInt32)42, integer.UInt32); // Explicitly converted to UInt32
DoNothing((UInt64)42, integer.UInt64); // Explicitly converted to UInt64

The explicit conversion worked because you purposely chose a number that was within the shared range of int and Int32 and Int64. If you change it to negative 42, it wouldn't work. (rather, it could if you ran it in an unchecked context and let the number overflow/wraparound.)

like image 145
Chris Sinclair Avatar answered Nov 13 '22 18:11

Chris Sinclair


When you just provide a literal, say 42 it always means an Int32 in c#. To answer your question

Why integer.UInt32 cannot be inferred?

because integer.UInt32 is of type UInt32 where as 42 is of type Int32 so compiler can't make assumptions about what you meant, hence it produces error.

You need to use U suffix which makes the compiler to infer it correctly as UInt32

MyGenericClass.DoNothing(42U, integer.UInt32);//No error

Why integer.UInt64 cannot be inferred?

refer above answer, it is same expect you need to use UL suffix :)

Why integer.UInt16 can be inferred?

You'll see that integer.UInt16 is actually inferred as Int32 and not UInt16 because any UInt16 value can be implicitly converted to Int32 because its range fits the Int32 which is 0 - 65535, and also 42 is a Int32 so compiler is happy to infer it as Int32 and not UInt16.

like image 2
Sriram Sakthivel Avatar answered Nov 13 '22 18:11

Sriram Sakthivel