C# String Equals: Choosing the Right Comparison
c# string equals: Learn how C# string equals works across ==, Equals, and String.Compare, including ordinal vs culture-sensitive behavior, null handling, and performan...
c# string equals requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to check whether two strings hold the same characters, C# gives you several ways to do it: the == operator, the instance Equals method, the static String.Equals method, and the String.Compare family. They look interchangeable, but they differ in null handling, culture behavior, and performance. Understanding those differences is the difference between a comparison that works in your tests and one that works in production.
The == operator on strings is the most common way to test equality. Unlike reference types where == compares references, the string class overloads the operator to compare the actual character sequence. So "hello" == "hello" returns true, and it also returns true when both operands are the same string instance. This behavior is convenient, but it hides the fact that the comparison is culture-sensitive by default.
How the == Operator Works for Strings
The == operator on strings compiles to a call to String.op_Equality, which performs an ordinal comparison. That means it compares the numeric Unicode code points of the characters, without any culture-specific rules. For most internal checks—like comparing a status code or an identifier—this is exactly what you want. It is fast and predictable across environments.
string left = "café"; string right = "café"; bool equal = left == right; // true
But ordinal comparison is case-sensitive. "Cafe" and "cafe" are not equal under ==. If you need case-insensitive comparison, you must use an overload that accepts a StringComparison value, or call Equals with StringComparison.OrdinalIgnoreCase.
Instance Equals vs Static String.Equals
The instance method string.Equals(string other) behaves like the == operator in most cases, but with one important difference: it is virtual. When you call left.Equals(right), the runtime dispatches to the actual type of left. If left is a string, it uses the string implementation. If you have a custom class that overrides Equals, the behavior can differ. For strings, the default instance Equals performs the same ordinal comparison as ==.
The static method String.Equals(string a, string b) is not virtual and always uses the string comparison logic. It also handles null operands gracefully. If both are null, it returns true. If one is null and the other is not, it returns false. The instance method, however, throws a NullReferenceException if you call it on a null reference.
string a = null; string b = null; bool staticResult = string.Equals(a, b); // true // bool instanceResult = a.Equals(b); // NullReferenceException
Because of this null safety, string.Equals is often the safer choice when either operand could be null. The == operator also handles null correctly: null == null is true, and null == "x" is false.
Ordinal vs Culture-Sensitive Comparisons
C# string comparison can be ordinal or culture-sensitive. An ordinal comparison looks at the raw Unicode code points. A culture-sensitive comparison applies the rules of a specific culture, such as case folding, accent sensitivity, and sorting order. For example, in Turkish, the uppercase of i is İ, not I. A culture-sensitive comparison using the Turkish culture would treat "i" and "I" as different, while an ordinal comparison would also treat them as different because the code points differ. But a culture-insensitive comparison like StringComparison.InvariantCultureIgnoreCase might treat them as equal depending on the culture.
The default for == and Equals(string) is ordinal. However, String.Compare defaults to culture-sensitive comparison. This is a common source of bugs when developers use String.Compare to check for equality.
string s1 = "Straße"; string s2 = "STRASSE"; bool ordinalIgnoreCase = string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); // false bool invariantIgnoreCase = string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase); // true
If you are comparing strings that come from user input and you need to follow the user's language rules, use a culture-sensitive comparison. If you are comparing internal identifiers, file names, or machine-generated values, use ordinal comparison to avoid surprises.
Case Sensitivity and Common Pitfalls
A frequent mistake is assuming that == is case-insensitive. It is not. To compare strings without regard to case, you must explicitly specify StringComparison.OrdinalIgnoreCase or a culture-aware option. The Equals method has an overload that accepts a StringComparison enum.
string input = "Admin"; bool isAdmin = input.Equals("admin", StringComparison.OrdinalIgnoreCase); // true
Using ToLower() or ToUpper() to normalize before comparison is another approach, but it allocates a new string and can introduce culture-specific issues. For example, ToLower() in Turkish culture can change the meaning of certain characters. Prefer StringComparison overloads when possible.
Performance Considerations
Ordinal comparisons are faster than culture-sensitive ones because they do not need to consult culture-specific tables or apply complex linguistic rules. For hot paths—like comparing keys in a dictionary or filtering large collections—use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase. The runtime can also optimize ordinal comparisons with direct memory comparisons, especially for short strings.
Culture-sensitive comparisons are significantly more expensive and can vary by culture. They are necessary when you are sorting strings for display in a user interface or when you need to respect linguistic conventions. But for equality checks, ordinal is almost always sufficient and safer.
If you are using a Dictionary<string, T> or HashSet<string>, the default comparer is StringComparer.Ordinal. That is a good default for most internal uses. If you need case-insensitive keys, use StringComparer.OrdinalIgnoreCase.
Choosing the Right Comparison for Your Scenario
The decision depends on the origin and purpose of the strings. Use ordinal comparison when:
- Comparing internal identifiers, tokens, or machine-generated values.
- Checking file paths or environment variable names.
- Implementing security-sensitive checks where culture rules could be exploited.
Use culture-sensitive comparison when:
- Sorting strings for display to users in a specific language.
- Comparing user-entered names or addresses where linguistic rules matter.
- You need to match the behavior of a specific locale.
For most equality checks, StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase is the right choice. It is deterministic and avoids the overhead of culture processing.
Handling Null and Empty Strings
When comparing strings, null and empty are different. An empty string is a valid instance with zero characters. Null means no instance at all. The == operator and string.Equals treat them differently: null == "" is false. The string.IsNullOrEmpty method is useful when you want to treat null and empty as equivalent for validation, but it does not perform equality comparison.
string value = null; if (string.IsNullOrEmpty(value)) { /* handles both null and empty */ }
If you need to compare a string that might be null against a known value, use string.Equals with a StringComparison overload, or use the == operator which handles null correctly. Avoid calling the instance method on a potentially null reference.
Common Mistakes and Edge Cases
One common mistake is using String.Compare to test equality. String.Compare returns an integer indicating the relative order, not a boolean. It is also culture-sensitive by default. If you use it for equality, you must check that the result is zero and specify the desired comparison type.
int result = string.Compare("apple", "Apple", StringComparison.OrdinalIgnoreCase); bool equal = result == 0; // true
Another edge case is comparing strings that contain combining characters. Ordinal comparison treats a base character and a combining mark as separate code points, while a culture-sensitive comparison may treat the composed and decomposed forms as equal. If you are dealing with user-visible text, you might need to normalize the strings first using string.Normalize().
Finally, remember that the == operator on strings is not the same as the == operator on object. If you cast a string to object, the == operator uses reference equality, not value equality. This can lead to surprising results in generic code or when using reflection.
string a = "hello"; string b = "hello"; object objA = a; object objB = b; bool refEq = objA == objB; // false, because reference comparison bool valEq = a == b; // true, because string overload
In most production code, you will not cast strings to object, but it is worth knowing when debugging or writing generic algorithms.