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            │
        └────────────────▶│          …                  │
                          └─────────────────────────────┘
  1. スタック
    • ローカル変数 p は 4 byte(x86)または 8 byte(x64)の 参照値(アドレス)を保持します。
    • その値はヒープ上オブジェクトの MethodTable へのポインタ(上図★)を指しています。
    • JIT 最適化中はレジスタに置かれることもありますが、論理的には “スタック側の領域” と考えます。 
  2. ヒープ
    • 参照先 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  (値比較:オーバーライド済み)
  • 参照型の == 既定は「参照等価」だけ。
  • “値比較” を提供したい場合は
    1. Equals をオーバーライド
    2. == / != を演算子オーバーロード
    3. 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 structC# 9+ で Equals/GetHashCode/ToString 自動実装。移行計画があるなら検討。

8. まとめ ― object を理解することは C# の「憲法」を読むこと

  1. 共通 API 確保と多態性の礎
  2. 正しいメモリモデルの把握 がパフォーマンスチューニングの第一歩
  3. Equals / GetHashCode / ToString を適切に実装して一人前
  4. Boxing / Unboxing のコストを意識し、必要な場面でのみ汎用 object を使う
  5. “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回

C#,継承

Posted by hidepon