50

I just observed a weird phenomenon in C#/.NET.

I created this minimal example to demonstrate:

if (new sbyte[5] is byte[])
{
 throw new ApplicationException("Impossible!");
}

object o = new sbyte[5];

if (o is byte[])
{
 throw new ApplicationException("Why???");
}

This will throw "Why???", but not "Impossible!". It works for all arrays of integral types of the same size. Can someone explain this to me? I'm confused. I'm using .NET 4 by the way.

P.S.: I know that I can get the expected result by using o.GetType() == typeof(byte[]).

user1047771
  • 707
  • 6
  • 10

4 Answers4

50

The CLR rules of casting specify that this is possible. The C# rules say it is not possible. The C# team consciously decided that they would tolerate this deviation from the spec for various reasons.

Why does the CLR allow this? Probably because they can conveniently implement it. byte and sbyte have the same binary representation so you can "treat" a byte[] as an sbyte[] without violating memory safety.

The same trick works for other primitive types with the same memory layout.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 3
    You should include the fact that this deviation is isolated to value typed arrays. – Michael Graczyk Aug 15 '12 at 20:53
  • 3
    It works for ref types, too (array co/contravariance). Ref typed arrays have runtime checks though. I quote Eric: One of the rules of the CLI is "if X is assignment compatible with Y then X[] is assignment compatible with Y[]". – usr Aug 15 '12 at 20:55
  • 1
    The deviation is only for value types. Replace `sbyte` with `string`, and `byte` with 'object'. The code throws at "Impossible!". – Michael Graczyk Aug 15 '12 at 21:07
  • BTW: why does e.g. `Array.Copy` throw an `ArrayTypeMismatchException` when I try to copy from `(byte[])o` to a real `byte[]` if the CLR rules say that the cast is possible?? I didn't think the implementation of `Array` was C#-specific... – user1047771 Aug 15 '12 at 21:59
  • I think the whole Microsoft toolchain is inconsistent around this issue because it has so little visibility. Doesn't matter much in practice, though. – usr Aug 15 '12 at 22:08
28

Funny, I got bitten by that in my question, Why does this Linq Cast Fail when using ToList?

Jon Skeet (of course) explains that my problem is the C# compiler, for whatever reason, thinks they could never be the same thing, and helpfully optimizes it to false. However, the CLR does let this happen. The cast to object throws off the compiler optimization, so it goes through the CLR.

The relevant part from his answer:

Even though in C# you can't cast a byte[] to an sbyte[] directly, the CLR allows it:

var foo = new byte[] {246, 127};
// This produces a warning at compile-time, and the C# compiler "optimizes"
// to the constant "false"
Console.WriteLine(foo is sbyte[]);

object x = foo;
// Using object fools the C# compiler into really consulting the CLR... which
// allows the conversion, so this prints True
Console.WriteLine(x is sbyte[]);

Joel asked an interesting question in the comments, "Is this behavior controlled by the Optimize Code flag (/o to the compiler)?"

Given this code:

static void Main(string[] args)
{
    sbyte[] baz = new sbyte[0];
    Console.WriteLine(baz is byte[]);
}

And compiled with csc /o- Code.cs (don't optimize), it appears that the compiler optimizes it anyway. The resulting IL:

IL_0000:  nop
IL_0001:  ldc.i4.0
IL_0002:  newarr     [mscorlib]System.SByte
IL_0007:  stloc.0
IL_0008:  ldc.i4.0
IL_0009:  call       void [mscorlib]System.Console::WriteLine(bool)
IL_000e:  nop
IL_000f:  ret

IL_0008 loads 0 (false) directly onto the stack, then calls WriteLine on IL_0009. So no, the optimization flag does not make a difference. If the CLR were to be consulted, the isinst instruction would get used. It would probably look something like this starting from IL_0008:

IL_0008:  ldloc.0
IL_0009:  isinst     uint8[]
IL_000e:  ldnull
IL_000f:  cgt.un
IL_0011:  call       void [mscorlib]System.Console::WriteLine(bool)

I would agree with the optimizer's behavior. The optimization flag should not change the behavior of your program.

Community
  • 1
  • 1
vcsjones
  • 138,677
  • 31
  • 291
  • 286
  • 1
    Does this mean that by changing compiler settings, you could get the "optimizes it to false" part to not happen? – Joel Rondeau Aug 21 '12 at 17:34
  • The accepted answer does not attempt to explain why the two supposedly identical if statements behave differently as this one does. – Chad Schouggins Aug 21 '12 at 23:56
2

VB.NET actually "throws" at compile time:

Expression of type '1-dimensional array of SByte' can never be of type '1-dimensional array of Byte'.

on the equivalent of the first if statement.

And the equivalent of the second if succeeds (i.e. it throws the coded exception) at runtime as expected because it is the same CLR.

Mark Hurd
  • 10,665
  • 10
  • 68
  • 101
  • Interesting. C# will emit a warning, so it's possible that if you use "treat warnings as errors" in C# then you can get the same behavior. – vcsjones Aug 22 '12 at 12:18
1

Here's a simpler example that shows the same issue:

static void Main(string[] args)
{
    bool a = ((object) new byte[0]) is sbyte[];
    bool b = (new byte[0]) is sbyte[];

    Console.WriteLine(a == b); // False
}

The inconsistency arises because the C# compiler decides that it knows the result of (new byte[0]) is sbyte[] at compile time, and just substitutes false. Perhaps it should really substitute true, to be more consistent with the CLR behaviour.

As far as I can tell, it's only this little optimization that's inconsistent. It occurs only when both sides of the is expression are statically typed as an array whose element type is a signed or unsigned integer or an enum, and the sizes of the integers are the same.

The good news is that while this might seem inconsistent, C# will always issue a warning when it substitutes false into such expressions – in practice, I think this might be more useful than quietly returning true.

Roman Starkov
  • 59,298
  • 38
  • 251
  • 324