Search code examples
c#genericstype-conversion

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?


Solution

  • 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.)