this 표현식
this 표현식 (This expressions)
this 표현식은 현재 리시버(receiver)를 가리켜요. 리시버는 함수가 작업하고 있는 객체를 말하죠. this를 어떻게 쓰느냐는 문맥에 따라 달라져요.
클래스의 멤버 안에서는 this가 해당 클래스의 현재 객체를 가리켜요. 예를 들어 아래 코드에서 this.name은 현재 Language 객체의 name 프로퍼티를 뜻해요.
확장 함수나 리시버가 있는 함수 리터럴 안에서는 this가 그 리시버를 가리켜요.
출처: Kotlin 공식 문서
본문
먼저 클래스 멤버 안에서의 this를 볼게요.
class Language(val name: String) {
fun printName() {
println(this.name)
}
}
fun main() {
val language = Language("Kotlin")
language.printName()
// Kotlin
}
이번에는 확장 함수 안에서의 this를 확인해 볼게요. 아래 예시에서 lastCharacter()는 문자열 "Kotlin"에 대해 호출되므로 "Kotlin"이 바로 리시버예요. 확장 함수 안에서 this는 "Kotlin"을 가리키고, 따라서 this.length는 "Kotlin"의 길이가 돼요.
fun String.lastCharacter(): Char {
println(this.length)
// 6
return this[this.length - 1]
}
fun main() {
println("Kotlin".lastCharacter())
// n
}
단순한 경우에는 this를 명시적으로 적지 않아도 돼요. Kotlin이 현재 스코프에서 알아서 풀어내거든요. 자세한 내용은 암시적 this를 참고하세요.
한정된 this (Qualified this)
리시버 스코프가 중첩되면 여러 리시버가 동시에 사용 가능해질 수 있어요. Kotlin은 사용 가능한 아무 리시버든 암시적으로 멤버에 접근할 수 있게 해 주는데, 안쪽 스코프의 리시버가 더 높은 우선순위를 가져요. 특정 리시버를 명시적으로 가리키려면 한정된 this를 사용해요. 특히 여러 리시버가 같은 이름의 멤버를 가질 때 바깥 리시버의 멤버에 접근해야 한다면 아주 유용하죠.
한정된 this를 쓰려면 라벨 한정자를 붙여요.
this@label
이 라벨은 컴파일러에게 어느 리시버에 접근할지 알려줘요. 감싸고 있는 클래스나 확장 함수의 이름을 쓸 수 있는데, 예를 들어 this@foo는 foo라는 확장 함수의 리시버를 가리켜요.
내부 클래스에서 바깥 클래스 접근하기
내부 클래스 안에서 한정되지 않은 this는 내부 클래스 인스턴스를 가리켜요. 바깥 클래스 객체에 접근하려면 한정된 this를 써요.
class User(val name: String) {
inner class Age(val value: Int) {
fun printInfo() {
// 현재 Age 객체의 value 프로퍼티를 가리킴
println(this.value)
// 22
// 바깥 User 객체의 name 프로퍼티를 가리킴
println([email protected])
// Jane Doe
}
}
}
fun main() {
val user = User("Jane Doe")
val age = user.Age(22)
age.printInfo()
}
바깥 클래스 인스턴스에 대한 참조를 보관하는 건 내부 클래스뿐이에요. 일반 중첩 클래스는 바깥 클래스의 this에 접근할 수 없어요.
확장 함수에서 클래스 접근하기
클래스 안에서 확장 함수를 선언하면 두 개의 리시버를 사용할 수 있게 돼요.
- 확장 리시버: 확장 함수를 호출하는 값.
- 디스패치 리시버: 확장 함수를 선언한 클래스의 현재 객체.
어느 리시버가 필요한지 한정된 this로 지정해요.
class User(val name: String) {
val prefix = "Name"
fun String.formatName(): String {
return "${[email protected]}: ${this.uppercase()}"
}
fun printName() {
println(name.formatName())
}
}
fun main() {
val user = User("Jane Doe")
user.printName()
// Name: JANE DOE
}
여기서의 동작을 정리하면:
this는 확장 리시버(String)를 가리키므로,this.uppercase()가 문자열을 대문자로 바꿔요.this@User는 현재User객체를 가리켜요.[email protected]는 현재User객체의prefix프로퍼티에 접근해요.
람다에서 접근하기
일반 람다와 달리 리시버가 있는 람다는 스코프에 리시버를 도입해요. 그 결과 람다 안의 this는 감싸고 있는 스코프의 리시버가 아니라 람다의 리시버를 가리켜요. 리시버가 있는 람다가 다른 리시버 스코프 안에 중첩되어 있다면, 람다에 라벨을 붙이고 한정된 this를 써서 람다의 리시버나 안쪽이 아닌 바깥쪽 스코프의 리시버를 명시적으로 가리킬 수 있어요.
class User(val name: String) {
fun printWithPrefix() {
val printString: String.() -> Unit = stringLabel@ {
println("${this@stringLabel}: ${[email protected]}")
}
printString("User")
}
}
fun main() {
val user = User("Jane Doe")
user.printWithPrefix()
// User: Jane Doe
}
여기서는:
stringLabel@이 람다에 라벨을 붙여요.this@stringLabel이 람다의 문자열 리시버를 가리켜요.this@User가 현재User객체를 가리켜요.
라벨은 아무것도 호출하거나 람다의 동작을 바꾸지 않아요. 단지 람다의 리시버를 가리키는 데 도움을 줄 뿐이에요.
익명 객체나 그 바깥 클래스 접근하기
익명 객체는 자기만의 리시버 스코프를 가져요. 객체 본문 안에서 한정되지 않은 this는 익명 객체 자신을 가리켜요. 다만 익명 객체에는 클래스 이름이 없으므로 this 한정자로 쓸 수 없어요. 그래서 익명 객체는 한정되지 않은 this로만 가리킬 수 있어요.
interface UserPrinter {
fun print()
}
fun main() {
val printer = object : UserPrinter {
val prefix = "User"
override fun print() {
// `this`는 익명 객체를 가리킴
// `this.prefix`는 그 `prefix` 프로퍼티에 접근함
println(this.prefix)
}
}
printer.print()
// User
}
다른 클래스 안에서 익명 객체를 선언했다면, 한정된 this를 써서 감싸고 있는 객체에 접근할 수 있어요.
interface UserPrinter {
fun print()
}
class User(val name: String) {
fun createPrinter(): UserPrinter {
return object : UserPrinter {
override fun print() {
// `this@User`는 감싸고 있는 `User` 객체를 가리킴
// `[email protected]`은 그 `name` 프로퍼티에 접근함
println([email protected])
}
}
}
}
fun main() {
val printer = User("Jane Doe").createPrinter()
printer.print()
// Jane Doe
}
암시적 this (Implicit this)
this에 대해 멤버 함수를 호출할 때는 this. 한정자를 생략할 수 있어요. 하지만 같은 이름의 다른 호출 가능 대상이 더 가까운 어휘 스코프에 있다면, Kotlin은 멤버 함수 대신 그 호출 가능 대상을 한정되지 않은 호출로 풀어내요. 멤버 함수를 명시적으로 호출하려면 this. 한정자를 사용해요.
fun main() {
class A {
fun printLine() {
println("Member function")
}
fun invokePrintLine() {
fun printLine() {
println("Local function")
}
printLine()
// Local function
this.printLine()
// Member function
}
}
A().invokePrintLine()
}
이 예시에서는 바깥 스코프의 지역 함수 printLine()이 멤버 함수보다 우선해서 호출되지만, this.printLine()으로 명시적으로 지정하면 멤버 함수가 호출돼요.