3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オフグリッドAdvent Calendar 2024

Day 12

オブジェクト指向のすごく噛み砕いた理解

Last updated at Posted at 2024-12-10

※ オブジェクト指向を考える上で脳内で想像していたことを噛み砕いて書いているので、完璧ではないと思います。

クラスで定義したものが、実際にインスタンス化されたオブジェクトの中でどのような形で保持されているかを考えるのが理解への近道かなと思います。


Javaの場合

クラス定義とオブジェクト生成

以下のようなクラスを考えます。

class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public String makeSound() {
        return name + " makes a sound.";
    }
}

class Dog extends Animal {
    private String owner;

    public Dog(String name, String owner) {
        super(name);
        this.owner = owner;
    }

    public String getOwner() {
        return this.owner;
    }
}

Dog クラスを使ってオブジェクトを生成します。

Dog dog = new Dog("Buddy", "Alice");

オブジェクトの中身(メモリ上のイメージ)

この場合、生成されたオブジェクトは次のような構造になります。

Dog {
  name: "Buddy",      // Animal クラスから継承されたプロパティ
  owner: "Alice"      // Dog クラスで追加されたプロパティ
}

Javaでは、インスタンス化されたオブジェクトはクラスのプロパティを**フィールド(メモリ上の変数)**として保持します。例えば、以下のようなコードを追加すればフィールド値が確認できます:

System.out.println(dog.name);    // Buddy
System.out.println(dog.getOwner()); // Alice

オブジェクトの実際のメモリ構造

メモリ上では以下のような形で保存されています:

Dog オブジェクト:
  - name: "Buddy"   // 継承元 (Animal)
  - owner: "Alice"  // Dog クラス独自

メソッドmakeSoundgetOwner)は、実際にはオブジェクトに直接保存されず、クラスの定義に基づいて参照されます。


TypeScriptの場合

クラス定義とオブジェクト生成

TypeScript で同じクラスを定義します。

class Animal {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound(): string {
    return `${this.name} makes a sound.`;
  }
}

class Dog extends Animal {
  private owner: string;

  constructor(name: string, owner: string) {
    super(name);
    this.owner = owner;
  }

  getOwner(): string {
    return this.owner;
  }
}

// オブジェクト生成
const dog = new Dog("Buddy", "Alice");

オブジェクトの中身(JavaScriptオブジェクトとしての構造)

TypeScript のクラスをインスタンス化すると、オブジェクトは以下のような形になります。

// dog オブジェクト
{
  name: "Buddy",   // Animal クラスから継承
  owner: "Alice"   // Dog クラスで追加
}

これは JavaScript のオブジェクトとして dog を表示すれば確認できます。

console.log(dog);
// 出力例:
// Dog { name: "Buddy", owner: "Alice" }

dog オブジェクトに格納されている内容

TypeScript では、オブジェクトの実態は JavaScript オブジェクトとして動作します。つまり、次のようにプロパティやメソッドにアクセスできます:

console.log(dog["name"]);  // Buddy
console.log(dog.getOwner()); // Alice

実際のオブジェクトの中身とプロトタイプチェーン

TypeScript のオブジェクトは プロトタイプチェーン を通じてクラスのメソッドを参照します。dog オブジェクトをデバッグツールで見ると、次のように表示されます。

Dog {
  name: "Buddy",
  owner: "Alice"
}
  [[Prototype]]: Animal {
    makeSound: f()
  }

ここで:

  1. nameownerdog オブジェクトに直接保持されています。
  2. makeSound メソッドは Animal のプロトタイプ(クラス定義)から参照されます。

比較:JavaとTypeScriptのオブジェクト構造

特徴 Java TypeScript
プロパティの格納 メモリ上のフィールドとして直接格納 JavaScriptオブジェクトのキーとして保持
メソッドの格納 クラスの定義に保存、オブジェクトは参照 プロトタイプチェーンで参照
プロパティの継承方法 継承元のフィールドとして保持 同じオブジェクト内に統合

こうしてオブジェクトの具体的な形を示すと、実際にクラスから作られたオブジェクトがどのようにデータを保持し、参照するかが理解しやすくなると思います!

ついでに同じ考え方で、TypeScriptを例にして、型推論がどのようにオブジェクトの形に基づいて行われるのかを説明します。


型推論とは?

TypeScriptの型推論は、変数や関数の型を明示的に指定しなくても、コードからその型を自動的に判断する仕組みです。これは、TypeScriptがオブジェクトの形やプロパティ、メソッドの定義を元に型を決定することで実現されています。


具体例: オブジェクトと型推論

以下のコードを見てみましょう。

1. クラスを使った型推論

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound(): string {
    return `${this.name} makes a sound.`;
  }
}

class Dog extends Animal {
  owner: string;

  constructor(name: string, owner: string) {
    super(name);
    this.owner = owner;
  }

  bark(): string {
    return `${this.name} barks!`;
  }
}

// オブジェクトを生成
const dog = new Dog("Buddy", "Alice");

この場合、TypeScriptは dog の型を次のように推論します:

Dog {
  name: string,        // 継承されたプロパティ
  owner: string,       // Dog で追加されたプロパティ
  makeSound(): string, // 継承されたメソッド
  bark(): string       // Dog で追加されたメソッド
}

つまり、Dog クラスの構造に基づいて dog の型が自動的に決定されます。


2. 型推論がオブジェクトの形に基づく例

オブジェクトリテラルを使った場合でも同じ原則が適用されます。

const cat = {
  name: "Whiskers",
  makeSound: () => "Meow!"
};

TypeScriptがこのオブジェクトの型を次のように推論します:

{
  name: string;
  makeSound: () => string;
}

つまり、オブジェクトの中身(キーと値)を見て、その型が自動的に決まります。


型推論のポイント:プロパティとメソッドの関係

クラスやオブジェクトのが型の基礎になります。例えば:

継承の場合

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  owner: string;
  constructor(name: string, owner: string) {
    super(name);
    this.owner = owner;
  }
}

const animal: Animal = new Dog("Buddy", "Alice");
  • animal の型は Animal と明示していますが、new Dog() を渡すと DogAnimal を拡張しているため問題ありません。
  • 型推論は「代入先の型」や「オブジェクトの形」に基づいて決まります

メソッド内で型が推論される例

class Dog {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  makeSound() {
    return `${this.name} barks!`;
  }
}

const dog = new Dog("Buddy");

// メソッドの戻り値も型推論される
const sound = dog.makeSound();
// TypeScript は sound の型を string と推論する

型推論がオブジェクトの形を使っている理由

型推論が「オブジェクトの形」に基づいている理由は、TypeScriptの型システムが構造的型システム(Structural Typing)に基づいているためです。
構造的型システムでは、次のように「型」ではなく「形」が一致していれば互換性があるとみなされます。

例: 形が一致する場合の互換性

type AnimalType = {
  name: string;
  makeSound: () => string;
};

const cat: AnimalType = {
  name: "Whiskers",
  makeSound: () => "Meow!"
};

AnimalType という型は明示的に定義していますが、cat オブジェクトの形がこれと一致しているため、問題なく代入できます。


型推論とオブジェクトの形のまとめ

  1. クラスやオブジェクトの形は、プロパティとメソッドをもとに型として定義されます。
  2. 型推論は、この形をもとに自動的に型を決定します。
  3. TypeScriptの構造的型システムにより、形が一致する限り互換性があります

オブジェクトの形を意識すると、型推論の動作が直感的に理解できるようになります。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?