📚 클래스(4)


📔 확장 함수


📖 확장 함수 정의와 호출 규칙

  • 확장 함수 정의: 함수 이름 앞에 리시버 클래스를 접두사로 붙여서 정의
  • 확장 함수 호출: 리시버 클래스 대신 리시버 객체를 지정하고 확장 함수를 메서드로 호출해서 실행


📖 내장 클래스의 확장 함수 정의

fun Int.swap(other: Int): Pair<Int, Int> { // Pair 클래스는 2개의 원소를 가진 튜플
    var (first, second) = other to this // 튜플 구조 분해로 변수 교환
    return first to second // to 메서드:  2개의 원소를 튜플화
}

fun String.swap(other: String): Pair<String, String> {
    var (first, second) = other to this
    return first to second
}

println((100).swap(200))
println(("1차").swap("2차"))
(200, 100)
(2차, 1차)


📖 최상위 클래스의 확장 함수 정의

최상위 클래스 Any에 확장 함수를 정의하면 이를 상속한 하위 클래스는 모두 이 확장 함수를 사용할 수 있다.

fun Any.dir(): Set<String> {
    val a = this.javaClass.kotlin
    
    var ll = mutableListOf<String>() // 빈 가변 리스트 생성
    for (i in a.members) { // 클래스 내의 멤버 조회
        ll.add(i.name)
    }
    return ll.toSet() // 동일한 이름 제거
}
val intM = (100).dir()
println(intM.count()) // 집합에 들어온 멤버의 개수

println(("str".dir().count())) // 문자열 내부의 멤버 개수
println((listOf(1, 2, 3).dir().count())) // 리스트 내부의 멤버 개수

class Person
println(Person().dir().count()) // 사용자 정의 내부의 멤버 개수
31
21
34
3


📖 사용자 클래스의 확장 함수 정의

class Person(val firstName: String, val lastName: String)

val p = Person("달", "문")

fun Person.getName() = this.firstName + this.lastName // 두 개의 속성을 조회하는 확장 함수

println(p.getName())
println(p::firstName.name) // 속성 참조 후 속성 값 조회
println(p::firstName.get()) // 속성 참조 후 속성 값 조회
달문
firstName
달


📖 사용자 클래스의 널러블 확장 함수 정의

class Person(val firstName: String, val lastName: String)

fun Person?.getFullName() : String? { // 널러블 자료형에 확장
    if (this == null) { return null } // 널 체크 처리
    else {
        return this.firstName + this.lastName // 널이 아닌 경우에만 속성 반환
    }
}

var p = null
println(p.getFullName())
null


📖 object 정의에 확장 함수 추가

object A

fun A.swap(one: Int, other: Int): Pair<Int, Int> { // 두 수 교환 확장 함수
    val (second, first) = one to other // 반환은 튜플
    return first to second
}

println(A.swap(100, 200))
(200, 100)


📖 companion object의 확장 함수 정의

class AA private constructor(val name: String) { // 접근 불가 생성자 정의
    companion object {
        fun create(name: String): AA { // companion object에서 객체 생성
            return AA(name)
        }
    }
}

fun AA.Companion.create2(name: String): AA { // 객체 생성 확장 함수
    return this.create(name) // 접근 불가 생성자 호출 대신 컴패니언 내의 생성자 메서드 호출
}

val aa = AA.create("dal")
println(aa.name)

val bb = AA.create2("moon")
println(bb.name)
dal
moon


📖 확장 함수 내부에서 객체를 처리할 때 리시버 객체 처리 방식

fun String.truncator(max: Int): String { // 문자열 자르기 확장 함수 작성
    if(length <= max) return this  // 길이가 작으면 확장 함수의 리시버 객체 처리
    else return this.substring(0, max) // 길이가 크면 문자열 자르기
}

println("문자열자르기".truncator(4)) // 문자열 자르기 처리

interface Actable { // 인터페이스 작성
    fun action(max: Int): String // 추상 메서드 작성
}

fun String.truncator2(max: Int): String {
    val aaa = object: Actable {
        override fun action(max: Int): String {
            if (length <= max) return this@truncator2
            else return this@truncator2.substring(0, max)
        }
    }
    return aaa.action(max)
}

println("문자열자르기".truncator2(5))
문자열자
문자열자르
interface This { // 인터페이스 작성
    val truncated: String // 추상 속성 정의
    fun getStr(): String // 추상 메서드 정의
}

fun String.truncator(max: Int) = object: This {
    override val truncated
        get() = if (length <= max) this@truncator 
                    else this@truncator.substring(0, max)
                    
    override fun getStr() = this@truncator
}

val trunc = "문자열 처리".truncator(4)

println(trunc.truncated)
println(trunc.getStr())
문자열 
문자열 처리


📖 클래스 내부에 확장 함수 정의

class Extension(var name: String, val n: Int) { // 클래스 정의
    fun String.product(x: Int) = this.repeat(x) // 문자열 확장 함수 정의
    
    fun query() = name.product(n) // 확장 함수를 랩핑한 메서드 정의
}

val e = Extension("Hello", 3)
println(e.query())
HelloHelloHello



📔 멤버와 확장의 주의 사항

확장 속성이나 확장 함수는 클래스의 멤버가 아니다. 그러므로 클래스의 멤버와 이름이 충돌되는 경우, 항상 멤버가 우선된다. 단, 멤버가 비공개일 경우에만 확장이 우선 사용된다.

📖 멤버와 확장 간 충돌 시 처리 기준 : 상속 관계

확장 함수와 멤버 또는 메서드 이름이 충돌한 경우, 항상 멤버가 우선이다.

open class Person(val name: String, var age: Int) { // 슈퍼 클래스 정의
    open fun say() = "super 정의 hello"
    
    fun eat() = "super eat"
}

class Student(name: String, age: Int, val school: String) : Person(name, age) { // 서브 클래스 정의
    override fun say() = "sub 재정의 hello" // 메서드 재정의
}

fun Student.eat() = "sub 확장 ehllo" // 확장 함수 정의

val p: Person = Student("학생", 13, "서울 중학교") // Person 클래스 변수

println(p.say()) // 상속관계에 따른 멤버 메서드 처리
println(p.eat()) // 확장 함수가 아닌 Person 클래스 메서드 처리
sub 재정의 hello
super eat

1.2.1 📖 멤버와 확장 간 충돌 시 처리 기준 : 속성과 메서드

클래스의 속성이나 메서드 중에 접근 지정자로 private이 사용된 경우, 외부에 정의된 확장 함수와 이름이 중복이 되더라도 비공개 멤버는 클래스 외부에서 사용할 수 없기 때문에 충돌이 생기지 않는다.

class Integer(val x: Int) {
    val value: Int = x // 속성 정의
        get() { // 속성 게터 정의
            println("클래스 필드")
            return field
        }
        
    private fun display() = x.toString() // 비공개 메서드 정의
    fun plus(other: Integer): Integer {
        println("클래스 메서드")
        return Integer(this.x + other.x)
    }
    override fun toString() = "Integer(value= $x)" // 재정의 메서드
}

fun Integer.plus(other: Integer): Integer { // 클래스 내부 멤버와 이름 충돌
    println("확장 함수") //  절대 호출되지 않음
    return Integer(this.x + other.x)
}

fun Integer.display(): String { // 멤버와 이름이 충돌되나 private 으로 인해 외부에서 호출 가능
    println("확장 함수")
    return this.x.toString()
}

val Integer.value: Int // 클래스 필드와 이름 충돌
    get() { // 확장 필드는 배킹 필드가 없음
        println("확장 필드")
        return 100
    }
    
val inte = Integer(100) // 객체 생성

println(inte.plus(Integer(300))) // 클래스 메서드 호출
println(inte.value) // 클래스 필드 호출
println(inte.display()) // 확장 함수 호출
클래스 메서드
Integer(value= 400)
클래스 필드
100
확장 함수
100