본문 바로가기

코틀린

코틀린: 3장 테스트에 도전해 보세요 (6번 정답 해설)

문제 6 휴대폰 번호에서 숫자만 추출하는 예제를 교재 p.141에서 소개했습니다. 이 코드를 참고하여 주민등록번호 13자리 중 앞 6 숫자와 뒤 7 숫자를 추출하는 프로그램을 만들어 보세요.  단, 아래 조건을 모두 만족해야 합니다.
(1) String 클래스의 확장 함수를 선언해야 합니다.
(2) 람다 식을 함수 타입의 형식 인자로 전달해야 합니다.

확장 함수의 서명(signature) 선언
(1)번 조건에 맞게 extractFirst()를  String 클래스의 확장 함수로 추가하려면 "fun String.extractFirst( ) "와 같이 선언하면 됩니다. 이 함수의 반환 타입은 부분 문자열을 추출한 결과이기 때문에 String입니다. 이어서 이 확장 함수의 형식 인자를 "op:String.( ) -> String" 처럼 함수 타입을 선언하면 됩니다. 함수 타입의 타입.() -> 에서 타입수신자 객체(receiver)를 가리킵니다.

fun String.extractFirst(op: String.() -> String): String { /* 함수의 몸체 */  }

 

확장 함수 구현 : 정답  1
확장 함수 extractFirst(또는 extractLast)에 람다 식을 인자로 전달하면 됩니다. "주민등록번호 13자리 중 앞 6 숫자 ... "와 같은 문제는 String 클래스의 메소드 substring()을 사용하면 좋습니다. 신용카드, 운전면허증, 여권처럼 정해진 숫자열을 갖는 응용에도 적용할 수 있겠죠! 주의할 점은 substring(startIndex, endIndex)에서 endIndex는 추출하려는 부분 문자열의 범위에 포함되지 않습니다. 또, 람다 식 블록에서 this는 수신자 객체를 가리킵니다. 

"이렇게 코딩하면 되는구나" 하는 감(느낌 → 이해 →  개념 정립 →  conceptualization)을 잡았으면 여러 가지 다른 형태의 정답도 알아봐야죠. 

fun main() {
    println("012345-9876543".extractFirst ())
    println("012345-9876543".extractLast ())
}

fun String.extractFirst(op: String.() -> String = { this.substring(0, 6) } ): String {
    return op(this)
}

fun String.extractLast(op: String.() -> String = { this.substring(7, this.length) } ): String {
    return op(this)
}

 

정답  2: 함수의 실 인자로 람다 식 전달
우선 확장 함수부터 식(expression) 형태를 사용해 줄일 수 있습니다. 그리고, 확장 함수에서 람다 식을 직접 지정하지 않고, 확장 함수를 호출할 때 람다 식을 전달할 수 있습니다. 람다 식이 마지막 인자이면 함수 괄호 밖으로 옮길 수 있습니다(책 p.135의 설명을 참고하세요). 

이렇게 바꾸면 확장 함수 extractFirst와 extractLast가 다른 점이 있나요? 비슷한데요... 합치면 안되나요? 

fun main() { 
    // "012345-9876543".extractFirst { this.substring(0, 6) } 
    println("012345-9876543".extractFirst { this.substring(0, 6) }) 
    println("012345-9876543".extractLast { this.substring(7, this.length) }) 
} 

fun String.extractFirst(op: String.() -> String) = op(this) 
fun String.extractLast(op: String.() -> String) = op(this)

아래처럼 1개의 확장 함수로 합칠 수 있습니다. 잠깐! this를 사용하지 않았어요. 람다 식 블록에서 this는 언제든 생략할 수 있습니다. 함수 타입에서 수신자 객체를 강조하기 위해 this를 사용했을 뿐입니다.

fun main() {
    println("012345-9876543".extract { substring(0, 6) })
    println("012345-9876543".extract { substring(7, this.length) })
}

fun String.extract(op: String.() -> String) = op(this)

 

 정답  3: 확장 함수에서 수신자 객체를 지정하지 않은 함수 타입 선언
확장 함수에서 함수 타입을 아래처럼 String.() -> String에서 (String) -> String으로 바꾸면 코드는 어떻게 달라질까요? 수신자 객체가 아니라 인자(argument)를 전달받게 됩니다. 이렇게 바꾸면 람다 식 블록에서 this를 사용할 수 없습니다. 대신 형식 인자 한 개를 전달받기 때문에, 인자 이름 대신 it를 사용할 수 있습니다(책 p.136의 내용을 참고하세요).

fun main() {
    println("012345-9876543".extract { it.substring(0, 6) })
    println("012345-9876543".extract { it.substring(7, it.length) })
}

fun String.extract(op: (String) -> String) = op(this)
// fun String.extract(op: String.() -> String) = op(this)

 

 정답 4:  람다 식을 사용
늘 그렇듯 앞에 설명한 정답처럼 어렵게 풀 필요가 없었습니다. 람다 식만 사용해도 String 클래스의 확장 멤버 함수로 extractFirst()를 추가할 수 있습니다(IDEA에서 "012..-...543", 까지 입력하고, 점(.)을 눌러 팝-업 창에 당당히 extractFirst()가 나타나는 걸 확인해보세요). 단, 람다 식의 함수 타입을 선언할 때 수신자 객체 지정 방식을 사용해야 합니다.

정답 4를 가장 먼저 생각했나요? 그것보다는 정답 1부터 차례대로 한 단계씩 이해하면서 코딩 스타일(coding style)을 이해하는 게 더 좋은 방법입니다. A long long journey begins with a single step...

fun main() {
    val extractFirst: String.() -> String = { this.substring(0, 6) }
    // extractFirst("012345-9876543")
    // "012345-9876543".extractFirst()
    println("012345-9876543".extractFirst ())
    
    // println("012345-9876543".substring (0, 6))
}