Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"is" operator in C# returns inconsistent results

Tags:

c#

.net

I'd like to use "is" operator in C# to check the runtime type of an object instance. But it doesn't seem to work as I'd expect.

Let's say we have three assemblies A1, A2 and A3 all containing just one class.

A1:

public class C1
{
    public static void Main()
    {
        C2 c2 = new C2();

        bool res1 = (c2.c3) is C3;
        bool res2 = ((object)c2.c3) is C3;
    }
}

A2:

public class C2
{
    public C3 c3 = new C3();
}

A3:

public class C3
{
}

A1 needs to reference A2 and A3.

A2 needs to reference A3.

After running Main() res1 and res2 are set to true as expected. The problem occurs when I start versioning A3 as strongly named assembly and make A1 to reference one version and A2 to reference another version of A3 (the source code of A3 remains the same). Btw. compiler allows this only if the version of A3 referenced by A2 is lower or equal than the version of A3 referenced by A1. The outcome of this program is now different (res1 = true, res2 = false).

Is this behaviour correct? Shouldn't they be both false (or perhaps true)?

According to C# 5.0 specification (chapter 7.10.10) both res1 and res2 should end up with the same value. The "is" operator should always consider run-time type of the instance.

In IL code I can see for res1 the compiler made the decission that both C3 classes coming from different A3 assemblies are equal and emitted the code without isinst instruction checking against null only. For res2 compiler has added isinst instruction which postpones the decision for run-time. It looks like C# compiler has different rule on how to resolve this than CLR run-time.

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       36 (0x24)
  .maxstack  2
  .locals init ([0] class [A2]C2 c2,
           [1] bool res1,
           [2] bool res2)
  IL_0000:  nop
  IL_0001:  newobj     instance void [A2]C2::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldfld      class [A3]C3 [A2]C2::c3
  IL_000d:  ldnull
  IL_000e:  ceq
  IL_0010:  ldc.i4.0
  IL_0011:  ceq
  IL_0013:  stloc.1
  IL_0014:  ldloc.0
  IL_0015:  ldfld      class [A3]C3 [A2]C2::c3
  IL_001a:  isinst     [A3_3]C3
  IL_001f:  ldnull
  IL_0020:  cgt.un
  IL_0022:  stloc.2
  IL_0023:  ret
} // end of method C1::Main

Could it be just trade-off for a faster and optimised implementation without using isinst (considering the compiler warning)?

Possible option to get around this is binding redirect (as suggested by the warning) but I can't use that as the versions may not always be backwards compatible (although C3 class always is). Changing the reference in A2 is also not an option for me.

EDIT: As it seems the easiest workaround is to always cast to object to get the correct result.

Anyway it would still be interesting to know if it's a bug in C# compiler (and possibly report it to MS) or not a bug per se (as compiler identifies a problem and reports a warning) although it could still generate a correct IL code.

like image 643
Erik Avatar asked Mar 13 '15 10:03

Erik


People also ask

Is operator in C language?

C language supports a rich set of built-in operators. An operator is a special symbol that tells the compiler to perform specific mathematical or logical operations.

What does %= mean in C?

%= Modulus AND assignment operator. It takes modulus using two operands and assigns the result to the left operand. C %= A is equivalent to C = C % A.

What type of operator is & in C?

For example, '+' is an operator used for addition, as shown below: c = a + b; Here, '+' is the operator known as the addition operator and 'a' and 'b' are operands. The addition operator tells the compiler to add both of the operands 'a' and 'b'.


2 Answers

Unfortunately, I don't have an answer to why the first result yields true. However, if the spec says that is is supposed to be based on the runtime type, Panagiotis is correct; the types are different and both should return false. GetType() and typeof behave as is should.

var res3 = c2.c3.GetType() == typeof(C3);              // is false
var res4 = ((object)c2.c3).GetType() == typeof(C3);    // is false

var localC3 = new C3();
var res5 = localC3 is C3;                              // is true
var res6 = ((object)localC3).GetType() == typeof(C3);  // is true

My knee-jerk reation would be get rid of the object cast as that seems to work as you want.

However, as that may change if is is fixed. You could resort to the following. Since your code was compiled against signed assemblies, people won't be able to substitute a fake assembly.

var res7 = c3.GetType().FullName == typeof(C3).FullName

Hopefully, some of this helps.

like image 193
Chuck Avatar answered Oct 14 '22 02:10

Chuck


Your problem is that res1's equation is compiled to true by the C# compiler (as shown by the IL). However res2 is performing the correct analysis as it is doing it at run-time (any time you cast to object it forces C# to fall back to run-time operations for most things).

So it appears that the compiler assumes the types are the same (likely not validating the version of the constituent DLL).

The only solution that comes to mind easily is to change the alias of one of them and see if qualifying which C3 you are talking about helps.

like image 39
Guvante Avatar answered Oct 14 '22 02:10

Guvante