C# すべての源 ― object クラス徹底解説
対象読者
- .NET Framework 4.8 環境で開発中の初学者〜中級者
- 「Equals と == の違いは?」「メモリ上ではどう配置される?」と疑問に感じている方
- CLR の内部動作まで踏み込みたい方
目次
1. なぜ object が必要か
観点 | 役割 |
---|---|
単一継承の根 | 全ての型が必ず System.Object(C# キーワード object)を直接または間接に継承し、共通 API(ToString() など)を保証する。 |
多態性の基盤 | 1 つのコレクションにあらゆる型を “object として” 収納できる。ジェネリック登場以前(.NET 1.x)では ArrayList などが代表例。 |
ランタイム最適化 | CLR は “object は参照型” という前提でメモリ管理・JIT 最適化を行うため、実装がシンプルになる。 |
2. object が提供する 6 つの基礎メソッド
メソッド | 既定の動作 | オーバーライド例 |
---|---|---|
ToString() | 型名を返す | 人間向けの情報を返す(DateTime など) |
Equals(object obj) | 参照等価 | 値等価に変更(string, 独自クラスなど) |
GetHashCode() | ランタイム依存のハッシュ値 | Equals と連動したハッシュを返す |
GetType() | 実行時型を返す | 変更不可 |
MemberwiseClone() | 浅いコピー(protected) | ICloneable 実装時のラッパー |
Finalize() | ガベコレクタが回収前に呼ぶ | アンマネージド資源の解放 |
鉄則
Equals をオーバーライドしたら 必ず GetHashCode も整合を取る。ハッシュテーブル系コレクションのバグ原因 No.1。
3. メモリ内部 ― オブジェクトはどう配置されるか
3.1 正しいメモリ配置イメージ
(1) スタックフレーム (2) マネージドヒープ
┌────────────────┐ ┌─────────────────────────────┐
│ … │ │ ← -4/-8 SyncBlockIndex │
│ p ───┐ │ │ 0x0000 MethodTablePtr ★ │◄─ ★ ここを変数 p が指す
│ … │ │ │ Field_A │
└───────┬─ ──────┘ │ Field_B │
└────────────────▶│ … │
└─────────────────────────────┘
- スタック
- ローカル変数 p は 4 byte(x86)または 8 byte(x64)の 参照値(アドレス)を保持します。
- その値はヒープ上オブジェクトの MethodTable へのポインタ(上図★)を指しています。
- JIT 最適化中はレジスタに置かれることもありますが、論理的には “スタック側の領域” と考えます。
- ヒープ
- 参照先 0 byte 位置に MethodTablePtr があり、‐4/‐8 byte の負オフセット側(負領域)に SyncBlockIndex(オブジェクトヘッダー) が存在します。
- 以降に実際のフィールド Field_A, Field_B… が並びます。
- 重要: 参照はオブジェクトの先頭(SyncBlockではなく MethodTablePtr)に張られる ため、デバッガで「p の値」を見るとヘッダー分だけ先に飛んだアドレスになります。
3.2 値型と Boxing / Unboxing
int x = 42;
object boxed = x; // Boxing: ヒープへコピーし参照型化
int y = (int)boxed; // Unboxing: 値をスタックへコピー
- Boxing は 暗黙的・Unboxing は 明示的キャスト必須。
- 頻繁な Boxing はヒープ断片化・GC 負荷増大を招く。性能が気になるループでは避ける。
4. object と演算子の罠
4.1 == と Equals
var a = new Person("Alice");
var b = new Person("Alice");
Console.WriteLine(a == b); // False (参照比較)
Console.WriteLine(a.Equals(b)); // True (値比較:オーバーライド済み)
- 参照型の == 既定は「参照等価」だけ。
- “値比較” を提供したい場合は
- Equals をオーバーライド
- == / != を演算子オーバーロード
- GetHashCode を整合
4.2 パターンマッチ & 型判定
if (obj is Person p)
{
Console.WriteLine(p.Name);
}
- 実行時型を確認するが ボックス解除は行わない。
- is object は obj が null でない限り 常に true。
5. サンプルコード ― “三種の神器” 正しい実装
// Domain/Person.cs
public class Person : IEquatable<Person>
{
public string Name { get; }
public DateTime Birthday { get; }
public Person(string name, DateTime birthday)
=> (Name, Birthday) = (name, birthday);
// ① ToString
public override string ToString()
=> $"{Name} ({Birthday:yyyy-MM-dd})";
// ② Equals
public override bool Equals(object obj)
=> obj is Person other && Equals(other);
public bool Equals(Person other)
=> Name == other.Name && Birthday == other.Birthday;
// ③ GetHashCode
public override int GetHashCode()
=> HashCode.Combine(Name, Birthday);
// ④ == / !=
public static bool operator ==(Person lhs, Person rhs)
=> lhs?.Equals(rhs) ?? rhs is null;
public static bool operator !=(Person lhs, Person rhs) => !(lhs == rhs);
}
- HashCode.Combine は .NET 4.8 では利用不可のため、Tuple.Create().GetHashCode() などで代替可能。
- IEquatable<T> 実装によりジェネリックコレクションでの Boxing を回避できる。
6. 派生クラスとメモリコストの真実
class Base { int A; }
class Derived : Base { int B; }
// ヒープ配置: [Header][MethodTablePtr][A][B] だけ
- 基底クラスのフィールドが重複コピーされるわけではない。
- メソッドは共通の MethodTable で共有され、メモリ効率は高い。
7. 設計を強化するテクニック
テクニック | ポイント |
---|---|
Template Method | 基底クラスに処理骨格を置き、派生で差し替え。virtual/override を活用。 |
プリミティブオブセッションの回避 | object に頼らず Money, Distance などドメイン固有型を早期に導入。 |
record / record struct | C# 9+ で Equals/GetHashCode/ToString 自動実装。移行計画があるなら検討。 |
8. まとめ ― object を理解することは C# の「憲法」を読むこと
- 共通 API 確保と多態性の礎
- 正しいメモリモデルの把握 がパフォーマンスチューニングの第一歩
- Equals / GetHashCode / ToString を適切に実装して一人前
- Boxing / Unboxing のコストを意識し、必要な場面でのみ汎用 object を使う
- “object を出発点に、適切な抽象型を設計する” クセをつける
付録: さらなる学び
リソース | 目的 |
---|---|
《CLR via C#》(Jeffrey Richter) | CLR 内部構造とメモリ管理を深掘り |
.NET Runtime Labs (GitHub) | GC・TypeSystem の実装例を読む |
公式ドキュメント: System.Object Class | 一次情報をチェック |
次の一歩
- ILDasm や dnSpy で object メソッドの IL を覗いてみる
- BenchmarkDotNet で Equals 実装のパフォーマンス差を計測してみる
object を“深く”理解すれば、あなたの C# コード全体の品質が確実に底上げされます。ぜひ本記事を参考に、実際のソースと IL を行き来しながら学習を進めてください。
訪問数 3 回, 今日の訪問数 3回
ディスカッション
コメント一覧
まだ、コメントがありません