함수를 사용하자

·2021년 12월 5일
0
post-thumbnail

코틀린은 개발자에게 무조건 클래스를 사용하라고 강요하지 않는다.
재사용 가능한 가장 작은 단위가 클래스인 Java와 달리, 코틀린에서는 클래스는 물론 단독함수까지 모두 재사용 가능하다.

📌 함수생성


키스(KISS)함수

코틀린은 함수를 정의할 때 KISS 원칙(Keep It Simple, Stupid)을 준수한다.
작은 함수들은 단순하게 작성하고, 방해요소가 없고, 실수가 없어야 한다.

함수 정의는 fun 키워드로 시작한다.
함수가 매우 짧은 단일표현함수 라면 바디를 { } 로 만드는 대신 함수 정의 부분과 함수 바디를 = 로 구분할 수 있다.

리턴타입과 타입 추론

greet()함수는 리턴타입을 지정하지 않았지만 문자열을 리턴한다.
코틀린은 {} 블록바디가 없는 함수에 대해 타입 추론을 해주기 때문이다.

fun greet() = "Hello"
println(greet())

val message: Int = greet() //ERROR : Type mismatch: inferred type is String but Int was expected

코틀린은 컨텍스트에 기반해 greet()의 리턴을 String이라고 결정했다.
Int형 변수인 message에 greet()의 결과를 할당하려 하면 미스매치 오류가 발생하는데 오류 결과를 통해 String으로 리턴타입 추론이 된 것을 확인 할 수 있다.

코틀린의 리턴타입 추론은 함수의 바디가 단일표현식이고 {}블록이 아닐 때만 가능하다.

fun greet(): String = "hello"

리턴타입을 명시하는것도 물론 가능하다.
리턴타입 앞에는 : 를 붙이고, 파라미터 리스트 뒤에 작성한다
return 키워드는 단일표현식 함수이고 바디가 블록이 아니라면 허용되지 않는다

모든 함수는 표현식이다

코틀린은 명령문 보다는 표현식을 좋아하고 그 원칙에 기반해서 함수는 명령문보다는 표현식으로 취급되어야 한다.

코틀린은 Java의 void 대신 Unit이라는 특별한 타입을 사용한다.
리턴할 게 없는 경우 Unit을 사용 할 수 있다.

fun sayHello() = println("Well, Hello")

val message: String =sayHello() //ERROR : Type mismatch: inferred type is Unit but String was expected

sayHello()의 결과를 String 타입 변수에 할당하면 오류가 발생한다.
오류 내용을 확인하면 타입 미스매치로 sayHello()의 리턴타입이 Unit 인걸 확인 할 수 있다.

입력

fun sayHello(): Unit = println("Well, Hello")
val message: Unit = sayHello()
println("The result of sayHello is $message")

💻 출력

Well, Hello
The result of sayHello is kotlin.Unit

Unit타입은 toString(), equals(),hashCode() 메소드를 가지고 있다.
위 코드의 출력내용을 보면 println()에서는 내부적으로 Unit의 toString()메소드를 호출한걸 알 수 있다.
Unit의 toString() 메소드는 kotlin.Unit이라는 클래스 이름만 리턴한다.

코틀린에서는 모든 함수들은 유용한 리턴을 준다.
void함수도 Unit을 리턴해주기 때문에 모든 함수가 표현식으로 취급될 수 있다.

파라미터 정의하기

코틀린은 함수나 메소드에 파라미터의 타입을 명시하도록 했다. 파라미터의 타입을 파라미터 이름 바로 뒤에 :로 구분해서 명시해 주는 것이다.

fun greet(name : String) = "Hello $name"
println(greet("Eve")) //Hello Eve

코틀린은 우리가 함수나 메소드 파라미터를 만들 때 이뮤터블이나 뮤터블을 선택하는 것을 원치 않는다.
파라미터를 val이나 var로 단정지을 수 없으며 함수나 메소드에서 파라미터의 값을 변화시키려는 시도는 컴파일 오류를 발생시킨다.

블록바디로 만든 함수

{ } 블록 바디를 이용햐서 함수를 정의하면 항상 리턴타입을 정의해줘야 한다.
정의하지 않는다면 리턴타입은 Unit으로 추론된다.

fun max(numbers: IntArray): Int {
    var large = Int.MIN_VALUE
    for (number in numbers) {
        large = if (number > large) number else large
    }
    return large
}

💡주의사항, =을 { } 블록 바디 대신 사용하면 안된다. 특정 리턴타입을 명시하고 = 을 사용한 뒤 { } 블록 바디를 사용한다면 컴파일러 오류가 발생한다.

리턴타입을 생략하고 단일표현식 대신 블록 바디를 사용하면 코틀린은 블록을 람다표현식이나 익명함수로 취급한다.

입력

fun f1() = 2
fun f2() = { 2 }
fun f3(factor: Int) = { n: Int -> n * factor }
println(f1())
println(f2())
println(f2()())
println(f3(2))
println(f3(2)(3))

💻 출력

2
() -> kotlin.Int
2
(kotlin.Int) -> kotlin.Int
6

f1()은 리턴타입을 Int로 추론했다
f2()은 파라미터가 없고 리턴타입이 Int인 람다표현식으로 추론했다
f3()은 Int를 파라미터로 하고 Int를 리턴타입으로 가지는 람다표현식으로 추론했다

이런 코드들은 좋은코드가 아니다.
=과 { } 블록 바디를 함께 사용하지 않는게 좋다는걸 기억하자

📌 기본 인자와 명시적 인자


기본 Argument를 통한 함수 변경

fun greet(name : String) = "Hello $name"
println(greet("Eve")) //Hello Eve

"Hello"라고 하드코딩 되어있는 greet()함수에 유연성을 제공하는 방법은 여러가지가 있다.

✔️ 함수에 새로운 파라미터 추가 => 그 전에 이미 함수를 호출하던 코드들은 오류가 날것이다.
✔️ 오버로딩을 사용 = > 코드의 중복 발생

⭕ 코틀린은 기본 아규먼트를 이용해 이런 문제를 쉽게 해결한다

fun greet(name: String, msg: String = "Hello"): String = "$msg $name"
println(greet("Eve")) //기존코드도 오류없이 작동
println(greet("Eve", "Howdy"))

greet()을 호출하는 코드는 name인자 하나만 넘겨도 상관없고 name, msg 인자를 모두 넘길 수도 있다.

fun greet(name: String, msg: String = "Hi ${name.length}"): String = "$msg $name"
println(greet("Scott", "Howdy"))
println(greet("Scott"))

위 코드는 전달받은 name 인자를 이용하여 msg의 기본값을 만든다.
여기서 name과 msg의 위치가 바뀌면 오류가 발생한다.
msg는 name 인자를 참고하지만 name은 초기화 되어있다 않기 때문이다.
이런 상황을 대비해 기본 아규먼트는 마지막에 위치시키는게 좋다.

명시적 아규먼트(Named Argument)를 이용한 기독성 향상

코드를 작성하는 건 한번이지만 그 코드를 업데이트 하는일은 여러번 발생하기 때문에 코드의 가독성은 매우 중요하다.

fun createPerson(name: String, age: Int = 1, height: Int, weight: Int) {
    println("$name $age $height $weight")
}

createPerson("Jake", 12, 152, 43)

두번째 코드만 봤을때 각 숫자들이 뭘 의미하는지 파악할수 없다

명시적 아규먼트는 코드의 가독성을 높여준다

createPerson(name = "Jake", age = 12, height = 152, weight = 43)

명시적 아규먼트를 통해 파라미터들이 어떤 의미인지 한번에 알 수 있다.

createPerson("Jake", age = 12, weight = 152, height = 43)
createPerson("Jake", height = 152, weight = 43)

명시적 아규먼트는 순서와 상관없이 사용할 수 있고 age는 기본값을 가지고 있기 때문에 전달하지 않아도 상관없다.

📌 다중인자와 스프레드


println()같은 함수는 여러 개의 인자를 받는다. 코틀린의 다중인다 기능은 함수가 한번에 여러 개의 인자를 받을 때 타입 안전성을 제공해주는 기능이다.

여러 개의 인자

코틀린 함수들은 많은 구의 인자를 받을 수 있다 호출할 때 좀 더 유연하게 사용할 수 있도록 max() 함수를 변경해 보자

fun max(vararg numbers: Int): Int {
    var large = Int.MIN_VALUE
    for (number in numbers) {
        large = if (number > large) number else large
    }
    return large
}

두 가지가 변경되었다

✔️ 파라미터 numbers가 vararg라는 키워드로 선언되었다.

✔️ 파라미터 타입이 IntArray에서 Int로 변경되었다.

println(max(1, 6, 3))//6
println(max(1, 6, 3, 9, 4, 12))//12

코드를 실행하면 아주 잘 작동한다
max()함수는 단 하나의 파라미터만을 취급하도로 정의되어있다. 하지만 vararg를 사용하면 하나 이상의 파라미터를 취급할수 있다.
단, vararg 키워드는 함수에서 하나의 파라미터에서만 사용할 수 있다.

입력

fun greetMany(msg: String, vararg names: String) {
    println("$msg ${names.joinToString(", ")}")
}
greetMany("Hello", "Tom", "Jerry", "Spike")

💻 출력

Hello Tom, Jerry, Spike

이 함수를 호출할 때, 첫번째 인자는 첫 번째 파라미터에 할당되고 나머지 인자들은 vararg 파라미터로 전달된다.

입력

fun greetMany(vararg names: String,msg: String) {
    println("$msg ${names.joinToString(", ")}")
}

greetMany("Hello", "Tom", "Jerry", "Spike")//ERROR : No value passed for parameter 'msg'

또한 vararg 파라미터는 마지막 파라미터로 사용하는것이 좋다
함수를 호출할 때 이름을 명시하지 않은 여러개의 String인자를 넘겨주면 컴파일러는 모든 인자들을 vararg 파라미터로 취급한다.

💡 vararg 를 마지막에 사용할때는 반드시 명시적 인자를 사용해야한다.

스프레드 연산자

max()를 다시보면 함수는 다중 인자를 받을 수 있도록 정의되어 있지만 배열이나 리스트를 직접 받을 수는 없다. 이럴때 스프레드 연산자를 사용한다

vararge는 하나의 파라미터에 많은 양의 인자를 넘길 수 있다는 뜻을 함축하고 있다. 하지만 배열을 인자로 넘기면 오류가 난다

val values = intArrayOf(1, 21, 3)
println(max(values)) //ERROR :Type mismatch: inferred type is IntArray but Int was expected

내부적으로 vararg 파라미터를 배열로 다루지만 코틀린은 인자로 배열을 넘기는 걸 좋아하지 않는다.

println(max(values[0], values[1], values[2]))

배열은 넘기기 위해서는 위에처럼 입력해야 하지만 이렇게 되면 코드가 너무 번거로워진다.

💡 이럴경우 스프레드 연산자 * 을 이용해서 배열을 넘길 수 있다.

println(max(*values))

배열은 스프레드를 직접 사용할 수 있지만 리스트에는 직접 스프레드를 적용할 수는 없다
리스트는 배열로 바꾸어준 후 스프레드를 적용한다.

println(max(*listOf(1, 4, 18, 12).toIntArray()))

vararg스프레드 연산자의 조합이 코드를 조화롭게 만드는걸 알 수 있다.

📌 구조분해


구조화란 다른 변수의 값으로 객체를 만드는 것이다
구조분해는 그 반대로 이미 존재하는 객체에서 값을 추출해 변수로 넣는 것이다.

코틀린의 구조분해는 속성의 이름이 아닌 속성의 위치를 기반으로 진행된다

입력

fun getFullName() = Triple("John", "Quincy", "Adams")

val result = getFullName()
val first = result.first
val middle = result.second
val last = result.third
println("$first $middle $last")

💻 출력

John Quincy Adams

함수의 리턴타입이 Triple,Pair 나 다른 데이터 클래스일 경우 구조분해로 명확하게 값을 변수에 할당할 수 있다.

val (first, middle, last) = getFullName()
println("$first $middle $last")

네 줄의 코드를 한 줄의 코드로 바꿨다

3개의 이뮤터블 값 first,middle,last 는 getFullName()함수가 리턴힌 Triple 프로퍼티 순서대로 값을 각각 할당받았다
이런 코드가 가능한 이유는 Triple 클래스가 구조변화를 위한 특별한 메소드를 가지고 있기 때문이다.

val (first,_,last)=getFullName()
println("$first $last")

필요없는 속성값이 있다면 언더스코어( _ )를 이용하며 해당 속성을 스킵할 수 있다.


🔑 정리


코틀린은 사용자가 메소드를 만들도록 강요하지 않는다.

코틀린의 기본인자 기능은 함수를 확장하기 쉽게 해주고 함수를 오버로드하는 일을 줄여준다.

vararg는 안전성을 제공하면서 여러 개의 인자를 넘기는 것을 아주 유연하게 해주고
스프레드 연산자는 vararg 파라미터에 배열을 넘기는 것을 쉽게 만들어 준다.

명시적 인자를 사용한다면 코드 자체가 문서화된다.

구조분해는 코드의 방해요소를 줄여주고 코드를 매우 간결하게 만들어 준다



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글