코딩의 간결성과 직관적 가독성을 위해 프로그램 작성법이 조금씩 변천되어 왔는데,
특히 함수 표현에 있어 획기적이라고 할 수 있는 표현법이 익명함수와 람다식 입니다.
그리고 프로그래밍 기법에 있어서도 특히 비동기 프로그래밍을 위해
고차함수, 콜백함수들이 도입되었습니다.
여기에서 개선됐다는 기준은 80, 90년대의 프로그램 기법에서 개선됐다는 말입니다. ;;
그러니까 이 기술들에 대한 지원이 시작된 것은 2000년대 부터 였던 것 같네요.
그 이후 지금까지 커다란 변화는 없는 것 같습니다.
각 용어와 표현 방법에 대해 간략하고 쉽게 정리해봤습니다.
람다식
함수를 간결화한 형태.
익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리함.
한편 코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있음.
아래 세 가지 표현은 모두 같은 의미임.
① 일반 함수
fun add(x: Int, y: Int) : Int { // 함수 선언부 (2개의 int형 인자를 받아 int형 값을 리턴)
return x + y // 함수의 내용 본체
}
val sum: Int = add(3, 4)
val sum = add(3, 4) // 이렇게 줄임 표현도 가능
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y // 함수 선언부에 인자 목록은 명시. 리턴 타입은 생략
// 함수 본체의 제일 마직막 표현식에 따라 자동으로 리턴 타입이 추론됨
val sum = add(3, 4)
③ 람다식
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y } // '{ 인수들 -> 함수본체 }' 부분이 람다식임
val sum: (Int, Int) -> Int = {x, y -> x + y } // 이렇게 줄여 표현 하든
var sum = {x: Int, y: Int -> x + y } // 이렇게 줄여 표현하든 모두 가능
// (전달인자든 람다식 안에서든 데이터형 명확히 지정됐으므로)
// 그리고 람다식이 저장된 변수 sum은 일반 함수처럼 호출이 가능함
sum(1,2) // 결과: 3
고차 함수 (High-order Function)
함수의 전달인자나 반환 값의 데이터형에 함수가 포함된 형태.
fun printResult( s: String, r: Float, ff: (num: Float) -> Float) { // *1
val result = ff(r)
println("$s 는 $result")
}
printResult("원의 넓이", 3.0f, {x -> 3.14f * x*x }) // *2
printResult("구의 체적", 3.0f, {x -> 4.0f/3.0f * 3.14f * x*x*x }) // *3
실행 결과)
원의 넓이 는 28.26
구의 체적 는 113.04001
*1 함수의 전달인자를 표시하는 방식은,
문자 : 전후에 함수내에서 사용할 전달인자 이름, 그 전달인자의 데이터형을 명기한 형태입니다.
결국 전달인자로 사용된 함수 부분은,
함수명은 ff, 데이터형은 (num: Float) -> Float) 라고 해석할 수 있습니다.
(num: Float) -> Float)
이 부분이 람다식으로 표현되어 있네요.
Float 형 인자 하나를 받고 Float 형으로 리턴하는 형태의 함수가
전달인자로 사용될 수 있음을 의미합니다.
*2, *3
그림으로 좀 더 쉽게 설명을 해 보자면,...
①,② 각각의 함수가 *1의 전달인자 선언을 통해 printResult()내에서 result 값을 계산하는데 사용되었습니다.
참고로 ①,②는 호출한 함수 printResult에 의해 거꾸로 호출되는데, 이와 같은 함수를 '콜백함수'라고 합니다.
한편, 이번 예에서는 이 함수들을 이름 없이 (이름이 없으므로 '익명(Anonymous) 함수'라고 부릅니다)
함수 본체만을 람다식 형태로 전달했지만, 여러 부분에서 이 함수들을 반복해서 참조한다면
이름을 갖는 함수를 정식으로 정의하고 그 함수명을 전달해줘도 됩니다.
참고) 람다식을 사용할 때 몇몇 표현의 융통성
*2, *3의 표현은 (눈치를 챘는지 모르겠지만) 아래의 표현에서 전달인자 데이터형을 생략한 것 입니다.
printResult("원의 넓이", 3.0f, {x: Int -> 3.14f * x*x })
// (데이터형이 printResult()선언부에 이미 작성되어 있으므로 생략 가능)
*2, *3처럼 함수를 호출할 때 제일 마지막 전달인자가 람다식인 경우,
람다식을 ()밖에 적어도 됩니다.
printResult("원의 넓이", 3.0f) {x -> 3.14f * x*x } // 람다식만 밖으로 꺼내 작성했음
*2, *3처럼 심지어 람다식의 전달인자가 단 하나 뿐이라면, 람다식의 전달인자를 아예 생략해도 됩니다.
그리고 그 전달인자는 키워드 it으로 참조할 수 있습니다.
printResult("원의 넓이", 3.0f) {3.14f * it*it }
fun sum( x: Int, y: Int, prt: (num: Int) -> Unit) { // Unit는 리턴형이 없다는 의미임(void와 같음)
val total: Int
total = x + y
prt( total )
}
sum(3,4) {println(it)} // 결과: 7 (람다식의 표현이 거의 깡패 수준이죠?...)
뿐만아니라, 고차함수의 전달인자가 람다식 하나 뿐이라면, 고차함수를 호출할 때 ()를 생략할 수도 있습니다.
fun sum(prt: (num: Int) -> Unit) { // 전달인자가 람다식 하나 뿐이면,
prt(100)
}
sum() {print(it)}
sum {print(it)} // 호출 때 빈 ()를 생략 가능함
※ 콜백함수의 활용
비동기 프로그래밍에서 작업시간이 오래 걸리는 함수에게 콜백함수를 전달해주고,
메인 프로그램은 다음 동작을 계속 진행.
예)
fun longTimeJob( ..., callback() ) {
(오래걸리는 작업 - DB, 통신, 사용자이벤트, ...)
callback() // 오래걸리는 작업 완료
}
메인프로그램작업1
longTimeJob( ..., cBackFun() ) // 콜백함수를 넣어 longTimeJob() 호출 후 다음 작업 계속
메인프로그램작업2
...
...
참고로, 이런 활용이 UI등에서 사용자 응답 이벤트를 다루는 경우에 이뤄진다면,
언제 발생할 지 모르는 응답에 대해 대기를 해야 하는 '이벤트 리스너' 함수에게
그 응답을 처리할 작업을 담고 있는 '이벤트 핸들러(= 이벤트 처리기)' 함수(콜백함수)를 전달하게 됩니다.
말하자면 다음과 같은 모양이 됩니다.
fun 리스너(핸들러) {
(센서등 응답 대기...)
핸들러()
}
메인프로그램작업1
리스너(핸들러)
메인프로그램작업2
...
...
읽을 거리) 그럼 함수의 전달인자로 함수를 받을 수 없었던 예전에는 버튼 이벤트를 어떻게 처리했나?
(함수가 객체는 받을 수 있었다. 자바에서 변수는 곧 객체)
이벤트 핸들러 구현용 인터페이스를 개발팀 공용으로 만들어 놓고,
각 팀원들이 그 인터페이스를 구현한 자신들만의 클래스를 만들고 객체화하여 리스너 함수에 전달함으로써
콜백함수를 구현했습니다.
아래 버튼 이벤트 처리의 경우, 인터페이스의 추상메서드 onClick()을 구현한 이벤트 핸들러 객체
'new onClickListener()'를 전달하고 있습니다.
Button button = findViewID(R.id.button)
button.setOnClickListener( new onClickListener() {
@override
public void onClick(View view) {
// 이벤트 처리
}
}
간략화된 표현식에 대해서는 오히려 혼동을 주는 경우도 있을 수 있습니다.
각자 편한대로 코딩하면 됩니다. 그래도 내용은 알고 있는 게 좋겠죠. 다른 프로그램을 해독하려면요.
그럼, 이만~
'프로그래밍' 카테고리의 다른 글
[코틀린의 고급 문법 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