Operator ==, IEquatable<T>.Equals(T), Object.Equals(object, object)

Roman Mejtes

Для того, чтоб сравнить 2 значения, в C# есть 3 способа (на сколько я знаю)А) Сравнить значения через оператор ==Б) Через метод IEquatable.Equals(T value)В) Через метод Equals(object, object) или Equals(object) который определен в классе Object(разницы между ними делать не буду, по факту оба варианта получают в качестве аргумента 2 значения и сравнивают их, отличие только в доп. проверке на null в 1 варианте)У каждого метода есть свои плюсы и минусы, уверен, что многих плюсов и минусов я не знаю или забуду упомянуть и надеюсь на вашу бдительность и ваши знания.А) + Простота использования+ Высокая скорость работы, если посмотреть получившийся исполняемый код, то при сравнении значений для простых числовых типов, сравнение осуществляется оператором cmp, если сравнивать 2 разных типа, то к сравнению еще прибавляется неявное преобразование типа. - Оператор может вводить в заблуждение неопытных программистов, так как непонятно как именно осуществляется проверка, по значению или по ссылке. Для классов где оператор явно не переопределен проверяется по ссылке, для структур по значению. То есть я должен знать заранее, определен там оператор явно или или нет, структура это или класс и т.д.- чтоб сравнить 2а разных типа, нужно быть уверенным в том, что хотя бы 1 из них неявно преобразовывается во 2ой (к счастью такие ошибки видны даже до компиляции)Б) Метод работает только с простыми типа или с типами которые наследуют интерфейс IEquatable. Для простых типов, это фактически пункт А, просто обёрнутый в отдельный вызов метода.+ Значение передается без упаковки, заданного типа.+ Высокая скорость работы- Косяк у данного способа в том, что если T это класс и 0ой аргумент не определен (аргумент this равен null), то метод нельзя будет вызвать и мы получим NullReferenceException. Если мне необходимо сравнить значение простого типа (класса, пусть будет string) с константой, то безопаснее будет написать примерно так
"100".Equals(strVarialble)
, а не наоборот, так как если переменная strVariable равна null, будут проблемыВ) + Метод удобен тем, что работает всегда, не важно какой из аргументов может быть null+ можно сравнить 2 значения разных типов. При условии, что метод object.Equals(object) переопределен и поддерживает сравнение с другим типом (плюс сомнительный)- Если они оба null мы получим True (что на мой взгляд бредово, как 2а неопределенных значения могут быть равными, ведь они не определены? %), фактически они могут даже представлять разные типы. Если мы сравним (object)null и (string)null мы получаем true.- Значения передаются через тип Object, а значит будут упакованы- Самый медленный вариантпроверил на скорость, на сравнение 1000000000 значений типа int Для DEBUG x64А) 00:00:03.4353033Б) 00:00:04.2280944В) 00:00:13.8591103Для RELEASE x64 с оптимизациейA) 00:00:00.9026884Б) 00:00:01.0187077В) 00:00:10.9358837Видно, что после оптимизации, вариант А, практически эквивалентен по скорости варианту Б. Что не может не радовать.Уверен, что мои знания вполне поверхностные и я чего то не учёл в данной теме, для этого я её и создал, вы и так всё это знаете, но может быть знаете больше и надеюсь добавите варианты и плюсы\минусы этих вариантов.Какой из методов вы выбираете и в каких случаях.
5 ответов

Roman Mejtes

Roman Mejtes,Для размышлений:
struct Foo
{
 public int N;
 public static bool operator ==(Foo f1, Foo f2)
 {
 return f1.N==f2.N;
 }
 public static bool operator !=(Foo f1, Foo f2)
 {
 return f1.N!=f2.N;
 }
}

class Bar
{
 public Bar(string s)
 {
 Zot = s;
 }

 public string Zot { get; }

 public override bool Equals(object obj)
 {
 return (obj is Bar b) && Zot.Equals(b.Zot);
 }

 protected bool Equals(Bar other)
 {
 return string.Equals(Zot, other.Zot);
 }

 public override int GetHashCode()
 {
 return (Zot != null ? Zot.GetHashCode() : 0);
 }
}

class Program
{
 static void Main()
 {
 Compare(123, 456);
 Compare(typeof(object), typeof(string));
 Compare(new Foo{N=1}, new Foo{N=2});
 Compare('a', 'b');
 Compare(new Bar("1"), new Bar("2"));
 Console.WriteLine("done");
 Console.ReadKey(true);
 }

 static void Compare(int n1, int n2)
 {
 Console.WriteLine(n1 == n2);
 }

 static void Compare(Foo f1, Foo f2)
 {
 Console.WriteLine(f1 == f2);
 }

 static void Compare(Type t1, Type t2)
 {
 Console.WriteLine(t1==t2);
 }

 static void Compare(char c1, char c2)
 {
 Console.WriteLine(c1.Equals(c2));
 }

 static void Compare(Bar b1, Bar b2)
 {
 Console.WriteLine(b1==b2);
 }
}
.class private auto ansi beforefieldinit test2.Program
 extends [mscorlib]System.Object
{
 .method public hidebysig specialname rtspecialname instance void .ctor () cil managed 
 {
 IL_0000: ldarg.0
 IL_0001: call instance void [mscorlib]System.Object::.ctor()
 IL_0006: nop
 IL_0007: ret
 }

 .method private hidebysig static void Compare (
 int32 n1,
 int32 n2
 ) cil managed 
 {
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: ldarg.1
 IL_0003: ceq 
 IL_0005: call void [mscorlib]System.Console::WriteLine(bool)
 IL_000a: nop
 IL_000b: ret
 }

 .method private hidebysig static void Compare (
 valuetype test2.Foo f1,
 valuetype test2.Foo f2
 ) cil managed 
 {
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: ldarg.1
 IL_0003: call <span>bool</span> test2.Foo::op_Equality(valuetype test2.Foo, valuetype test2.Foo)
 IL_0008: call void [mscorlib]System.Console::WriteLine(bool)
 IL_000d: nop
 IL_000e: ret
 }

 .method private hidebysig static void Compare (
 class [mscorlib]System.Type t1,
 class [mscorlib]System.Type t2
 ) cil managed 
 {
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: ldarg.1
 IL_0003: call <span>bool</span> <span>[mscorlib]S</span>ystem.Type::op_Equality(<span>class</span> <span>[mscorlib]S</span>ystem.Type, <span>class</span> <span>[mscorlib]S</span>ystem.Type)
 IL_0008: call void [mscorlib]System.Console::WriteLine(bool)
 IL_000d: nop
 IL_000e: ret
 }

 .method private hidebysig static void Compare (
 char c1,
 char c2
 ) cil managed 
 {
 IL_0000: nop
 IL_0001: ldarga.s c1
 IL_0003: ldarg.1
 IL_0004: call instance <span>bool</span> <span>[mscorlib]S</span>ystem.<span>Char</span>::<span>Equals</span>(<span>char</span>)
 IL_0009: call void [mscorlib]System.Console::WriteLine(bool)
 IL_000e: nop
 IL_000f: ret
 }

 .method private hidebysig static void Compare (
 class test2.Bar b1,
 class test2.Bar b2
 ) cil managed 
 {
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: ldarg.1
 IL_0003: ceq
 IL_0005: call void [mscorlib]System.Console::WriteLine(bool)
 IL_000a: nop
 IL_000b: ret
 }

 .method private hidebysig static void Main () cil managed 
 {
 .entrypoint
 .locals init (
 [0] valuetype test2.Foo V_0
 )

 IL_0000: nop
 IL_0001: ldc.i4.s 123
 IL_0003: ldc.i4 456
 IL_0008: call void test2.Program::Compare(int32, int32)
 IL_000d: nop
 IL_000e: ldtoken [mscorlib]System.Object
 IL_0013: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
 IL_0018: ldtoken [mscorlib]System.String
 IL_001d: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
 IL_0022: call void test2.Program::Compare(class [mscorlib]System.Type, class [mscorlib]System.Type)
 IL_0027: nop
 IL_0028: ldloca.s V_0
 IL_002a: initobj test2.Foo
 IL_0030: ldloca.s V_0
 IL_0032: ldc.i4.1
 IL_0033: stfld int32 test2.Foo::N
 IL_0038: ldloc.0
 IL_0039: ldloca.s V_0
 IL_003b: initobj test2.Foo
 IL_0041: ldloca.s V_0
 IL_0043: ldc.i4.2
 IL_0044: stfld int32 test2.Foo::N
 IL_0049: ldloc.0
 IL_004a: call void test2.Program::Compare(valuetype test2.Foo, valuetype test2.Foo)
 IL_004f: nop
 IL_0050: ldc.i4.s 97
 IL_0052: ldc.i4.s 98
 IL_0054: call void test2.Program::Compare(char, char)
 IL_0059: nop
 IL_005a: ldstr "1"
 IL_005f: newobj instance void test2.Bar::.ctor(string)
 IL_0064: ldstr "2"
 IL_0069: newobj instance void test2.Bar::.ctor(string)
 IL_006e: call void test2.Program::Compare(class test2.Bar, class test2.Bar)
 IL_0073: nop
 IL_0074: ldstr "done"
 IL_0079: call void [mscorlib]System.Console::WriteLine(string)
 IL_007e: nop
 IL_007f: ldc.i4.1
 IL_0080: call valuetype [mscorlib]System.************** [mscorlib]System.Console::ReadKey(bool)
 IL_0085: pop
 IL_0086: ret
 }
}


Roman Mejtes

Ну, и пояснения к вышеприведенному.1. В случае int и Bar сравнение проверяется через ==, перегруженных опреторов равенства нет - в IL-код используется инструкция сeq. При этом для сравнения Bar решарпер выдает предупреждение "Possible unintended reference comparision; to get a value comparision, use Equals method". При добавлении в Bar операторов равенства предупреждение пропадает.2. В случае Foo и Type сравнение проверяется через ==, и обатипа имеют перегруженные операторы равенства/неравенства. В данном случае в IL-коде уже используется не ceq, а вызов op_Equality - перегруженного оператора равенства.3. В случае char используется проверка через char.Equals - реализацию IEquatable. Реализация выглядит так:
public bool Equals(Char obj)
{
 return m_value == obj;
}
И обратите внимание, что при этом имеются перегруженные Equals(object) и GetHashCode() - это не частный случай char, а общее правило:
If you implement IEquatable, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable.Equals method. If you do override Object.Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class. In addition, you should overload the op_Equality and op_Inequality operators. This ensures that all tests for equality return consistent results.
И из последнего пункта понятно, почему IEquatable.Equals быстрее, чем object.Equals - он быстрее именно в случае value-типов за счет избавления от боксинга-анбоксинга (value-типы, завернутые в генерик-параметр, боксинга-анбоксинга не требуют).Что же до того, что сравнение интов через == работает чуть быстрее, чем через int.Equals - есть подозрение, что там используется какое-то сравнение, зашитое глубоко внутрь CLR, и специфичное именно для примитивных типов.Резюме.1. Для примитивных типов вполне можно использовать операцию ==. Для значимых типов в общем случае - T.Equals(T). Наличие переопределенного оператора == роли не играет, т.к. его реализация будет осуществляться за счет всё тех же == и Equals внутри логики оператора - за редким исключением типа того же Type, у которого оператор сравнения выглядит так:
[System.Security.SecuritySafeCritical]
[Pure]
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static <span>extern</span> bool operator ==(Type left, Type right);
2. Для ссылочных типов разница между ==, T.Equals(T) и object.Equals(object) может быть только за счет отсутствия приведения типа - я сомневаюсь, что это съест много ресурсов. И оператор == к ним можно применять только в случае уверенности наличия перегруженного оператора равенства - иначе можно нарваться на сравнение ссылок.


Roman Mejtes

Roman Mejtes,1. Два КАКИХ значения? Значимых или ссылочных?2. Операторы можно перегружать.3. Equals может использовать типизированную реализацию IEquatabl4. Сравнивать можно ещё и через IComparable5. https://msdn.microsoft.com/ru-ru/library/ms224763(v=vs.110).aspx


Roman Mejtes

может быть только за счет отсутствия приведения типа
В Equals всё равно будут сравниваться типы.


Roman Mejtes

Для того, чтоб сравнить 2 значения, в C# есть 3 способа (на сколько я знаю)А) Сравнить значения через оператор ==Б) Через метод IEquatable.Equals(T value)В) Через метод Equals(object, object) или Equals(object) который определен в классе Object
Г) EqualityComparer.Default.Equals(a, b)