[코틀린의 고급 문법 2]
고급 문법 마지막 시간입니다, 힘내자구요~~ 화이팅!
람다식
람다식이란 함수를 간결하게 표시한 형태입니다. 익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리합니다.
코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있습니다.
아래 세 가지 표현은 모두 같은 의미입니다. (코딩 간결화의 변천 과정이라고 생각해도 됨)
① 일반 함수
fun add(x: Int, y: Int) : Int { return x + y }
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y
③ 람다식
var add = {x: Int, y: Int -> x + y } // { } 부분이 람다식임. '{ 인수들 -> 함수본문 }' 형태임
사용 예)
println(add(3+4)) // 결과: 7
SAM 변환
JAVA에서 메소드가 하나뿐인 인터페이스는 인터페이스를 구현하는 대신 함수로 구현할 수 있는데,
이것을 'SAM변환'이라고 합니다. SAM (Simple Abstract Method ; 간단한 추상메소드)
코틀린 함수는 매개변수로 한 개의 추상메소드를 받는 형태를 취할 수 있는데,
이 때는 자바에서 작성된 인터페이스인 경우에 한해,
자바의 SAM변환과 유사하게 함수로 구현된 매개변수를 전달할 수 있습니다.
예를 들어 버튼의 클릭 이벤트를 구현할 때, View.OnClickListener 인터페이스를 구현하는데,
(이 View.OnClickListener는 onClick() 이라는 추상메소드 한 개만을 가지고 있음)
이 인터페이스를 함수로, 더 나아가 익명메소드 형태로 구현해서 전달해 봅시다.
button.setOnClickListener(object : View.OnClickListener { //OnClickListener는 OnClick메소드 하나만 포함
override fun onClick(v: View?) { // 익명메소드 형태
// 클릭시 처리 코드
}
}
)
그리고 위 익명메소드는 람다식으로 표현할 수 있으므로,
button.setOnClickListener({ v: View? -> // OnClickListener가 람다식화된 형태
// 클릭시 처리 코드(람다 블럭)
})
코틀린에서 메소드 호출 시 제일 뒤의 매개변수가 람다식인 경우에는,
가독성을 위해 람다식을 ( ) 밖으로 빼서 작성할 수 있습니다.
button.setOnClickListener( ) { v: View? ->
// 클릭시 처리 코드(람다 블럭)
}
그리고 람다식이 어떤 메소드의 유일한 매개변수인 경우에는 메소드의 ( ) 를 생략할 수 있습니다. 게다가 컴파일러가 자료형을 추론할 수 있다면 자료형을 생략할 수 있는 것이므로, 최종적으로 다음과 같이 되겠네요.
button.setOnClickListener{ v ->
// 클릭시 처리 코드(람다 블럭)
}
※ 만약에 '클릭시 처리 코드'에서 v라는 매개변수를 사용하지 않는다면,
v를 _ 기호로 대치할 수도 있습니다.
button.setOnClickListener{ _ ->
// 클릭시 처리 코드(람다 블럭)
}
※ 또 만약에 그리고 람다식에서 매개변수가 하나 뿐인 경우라면,
매개변수를 아예 생략할 수도 있습니다.
button.setOnClickListener{
// 클릭시 처리코드(람다 블럭)
}
그리고 이 때 람다블럭내에서는 매개변수를 it로 접근할 수 있습니다.
예를들면,
button.setOnClickListener{
it.visiblity = view.GONE // 여기서 it는 View? 형 v를 의미함 ----- (K)
}
위에서 SAM변환을 이용한 익명메소드에서 람다식화, 그리고 코틀린의 편리한 생략 표현 기법들을 총동원해 코드를
간결화시켜봤고 코드들 모두 같은 의미입니다. 어쨌거나 가장 가독성이 뛰어난 형태는 (K)의 코드죠. 앞으로 종종 등장합니다.
주의해야 할 점은 코틀린에서의 SAM변환은 자바에서 작성한 인터페이스일 때에만 동작한다는 것~!.
확장 함수
코틀린에서는 .연산자를 이용해 이미 정의된 클래스에 메소드를 쉽게 추가할 수 있습니다.
(보통 Java나 C#에서는 final로 상속이 봉인되어 있어서 메소드를 추가하지 못하는 경우가 많은데 편리한 기능인 것 같네요)
(확장 함수 내부에서 해당 객체로의 접근은 this를 사용)
예) Int 클래스에 isEven()을 추가해 보자.
fun Int.isEven( ) = this % 2 == 0 // 'this % 2 == 0'의 결과인 부울 값이 함수의 결과 값이 됨
println(5.isEven()) // 결과: false
형 변환
val a = 10L
b = a.toInt()
c = a.toDouble()
d = a.toString()
e = Integer.parseInt(d) // 문자열 형을 숫자로 변환
※ 일반 클래스 간 형 변환 (as 키워드)
open class Human()
class Man: Human()
val man = Man()
val human = man as Human // Human 형으로 형 변환
형 체크
val st = "hello"
if (st is String) { println(st.toUpperCase() }
고차 함수
함수를 매개 변수로 전달하거나 함수 형으로 반환할 수 있습니다. 이렇게 사용되는 함수를 '고차 함수'하고 합니다.
fun add( x: Int, y: Int, callback: (sum: Int) -> Unit) { // 두 개의 Int 매개변수와 한 개의 익명함수 매개변수
// 매개 변수로 쓰인 익명함수는 한 개의 Int 매개변수를 받고 리턴 값은 없음
callback( x + y )
}
add(3,4, {println(it)}) // 결과: 7 함수를 { }로 감싸고, 이 함수 내부에서는 반환 값을 it로 접근한 예임
동반 객체
'팩토리 메소드'
코틀린에서는 클래스를 객체화 하는 것과는 별개로 메소드를 이용해 객체를 생성하는 코딩 패턴을 지원하는데, 이를 '팩토리 메소드'라고 합니다.
(나중에 다루게 될 프래그먼트 컴포넌트는 특수한 제약 때문에 팩토리 메소드로만 객체를 생성할 수 있음)
코틀린에서는 타 언어에서 정적인 메소드를 만들 때 사용하는 static 키워드 같은 게 없습니다. 그 대신 '동반 객체 (companion object)'라는 것을 통해 이를 구현합니다. (companio : 동반자, 동료, 친구)
다음은 newInstance() 정적 메소드를 사용해서 Fragment 객체를 생성하는 팩토리 패턴을 구현한 것입니다.
여기서 동반 객체 내부의 메소드는 Fragment 클래스와 아무 관계가 없는 정적인 존재입니다.
class Fragment {
companion object {
fun newInstance(): Fragment { // shs: 함수의 반환 형이 Fragment 형이라...
println("생성됨")
}
}
}
val fragment = Fragment.newInstance() // shs: 이거 정적 Fragment 안의 newInstance 멤버메소드를 액세스 하는 것과 비슷한데요...
<<< 코틀린 기본 라이브러리에서 유용한 함수들 >>>
let()
블럭에 자기 자신을 인수로 전달(it로 참조)하고 실행 결과를 반환합니다.
'안전한 호출' 즉, str이 null이 아닐때만 호출 되도록 ?.연산자를 이용하면 더 좋습니다.
val result = str?.let { // 결과는 Int형
Integer.parseInt(it)
}
// fun <T, R> T.let(block: (T) -> R): R
with()
객체를 매개변수로 받고 블럭에 리시버 객체형으로 전달(this로 참조)해 준 후, 실행 결과를 반환합니다.
단, '안전한 호출'이 불가능하기 때문에 반드시 str이 null아닐 때에만 호출해야 합니다.
with(str) {
println(toUpperCase()) // this.toUpperCase()에서 this.를 생략할 수도 있다.
}
// fun <T, R> with(receiver: T, block T.() -> R): R
apply()
블럭에 객체 자신이 리시버 객체형으로 전달되고 그 객체형으로 반환됩니다.
(주로 객체의 상태를 변경해서 반환할 때 사용함)
val result = car?.apply {
car.setColor(Color.BLACK)
car.setPrice(1000000)
}
// fun T.apply(block: T.() -> Unit): T
run()
run() 함수는 익명 함수처럼 쓰는 법과 객체에서 호출하는 법 모두를 제공합니다.
i) 익명 함수처럼 쓸 때
블럭의 결과를 반환합니다.
블럭 안에 선언된 변수들은 모두 임시로 잠깐 사용하는 변수들인데, 임시 변수를 많이 사용하는 복잡한 계산에 유용하겠네요.
val avg = run{
val kor = 90
val eng = 80
val math = 90
(kor + eng + math) / 3.0f
}
// fun run(block: () -> R): R
ii) 객체에서 호출 할 때
객체를 블럭의 리시버 객체로 전달하고 결과를 반환합니다.
안전한 호출이 가능하므로 with()보다 더 유용합니다~
str?.run {
println(toUpperCase())
}
// fun <T, R> T.run(block: T.() -> R): R