Description
When generating the CompareTo(object, IComparer)
for a record type, there is no type-safety for casting. Instead, the CompareTo(object, IComparer)
simply does a direct-cast to the record type. This means callers from C#/VB.NET to CompareTo(object, IComparer)
will get an InvalidCastException
if they send an object that is not the correct type.
Repro steps
F# Code:
type TestType =
{ Name : string }
C# Code:
var testType = new FsLibrary.TestType("Test");
Console.WriteLine(testType.CompareTo("string", null));
The generated ILASM for the TestType::CompareTo
:
.method public hidebysig virtual final instance int32
CompareTo(object obj,
class [mscorlib] System.Collections.IComparer comp) cil managed
{
.custom instance void [mscorlib]
System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
// Code size 59 (0x3b)
.maxstack 4
.locals init ([0] class FsLibrary.TestType V_0)
IL_0000: ldarg.1
IL_0001: unbox.any FsLibrary.TestType
IL_0006: stloc.0
IL_0007: ldarg.0
IL_0008: ldnull
IL_0009: cgt.un
IL_000b: brfalse.s IL_002c
IL_000d: ldarg.1
IL_000e: unbox.any FsLibrary.TestType
IL_0013: ldnull
IL_0014: cgt.un
IL_0016: brfalse.s IL_002a
IL_0018: ldarg.0
IL_0019: ldfld string FsLibrary.TestType::Name@
IL_001e: ldloc.0
IL_001f: ldfld string FsLibrary.TestType::Name@
IL_0024: call int32 [mscorlib]System.String::CompareOrdinal(string,
string)
IL_0029: ret
IL_002a: ldc.i4.1
IL_002b: ret
IL_002c: ldarg.1
IL_002d: unbox.any FsLibrary.TestType
IL_0032: ldnull
IL_0033: cgt.un
IL_0035: brfalse.s IL_0039
IL_0037: ldc.i4.m1
IL_0038: ret
IL_0039: ldc.i4.0
IL_003a: ret
} // end of method TestType::CompareTo
The obvious problem here is the unconditional unboxing of the argument to the FsLibrary.TestType
. There is no attempt made to test the type for compatibility.
Additionally, this unboxing is always done, is done several times, and is not saved until the branch for which it is needed (IL_001e).
This can be more easily seen on sharplab.io
:
https://sharplab.io/#v2:DYLgZgzgPgLgngBwKYAIAqSIzY1BeAWACgVSUBvFAOQEMBbVEFLAJwEsA7AcxQF9igA=
The generated C# for CompareTo(object, IComparer)
is:
[CompilerGenerated]
public sealed override int CompareTo(object obj, IComparer comp)
{
TestType testType = (TestType)obj;
if (this != null)
{
if ((TestType)obj != null)
{
return string.CompareOrdinal(Name@, testType.Name@);
}
return 1;
}
if ((TestType)obj != null)
{
return -1;
}
return 0;
}
The culprit here can be easily identified as TestType testType = (TestType)obj
, as well as the location of that cast, the fact that we cast obj
to TestType
in two other locations (rather than using the previously-casted version).
Expected behavior
The CompareTo(object, IComparer) should be generated with a type-check, then if the type is compatible it should be casted. It should also do the cast only when necessary, rather than as the first step.
Ideally, a more-proper C# version would look like:
[CompilerGenerated]
public sealed override int CompareTo(object obj, IComparer comp)
{
if (this != null)
{
if (this is TestType) // Test for type here, can also use `as`
{
TestType testType = (TestType)obj; // Move this into the block where it's absolutely necessary
if (testType != null)
{
return string.CompareOrdinal(Name@, testType.Name@);
}
return 1;
}
}
if (obj != null) // No cast needed here, but not actively harmful to keep
{
return -1;
}
return 0;
}
Actual behavior
The generated CompareTo(object, IComparer) is not properly compatible with other .NET languages.
Known workarounds
None. The C# version must implement it's own comparison.
Related information
- Windows 10 Pro for Workstations (10.0.16299.492)
- Public Visual Studio 2017 Community
- .NET 4.7.2
- Visual Studio 2017 Community 15.7.2
- Low/Medium, for someone like me who does a lot of C# <-> F# Interop it's far more severe
Metadata
Metadata
Assignees
Type
Projects
Status