Equality in C#
Table of Contents
Introduction
Operator ==()
Equals()
Equality of Built-in Value Types
Equality of User-Defined Value Types
Equality of Reference Types
Equality and Boxing
Equality of Strings
Conclusion
Introduction
In languages that deal with obects and object references, a question of
equality may become quite confusing. For starters, C# provides not one,
but two ways to compare things: operator==()
and method
Object.Equals()
. Since everything in C# derives
from Object
, method Equals()
can be
universally applied to any value.
operator==()
and Equals()
can mean
different things in different contexts. Furthermore, result of
x==y
may be different from x.Equals(y)
!
First of all, one must realize, that operator==()
and Equals()
are two different methods, and they
are not formally related. It is not guaranteed that expressions
x==y
and x.Equals(y)
will yield
the same result, even if both x
and y
belong to system-defined types such as int
and double
.
Operator ==()
In C# operator==()
must be static
member of a class
(or struct), have return value of type bool
, and
two parameters, at least one of which has the same type as the
enclosing class.
x==y
occurs in a program,
the compiler looks for a matching operator==()
inside
the classes to which x
and y
belong,
and their base classes. If there is no matching operator==()
,
or there is more than one matching operator==()
,
the program will not compile. Version of operator==()
that will be called is always known at compile time.
Example:
class Base { public static bool operator==(Base x, Base y) { ... } } class Derived : Base { public static bool operator==(Derived x, Derived y) { ... } } |
Here, Base::operator==()
will be called when calculating
a==b
, despite the fact that both a
and b
actually reference instances of type Derived
.
Equals()
Equals()
is defined as a virtual method in class Object
.
Since everything in C# derives from Object
, this method can be
called on value of any type. This is in contrast with operator==()
that may or may not be defined for particular type.
Since call to Equals()
is virtual, exact version of the method
that will be called by x.Equals(y)
is determined by dynamic
type of x
, that usually is not known at compile time. Note
also, that unlike a==b
, expression x.Equals(y)
is inherently asymmetrical. Only x
dictates what version
of Equals()
will be called. y
has absolutely
no say in the matter.
Example:
class Base { public override bool Equals(object obj) { ... } } class Derived : Base { public override bool Equals(object obj) { ... } } |
a.Equals(b)
will call Derived.Equals()
,
because variable a
references isntance of class Derived
.
Equality of Built-in Value Types
operator==()
compares built-in value types by value,
performing type conversion if necessary. In particular, all of
the following is true
:1==1
1==1.0
1==1L
'1'==49
The latter is true
, because 49 is ASCII code for character '1'.
Equals() always returns false
if operands are
of different types. If operands are of the same type,
they are compared by value:
Expression | Value | Commenct |
---|---|---|
1.Equals(1) | true | comparing int to int |
1.Equals(1.0) | false | comparing int to double |
1.Equals(1L) | false | comparing int to long |
'1'.Equals(49) | false | comparing char to int |
Note: string
is not a value type. Things like 1=="1"
are not allowed and will lead to compilation error. 1.Equals("1")
is,
of course, legal, and it returns false
.
Equality of User-defined Value Types (Structs)
By default operator==()
is not implemented for structs.
If you want to compare structs using ==
, you must
define an operator==()
in your struct
.
Note, that you may define versions of the operator, that
compare your struct
to other types (built-in types,
other structs, or even object types).
Default implementation of Equals()
comes from class ValueType
,
which is implicit base class of all value types. You may override
this implementation by defining your own Equals()
method in your struct
.
ValueType.Equals()
always returns false
when one compares objects of different (dynamic) types.
If objects are of the same type, it compares them by calling
Equals()
for each
field. If any of these returns false
, the whole process
is stopped, and final result is false
. If all field-by-field
comparisons return true
, final result is true
.
Equality of Reference Types
By default, operator==()
on object types means reference
equality. That is, a==b
is true
if and only if
a
and b
reference the same object. Static
method Object.ReferenceEquals()
has the same effect.
You may override default implementation and provide your own
logic for operator==()
. However, keep in mind that
operator==()
is tied to the static types of the operands.
If your objects are passed to code that does not know their
static type, this code will not use your version
of operator==()
.
Example:
class Orange { int Weight; public Orange( int weight ) { Weight = weight; } public static bool operator ==(Orange x, Orange y) { return (x.Weight == y.Weight); } public static bool operator !=(Orange x, Orange y) { return (x.Weight != y.Weight); } } |
Object.Equals()
also uses reference equality. However,
Equals()
is a virtual method, and can be overridden.
Object
also defines static version of Equals()
that takes two parameters. Object.Equals(a,b)
is essentially
the same as a.Equals(b)
, but it will not blow up if a
is null
.
Equality and Boxing
Boxing is a conversion of value type to reference type. Let's consider this example:
class Test { private static bool MyEquals( object a, object b ) { return a==b; // Reference equality } public static void DoTest() { bool result = MyEquals(1,1); } } |
In this example, value of result
will be false
.
Let's analyze why it is so.
A literal of type int
(which is actually an alias for Int32
)
must be passed to a method that takes parameter of type object
.
Internally variables of type Int32
are represented as 32-bit
values. They are not object references. At the same time, Test.MyEquals()
expects an object reference to be passed to it. C# solves this
problem by boxing the integer value. It creates a temporary object
of unnamed type and passes this object to MyEquals()
. When asked
about its type, the object reports Int32
. When converted
back to Int32
, the object yields the original value
(in this case 1).
A crucial point is that each boxing action creates new temporary object.
Although both parameters of MyEquals
are 1, separate temporary
object will be created for each parameter.
operator==()
inside MyEquals()
works on
variables of static type object
, so it is in fact
Object.operator==()
. As we mentioned earlier, Object.operator==()
implements reference equality only. Since a
and b
don't
reference the same object, result
will be false
.
Equality of Strings
In C# strings are reference types. However, operator==()
is overloaded
for strings and compares string contents, not reference equality. String.Equals()
also compares string contents. What makes strings unique is that string literals are pooled.
For instance, if we have the following code:
string s1 = "abc"; string s2 = "abc"; bool result = Object.ReferenceEquals(s1,s2); |
the value of result
will be true
. ReferenceEquals()
is not lying here. s1
and s2
indeed reference
the same object. The system recognized that we have two equivalent string
literals, and merged them into one. It can get away with it, because string
objects are immutable. This check for duplicate string
literals occurs when you code is JIT-compiled, i.e. converted from
IL (intermediate language) to native (x86) machine codes. Thus,
string literals are merged even when they are from different
assemblies.
Note, however, that merging process affects only string literals, not calculated strings. E.g., in the following code:
string s1 = "abc"; string s2t = "ab"; string s2 = s2t + "c"; bool result = Object.ReferenceEquals(s1,s2); bool result2 = (s1==s2); |
value of result
will be false
. s1
and s2
are not merged, since s2
is not a literal.
Nevertheless, the value of result2
is true
, because
for strings operator==()
compares string contents, not
object references.
Conclusion
A matter of equality in C# is not a simple one. Results of equality comparisons are not always intuitive. Understanding the theory behind equality operations helps to prevent surprises and hard-to-find bugs. Hopefully, this article removed a shroud of mystery from C# equality and made it your friend.