От переводчика:
Это вольный перевод блогозаписи Эрика Липперта (Eric Lippert), в прошлом одного из разработчиков языка C#. Запись оформлена в виде «вопрос-ответ», я пропущу вопрос и перейду к ответу, вы можете ознакомиться с вопросом в оригинале, но там ничего особо интересного.
Но, для начала, я попрошу взглянуть на следующий код, и без гугла и компилирования, попробовать выяснить что произойдет в 9 случаях сравнения и свериться с ответами (для опроса):
int myInt = 1;
short myShort = 1;
object objInt1 = myInt;
object objInt2 = myInt;
object objShort = myShort;
Console.WriteLine(myInt == myShort); // scenario 1
Console.WriteLine(myShort == myInt); // scenario 2
Console.WriteLine(myInt.Equals(myShort)); // scenario 3
Console.WriteLine(myShort.Equals(myInt)); // scenario 4
Console.WriteLine(objInt1 == objInt1); // scenario 5
Console.WriteLine(objInt1 == objShort); // scenario 6
Console.WriteLine(objInt1 == objInt2); // scenario 7
Console.WriteLine(Equals(objInt1, objInt2)); // scenario 8
Console.WriteLine(Equals(objInt1, objShort)); // scenario 9
Язык C# был спроектирован так, чтобы работать так, как этого ожидает разработчик: то есть, язык где очевидные техники и правильные техники одно и тоже. И по большей части это так. К сожалению, сравнение это одна из частей языка, в которой есть ловушки.
Напишем следующий код, чтобы проиллюстрировать различные степени сравнения.
int myInt = 1;
short myShort = 1;
object objInt1 = myInt;
object objInt2 = myInt;
object objShort = myShort;
Console.WriteLine(myInt == myShort); // scenario 1 true
Console.WriteLine(myShort == myInt); // scenario 2 true
Console.WriteLine(myInt.Equals(myShort)); // scenario 3 true
Console.WriteLine(myShort.Equals(myInt)); // scenario 4 false!
Console.WriteLine(objInt1 == objInt1); // scenario 5 true
Console.WriteLine(objInt1 == objShort); // scenario 6 false!!
Console.WriteLine(objInt1 == objInt2); // scenario 7 false!!!
Console.WriteLine(Equals(objInt1, objInt2)); // scenario 8 true
Console.WriteLine(Equals(objInt1, objShort)); // scenario 9 false!?!
Что за черт? Разберем все по-порядку.
В первом и втором случае, мы должны вначале определить что значит оператор ==
. В C# существует более десятка встроенных операторов ==
для сравнения различных типов.
object == object
string == string
int == int
uint == uint
long == long
ulong == ulong
...
Так как не существует операторов int == short
или short == int
, должен быть выбран самый подходящий оператор. В нашем случае это оператор int == int
. Таким образом, short
конвертируется в int
и затем две переменные сравниваются по значению. Следовательно, они равны.
В третьем случае, вначале мы должны определить, какой из перегруженных методов Equals будет вызван. Вызывающий экземпляр является типом int
, и он реализует три метода Equals
.
Equals(object, object) // статический метод унаследованный от object
Equals(object) // виртуальный метод унаследованный от object
Equals(int) // реализация метода интерфейса IEquatable<int>.Equals(int)
Первый нам не подходит потому что у нас недостаточно аргументов для его вызова. Из двух других методов, больше подходит метод который принимает int
как параметр, всегда лучше сконвертировать аргумент типа short
в int
, чем в object
. Следовательно, будет вызван Equals(int)
, который сравнивает две переменные типа int
используя сравнение по значению, таким образом это выражение истинно.
В четвертом случае мы снова должны определить какой именно метод Equals
будет вызван. Вызывающий экземпляр имеет тип short
, который опять же имеет три метода Equals
.
Equals(object, object) // статический метод унаследованный от object
Equals(object) // виртуальный метод унаследованный от object
Equals(short) // реализация метода интерфейса IEquatable<short>.Equals(short)
Первый и третий методы нам не подходят, потому что для первого у нас слишком мало аргументов, а третий метод не будет выбран потому что нет неявного приведения типа int
к short
. Остается метод short.Equals(object)
, реализация которого равна следующему коду:
bool Equals(object z)
{
return z is short && (short)z == this;
}
То есть, чтобы этот метод вернул true
, упакованный элемент должен являться типом short
, и после распаковки он должен равняться экземпляру который вызвал Equals
. Но, так как, передаваемый аргумент является типом int
, метод вернет false
.
В пятом, шестом и седьмом, будет выбрана форма сравнения object == object
, что эквивалентно вызову метода Object.ReferenceEquals
. Очевидно, что две ссылки равны в пятом случае и неравны в шестом и седьмом. Значения которые содержатся в переменных типа object
неважны, потому что сравнение по значению не используется совсем, сравниваются только ссылки.
В восьмом и девятом случае, будет использован статический метод Object.Equals
, который реализован следующим образом:
public static bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
return x.Equals(y);
}
В восьмом случае, мы имеем две ссылки которые не равны и не равны null
, поэтому, будет вызван int.Equals(object)
, который как вы можете предположить смотря на код метода short.Equals(object)
, реализован следующим образом:
bool Equals(object z)
{
return z is int && (int)z == this;
}
Так как аргумент является упакованной переменной типа int
, будет произведено сравнение по значению и метод вернет true
. В девятом случае упакованная переменная имеет тип short
, следовательно проверка типа (z is int
) завершится неудачей, и метод вернет false
.
Итог:
Я показал девять различных способов сравнения двух переменных, несмотря на то, что во всех случаях, сравниваемые переменные равны единице, только в половине случаев сравнение возвращает true
. Если вы думаете, что это сумасшествие и все запутанно, вы правы! Сравнение в C# очень коварно.
Автор: BloodUnit