かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

手軽なスクリプト言語としてのF# その9「クラス」

ついにオブジェクト指向プログラミングの基本構成要素のクラスまできました!いや〜長かった。まぁF#は関数型言語という色合いが強いのですが普通にオブジェクト指向プログラミングでもいけますからね。OOP的な部分をさらっとやってC#とかの代わりに書けるようになったら関数型言語っぽい機能を紹介していこうと思ってます。まぁOOP的な部分をさらっとのつもりが8回目まできて、やっとクラスの定義ですから関数型チックなエントリになるのは何回目からになることやら・・・。


ということで気を取り直してクラスについてみてみましょう。といっても前回やったレコードってクラスっぽくね?という感じですが、あれもC#とかから見るとクラスになってました。しかしsealedつきの継承できないクラスになってます。今回やるのは、ちゃんと継承とかも出来るクラスです。


プロパティの定義やらメソッドの定義やら色々ありますが、とりあえず何も無いクラスから定義して順に色々足して行こうと思います。まずは、クラスの定義とプライマリコンストラクタと呼ばれる、まぁデフォルト?のコンストラクタみたいなものの定義からやってみます。

type 型名(プライマリコンストラクタの引数) =
  let フィールド名 = 初期値
  let フィールド名 = 初期値
  ...

  do
    副作用のある処理

ということで、名前のフィールドを持つPersonクラスを定義すると以下のようなコードになります。

type Person() =
    // プライマリコンストラクタでのフィールドの初期化
    let name = ""
    // プライマリコンストラクタでの副作用のある処理
    do
        printfn "Personが作成されました"

// クラスのインスタンスを生成
let p = Person()

実行すると、以下のようになります。

Personが作成されました

因みに、フィールドは外部から触る事は出来ないので、このクラスは実質何もできないクラスということになります。しかもnameが空文字固定なのでコンストラクタで引数に渡して指定できるようにして、プロパティでフィールドの値を取得できるようにしてみます。
プロパティの定義は以下のようになります。

member 自己識別子.プロパティ名
    with get() = 値を取得する式
    and set(v) = 値を設定する式

ということで早速実装です。

// コンストラクタの引数でnameを指定できるようにする
type Person(name) =
    // コンストラクタの引数で与えられた値でフィールドを初期化
    let name = name
    // プライマリコンストラクタでの副作用のある処理
    do
        printfn "Personが作成されました %s" name

    // nameフィールドの値を取得するプロパティ
    member this.Name with get() = name

// クラスのインスタンスを生成
let p = Person("田中")
// プロパティから値が取得できる
printfn "p.Name = %s" p.Name

これを実行すると以下のような結果になります。コンストラクタの処理が実行されてプロパティの値が取得出来ています。

Personが作成されました 田中
p.Name = 田中

さて、Nameプロパティは読み取り専用なので、年齢というプロパティを定義して、こいつもコンストラクタで値を指定できて、その上プロパティからも値を設定できるようにしてみます。変数を読み書きできるようにするにはmutableを使います。

// コンストラクタの引数でname, ageを指定できるようにする
type Person(name, age) =
    // コンストラクタの引数で与えられた値でフィールドを初期化
    let name = name
    // ageは読み書きできる
    let mutable age = age
    // プライマリコンストラクタでの副作用のある処理
    do
        printfn "Personが作成されました %s %d" name age

    // nameフィールドの値を取得するプロパティ
    member this.Name with get() = name
    // 読み書きできるプロパティ
    member this.Age
        with get() = age
        and set(v) = age <- v

// クラスのインスタンスを生成
let p = Person("田中", 48)
// プロパティから値が取得できる
printfn "p.Name = %s, p.Age = %d" p.Name p.Age

// Ageプロパティは値を設定することも可能
p.Age <- 18
printfn "p.Name = %s, p.Age = %d" p.Name p.Age

実行結果は以下のようになります。

Personが作成されました 田中 48
p.Name = 田中, p.Age = 48
p.Name = 田中, p.Age = 18

段々クラスっぽくなってきました。次はメソッドを定義してみようと思います。メソッドの定義の仕方は、レコードと同じです。とりあえず年齢に引数で渡された数だけ追加するメソッドを書いてみます。

// コンストラクタの引数でname, ageを指定できるようにする
type Person(name, age) =
    // コンストラクタの引数で与えられた値でフィールドを初期化
    let name = name
    // ageは読み書きできる
    let mutable age = age
    // プライマリコンストラクタでの副作用のある処理
    do
        printfn "Personが作成されました %s %d" name age

    // nameフィールドの値を取得するプロパティ
    member this.Name with get() = name
    // 読み書きできるプロパティ
    member this.Age
        with get() = age
        and set(v) = age <- v

    // 年齢を加算するメソッド
    member this.AddAge(age) =
        this.Age <- this.Age + age

// クラスのインスタンスを生成
let p = Person("田中", 48)
// プロパティから値が取得できる
printfn "p.Name = %s, p.Age = %d" p.Name p.Age

// Ageプロパティは値を設定することも可能
p.Age <- 18
printfn "p.Name = %s, p.Age = %d" p.Name p.Age

// 年齢を加算するメソッドを使ってみる
p.AddAge(100)
printfn "p.Name = %s, p.Age = %d" p.Name p.Age

実行してみると、ちゃんと年齢加算メソッド動いてるのがわかります。

Personが作成されました 田中 48
p.Name = 田中, p.Age = 48
p.Name = 田中, p.Age = 18
p.Name = 田中, p.Age = 118

次に、プライマリコンストラクタ以外のコンストラクタを定義してみようと思います。プライマリコンストラクタ以外のコンストラクタは、newというキーワードを使って定義します。定義は以下のような感じです。

new(引数) =
  他のコンストラクタの呼び出し
  then
    副作用のある処理の呼び出し

thenの中に書く処理が無ければ省略することも出来ます。


ということで、名前だけ指定して年齢は0歳としてクラスを作成するコンストラクタを作ってみます。

// コンストラクタの引数でname, ageを指定できるようにする
type Person(name, age) =
    // コンストラクタの引数で与えられた値でフィールドを初期化
    let name = name
    // ageは読み書きできる
    let mutable age = age
    // プライマリコンストラクタでの副作用のある処理
    do
        printfn "Personが作成されました %s %d" name age

    // 名前だけ指定するコンストラクタ。年齢は0歳
    new(name) =
        Person(name, 0) // 初期化のためにコンストラクタを呼ぶ
        then
            // 副作用のある処理をここに書く
            printfn "%s さんが誕生しました!" name

    // nameフィールドの値を取得するプロパティ
    member this.Name with get() = name
    // 読み書きできるプロパティ
    member this.Age
        with get() = age
        and set(v) = age <- v

    // 年齢を加算するメソッド
    member this.AddAge(age) =
        this.Age <- this.Age + age

// 年齢を指定しないでPersonクラスのインスタンスを作成
let p = Person("田中")

実行結果は以下のようになります。

Personが作成されました 田中 0
田中 さんが誕生しました!

プライマリコンストラクタの処理が実行されてから、新たに定義したコンストラクタのthenの中の処理が行われているのがわかります。まぁ、書いてある順番通りですね。

とまぁ、こんな感じでF#でクラスを定義することが出来ます。ここまで出来たら後は継承やインターフェースの実装とかをやれば!!というところで力つきたので、それはまた次回やります。ではでは。