Control Flow (if, when, for, while) #1


 1) if


 코틀린에도 if문이 있습니다.


 fun main(args: Array<String>) {

    println(getBiggerNumber(1, 10))

}


fun getBiggerNumber(a: Int, b: Int) : Int {

    var bigger = a

    if (a < b) bigger = b

    return bigger

}

 10 


 if문이 값을 리턴하기 때문에 일반적인 if문이 삼항연산자(ternary operator)의 역할을 하기 때문에 삼항연산자가 없습니다.


 fun main(args: Array<String>) {

    println(getBiggerNumber(1, 10))

}


fun getBiggerNumber(a: Int, b: Int) : Int {

    val bigger = if (a > b) a else b

    return bigger

}

 10 


 당연히 if else문도 가능합니다.


 fun main(args: Array<String>) {

    println(getBiggerNumber(1, 10))

}


fun getBiggerNumber(a: Int, b: Int) : Int {

    var bigger = a

    if (a > b) {

        bigger = a

    } else {

        bigger = b

    }

    return bigger

}

 10 



 if 분기문은 블록을 가질 수 있는데 블록의 마지막 표현식(expression)이 블록의 값입니다. if 분기문을 명령문(statement)이 아닌 표현식으로 사용하면 (값을 리턴하거나 변수에 할당하는 경우인) 표현식은 else가 필수입니다.


fun main(args: Array<String>) {

    println(getBiggerNumber(1, 10))

}


fun getBiggerNumber(a: Int, b: Int) : Int {

    println("a = $a, b = $b")

    val bigger = if (a > b) {

        b

        a // 마지막 표현식

    } else {

        a

        b // 마지막 표현식

    }

    return bigger

}

 10 



2) when


 when은 생소한 키워드입니다. 대신 switch는 익숙하실겁니다. when은 바로 switch를 대체하는 존재입니다. 사용하는데 있어서 완전히 같지는 않지만 매우 유사합니다.

 예를 들어 다음의 Java코드는 코틀린으로 조금 다른 형태로 표현됩니다. 기본 동작은 switch와 같이 분기문의 조건이 만족될 때까지 순차적으로 모든 분기문에 대해 인자를 매칭시킵니다. 만족하는 경우가 없으면 else로 갑니다.

 컴파일러가 모든 케이스가 분기 조건으로 커버되는지 증명할 수 없다면 else 분기는 필수(mandatory)입니다.


Java

 Int  a = 1;

 switch (a) {

   case 1:

     System.out.println("a = 1");

     break;

   case 2:

     System.out.println("a = 2");

     break;

   default:

     System.out.println("a is neither 1 nor 2");

}


코틀린

 val a = 1

 when (a) {

   1 -> println("a = 1")

   2 -> println("a = 2")

   else -> println("a is neither 1 nor 2")

 }


 만약 많은 케이스가 같은 방식으로 처리되야 한다면 분기 조건은 ,(comma)로 결합될 수 있습니다. 만약 Java 라면 break가 줄어든 모습이겠죠.


Java

       switch(a) {

      case 1:

      case 2:

         System.out.println("a = 1 or a = 2");

         break;

      default:

         System.out.println("a is neither 1 nor 2");

      }


코틀린

  val a = 1

 when (a) {

   1, 2 -> println("a = 1 or a = 2")

   else -> println("a is neither 1 nor 2")

 }


 when 역시 명령문(statement)이나 표현식(expression)으로 사용가능합니다. 명령문으로 사용하면 각 분기문의 값은 무시됩니다. 

 위와 같이 명령문(statement)이 아닌 표현식(expression)일 경우에는 if문과 마찬가지로 블록의 마지막 표현식이 값이 됩니다. 


fun main(args: Array<String>) {

    getNumber(1)

}


fun getNumber(a: Int) {

    var result = when (a) {

        1 -> {

            println("a = 1") ; 1 // 같은 라인에서 표현식을 분리하려면 ;(세미콜론)을 사용해야 합니다.

        }

        2 -> {

            println("a = 2")

            2

        }

        else -> {

            println("x is neither 1 nor 2")

        }

    }

    println("result = $result")

}

 a = 1

 result = 1


 분기 조건으로 임의의 표현식을 사용할 수도 있습니다.


import java.lang.Integer.parseInt


fun main(args: Array<String>) {

    getNumber(100)

}


fun getNumber(a: Int) {

    when (a) {

        parseInt("100") -> println("a = 100")

        else -> println("a is neither 1 nor 2")

    }

 a = 100 


 또한 range나 collection에 값이 존재하는지 확인할 수도 있습니다. 이때는 in(포함하는 경우) 또는 !in(포함하지 않는 경우)을 사용합니다.


fun main(args: Array<String>) {

    getNumber(5)

}


fun getNumber(a: Int) {

    when (a) {

        in 1..10 -> print("a is in the range")

        !in 10..20 -> print("a is outside the range")

    }

}

 a is in the range


 특정 타입의 is 또는 !is 값을 확인할 수도 있습니다. is는 객체가 주어진 타입을 따르는지 아닌지 확인합니다. Java의 instanceof를 생각하시면 될 것 같습니다.


fun main(args: Array<String>) {

    getType("ABC")

}


fun getType(a: Any) = when (a) {

    1 -> print("a = $a")

    is String -> print("a is String $a")

    else -> print("etc")

 a is String ABC


확장 함수 (Extension Function) 더보기


 확장 함수는 [확장을 하려는 대상.함수]로 만들 수 있다고 하였습니다. 여기서 함수는 일반 함수의 형태입니다.


[Remind] 함수의 기본 형태

 fun 함수명(변수): Unit {

 }


 or


 fun 함수명(변수): 리턴타입 {

  return 값

 }


 여기에서 '확장을 하려는 대상'을 추가해주면 확장 함수가 됩니다. 엄밀히 말하자면 '확장 하려는 대상'에 함수를 추가해주는 것이겠죠.


  fun 확장 하려는 대상.함수명(변수): Unit {

 }


 or


 fun 확장 하려는 대상.함수명(변수): 리턴타입 {

  return 값

 }


 앞서 직접 만든 Car 클래스를 '확장 하려는 대상(receiver type)'으로 확장 함수를 만들어 봤습니다. 그런데 Basic Type을 이용하면 좀 더 재미있는 일을 할 수 있을 것 같습니다. Basic Type은 Int, Double, Byte와 같은 Number를 비롯에 Boolean, Array, String까지 다양합니다.


 예를 들어 String의 길이를 비교하여 길이가 더 긴 String을 리턴 받는 확장 함수를 만들어 사용할 수 있습니다.


fun main(args: Array<String>) {

    println("Hello".getLonggerString("Hi"))

}


fun String.getLonggerString(x: String) : String {

    return if(this.length > x.length) this else x

 Hello 


 예제를 분석해보면 확장 함수를 사용할 때의 형태가 보입니다.


 "Hello".getLongger("Hi")

 확장을 하려는 대상.확장 함수명(확장함수 파라미터 타입)


 String.getLonggerString(String)


 여기서는 확장을 하려는 대상, 즉 receiver type이 String이고 확장 함수명은 getLonggerString 그리고 파라미터 타입은 String입니다. 


 곧이곧대로 사용하면 재미없으니 이걸 약간 변형시켜 응용해볼까요?


fun main(args: Array<String>) {

    println("Hello".getLonggerLength(3))

}


fun String.getLonggerLength(x: Int) : Int {

    return if(this.length > x) this.length else x

 5 


 이번엔 String인 Hello의 길이와 3을 비교하여 큰 수를 리턴 받습니다. 확장 함수의 리턴타입과 파라미터 타입을 변경한 것입니다. 그러면 파라미터 타입은 String으로 유지한채 두 String 중 길이가 더 긴 String의 length를 리턴 받는 확장 함수도 만들 수 있겠죠? 또 파라미터 2개를 받아서 뭔가를 할 수도 있을 것입니다. 한 번 생각해보시면 좋을 것 같습니다.



중위 표기법 (Infix notation)


 함수는 중위 표기법을 사용해서 호출될 수 있습니다. 이때 세 가지 조건이 있습니다.


1. 멤버 함수 또는 확장 함수여야 한다.
2. 하나의 파라미터를 가져야 한다.
3. infix 키워드를 사용해야 한다. 


 그럼 '두 String 중 길이가 더 긴 String의 length를 리턴 받는 확장 함수'를 이참에 한 번 만들어 보겠습니다.


 fun main(args: Array<String>) {

    println("Hello".getLonggerStringLength("Have a nice day!"))

}


fun String.getLonggerStringLength(x: String) : Int {

    return if(this.length > x.length) this.length else x.length

}

 16 


 함수명이 점점 거창해지고 가독성이 떨어지고 있습니다. (^^;)

 자, 이 예제는 확장 함수이며 하나의 파라미터를 가지고 있습니다. 따라서 infix만 선언해주면 중위 표기법으로 호출이 가능하게 됩니다.


fun main(args: Array<String>) {

    println("Hello" getLonggerStringLength "Have a nice day!")

}


infix fun String.getLonggerStringLength(x: String) : Int {

    return if(this.length > x.length) this.length else x.length

}

 16 


 기존 방식과 중위 표기법으로 호출한 방식이 차이는 다음과 같습니다.


 "Hello".getLonggerStringLength("Have a nice day!")

 "Hello" getLonggerStringLength "Have a nice day!"


 기본 형태로 보면 좀 더 간결합니다. .(dot) 대신에 공백 그리고 함수명 다음에 공백을 추가하고 파라미터의 괄호가 사라졌습니다.


 receiverType.함수명(파라미터)

 receiverType 함수명 파라미터



확장 함수 vs 멤버 함수


 클래스가 멤버 함수를 가지고 동일한 receiver type과 동일한 이름으로 정의된 확장 함수가 있으면 멤버 함수가 항상 우선입니다.

 아래 예제를 보면 Car 클래스 내에 getPrice라는 멤버 함수가 존재하고 receiver type으로 Car를 사용한 getPrice라는 확장 함수가 있습니다. 이 경우 getPrice를 호출하면 멤버 함수가 우선적으로 호출이 됩니다.


 fun main(args: Array<String>) {

    val car = Car()

    println(car.getPrice())

}


class Car {

    fun getPrice(): Int {

        return 10000

    }

}


fun Car.getPrice(): Int {

    return 20000

}

 10000 


 하지만 함수 이름이 같아도 멤버 함수를 overload한 확장 함수는 확장 함수가 호출이 됩니다.


fun main(args: Array<String>) {

    val car = Car()

    println(car.getPrice(0))

}


class Car {

    fun getPrice(): Int {

        return 10000

    }

}


fun Car.getPrice(price: Int): Int {

    return 20000

}

 20000 




확장 (Extension)


 코틀린은 클래스 상속이나 데코레이터 패턴과 같은 디자인 패턴을 사용하지 않고도 새로운 기능을 가지는 클래스로 확장할 수 있습니다. 그것이 바로 extension(이하 확장)입니다. 코틀린은 extension function과 extension property를 지원합니다.


확장 함수 (Extension Function)


 확장 함수는 [확장을 하려는 대상.함수]로 쉽게 생성이 가능합니다.

 일단 확장 함수의 예를 살펴보겠습니다. 아래 코드는 Car 타입에 getBrandName()이라는 확장 함수를 만든 것입니다.

fun main(args: Array<String>) {

    val car = Car()

    println(car.getBrandName())

}


class Car {

    fun getPrice() : Int {

        return 10000

    }

}


fun Car.getBrandName() : String {

    return "BMW"

}

BMW 


 Car 클래스에는 존재하지 않는 getBrandName()이라는 함수가 아주 쉽게 만들어졌습니다. 심지어 Car 클래스에 멤버로 추가되거나 기존 함수를 수정하지도 않았습니다. 이런 것이 바로 확장입니다.


 그런데 확장 함수의 Car에 커서를 올리면 다음과 같이 나옵니다.



receiver 파라미터가 사용되지 않아서 발생한 경고입니다. receiver? 그게 뭘까요?


 위 코드에서 보듯이 '확장을 하려는 대상'입니다. 현재 Car 타입을 확장하고 있다는 것을 기억하셔야 합니다. 그럼 이번에는 해당 확장 함수에 receiver type과 동일한 return type을 선언하여 실제 receiver를 사용해보겠습니다.


fun main(args: Array<String>) {

    val car = Car()

    println(car.getBrandName().getPrice())

}


class Car {

    fun getPrice() : Int {

        return 10000

    }

}


fun Car.getBrandName() : Car {

    println("BMW")

    return this

}

BMW

10000 


 car.getBrandName()를 통해서 println("BMW")가 실행되고 return type이 receiver type과 동일한 Car이므로 멤버 함수인 getPrice()까지 호출이 됩니다.



Extensions are resolved statically


 위에서 살펴봤듯이 확장(Extension)은 실제로 확장(Extension)으로 확장한 클래스를 수정하는 것은 아닙니다. 클래스에 새로운 멤버를 추가하지 않고 확장을 정의만 하면 그(=receiver) 타입의 변수에 .(dot)로 호출할 수 있는 새 함수를 만듭니다.


 코틀린 레퍼런스에서는 확장 함수가 정적으로 전달된다는 것을 강조하고 있습니다. 즉, receiver type에 의해서 가상화 되지 않습니다. 호출되는 확장 함수는 런타임에 해당 표현식(expression)을 평가한 결과의 type에 의한게 아니라 함수가 호출되는 표현식의 type에 의해 결정됩니다. (원문을 직역했더니 표현이 참 매끄럽지 못하네요. 그래서 이 말의 의미를 이해하기가 아주 힘들었습니다.)


 한국어인데도 무슨 말인지 선뜻 이해가 되지 않습니다. 이럴 때는 코드를 봅시다.


fun main(args: Array<String>) {

    printFuel(Bus())

}


open class Car

class Bus: Car()


fun Car.getFuel() = "gasoline"

fun Bus.getFuel() = "lpg"


fun printFuel(fuel: Car) {

    println(fuel.getFuel())

}

  gasoline


 먼저 확장 함수가 정적으로 전달된다고 한 것은 printFuel 함수에 파라미터 fuel은 Car 타입으로 정적인 상태로 있다는 것을 의미합니다. 따라서 Bus 타입으로 호출하여도 Car 타입의 확장 함수가 실행이 되어 gasoline이 출력된 것입니다.


 receiver type에 의해서 가상화가 되지 않는다는 것은 receiver로 Car를 사용하여 Bus를 만들어내는 작업이 되지 않았다는 것입니다. 예를들면 다음과 같이 Int를 Double로 바꾸는 작업같은 개념입니다. 이건 지금 잘 이해가 안되더라도 추후 다시 공부할 시간이 있을 것 같습니다. (제발 누가 알려주세요!)


 val intToDouble: Int.() -> Double = { toDouble() } 


 printFuel 함수 호출부와 선언부의 Java 변환 코드를 보면 파라미터 부분이 명확하여 이해하는데 도움이 됩니다.


 printFuel((Car)(new Bus()));

 ...

 ...

    public static final void printFuel(@NotNull Car fuel) {

      Intrinsics.checkParameterIsNotNull(fuel, "fuel");

      String var1 = getFuel(fuel);

      System.out.println(var1);

   }



확장(Extension) #1을 마치며


 확장 함수를 보기 전에 Extensions are resolved statically 레퍼런스의 설명이 이해되지 않아 같은 문장을 새벽까지 수십번 읽은 것 같습니다. 이게 확장 함수를 이해하는데 한결 도움이 되었네요. 그래서 그 후 확장 함수 (Extension Function)를 작성했습니다.

+ Recent posts