Comparison Concepts

.Comparison Concepts

The way comparisons are performed, and the underlying concept of equality, depend upon the nature of the types being compared and the purpose for the comparisons. C# makes distinctions between value equality and reference equality. Consider how the "==" operator varies its notion of equality based upon the types involved in the comparison:

// Integer is a value type (Value Comparison)
int x = 7;
int y = 7;
Console.WriteLine(x==y); // Prints: True

// Object is a reference type (Reference Comparison)
object o1 = 7;
object o2 = 7;
Console.WriteLine(o1 == o2); // Prints: False

// String is a reference type (Value Comparison)
object s1 = "7";
object s2 = "7";
Console.WriteLine(s1 == s2); // Prints: True ! (Unlike Java)

Equality Comparison: The == Operator vs Equals() Method

A rule of thumb for testing for equality is to use the "== operator" for value types and Strings. Use the ".Equals() method" for reference types other than Strings". A safe way to perform a reference comparison is with the ".Reference Equals() method" as its logic can not be overridden.

The two common ways C# provides for determining equality is the "== operator" and ".Equals() method". Both, can be modified to change their behavior, but by default their comparison logic is the same:

  • If operands are value types, perform a value comparison.

  • If operands are reference type, perform a reference comparison ... unless it further determines operands are String type, then it will perform a value comparison.

Even though both ways for determining equality use the same evaluation logic, the reason the same operands may evaluate differently is because of how the types are being determined. The "== operator" determines the type of the operands at compile-time, while the ".Equals() method" using reflection to determine the type of the operands at runtime. Sometimes the compile-time and runtime determinations of the operand types are different, which can result in different evaluations of equality. The following example code shows the compiler can not determine that the "s3" variable is a String, so the "== operator" performs a reference comparison. While the runtime (using reflection) can determine that "s3" is a String, so the ".Equals() method" performs a value comparison.

  1. == Operator - (Resolved at compile-time) Is an operator which can be overloaded. At compile time, the compiler chooses the overloaded version based upon it's determination of the data types.

  2. .Equals() Method - (Resolved at run time) Is a virtual method which can be overridden by a local implementation. At run time, reflection is used to determine the data types and call the overridden method.

// Compiler knows s1 and s2 are both strings
// Compiler can not determine that s3 is a string          
object s1 = "7";
object s2 = "7";
object s3 = new String(new char[] {'7'});

Console.WriteLine(s1 == s2); // 1. Prints: True (Uses overloaded version - value)
Console.WriteLine(s1 == s3); // 2. Prints: False (Uses non-overloaded version - reference)

Console.WriteLine(s1.Equals(s2)); // 3. Prints: True (Reflection knows string)
Console.WriteLine(s1.Equals(s3)); // 4. Prints: True (Reflection knows string)

Note: The IEquatable<T> interface defines the .Equals() method which determines the equality of instances of the implementing type.

Comparisons and Collation Sequences

Equality comparisons are used to determine if two instances are semantically the same. Order comparisons are used when arranging instances in ascending or descending order. Both equality and order comparisons rely on collation sequences to define how characters relate to each other in a character set. C# provides two categories of collation sequences:

  • ordinal - interprets characters simply as numbers (according to their Unicode numeric value). This puts uppercase letters first, then lowercase letters.

  • culture-sensitive - interprets characters with reference to a particular alphabet. The "current culture" is one's based on the computer's settings while the "invariant culture" is the same on every computer (closely resembling the American culture). This encapsulate an alphabet which puts uppercase and lowercase letter adjacent to each other.

    Example Ordinal: ("Ape", "Zebra", "ape")

    Example Invariant Culture: ("Ape", "ape", "Zebra")

Ordinal is the default sequence for both the "== operator" and the ".Equals() method". However the ".Equals() method" can specify sequence (and if to ignore case) on both it's instance and static methods:

Console.WriteLine(s1.Equals(s2, StringComparison.CurrentCulture));         // Instance Method

Console.WriteLine(String.Equals(s1, s2, StringComparison.CurrentCulture)); // Static Method

The StringComparison parameter is an enumeration defined as:

public enum StringComparison
    CurrentCulture = 0,
    CurrentCultureIgnoreCase = 1,
    InvariantCulture = 2,
    InvariantCultureIgnoreCase = 3,
    Ordinal = 4,
    OrdinalIgnoreCase = 5,

Order Comparison Methods

The ordinal sequence is generally used for equality comparisons of character data, while culture-sensitive sequences are much more useful for order comparisons of character data.

.CompareTo() is an instance method designed for use in sorting or alphabetizing operations. It performs a case-sensitive comparison using the current culture. .CompareTo() is the method specified in the IComparable interface which forms the standard comparison behavior used throughout the .NET framework. For example, the String's .CompareTo() method defines the default ordering behavior for strings in ordered collections, such as List.sort

Note 1: CompareTo() should not be used to determine equality. To determine if two strings are equal use the "== operator" or ".Equals() method".
Note 2: To create sorting for custom types, implement the IComparable and IComparer interfaces in class defining the type.

.Compare() and .CompareOrdinal() are two static methods used to perform order comparisons. There are several overloaded versions, some of which allow the specification of culture and case. For order comparisons, it is recommended to call a method overload that specifies culture (StringComparison parameter) whenever possible.

All the order comparison methods (CompareTo(), Compare(), CompareOrdinal()) return one of the three following values which determines the order of the operands in relation to each other:

  • Less than zero - first operand is less than (precedes) the second operand
  • Zero - first and second operands are equal (have the same position in the sort order)
  • Greater than zero - first operand is greater than (follows) the second operand

Use Delegate in List.Sort() Method

A simple way to specify sorting logic is to use named or anonymous methods for the delegate parameter on the Sort() method. The program below sorts a List of Person objects by Age using a named method and also sorts the List by Name using an anonymous method.

* Use Delegate in List.Sort() Method *
using System.Collections.Generic;
using System;
namespace SortDelegate
    class Program
        public class Person
            public string Name { get; set; }
            public int Age { get; set; }

            // Sort by Age
            public static int CompareByAge(Person x, Person y)
                return x.Age.CompareTo(y.Age);

        static void Main()
            List<Person> list = new List<Person>
                new Person(){Name = "Zachary", Age = 43},
                new Person(){Name = "Bartholomew", Age = 63},
                new Person(){Name = "Elijah", Age = 98}

            // Sort by Age (Named Method as Delegate)
            foreach (Person p in list)
                Console.WriteLine(p.Name + ' ' + p.Age);

            // Sort by Name (Anonymous Method)
            list.Sort(delegate(Person x, Person y) { return x.Name.CompareTo(y.Name); });
            foreach (Person p in list)
                Console.WriteLine(p.Name + ' ' + p.Age);

Reference Articles