Kotlinの拡張関数と拡張プロパティについて

  • 34
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

 この投稿は、Kotlin Advent Calendar 2014の2日目の投稿です。

 前日1日目は、@ngsw_taroさんの、今年1年のKotlinで何があったかを紹介している「Kotlin Advent Calendar 2014」です。

 明日3日目は、@yy_yankさんの、「声に出して読みたいKotlin」です。

 この投稿では、JVM言語Kotlinの言語機能である、拡張関数と拡張プロパティを紹介します。Kotlin公式サイトの、Extensionsの内容に加えて、少しだけ「こういう使い方もあるよね」とか、「C#と比べてこうだね」ということも述べます。

このクラスにこんなメソッドがあったらいいなという時は?

 「このクラスにこんなメソッドがあったらいいな?」そんな時はありませんか?

 そんな時、Javaの場合は次のようなことが考えられます。

  • そのクラスを継承して、サブクラスでメソッドを追加する
  • *Utilitiesクラスにstaticメソッドを定義し、static importでメソッド呼び出し。(継承禁止クラスなど)

 Kotlinの場合、「このクラスにこんなメソッドがあったらいいな」という場面では、拡張関数を利用することができます。

拡張関数とは?

 Kotlinは継承をせずとも、既存のクラスを拡張できる言語機能を持っています。その一つが拡張関数です。

 拡張関数を用いることで、継承禁止クラスを含む既存のクラスに、新たに関数を追加することができます。(正確には、実際にクラスメンバを追加している訳ではないです)

 拡張関数の例を紹介します。

 まず拡張関数を定義します。今回はAndroidのActivityクラスに、findByIdという関数を追加します。ButterKnifeのButterKnife#findByIdのような型推論をしてくれるfindViewByIdです。

 Kotlinでは、名前空間直下に関数を定義することができます。ここでは、com.mrstar.extensions名前空間直下にfindByIdを定義しました。下記のように拡張関数では、ジェネリックな関数も定義することが可能です。

拡張メソッドを定義
package com.mrstar.extensions
import android.app.Activity
import android.view.View

fun <T : View> Activity.findById (id : Int) : T = findViewById(id) as T

 利用側のコードは、次のようになります。

拡張メソッドを利用
package com.mrstar.android_with_kotlin
// 略
import com.mrstar.extensions.findById // <- 注目

public class MainActivity() : FragmentActivity() {
    protected override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val imageView: ImageView = findById (R.id.image_view) // <- 注目
        // 略
    }
}

 findByIdの呼び出し箇所では、普通の関数のように拡張関数を呼び出しています。

 拡張関数を定義した名前空間と利用側のMainActivityの属する名前空間は異なります。import com.mrstar.extensions.findByIdで、定義した拡張関数をインポートしているのに注目してください。

 普通の関数のように呼び出せますが、実際にはクラスメンバとして関数を追加している訳ではありません。実際にはstaticメソッドのバイトコードに変更されるようです。

拡張プロパティとは?

 既存のクラスを拡張するもう一つの方法である、拡張プロパティを紹介します。

 Kotlinでは、既存のクラスに関数だけでなくプロパティを追加することが可能です。(以下、公式ドキュメントのサンプルコードより)

拡張プロパティの例
val <T> List<T>.lastIndex: Int
  get() = size - 1

 ここでは長さでなく、最後のインデックスを取得するプロパティをList<T>に追加しています。

拡張関数とメンバの関数、どちらが呼ばれる?

 さて、ここで疑問に思う方がいるかもしれません。「もともとfooという関数を持っているクラスに、fooという拡張関数を定義した場合、どうなるのか」と。

 Kotlinではこの場合、拡張関数自体は定義可能です。定義は可能ですが、拡張関数でなくメンバである関数が呼び出されます。

メンバの関数と拡張関数の衝突
class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

 上記のCというクラスのインスタンスcで、c.foo()を呼び出した場合、"member"と表示されるそうです。(参考)

 これはC#の拡張メソッドと同じですね。(参考)

継承では拡張できないクラスにも拡張できる

 拡張関数・拡張プロパティのメリットとして、「継承では拡張できないクラスにも拡張できる」ということがあると思います。

継承禁止クラス

 継承禁止なクラスは継承によりメソッドが追加できません。

 しかし、拡張関数・拡張プロパティを利用すればクラス拡張をすることが可能です。

複数のクラスから継承されているスーパークラス

 Androidのクラスの例を挙げます。

 AbsListViewという抽象クラスがあります。

 GridViewListViewが、AbsListViewを継承しています。

 AbsListViewにメソッドを追加したくなった場合、つまりGridViewでもListViewでも使えるメソッドを追加したくなった場合、GridViewとListViewをそれぞれを継承したクラスを作る必要があります。同じ処理のコードが重複してしまいますね。

 Kotlinの場合、拡張関数によりAbsListViewに関数を拡張することで、GridViewでもListViewでも使える関数を追加することが可能です。

いろいろな場所で定義できる!

 Kotlinでは拡張関数は様々な場所で定義することが可能です。

  • privateアクセス修飾子付き名前空間下で定義して、その名前空間とそのサブ名前空間限定で。
  • クラス内にprivateアクセスレベルで定義して、そのクラス限定で。
  • 関数内にローカル関数内として、ローカルスコープ限定で。

呼び出す時もthisがいらない!

 普段C#を使っている私としては、実はこれが結構うれしいです。

 最初に見せたActivityなどのスーパークラスに拡張関数を定義し、

拡張メソッドを定義(再掲)
package com.mrstar.extensions
import android.app.Activity
import android.view.View

fun <T : View> Activity.findById (id : Int) : T = findViewById(id) as T

  スーパークラスを継承したサブクラスで、定義した拡張関数を場合(ここでは、MainActivity)、

拡張メソッドを利用
package com.mrstar.android_with_kotlin
// 略
import com.mrstar.extensions.findById // <- 注目

public class MainActivity() : FragmentActivity() {
    protected override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val imageView: ImageView = findById (R.id.image_view) // <- 注目
        // 略
    }
}

 拡張関数の呼び出しは、this.findById (R.id.image_view)ではありませんね。(thisがついていませんね。)

 Kotlinではこのような状況で拡張関数を呼び出す際、メソッドのレシーバであるthisを書く必要がありません。(コンパイラがいい感じにメソッド呼び出しを解決してくれているようです。)

 C#にはだいぶ前から拡張メソッドがあります。(こちらは拡張関数ではなくて、拡張メソッド(Extension Methods)) C#の場合は、拡張メソッドの呼び出しには、メソッドのレシーバが必ず必要です。そのため、継承前提のクラス(例えば、UnityゲームエンジンのMonoBehaviour)に拡張メソッドを定義し、そのサブクラスで利用する際、thisをつけてメソッド呼び出しをしないといけない。これがちょっと不格好。

C#の拡張メソッドには、必ずレシーバが必要
this.MyExtensionMethod();

 ちなみに、CodePlexでこんなDiscussionがあったみたいです。

まとめ

 この投稿では、拡張関数と拡張プロパティについて説明しました。

 拡張関数・拡張プロパティにより、Kotlinは柔軟な記述ができたりや可読性の高いコードが書けるようになると思います。

リンク集

関連投稿