Kotlin 백과사전 2탄

 

고차 함수

  • 함수를 마치 클래스에서 만들어낸 인스턴스처럼 취급하는 방법
    • 함수를 파라미터로 넘겨줄 수 있음
    • 결과값으로 반환 받을 수 있음
  • 코틀린은 모든 함수를 고차함수로 사용 가능
fun main(){
    b(::a)      // 고차함수 형태로 넘겨주기 위해서는 콜론을 두개 붙여줘서 넘겨줘야함
}

fun a (str: String){
    println("$str 함수 a")
}

fun b(function: (String) -> Unit){
    function("b가 호출한")
}
  • fun b의 파라미터로 함수 a를 받고싶음. 이때 a는 타입이 아니라 함수명임. 어떻게 함수 a의 타입을 전달할 수 있을 까?
    • 입력 파라미터 타입 -> 반환형 타입

      • 즉, (String) -> Unit) 이 하나의 타입

파라미터로 넘겨줄 함수의 이름을 굳이 정해줘야 할까? 그렇지 않다. 람다함수를 쓰자!

람다식

  • 람다식은 코드조각이다

  • 기본형

      { argumentList -> codeBody}
    
    • 화살표앞은 인자와 그 타입, 화살표뒤는 코드바디
      • 화살표 뒤는 함수처럼 어떤 의도한 기능들을 수행할 수 있는 코드가 올 수 있다!
      • 코드바디니까 여러줄이 올 수 있는것임!!!
    • 람다식에선 항상 마지막에 오는 코드가 return 값

      val square  = {number:Int -> number * number}
      val nameAge = {name:String, age:Int -> 
      "my name is $name and $age years old"
      3+5
      }
      
      fun main(){
          println(square(15))
          println(nameAge("hojun", 27))
      }
      

      • my name is hojun and 27 years old 는 출력 되지 않는것을 확인할 수 있다
      • 마지막 값이 중요하며 이것이 타입추론에 쓰인다!

람다함수

  • 람다 함수는 그 자체가 고차 함수여서 별도의 연산자 없이 변수에 담을 수 있음
fun main(){
    b(::a)

    val c:(String) -> Unit = {str -> println("$str lambda")}
    b(c)
}

fun a (str: String){
    println("$str function a")
}

fun b(function: (String) -> Unit){
    function("b call")
}
  • 여러 표현 방법

      // 생략 하나도 없는 전체 표현
      val sum: (Int, Int) -> Int = {x:Int, y:Int -> x + y}  // 항상 중괄호 사이에 존재
                                                              // x:Int, y:Int가 파라미터, -> 뒤에가 본문
                                                              // 이렇게 변수에 할당하면 그 변수에서 인자 받아들임
      // 선언 자료형 생략
      val sum = {x: Int, y: Int -> x + y}
    
      // 람다식 매개변수 자료형 생략
      val sum: (Int, Int) -> Int = {x, y -> x + y}
    
      // 에러
      val sum: {x, y -> x + y}
    
      // 주의사항!!
      // 아래와같이 선언하면 에러남
      val sum: Int -> Int = {x:Int -> x}
    
      // 따라서 람다의 입력인자 타입을 지정해줄 땐 항상 ( ) 로 해줘야함
      val sum: (Int) -> Int = {x:Int -> x}
    
      // 인자로 아무것도 안 받으며 반환값 없는 람다식 선언
      val out: () -> Unit = {println("Hello world!")}
    
      // 인자가 하나인 경우 it 키워드로 인자값을 사용할 수 있음
      val c:(String) -> Unit = {println("$it 람다함수")}
    

안드로이드에서 많이 쓰는 람다식 표현법

fun main(){
    val lambda:(Double) -> Boolean = {number:Double ->
        number == 4.3213
    }

    println(invokelambda(lambda))
    println(invokelambda({it > 5}))     // invokelambda 함수의 인자를 람다식 형태로 구성
                                        // invokelambda 함수인자가 요구하는 람다식의 입력인자가 1개 이므로 it으로 대체 가능
                                        // 즉 여기서의 it은 invokelambda 내의 lambda(4.3213)에 의해 4.3213이 됨
                                        // 람다식 내의 입력인자가 하나 혹은 없을 경우 `->` 생략 가능하므로 it > 5은 코드바디 이면서 boolean 형식
}

fun invokelambda(lambda: (Double) -> Boolean): Boolean{
    return lambda(4.3213)
}
  • 위 코드의 주석과 더불어서 몇가지 조건에 의해 람다식을 더 간단하게 만들 수 있음

    • 클래스 혹은 함수의 마지막 파라미터가 람다식이라면 소괄호 바깥으로 뺄 수 있음

      invokelambda(){lambda}
      invokelambda(){it > 5}
      
    • 입력 파라미터가 람다식뿐이라면 소괄호 생략 가능

      invokelambda{lambda}
      invokelambda{it > 5}
      
    • 버튼 리스너를 통한 이해

      • object 키워드를 활용한 익명 객체 방식

          button.setOnClickListener{object : View.OnClickListener{
              override fun onClick(p0: View?){
                  // to do..
              }
          }
        
      • 람다식을 활용한 방식 (요구조건 2가지 있음)

        1. Kotlin interface가 아닌 Java interface 이어야함
        2. 그 interface는 딱 하나의 메소드만 가져야함
         button.setOnClickListener{
             // to do...
         }
        
        • 위 익명 객체 방식 코드와 완벽히 똑같은 코드임

        • 1, 2 조건을 모두 만족하며 onClick 메소드가 void 타입이므로 람다식의 반환형이 Unit? 처리되어있는것을 알 수 있다
        • 이런식으로 오직 하나의 추상 메소드를 가진 인터페이스를 함수형 인터페이스 또는 SAM 인터페이스(Single Abstract Method)라고 부름

확장함수

fun main(){
    println(pizzaIsGreat("Wow!"))
    println(expandString("HHHH", 25))
}

val pizzaIsGreat:String.() -> String ={ // 입력 인자가 하나거나 아예 없을경우 -> 생략가능
    this + "Pizza is the best!"
}

fun expandString(name: String, age: Int): String{   // 함수 선언부이므로 : String은 함수 반환 타입
    val introduceMyself:String.(Int) -> String = {"I am ${this} and ${it} years old"}
    return name.introduceMyself(age)
}
  • :String.(Int) -> String
    • 나는 String object의 확장함수로 사용될 것이며 사용되는 순간에 내가 받을 인자는 Int형 이야
    • 그리고 나의 반환형은 String이야

object, companion object

Singleton Patten(클래스의 인스턴스를 단 하나만 만들어 사용하도록 하는 코딩 아키텍쳐 패턴)을 언어차원에서 지원함

  • object
    • 생성자가 필요없음
      • 최초 호출 시 그 자체가 객체가 되기 때문
fun main(){
    println(Counter.count)

    Counter.countUp()
    Counter.countUp()

    println(Counter.count)

    Counter.clear()

    println(Counter.count)
}

object Counter{
    var count = 0

    fun countUp(){
        count++
    }

    fun clear(){
        count = 0
    }
}

// 출력결과
0
2
0
  • companion object
    • Class 안에 object가 들어가 있는 것
      • 즉 클래스들간의 공용 속성 및 함수를 사용하겠다!

fun main(){
    var A = FoodVote()
    var B = FoodVote()

    A.vote()
    A.vote()
    A.vote()

    B.vote()
    B.vote()

    println("${FoodVote.total}")        // companion object에 접근할 땐 클래스명으로 접근
}

class FoodVote(){
    var count = 0

    companion object{
        var total = 0
    }

    fun vote():Unit{
        count ++
        total ++
    }
}

// 출력결과
5

옵저버 패턴, 콜백 메서드

  • 옵저버는 이벤트 발생하는것을 감시하는것을 의미함
  • 이벤트란?
    • 함수로 직접 요청하지 않았지만, 시스템이나 루틴에의해 발생하는 동작
  • 바로 위 문구에서 언급한 동작이 발생할 때 마다 즉각 처리 가능하게 만드는것이 옵저버 패턴

개념 이해

  • 옵저버 패턴 구현을 위해 필요한 클래스는 2개
    1. 이벤트 수신하는 클래스(A)
    2. 이벤트를 발생 및 전달하는 클래스(B)
  • 안드로이드에서는 Button에 리스너 객체를 다는것을 예시로 들 수 있음. 즉 통상적으로 A 클래스에서 B 클래스의 인스턴스를 만들어서 구현함
    • 문제 발생
      • A 클래스 내에서 인스턴스를 만들기 때문에 A 클래스는 B 클래스를 참조 가능하지만, B 클래스가 A 클래스를 참조할 수는 없음(상속관계 아님). 즉, 통신 불가. 이를 해결하기 위한 수단이 인터페이스

인터페이스를 활용한 통신 구현

  1. B 클래스가 Interface를 어떻게 활용할 것인지를 선언함
    • 나에게 종이 주어진다면 나는 그 종을 자진모리 장단으로 흔들다가 한번씩 추임새를 올리겠노라. 그 종은 색깔이 푸른색이도다
  2. A 클래스가 Interface를 구현해서 B에게 전달함(A 클래스 내에 B 클래스의 인스턴스가 있으므로 참조 가능)
    • 푸른색 빛이 은은한 종을 만들었오. 그대(B)에게 드리겠소.
  3. 코드 작동이 시작되고 A 클래스가 B 클래스의 인스턴스 메서드를 호출하면 B 클래스가 사전에 계획했던 Interface 활용이 시작됨
    • 헤이, 내가(B) 자진모리 장단을 지금부터 출터이다. 내가 추임새를 올리는 순간 너가 만든 종의 청량함 (구현된 기능)을 맛보겠도다
fun main(){
    val button = EventPrinter()
    button.start()
}

interface EventListener{
    fun onEvent(count: Int)
}

class Counter(var listener: EventListener){
    // 상속해서 쓰는게 아니라 속성으로 사용한다는것에 주목
    // 어떤 의미가 있는가?
    // 직접 인터페이스의 구현된 기능을 사용하겠다! 로 보임
    // 상속을 받으면 직접 구현을 해줘야 하는 것이고

    fun count(){
        for(i in 1..100){
            if(i%5 == 0) listener.onEvent(i)
        }
    }

}

class EventPrinter:EventListener{
    override fun onEvent(count:Int){
        print("${count}-")
    }

    fun start(){
        val counter = Counter(this)     // 다형성 개념에서 추가설명 할것이긴 하겠지만 약간만 설명하자면
                                        // this는 EventPrinter의 인스턴스임
                                        // Counter가 받는 파라미터는 EventListener이므로 EventPrinter 인스턴스에서
                                        // onEnvet 함수의 구현부만 가져온다. (이건 코틀린 고유 문법으로 보임)
        
        counter.count()                 // 이 코드가 두 클래스간의 약속으로 보면 될 듯
                                        // Counter가 말하길.. 종이 나한테 온다면 내가 그걸 흔들께! 대신 그 종은 너가 만들어서 나한테 줘!
                                        // EventPrinter가 말하길.. 알겠어 내가 종을 만들어서(override) 너한테 줄게! (counter.count())
    }
}

// 출력결과
5-10-15-20-25-30-35-40-45-50-55-60-65-70-75-80-85-90-95-100-