안드로이드 개발하는 kancho입니다.
이번 포스팅에서는 코틀린의 확장 기능에 대해 알아보고자 합니다.
'코틀린 완벽 가이드' 책을 참고하였습니다.
확장 (Extension)
확장은 말 그대로 기존의 것을 더 넓고 크게 사용한다는 것이다.
코드를 작성하다 보면 기존의 클래스를 확장시켜야 하는 경우가 생긴다. 내가 직접 만든 클래스에서는 쉽게 함수를 추가하거나 커스텀하게 원하는 동작을 추가할 수 있다. 하지만 자바나 코틀린에서 기본적으로 제공되는 라이브러리나 외부 라이브러리 같은 경우에는 이미 정의된 것 외에는 동작을 추가하기가 어렵다.
그래서 코틀린에서는 클래스 밖에서 함수나 프로퍼티를 선언할 수 있도록 확장이라는 기능을 제공한다.
확장을 사용하면 기존 클래스를 변경하지 않고 확장할 수 있어 OCP(개방-폐쇄 원칙)를 지킬 수 있다.
확장은 크게 확장 함수와 확장 프로퍼티로 나눌 수 있다.
확장 함수
확장 함수(Extension function)는 어떤 클래스의 멤버인 것처럼 호출할 수 있는 함수이다.
구조
(수신 객체의 클래스 이름).(함수 이름)
/*
String : 클래스 이름 ( nullable한 타입도 가능 )
validateEmail : 함수 이름
*/
fun String?.validateEmail() { }
위 확장 함수의 코드는 JVM에서 아래와 같이
수신 객체(String?)를 가리키는 파라미터가 추가된 정적 메서드로 표현된다.
public final class UtilKt {
public static final void validateEmail(@Nullable String $this$validateEmail) {
}
}
사용
예를 들어, 어떤 String이 이메일 형식인지 판별하기 위해 기존에는 없던 validateEmail이라는 확장 함수를 만든다고 할 때,
아래와 같이 만들 수 있다.
/*
본문 안에서 this로 수신객체(String?) 접근 가능
수신 객체가 nullable한 경우 null check도 필요하다
*/
fun String?.validateEmail() =
if (this == null) false
else Pattern.compile(
"[a-zA-Z0-9+._%\\-]{1,256}" + "@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" +
"\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+"
).matcher(this).matches()
fun main() {
println("kancho@tistory.com".validateEmail()) // true
println("123456789".validateEmail()) // false
/*
null safety를 사용하지 않고 확장 함수를 호출할 수 있다
validateEmail 확장 함수가 null check를 담당한다
*/
val s = readLine()
println(s.validateEmail())
}
확장 함수 자체는 수신 객체가 속한 타입의 캡슐화를 깰 수 없다.
하지만 클래스 본문 안에서의 확장 함수는 수신 객체의 멤버인 동시에 확장 함수라 접근이 가능하다.
// 비공개 멤버인 name, email에 접근 불가능
class User(private val name: String, private val email: String)
fun User.showInfo() = println("$name $email") // error
// 비공개 멤버인 name, email에 접근 가능
class User(private val name: String, private val email: String) {
fun User.showInfo() = println("$name $email") // ok
}
확장 함수는 바인딩된 호출 기능 참조(bound callable reference)로 사용 가능하다.
class User(val name: String, val email: String)
fun User.hasName(name: String) = name.equals(this.name, ignoreCase = true)
fun main() {
val a = User("kancho", "kancho@tistory.com")::hasName
println(a("kancho")) // true
println(a("tistory")) // false
}
만약 클래스 멤버와 동일한 이름, 파라미터 개수와 타입을 가지는 확장 함수가 있다면 어떻게 될까?
// 컴파일러가 항상 멤버 함수를 우선적으로 선택
class User(val name: String, val email: String) {
fun userInfo() = "$email $name"
}
// 사용되지 않음
fun User.userInfo() = "$name $email"
fun main() {
val a = User("kancho", "kancho@tistory.com")
println(a.userInfo())
}
/* result ->
kancho@tistory.com kancho
*/
그래서 확장 함수는 코드의 가독성을 해치지 않으며 확장하는 구조와 의미를 잘 생각하며 정의해야 한다.
또한, 다른 패키지에 확장 함수가 정의된 경우 확장을 import 해야 한다.
// data
package data
fun String.truncate(maxLength: Int) =
if (this.length <= maxLength) this else substring(0, maxLength)
// util
package util
import data.truncate
fun main() {
"Hello".truncate(3)
}
확장 프로퍼티
확장 프로퍼티(Extension property)는 어떤 클래스의 멤버인 것처럼 호출할 수 있는 프로퍼티이다.
구조
(수신 객체의 타입).(이름)
// Long클래스에 대해 getDay라는 확장 프로퍼티 정의
val Long.getDay: Int
get() = (this / 1000 / 60 / 60 / 24).toInt()
사용
확장 프로퍼티에는 backing field를 사용할 수 없다.
따라서 프로퍼티 초기화가 불가능하고 접근자 내에서 field를 사용하지 못한다.
immutable 변수는 getter를 명시해야 하고 mutable 변수는 setter, getter를 명시해야 한다.
또한 확장 프로퍼티에 위임을 사용할 수 있다.
// 초기화 할 수 없다
val Long.getDay: Int = 10 // error
// 불변 변수에 getter 명시
val Long.getDay: Int
get() = (this / 1000 / 60 / 60 / 24).toInt()
// 가변 변수에 setter, getter 명시
var IntArray.midValue
get() = this[lastIndex/2]
set(value) {
this[lastIndex/2] = value
}
fun main() {
val numbers = IntArray(6) { it * it } // 0, 1, 4, 9, 16, 25
println(numbers.midValue) // 4
numbers.midValue *= 10
println(numbers.midValue) // 40
}
// 위임 사용 가능
val String.message by lazy { "Hello" }
동반 확장
동반 객체(companion object)에 대해 확장할 수 있다.
구조
(수신 객체).(Companion).(이름)
fun IntRange.Companion.singletonRange(n: Int) = n..n
사용
클래스 이름을 사용해 호출한다.
fun IntRange.Companion.singletonRange(n: Int) = n..n
val String.Companion.HELLO
get() = "Hello"
// 완전한 동반 객체 이름을 사용해도 된다
fun main() {
println(IntRange.singletonRange(5)) // 5..5
println(IntRange.Companion.singletonRange(3)) // 3..3
println(String.HELLO)
println(String.Companion.HELLO)
}
동반 객체가 존재하는 경우에만 동반 객체에 대한 확장을 정의할 수 있다.
class User(val name: String, val email: String) {
companion object { }
}
val User.Companion.UNKNOWN by lazy {
User("kancho", "kancho@tistory.com")
}
// Any는 동반객체가 존재하지 않는다.
fun Any.Companion.hello() = println("Hello") // error
람다, 익명 함수 확장
코틀린에서는 람다나 익명 함수에 대해 확장 수신 객체를 활용할 수 있다.
수신 객체 지정 함수 타입(functional type with receiver)으로 표현된다.
/*
2개의 parameter를 받는 aggregate 함수
Int.(Int) -> Int 수신객체 타입
*/
fun aggregate(numbers: IntArray, op: Int.(Int) -> Int): Int {
var result = numbers.firstOrNull()
?: throw IllegalArgumentException("Empty Array")
numbers.forEach {
result = result.op(it)
}
return result
}
// aggregate에 전달한 람다는 수신객체를 가져 this 사용 가능
fun sum(numbers: IntArray) = aggregate(numbers) { op -> this + op }
// 익명 함수에서는 수신 객체 타입을 함수의 파라미터 목록 앞에 추가하면 된다
fun sum(numbers: IntArray) = aggregate(numbers, fun Int.(op: Int) = this + op)
또 하나의 특징은
수신 객체가 있는 함숫값을 호출할 때는 수신 객체를 처음 파라미터로 넣어
확장 함수가 아닌 일반 함수 형태로 호출할 수 있다. (비확장 함수 호출)
val min1: Int.(Int) -> Int = { if (this < it) this else it }
val min2: (Int, Int) -> Int = min1 // 수신 객체가 첫 번째 파라미터인 일반 함수 타입
val min3: Int.(Int) -> Int = min2 // 수신 객체가 있는 함수 타입
/*
수신 객체가 있는 함숫값은 확장, 비확장 형태 중 선택 가능하지만
수신 객체가 없는 함숫값은 비확장 형태로만 호출 가능하다
*/
fun main() {
val min1: Int.(Int) -> Int = { if (this < it) this else it }
val min2: (Int, Int) -> Int = min1
println(3.min1(2)) // min1을 확장 함수로 호출
println(min1(1, 2)) // min1을 비확장 함수로 호출
println(3.min2(2)) // error
println(min2(1, 2)) // min2를 비확장 함수로 호출
}
이렇게 수신 객체가 있는 함수 타입 값과 수신 객체가 첫 번째 파라미터인 일반 함수 타입 값을 바꿔 사용할 수 있는 이유는
두 타입의 값이 런타임에 똑같이 표현되기 때문이다.
위 코드의 min1, min2, min3는 모두 같은 타입으로 컴파일되는 것을 볼 수 있다.
public final class UtilKt {
@NotNull
private static final Function2 min1;
@NotNull
private static final Function2 min2;
@NotNull
private static final Function2 min3;
@NotNull
public static final Function2 getMin1() {
return min1;
}
@NotNull
public static final Function2 getMin2() {
return min2;
}
@NotNull
public static final Function2 getMin3() {
return min3;
}
static {
min1 = (Function2)null.INSTANCE;
min2 = min1;
min3 = min2;
}
}
'코틀린' 카테고리의 다른 글
[코틀린] 람다와 고차함수 (0) | 2022.10.17 |
---|---|
[코틀린] Scope function (영역 함수) (0) | 2022.10.10 |
[코틀린] Lazy 초기화 (0) | 2022.10.09 |
코틀린 정리 - (3) (0) | 2022.10.03 |
코틀린 정리 - (2) (0) | 2022.09.30 |