String.Equals and == are basically the same

Except for all the differences

Ever thought about why there’s a String.Equals, or if you should be using it instead of == ? Of course you have, but for everyone else that’s not you, let’s break it down.

If you’re playing along, we’re compiling in debug mode so that the compiler won’t optimize out the good bits.

Let’s setup some seemingly identical code and see what happens in the IL.

void Main()

{

var string1 = “Alice”;

var string2 = “Bob”;

if (string1 == string2)

{

Console.WriteLine(“strings match”);

}



if (string1.Equals(string2))

{

Console.WriteLine(“strings match”);

}

} IL_0000: nop

IL_0001: ldstr “Alice”

IL_0006: stloc.0 // string1

IL_0007: ldstr “Bob”

IL_000C: stloc.1 // string2

IL_000D: ldloc.0 // string1

IL_000E: ldloc.1 // string2

IL_000F: call System.String.op_Equality

IL_0014: ldc.i4.0

IL_0015: ceq

IL_0017: stloc.2 // CS$4$0000

IL_0018: ldloc.2 // CS$4$0000

IL_0019: brtrue.s IL_0028

IL_001B: nop

IL_001C: ldstr “strings match”

IL_0021: call System.Console.WriteLine

IL_0026: nop

IL_0027: nop

IL_0028: ldloc.0 // string1

IL_0029: ldloc.1 // string2

IL_002A: callvirt System.String.Equals

IL_002F: ldc.i4.0

IL_0030: ceq

IL_0032: stloc.2 // CS$4$0000

IL_0033: ldloc.2 // CS$4$0000

IL_0034: brtrue.s IL_0043

IL_0036: nop

IL_0037: ldstr “strings match”

IL_003C: call System.Console.WriteLine

IL_0041: nop

IL_0042: nop

IL_0043: ret

Fun Fact: Debug mode puts in nops but release mode doesn’t. They’re put in for curly braces and other such non-code lines so you can breakpoint there. They’ll get left out by the JIT later.

The compiler generates the same IL code except for one line. string1 == string2 generates call System.String.op_Equality and string1.Equals(string2) generates callvirt System.String.Equals.

Checking out the String.Equals page we see…

https://msdn.microsoft.com/en-us/library/858x0yyx(v=vs.110).aspx

This method performs an ordinal (case-sensitive and culture-insensitive) comparison.

And then the real fun bit is on the op_equality page.

https://msdn.microsoft.com/en-us/library/system.string.op_equality(v=vs.110).aspx

The Equality method defines the operation of the equality operator for the String class. The operator, in turn, calls the static Equals(String, String) method, which performs an ordinal (case-sensitive and culture-insensitive) comparison.

But I imagine the JIT will compile everything down to the same thing regardless of which you use. So use just use == when you code unless you have a special case.

1,2,3 Not It

It gets a bit more interesting though with inequality. Let’s compare != to !String.Equal.

void Main()

{

var string1 = “Alice”;

var string2 = “Bob”;

if (string1 != string2)

{

Console.WriteLine(“strings do not match”);

} if (!string1.Equals(string2))

{

Console.WriteLine(“strings do not match”);

}

} IL_0000: nop

IL_0001: ldstr “Alice”

IL_0006: stloc.0 // string1

IL_0007: ldstr “Bob”

IL_000C: stloc.1 // string2

IL_000D: ldloc.0 // string1

IL_000E: ldloc.1 // string2

IL_000F: call System.String.op_Inequality

IL_0014: ldc.i4.0

IL_0015: ceq

IL_0017: stloc.2 // CS$4$0000

IL_0018: ldloc.2 // CS$4$0000

IL_0019: brtrue.s IL_0028

IL_001B: nop

IL_001C: ldstr “strings do not match”

IL_0021: call System.Console.WriteLine

IL_0026: nop

IL_0027: nop

IL_0028: ldloc.0 // string1

IL_0029: ldloc.1 // string2

IL_002A: callvirt System.String.Equals

IL_002F: stloc.2 // CS$4$0000

IL_0030: ldloc.2 // CS$4$0000

IL_0031: brtrue.s IL_0040

IL_0033: nop

IL_0034: ldstr “strings do not match”

IL_0039: call System.Console.WriteLine

IL_003E: nop

IL_003F: nop

IL_0040: ret

Doing a != uses a string equality function called System.String.op_Inequality. It takes the result of that and checks if it’s equal to false and then if it’s true that it’s false it branches past the code block. It’s right there clear as day

IL_000F: call System.String.op_Inequality // true if the value of a is different from the value of b; otherwise, false.

IL_0014: ldc.i4.0 // then we put 0 on the eval stack

IL_0015: ceq // checks if result and 0 are equal. puts a 0 or 1 on the stack

IL_0017: stloc.2 // CS$4$0000

IL_0018: ldloc.2 // CS$4$0000

IL_0019: brtrue.s IL_0028 // if ceq returned true because it was true that it was different. then jump past the code block.

And of course it still works it’s way back to String.Equals somehow

https://msdn.microsoft.com/en-us/library/system.string.op_inequality(v=vs.110).aspx

The Inequality operator in turn calls the static Equals(String, String) method, which performs an ordinal (case-sensitive and culture-insensitive) comparison.

However doing a !String.Equals runs the String.Equals function, and if it’s true it just branches past the code block. Basically doing a ! on a function will still always have to run the function to get the result. But instead of checking for false it just compiles the ! as a branch past the code block if it’s true.

IL_0028: ldloc.0 // string1

IL_0029: ldloc.1 // string2

IL_002A: callvirt System.String.Equals

IL_002F: stloc.2 // CS$4$0000

IL_0030: ldloc.2 // CS$4$0000

IL_0031: brtrue.s IL_0040

Conclusion

At the end of the day you’ll compile in Release mode which will optimize the code to do the thing that makes the most sense no matter which one you typed. Then the JIT will optimize the optimized IL code when it’s making the executable. So don’t sweat the small stuff. You focus on readable and maintainable code, and let the tooling do its job.