Your generic method will basically be performing a reference equality check - and the values of s1 and s2 refer to different but equal strings. You can show this more easily like this:
string x = "test"; string y = new string(x.ToCharArray()); Console.WriteLine(x == y); // Use string overload, checks for equality, result = true Console.WriteLine(x.Equals(y)); // Use overridden Equals method, result = true Console.WriteLine(ReferenceEquals(x, y)); // False because they're different objects Console.WriteLine((object) x == (object) y); // Reference comparison again - result = false
Note that your constraint in OpTest doesn't change which == operator is used. That's determined at compile-time, based on the constraints on T. Note that operators are never overridden, only overloaded. That means the implementation is chosen at compile-time, regardless of the type at execution time.
If you constrained T to derive from some type which overloads the == operator, then the compiler will use that overload. For example:
using System; class SillyClass { public static string operator ==(SillyClass x, SillyClass y) => "equal"; public static string operator !=(SillyClass x, SillyClass y) => "not equal"; } class SillySubclass : SillyClass { public static string operator ==(SillySubclass x, SillySubclass y) => "sillier"; public static string operator !=(SillySubclass x, SillySubclass y) => "very silly"; } class Test { static void Main() { var x = new SillySubclass(); var y = new SillySubclass(); OpTest(x, y); } static void OpTest<T>(T x, T y) where T : SillyClass { Console.WriteLine(x == y); Console.WriteLine(x != y); } }
Here the OpTest method does use the overloaded operators - but only ever the ones from SillyClass, not SillySubclass.
EqualityComparer<T>.Default.Equals(s, t)and optionally allow the user to pass their ownIEqualityComparer<T>.