【C#】C#でつまづいたところを「なるほどなっとくC#入門」読みながら再入門


C# Advent Calendar 2020

この記事は C# Advent Calendar 2020 の 13日目の記事です。

はじめに

私はXR関連の開発のお仕事をしています。
もともとUnityきっかけでC#を触り始め、それが一番最初に触れたプログラム言語でした。
C#の基礎から学ぶというよりか、「UnityでこれをするにはC#のコードはこう書けば動く」
といった解釈を繋ぎ合わせてプログラムを書いてきました。

それでちゃんとお金ももらえていたので不自由はそこまでなかったのですが、
転職してよりレベルの高い環境に身を置いて感じたのは、
人のコードを読んだり、きれいに設計してコードを書くような場面では基礎が無いと辛い 
ということでした。

まとめると、自分の中で基礎の理解が乏しいと感じたので読みました。

なるほどなっとくC#入門

C#の入門書です。

【リンク】:なるほどなっとく C#入門

ベースとなる知識が無い状態で読んでいたらきっと一発で理解はできてないだろうな、、、
という内容もありましたが、
一通りC#で何かしら開発した人ならスラスラ読めると思います。

具体例が多く、とてもわかりやすい本でした。

オブジェクト指向


オブジェクトとは

データ(属性)手続き(振る舞い)を1つのまとまりとして考えたもの

例)
オブジェクト:車
データ(属性):車種名、排気量など
手続き(振る舞い):前進、停止など


オブジェクト指向の利点

細かで面倒くさいことを忘れてしまえること


カプセル化

複雑なことはすべてオブジェクトの中に隠してしまうことができる

オブジェクト指向の概念の1つである、カプセル化、よく聞きますよね。

中身を知らなくても手続き(振る舞い)を知っていれば
オブジェクトの操作ができような状態を指します。

車の構造に詳しくない人間でも
「これは車!だからアクセル踏むと前進ができる」ってな感じで運転ができるような状態が
カプセル化ってことです。


関連データを1つにまとめる

Zennに公開されていたポケモンの例えが非常にわかりやすかったので
ここでも同様の例えを用いようと思います。

2020年12月現在、ポケモンの総数は898匹いるそうです。
【参考リンク】:全国ポケモン図鑑順のポケモン一覧

このポケモンたちそれぞれの能力値をプログラムで登録する際に、
下記の具合でひたすら898個の変数を用意するのはとてつもない作業になってしまいます。

string なまえ1;
int HP1;
int こうげき1;
int ぼうぎょ1;
int とくこう1;
int とくぼう1;
int すばやさ1;

string なまえ2;
int HP2;
int こうげき2;
int ぼうぎょ2;
int とくこう2;
int とくぼう2;
int すばやさ2;

...

これらの変数は「ポケモン」というくくりで1つにまとめた方が良さそうです。

クラス

1つにまとめてポケモンという概念を表現するためにクラス(class)を作ります。

class Pokemon
{
    public string なまえ;
    public int HP;
    public int こうげき;
    public int ぼうぎょ;
    public int とくこう;
    public int とくぼう;
    public int すばやさ;
}

オブジェクト(インスタンス)の生成

先ほど定義したPokemonクラスを利用して複数のポケモンを定義することができます。
そのためにはインスタンスを生成します。
下記はインスタンス生成と初期化を同時に行った例です。

Pokemon pokemon1 = new Pokemon
{
    string なまえ = ピカチュウ;
    int HP = 35;
    int こうげき = 55;
    int ぼうぎょ = 40;
    int とくこう = 50;
    int とくぼう = 50;
    int すばやさ = 90;
}

Pokemon pokemon2 = new Pokemon
{
    string なまえ = ライチュウ;
    int HP = 60;
    int こうげき = 90;
    int ぼうぎょ = 55;
    int とくこう = 90;
    int とくぼう = 80;
    int すばやさ = 110;
}

このように、クラスを使えば同じ特徴を持っていながら、
それぞれ中身は別々のオブジェクトを作成
することができます。

継承

あるクラスの性質を受け継いで新しいクラスを作成すること

継承は派生とも呼ばれている

継承もオブジェクト指向に欠かせない要素の1つです。

まず基底クラス(スーパークラス)派生クラス(サブクラス)を知る必要があります。

例を挙げると、
ピカチュウ(派生クラス)はポケモン(基底クラス)であるとなります。

クラスの関係図(クラス図)を書くと下記のようになります。

私は初見で矢印逆では?と思いましたが、これで合っています。
基底クラスは派生クラスを知りません。
矢印の向きはピカチュウクラスがポケモンクラスを知っていることを表しているわけですね。


差分プログラミング

基底クラスと派生クラスに違いを持たせることを差分プログラミングと呼ぶそうです。

ピカチュウだけ登録した人語を喋る機能があったとしましょう。
他のポケモンは人語を喋りません。
下記がその例です。

class Pokemon
{
    string なまえ;
    int HP;
    int こうげき;
    int ぼうぎょ;
    int とくこう;
    int とくぼう;
    int すばやさ;
}


class Pikachu : Pokemon
{
    string セリフ;
}

派生クラスのPikachuクラスでは基底クラスとの違いのみを定義しています。


is a 関係

is a 関係が成り立たないときは、継承を使ってはなりません

これは、例えばサトシがポケモンクラスを継承するのはおかしいので
間違っているよということです。
サトシ is ポケモン が成り立たないからです。

前述の差分プログラミングの考えだけ取り入れて、
共通部分があるから基底クラスを作って派生させよう!だけではダメってことですね。

ポリモーフィズム

日本語に直訳すると多態性、多相性という意味らしいです。

ポリモーフィズムにより、
ゼニガメだろうが、ヒトカゲだろうが
ポケモンという同一のオブジェクトとして見なしたうえで
それぞれに応じた攻撃の処理を呼び出すことができます。

ポリモーフィズムの考えを取り入れれば不要な条件分岐を排除できます。

ポリモーフィズムを取り入れないコードの場合は
このポケモンはゼニガメだから攻撃時この処理をして、
このポケモンはヒトカゲだから攻撃時この処理をして、、、

というのを898回、if文やswitch文を書かなくてはなりません。


public void Attack()
{
    if(ポケモンの名前 == ゼニガメ)
    {
        //ゼニガメの攻撃
    }
    else if(ポケモンの名前 == ヒトカゲ)
    {
        //ヒトカゲの攻撃
    }
    else if(...)
    .
    .
    .
}


【参考リンク】:継承とポリモーフィズム


次に、継承によるポリモーフィズムを導入した例を見ていきます。

まずは基底クラスとなるポケモンクラスを定義します。

class Pokemon
{
    public virtual void Attack()
    {
    }
}

virtualキーワードをメソッドにつけると派生クラスで中身を上書きすることができます。

基底クラスに具体的な攻撃の処理は書かずに、派生クラスで具体的な攻撃の処理を実装することで
派生クラスごとに独自の攻撃を行わせることが可能となります。

派生クラスでvirtualキーワードのついたメソッドを上書きする際には
overrideキーワードを使用します。

class Hitokage : Pokemon
{
    public override void Attack()
    {
        //ヒトカゲの攻撃処理
    }
}


class Zenigame : Pokemon
{
    public override void Attack()
    {
        //ゼニガメの攻撃処理
    }
}

上記クラスのAttackメソッドを利用する際には下記のように記述できます。

Pokemon p1 = new Hitokage();
Pokemon p2 = new Zenigame();

p1.Attack(); //ヒトカゲの攻撃処理が実行される
p2.Attack(); //ゼニガメの攻撃処理が実行される

一見するとPokemonHitokage,Zenigameは型が違う変数同士なので
コンパイルエラーになりそうですが、格納が可能です。

継承関係がある場合においては、派生クラスのオブジェクト(インスタンス)を
基底クラスの変数に代入することが可能です。

つまり、Hitokage,ZenigamePokemonとして同一視したうえで
同じ名前であるAttackメソッドをそれぞれ別々のメソッドとして呼び出せるようになっている
ということです。

これがポリモーフィズムらしいです。

抽象クラス、抽象メソッド

ポリモーフィズムの説明で、

基底クラスに具体的な攻撃の処理は書かずに、派生クラスで具体的な攻撃の処理を実装する

という表現をしました。

ポリモーフィズムの説明の例では基底クラスに具体的な処理は書かないので、
通常、インスタンスを生成することもありません。
下記のようにポケモンクラスのインスタンスを生成して使うことはないということです。

Pokemon p = new Pokemon();

表現を変えると、ポリモーフィズムの説明の例において
基底クラスは概念を表しているクラスなので
継承されることを前提とした抽象的なクラス
です。

今説明したようなクラスのことを抽象クラスと言います。
abstractキーワードを付けることで抽象クラスとなります。

メソッドも同様に抽象的な状態で中身を定義せず、継承先で具体的に実装するものが存在します。
それを抽象メソッドと言います。
抽象メソッドは継承先で必ず実装する必要があります。

abstract class Pokemon
{
    public abstract void Attack();
}

さいごに

結論言うと、読んで正解でした。
まず、当然ですが賢くなります。これは当たり前ですね。
もう1つプラスとなる要素として、モチベーションが上がりました。

エンジニアになる前に入門書を読んだ際は
難しすぎて本を噛みちぎってやろうかと何度も思いましたが、
今読むと、ある程度サクサクと理解できるようになっていました。

本を噛みちぎりたくなっても、諦めずに
その段階で理解できるところだけでも知識として拾っていくというのを繰り返せば
例えその成長曲線が緩やかだったとしても、
"ちゃんとレベルアップできるんだな"
ということが確認でき、自信が持てました。

基本構文から丁寧に説明されている良い本なので
おススメです。

【リンク】:なるほどなっとく C#入門