0

What is the difference between == and Equals in generic class if T is a string.

I prepared sample code with 2 tests. TestMethod1 fails on 2nd assert, but TestMethod2 passes.

Source code:

[TestFixture]
public class Test
{
    class Foo<T> where T : class
    {
        public static bool Foo1(T item1, T item2)
        {
            return item1 == item2;
        }

        public static bool Foo2(T item1, T item2)
        {
            return item1.Equals(item2);
        }
    }

    [TestCase]
    public void TestMethod1()
    {
        var name = "str" + 1;
        // passes
        Assert.IsTrue(Foo<string>.Foo2(name, "str1"));
        // fails !
        Assert.IsTrue(Foo<string>.Foo1(name, "str1"));
    }

    [TestCase]
    public void TestMethod2()
    {
        // passes
        Assert.IsTrue(Foo<string>.Foo2("str1", "str1"));
        // also passes!
        Assert.IsTrue(Foo<string>.Foo1("str1", "str1"));
    }
}

Related question: c# compare two generic values

rraszewski
  • 1,135
  • 7
  • 21

2 Answers2

2

Using == in this case resolves to System.Reference.Equals.

.Equals is a virtual method so the overridden version will be used, in this case it will be content comparison for string.

The first test compares a local variable to a literal (interned) string.

The second test compares the same interned string so both == and Equals return true.

If you change your TestMethod1() to this:

        Assert.IsTrue(Foo<string>.Foo1(name, name));

it will pass because you are comparing the same object, so the reference equality returns true.

Edit: If we add this method:

    public static bool Foo3(string item1, string item2)
    {
        return item1 == item2;
    }

Then the following test will return true:

    Assert.IsTrue(Foo<string>.Foo3(name, "str1"));

because C# will resolve == in this case to the string value check.

This question is really a duplicate of this, with additional generic complexity.

Edit 2: OK, time to dive into the MSIL. Foo1 looks like this:

.method public hidebysig static bool  Foo1(!T item1,
                                           !T item2) cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals init ([0] bool V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  box        !T
  IL_0007:  ldarg.1
  IL_0008:  box        !T
  IL_000d:  ceq
  IL_000f:  stloc.0
  IL_0010:  br.s       IL_0012
  IL_0012:  ldloc.0
  IL_0013:  ret
} // end of method Foo`1::Foo1

You can see here it is using ceq which is Push 1 (of type int32) if value1 equals value2, else push 0.

Here is Foo2:

.method public hidebysig static bool  Foo2(!T item1,
                                           !T item2) cil managed
{
  // Code size       23 (0x17)
  .maxstack  2
  .locals init ([0] bool V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  box        !T
  IL_0007:  ldarg.1
  IL_0008:  box        !T
  IL_000d:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
  IL_0012:  stloc.0
  IL_0013:  br.s       IL_0015
  IL_0015:  ldloc.0
  IL_0016:  ret
} // end of method Foo`1::Foo2

This uses System.Object::Equals - this is the virtual method and behaves as such, being overridden in the concrete type of the parameter - in this case string.

Foo3 (which I added, which uses == on declared string parameters) looks like this:

.method public hidebysig static bool  Foo3(string item1,
                                           string item2) cil managed
{
  // Code size       13 (0xd)
  .maxstack  2
  .locals init ([0] bool V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.1
  IL_0003:  call       bool [mscorlib]System.String::op_Equality(string,
                                                                 string)
  IL_0008:  stloc.0
  IL_0009:  br.s       IL_000b
  IL_000b:  ldloc.0
  IL_000c:  ret
} // end of method Foo`1::Foo3

Here we observe explicit use of the System.String::op_Equality(string, string) which is the string value check comparison.

Matt Hogan-Jones
  • 2,981
  • 1
  • 29
  • 35
  • but `Assert.IsTrue(name == "str1");` will also pass even though references will be different. String has its own `== operator` and I am asking why it is not used in generic classes. – rraszewski Jun 05 '17 at 09:15
  • Because `Assert.IsTrue(name == "str1");` is comparing an explicitly declared `string` with an interned string so it is using the string value check version of `==`. In your generic method the object reference version of `==` is being used because that's how C# is resolving it. – Matt Hogan-Jones Jun 05 '17 at 09:18
2

As Matt has already mentioned, half of your problem is interning.
And the other half is that the compiler has no idea about your type parameter being string and treats it like just some reference type, hence it calls object's methods.
But don't take it on faith, let's take a look at IL.

ceq here tells us that there's no == operator overload, so reference check is performed on boxed arguments.

Foo`1.Foo1:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  box         Test+Foo<>.T
IL_0007:  ldarg.1     
IL_0008:  box         Test+Foo<>.T
IL_000D:  ceq         
IL_000F:  stloc.0     
IL_0010:  br.s        IL_0012
IL_0012:  ldloc.0     
IL_0013:  ret         

On the other hand, here we can see callvirt of System.Object.Equals and at runtime it will be resolved into a call of whatever implementation actual type parameter has, i.e. string.Equals.

Foo`1.Foo2:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  box         Test+Foo<>.T
IL_0007:  ldarg.1     
IL_0008:  box         Test+Foo<>.T
IL_000D:  callvirt    System.Object.Equals
IL_0012:  stloc.0     
IL_0013:  br.s        IL_0015
IL_0015:  ldloc.0     
IL_0016:  ret 
Lemx
  • 166
  • 2
  • 6