はじめに
C#の文字列において、主に比較操作について調べたことをまとめます。
==演算子のオーバーロード
C#では、文字列比較において==演算子を使うと、参照比較ではなく値比較をします。
// C#の文字列比較
string str1 = new string("hoge");
string str2 = new string("hoge");
Debug.WriteLine(str1.Equals(str2)); // true
Debug.WriteLine(str1 == str2); // true ←値を比較するのでtrue
これは、Javaと比較した場合、特徴的な違いの一つです。
// Javaの文字列比較
class Sample {
public static void main(String[] args) {
String str1 = new String("hoge");
String str2 = new String("hoge");
System.out.println(str1.equals(str2)); // true
System.out.println(str1 == str2); // false ←参照を比較するのでfalse
}
}
メモリ上の動きは、どちらのプログラムも同様ですが、先述の通り比較対象が異なります。
| Equals() / equals() | == | |
|---|---|---|
| C# | 値比較 | 値比較 |
| Java | 値比較 | 参照比較 |
仕組み
❓なぜこのような動きをするのか
⇒C#のStringクラスでは、==演算子で値比較を行うよう、処理をオーバーロードしているためです。
前提として、他の参照型のオブジェクト(配列やobject型など)に対しては、==演算子を使用すると参照比較を行います。
これは、==演算子がオーバーロードされていない場合のデフォルトの動作です。
一方でStringクラスは、operator ==をオーバーロードすることで明示的に振る舞いを変えています。

↑実際に、Visual Studioから==演算子の定義を見てみると、オーバーロードされた処理が確認できます。
(他の参照型オブジェクトにおける==演算子では、このような実装はされていない)

↑さらに潜っていくと、Stringクラスの内部的な値比較ロジック(EqualsHelperメソッド)を呼び出していることが分かります。
実はこのロジックは、str1.Equals(str2)で使用しているEqualsメソッドからも呼び出されています。
このことから、Stringクラスでは、Equalsメソッドと==演算子、どちらも同じように値比較をしていることが分かります。
Equals() vs ==演算子
では何が違うかというと、==演算子の場合、nullを比較できるという特徴があります。
string str1 = null;
string str2 = new string("hoge");
Debug.WriteLine(str1.Equals(str2)); // NullReferenceException
Debug.WriteLine(str1 == str2); // false
↑==演算子は静的メソッドとして実装されているため、左辺がnullでもNullReferenceExceptionが発生しません。
string str1 = null;
string str2 = null;
Debug.WriteLine(str1 == str2); // true
↑また、null同士の比較でtrueを得ることができます。
Equalsメソッドでも、以下のように使用すればnullセーフな比較が可能です。
str1?.Equals(str2) // null条件演算子を使用
string.Equals(str1, str2) // 静的メソッド版を使用
参照比較(object.ReferenceEquals())
では、C#で参照比較を行いたい時はどうするかというと、Objectクラスに実装されているReferenceEqualsメソッドを使用します。
string str1 = new string("hoge");
string str2 = new string("hoge");
Debug.WriteLine(object.ReferenceEquals(str1, str2)); // false
ちなみに、このReferenceEqualsメソッドは、先ほど見たEqualsメソッドや==演算子から、値比較ロジックより前に呼び出されていることが確認できます。
これは、「参照が等しければ、値が必ず等しくなる」が成り立つからだと考えられます。
文字列プール(インターンプール)
最後に、C#は文字列プールの機能を実装しています。
これは、プログラム内で同じ値の文字列リテラルが複数回使用された時に、同一オブジェクトを参照する仕組みです。(メモリ使用量の節約)
string str1 = "hoge";
string str2 = "hoge";
Debug.WriteLine(object.ReferenceEquals(str1, str2)); // true
この機能はJavaにも実装されています。
class Sample {
public static void main(String[] args) {
String str1 = "hoge";
String str2 = "hoge";
System.out.println(str1 == str2); // true
}
}
メモリ上の動きとしては、ヒープエリアに内包されたインターンプールで文字列のオブジェクトを管理し、文字列リテラルが現れるとまずはインターンプールをチェックする、といった動きをするようです。

動作環境
- Windows 11
- C# 12.0
- .NET 8.0
- Visual Studio 2022
- Java 21
