Kotlin’de Eklenti Fonksiyonlar (EXTENSIONS)

Tamer Sarioglu
Mobillium
Published in
5 min readApr 7, 2022

Herkese merhaba!

Bu yazımızda Kotlin’in güçlü yönlerinden biri olan Extensions’dan bahsedip https://kotlinlang.org/docs/extensions.html adresindeki dokümantasyonu Türkçe’ye çevirmeye çalışacağız.

Kotlin, herhangi bir sınıftan (class) miras almadan (inheritance) veya Decorator gibi tasarım desenlerini kullanmak zorunda kalmadan bir sınıfı yeni işlevlerle genişletme özelliğini biz geliştiricilere sunuyor.

Örneğin, üzerinde değişikliğe izin vermeyen üçüncü taraf kitaplığından(library) bir sınıf için yeni işlevler yazma imkanı sunar. Bu tür işlevler, sanki orijinal sınıfın yöntemleriymiş gibi gayet normal şekilde çağrılabilir. Bu mekanizmaya eklenti fonksiyonu (extension function) denir. Aynı zamanda mevcut sınıflar için yeni özellikler tanımlamanıza da izin verir.

Extension functions (Eklenti Fonksiyonlar)

Bir uzantı işlevi bildirmek için, adının önüne, genişletilen türü ifade eden bir alıcı türü eklenir. Aşağıdaki kod bloğunda, MutableList<Int> öğesine bir takas(swap) işlevi eklendiği görülür.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}

Yukardaki kod bloğu içindeki this anahtar sözcüğü, alıcı nesneye (noktadan önceki nesne) karşılık gelir (Bu kod bloğunda karşılığı Int tipindeki MutableList’tir). Şimdi, herhangi bir MutableList<Int> üzerinde böyle bir işlevi çağırabilirsiniz:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'list'

Bu fonksiyon, herhangi bir MutableList<T> için kullanılabilir:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}

Fonksiyonu alıcı tipine göre kullanılabilir hale getirmek için, genel tür parametresini fonksiyon adından önce bildirmeniz gerekir. Jenerikler hakkında daha fazla bilgi için jenerik fonksiyonlara bakabilirsiniz.

Eklenti Fonksiyonlar Statik Olarak Çözümlenir

Eklenti fonksiyonlar, genişlettikleri(eklendikleri) sınıfları gerçekten tamamen değiştirmezler. Bir eklenti tanımlayarak, eklenen sınıfa yeni özellikler eklemiyoruz, sadece bu tür değişkenler üzerinde nokta gösterimi ile yeni işlevleri çağırılabilir hale getiriyoruz.

Eklenti Fonksiyonlar statik olarak gönderilir; bu, alıcı türüne göre göreceli olmadıkları anlamına gelir. Çağrılan bir eklenti fonksiyon, çalışma zamanında bu ifadenin değerlendirilmesinden elde edilen sonucun türüne göre değil, işlevin çağrıldığı ifadenin türüne göre belirlenir.

Örneğin:

fun main() {
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle())
}

Çağrılan uzantı işlevi yalnızca Shape sınıfı olan s parametresinin bildirilen türüne bağlı olduğundan, bu örnek Shape yazdırır.

Bir sınıfın bir üye (member) işlevi varsa ve aynı alıcı tipine, aynı ada sahip ve verilen argümanlara uygulanabilir bir uzantı işlevi tanımlanırsa, üye her zaman kazanır.

Örneğin:

fun main() {
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }Example().printFunctionType()
}

Bu kod, Class metodunu yazdırır.

Bununla birlikte, eklenti fonksiyonların aynı ada; ancak farklı bir imzaya sahip üye işlevlerini(member fonkiyonlar) aşırı yüklemesinde(overloading) bir sakınca yoktur:

fun main() {
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType(i: Int) { println("Extension function #$i") }Example().printFunctionType(1)
}

Null Değer Alabilme

Eklentilerin, null değer alabilir bir alıcı türüyle tanımlanabileceğini de unutmamak gerekiyor. Bu eklentiler, değeri null olsa bile bir nesne değişkeninde çağrılabilir ve gövde içinde ‘this == null’ olup olmadığını kontrol edebilirler.

Bu şekilde, kontrol eklenti fonksiyonunun içinde gerçekleştiğinden, Kotlin’de toString() öğesini null öğesini kontrol etmeden de çağırabilirsiniz:

fun Any?.toString(): String {
if (this == null) return "null"
// null kontrolünden sonra, 'this' boş olmayan(non-null) bir türe otomatik olarak cast edilir, bu nedenle aşağıdaki toString()
// Any sınıfının üye işlevini(member) çözer
return toString()
}

Eklenti Özellikleri

Kotlin, eklenti fonksiyonları desteklediği gibi bu fonksiyonların özelliklerini de destekler:

val <T> List<T>.lastIndex: Int
get() = size - 1

Eklenti fonksiyonlar aslında sınıflara yeni üyeler(member) eklemediğinden, bir eklenti özelliğinin herhangi bir destek alanına sahip olmasının verimli bir yolu yoktur. Bu nedenle, uzantı özellikleri initialize edilemezler (başlatılamazlar). Yalnızca alıcılar/ayarlayıcılar (getter/setter) tarafından tanımlanabilirler.

val House.number = 1 // hata: initializers(başlatıcılar) eklenti özellikleri için izin verilmez

Tamamlayıcı Nesne Eklentileri (Companion Object Extensions)

Bir sınıfın tanımlanmış bir companion nesnesi varsa, companion nesne için de eklenti fonksiyon özelliklerini tanımlayabilirsiniz. Companion nesnenin normal üyeleri gibi, niteleyici olarak yalnızca sınıf adı kullanılarak çağrılabilirler:

class MyClass {
companion object { } // "Companion" olarak çağırılacak.
}
fun MyClass.Companion.printCompanion() { println("companion") }fun main() {
MyClass.printCompanion()
}

Eklenti Fonksiyonların Kapsamı

Genelde, eklenti fonksiyonlar en üst düzeyde, doğrudan paketlerin altında tanımlanır:

package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

Declarations paketinin dışında bir eklentiyi kullanmak için, çağırılan paket içine o eklentiyi import etmelisiniz:

package org.example.usage

import org.example.declarations.getLongestString //import

fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}

Daha fazla bilgi için Imports sayfasını ziyaret edebilirsiniz.

Eklentileri Üye(Member) Olarak Bildirme

Bir sınıf için eklentileri başka bir sınıf içinde de çağırabilirsiniz. Böyle bir eklentinin içinde, üyelerine niteleyici olmadan erişilebilen birden çok örtük alıcı vardır. Eklentinin bildirildiği sınıfın örneğine(instance) gönderme alıcısı, eklenti metodunun alıcı türünün diğer örneğine(instance) eklenti alıcısı denir.

class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
fun Host.printConnectionString() {
printHostname() // calls Host.printHostname()
print(":")
printPort() // calls Connection.printPort()
}
fun connect() {
/*...*/
host.printConnectionString() // calls the extension function
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
// Host("kotl.in").printConnectionString()
// error, the extension function is unavailable outside Connection
}

Gönderim(dispatch) alıcısı ile dahili eklenti alıcıları arasında bir isim çakışması olması durumunda dahili eklenti alıcısı öncelik kazanır. Gönderi alıcısının üyesine(member) ulaşmak için daha etkili olan bu syntax’i kullanabilirsiniz:

class Connection {
fun Host.getConnectionString() {
toString() // calls Host.toString()
this@Connection.toString() // calls Connection.toString()
}
}

Üye olarak bildirilen eklentiler, açık(open) olarak bildirilebilir ve alt sınıflarda override edilebilirler. Bu, bu tür işlevlerin gönderiminin, gönderim alıcı tipine göre sanal, ancak dahili alıcı tipine göre statik olduğu anlamına gelir.

open class Base { }class Derived : Base() { }open class BaseCaller {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}
open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}
fun call(b: Base) {
b.printFunctionInfo() // call the extension function
}
}
class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}
override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}
fun main() {
BaseCaller().call(Base()) // "Base extension function in BaseCaller"
DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

Görünürlük Notları

Eklentiler, aynı kapsamda bildirilen fonksiyonlarla aynı görünürlük değiştiricilerini kullanır. Örneğin:

  • Bir dosyada en üst düzeyde bildirilen bir eklenti, aynı dosyadaki diğer özel üst düzey bildirimlere erişebilir.
  • Bir eklenti, alıcı türünün dışında bildirilirse, alıcının özel veya korumalı üyelerine erişemez.

Keyifli okumalar dileriz.

--

--