Strong 감자의 공부

Ch_04 함수 알아보기 본문

문법_Kotlin

Ch_04 함수 알아보기

ugyeong 2023. 8. 4. 17:20

목차 

1. 함수 알아보기

2. 익명함수와 람다표현식 알아보기

3. 함수 자료형 알아보기

 


1. 함수 알아보기

🔶 1.1 함수 정의와 실행 

코틀린은 유니코드문자를 지원하기 때문에 함수명, 변수명 등을 한글로 지정해도 된다.

fun main() {
    val 결과값 = 함수명("함수", "호출")
    println(결과값)
}

fun 함수명(매개변수명1: String, 매개변수명2: String): Pair<String, String> {
    val 지역변수1 = 100
    val 지역변수2 = 300

    fun 지역함수명(매개변수명: String): String {
        return "매개변수명"
    }

    class 지역클래스명 {}

    println("$지역변수1, $매개변수명2")
    return Pair(매개변수명1, 매개변수명2)
}
//결과값
100, 호출
(함수, 호출)

 

⚫ 반환값이 없는 함수 정의와 실행 

 

반환자료형 자리에 Unit

반환자료형 자리에 아무것도 표시하지 않으면 반환 자료형을 Unit으로 추론한다. 

fun main() {
    fun func(): Unit {
        println("특정 처리 기능이 없음")
    }
    func()
    fun funcNoReturn() {
        println("반환값 생략 가능")
    }

    funcNoReturn()
}
// 결과값 
특정 처리 기능이 없음
반환값 생략 가능

 

 🔶 1.2 함수 몸체부(블록) 처리

간단한 표현식만 있는 경우는 함수의 블록을 생략하고 한 줄로 작성하는 단일표현식 가능

 

⚫ 단일 표현식으로 대체

 

함수 코드 블럭이 하나의 라인으로 처리 될 경우 간략히 작성 가능 
= 연산자 다음에 표현식을 사용해서 처리한다. 보통 단일 표현식으로 함수 코드 블럭을 구성하면  반환 자료형을 생략한다. (이유: 단일표현식을 추론해 반환 자료형을 추론할 수 있으므로)

 

fun main() {
    fun add(x: Int, y: Int): Int {
        return x + y // 코드블록으로 처리
    }

    fun add1(x: Int, y: Int): Int = x + y // 코드블럭대신 = 사용
    fun add2(x: Int, y: Int) = x + y // 반환자료형 생략

    println(add(10, 20))
    println(add1(10, 20))
    println(add2(10, 20))
}
// 결과값 
30
30
30

 

⚫ 복잡한 단일 표현식 

 

조건문, object 표현식 등은 하나의 표현식으로 작성 가능. 특히 object 표현식은 복잡하게 작성되지만, 하나의 결과인 객체를 만들므로 단일 표현식으로 작성 가능.

fun main() {
    fun comSingle(x: Int, y: Int) = if (x > y) x else y // 단일 표현식사용
    val r = comSingle(20, 20)
    println(r) // 20

    open class People {
        fun hello() = println("Hello World")
    }

    fun comSingle1() = object : People() {} // People클래스를 상속한 익명 객체 반환
    val p = comSingle1()
    p.hello() // Hello World
}

+ 코틀린은 open 키워드를 사용하지 않고 상속받는 코드를 작성할 경우

This type is final, so it cannot be inherited from 에러 문구가 뜬다. 

open키워드가 없을 경우 다른 곳에서 상속받지 못하는 final클래스로 정의 되기 때문 

참고) https://androidtest.tistory.com/102

 

 🔶 1.3 함수의 매개변수(parameter)와 인자(argument)

함수를 식별하는 기준이 함수 시그니처(함수이름, 매개변수 자료형, 매개변수 개수)

인자(argument) : 함수 호출시 넣는 값

매개변수(parameter) : 함수 정의 시 넣는 값 

 

위치(position)인자 -> 아래 코드 참고

이름(named)인자 -> 아래 코드 참고

가변인자(variable argument) : 인자의 개수가 미정일때 사용, 가변 인자를 작성할 때는 매개변수 이름 앞에 vararg 예약어 사용. 가변인자는 이 매개변수 이름으로 여러 개의 원소를 가지는 배열로 처리한다. 

 

fun main() {
    fun addVar(x: Int, y: Int, z: Int) = x + y + z

    // 매개변수 위치에 따라 인자를 매핑
    println("위치인자=" + addVar(10, 20, 30))
    println("위치인자=" + addVar(20, 30, 10))

    // 매개변수 이름과 인자를 같이 처리
    println("이름인자=" + addVar(z = 10, x = 20, y = 30))
    println("이름인자=" + addVar(y = 10, z = 20, x = 30))

    // 혼합 인자
    println("인자혼합=" + addVar(30, z = 20, y = 30))
}
// 결과값
위치인자=60
이름인자=60
이름인자=60
인자혼합=80

 

⚫ 매개변수에 초깃값 지정

 

인자의 개수가 불일치 할 것을 대비해 함수를 정의 할 때 매개변수에 초기값을 지정할 수 있다. 

- 아래 예제에서 함수 호출 시 인자 2개를 전달하면 초기값은 전부 무시된다.

- 인자가 전달되지 않으면 초기값을 인자로 사용해 함수의 결과가 반환된다.

 

fun main() {
    fun defaultArg(x: Int = 100, y: Int = 200) = x + y

    println("위치인자 전부 전달" + defaultArg(300, 400))
    println("이름인자 전부 전달" + defaultArg(y = 400, x = 300))

    println("인자 전달 없음" + defaultArg())
    println("인자 하나 전달" + defaultArg(300))
    println("인자 하나 전달" + defaultArg(x = 300))
}
// 결과값
위치인자 전부 전달700
이름인자 전부 전달700
인자 전달 없음300
인자 하나 전달500
인자 하나 전달500

 

⚫ 가변인자 지정

 

가변인자(vararg)를 사용하면

함수의 인자가 고정되지 않아있다면 인자를 추가할때마다 함수를 정의하지 않아도 된다. 

아래 코드 ...1번에서 보이는것처럼 이 가변인자에 대한 변수의 자료형은 배열이고 실제 합산을 할려면 이 배열을 순환해서 모든 원소를 더해야한다. 

배열로 정의된 것을 가변인자로 처리하려면 *(스프레드 연산자)를 붙여 모든 원소가 전달 되어야 가변인자가 처리된다. 

->안붙이면

Type mismatch.
Required:Int
Found:IntArray 뜬다. 

주의 : 배열일 경우에만 이 연산자가 작동한다. 

다른 리스트를 쓰면 Required: IntArray라고 뜬다. 

fun main() {
    fun addVarArg(vararg x: Int): Int {
        var result = 0
        for (i in x) {
            result += i // ...1번
        }
        return result
    }
    println("가변인자 0개 " + addVarArg())
    println("가변인자 4개 " + addVarArg(1, 2, 3, 4))
    println("가변인자 6개 " + addVarArg(1, 2, 3, 4, 5, 6))

    val ll = intArrayOf(1, 2, 3, 4)
    println("스프레드 연산 사용 경우 : " + addVarArg(*ll))
}
// 결과값
가변인자 0개 0
가변인자 4개 10
가변인자 6개 21
스프레드 연산 사용 경우 : 10

 

 

⚫  함수 인자를 전달할 때의 주의 사항

 

코틀린은 가변 객체(코드3에서 m1)와 불변 객체(코드3에서 ll)가 있다. 가변객체를 함수의 인자로 전달하면 가변객체 내부의 값을 변경할 수 있다. 

리스트인 경우는 가변(코드3에서 m1)과 불변(코드3에서 ll)을 지정할 수 있다. 

함수를 정의할 때 매개변수에 가변 매개변수를 지정하면(코드에서 addList1) 가변 리스트를 인자로 전달할 경우 함수 밖에 있는 리스트도 같이 변경된다. 

함수 내부에서 외부값을 변경하지 않으려면 가변리스트를 복사해서 함수의 인자로 전달해야한다. 

아래 코드는 참조타입으로써 같은 것을 참조하고 있기 때문에 m2를 변경시  m1도 변경된다. 

 

코드1

val m2 = ml.toMutableList()

+리스트가 변화가 없을 것이라고 예상된다면 List를 사용하고

리스트의 원소를 추가하거나 삭제하거나 변화가 있다면 MutableList를 사용하면 된다.

 

✔️  변경 가능한 인자를 복사해서 전달

mutableList내에 복사함수가 없기 때문에 List로 변환하고 니서 다시 MutableList로 재변환하여 함수의 인자로 전달하기 

m1.toList() 로 변경 

 

-코드2

fun main() {
    val m1 = mutableListOf<Int>(1, 2, 3)
    println(m1)

    fun addList(m1: MutableList<Int>): MutableList<Int> {
        m1.add(5)
        return m1
    }

    val m2 = addList(m1.toList().toMutableList()) // 변경가능한 리스트를 복사해서처리
    println(m2)
    println(m1)
    
}
// 결과값 
[1, 2, 3]
[1, 2, 3, 5]
[1, 2, 3]

 

-코드3

fun main() {
    val ll = listOf(1, 2, 3, 4, 5) // 변경 불가능한 리스트 생성

    fun addList(ll: List<Int>): List<Int> {
        val result = ll + listOf(6, 7)
        return result
    }

    val ll2 = addList(ll)
    println(ll2 == ll) // 리스트 비교

    // 변경 가능한 리스트 생성
    val ml = mutableListOf(1, 2, 3, 4)

    fun addList1(ml: MutableList<Int>): MutableList<Int> {
        ml.add(5)
        return ml
    }

    val ml2 = addList1(ml)
    println(ml == ml2)
}
// 결과값
false
true

참고) https://notepad96.tistory.com/71

 

🔶1.4 지역변수, 지역함수와 변수 스코프

보통 함수가 정의 되면 지역변수를 관리하는 변수 스코프가 만들어짐. 해당변수가 없으면 상위에서 참조할 수 있는 변수 스코프를 조회.

 

⚫ 함수 내부에 내포된 변수, 함수, 클래스 정의

지역변수 : 초기값을 지정하지 않고 정의해도 컴파일 에러는 발생하지 않지만 꼭 초기화 해야한다.

지역함수 : 보통 클로저 환경을 구성하거나 세부 기능 분리 처리 시 사용 -> 2.3에서 다룹니다.

지역클래스 

 

+Closure 란? 

더보기

 

+참고) https://jizard.tistory.com/472

Kotlin에서 Closure는 상위 함수의 영역에 있는 변수에 접근할 수 있는 함수다. Kotlin은 Closure를 지원하므로 익명함수(Anonymous function)는 함수 밖에서 정의된 변수에 접근할 수 있다. 

var sum = 0
ints.filter { it > 0 }.forEach { sum += it }
print(sum)

Closure를 만드는 방법은 람다식(Lambda expression)이나 익명함수를 사용하면 된다. 람다식은 중괄호 안에 매개변수와 화살표(->) 그리고 함수 본문을 작성한다. 익명함수는 fun 키워드와 매개변수, 반환타입 그리고 함수 본문을 작성한다. 

val add: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val multiply = fun(x: Int, y: Int): Int { return x * y }

위의 코드에서 add는 람다식으로 정의된 Closure이고 multiply는 익명함수로 정의된 Closure이다. 두 함수 모두 outer scope에 있는 변수에 접근할 수 있다.

 

fun main() {
    fun outerFunc(x: Int): Int {
        val y = 100
        fun localFunc() = x + y
        return localFunc()
    }
    println("locaFunc실행 : " + outerFunc(100))
}
// 결과값 
locaFunc실행 : 200

 

렉시컬 스코프(lexical scope) : 함수가 계층구조로 정의되면 . 현재 지역에 없는 변수를 상위 레벨서 검색해서 사용하는것을 말함 .

 

⚫ 변수 스코프 : 전역과 지역처리 

 

변경할 수 있는 전역변수는 함수 내에서 변경 할 수 있음에 주의 

fun main() {
    var outVar = 300
    val outVarR = 400

    fun outerFun1(x: Int): Int {
        val y = 100
        fun localFun() = x + y + outVarR // 전역함수 사용
        return localFun()
    }

    fun outerFun2(x: Int): Int {
        val y = 100
        fun localFun2(): Int {
            outVar += x // 전역변수 갱신
            return outVar
        }
        return localFun2()
    }
    println("전역변수 참조 : " + outerFun1(100))
    println("전역변수 갱신 : " + outerFun2(100))
    println("전역변수 : " + outVar)
}
// 결과값
전역변수 참조 : 600
전역변수 갱신 : 400
전역변수 : 400

전역변수 참조 

fun main() {
    var outVar = 300

    fun outerFun1(x: Int): Int {
        fun outerFun2(): Int {
            fun localFun2(): Int {
                outVar += x // 전역변수 갱신
                println(x)
                println(outVar)
                return outVar
            }
            return localFun2()
        }
        return outerFun2()
    }
    println("전역변수 참조 : " + outerFun1(100))
    println("전역변수 : " + outVar)
}

// 결과값
100
400
전역변수 참조 : 400
전역변수 : 400

 

 

2. 익명함수와 람다 표현식 알아보기

🔶2.1 익명함수

익명 함수는 함수 정의와 동일하지만 함수의 이름을 가지진 않는다. 그래서 일회성으로 처리하는 용도로 사용

 

⚫ 익명함수 정의와 실행

익명함수는 함수 정의와 동일하게 작성. 차이점은 함수는 함수를 호출해서 실행하지만 익명함수 정의하면 바로 호출해서 실행가능 

- 정의하면 바로 실행하거나, 변수, 매개변수에 할당하거나 바로 반환값으로 할당해야한다. 

 

fun main() {
    // 즉시 익명함수 실행
    println((fun(매개변수1: Int, 매개변수2: Int): Int {
        return 매개변수1 + 매개변수2
    })(100, 200))

    // 변수에 익명함수 할당
    val 덧셈 = fun(매개변수1: Int, 매개변수2: Int): Int {
        return 매개변수1 + 매개변수2
    }
    val res1 = 덧셈(300, 200)
    println(res1)

    // 익명함수 즉시 실행
    val res2 = (fun(매개변수1: Int, 매개변수2: Int): Int {
        return 매개변수1 + 매개변수2
    })(500, 200)
    println(res2)
}
// 결과값
300
500
700

 

⚫ 익명함수 내부에  지역 익명함수 정의

 

익명함수도 함수코드블록 내부에 지역변수, 매개변수, 반환 자료형, 지역함수를 익명함수로 지정해 사용할 수 있다.

익명함수를 반환하는 경우는 함수의 반환 자료형을 함수 자료형으로 지정해줘야한다.

- 익명함수 내부에 익명함수를 지역함수로 정의가능

- 함수의 반환 자료형을 처리할 때 함수 반환가능 

- 함수의 매개변수로 함수 자료형을 지정하면 함수 호출시 익명함수를 인자로 전달 가능

- 함수 자료형 지정은 매개변수는 괄호 안에 표시하고 -> 다음에 반환값을 지정. 

 코틀린에서는 함수자료형을 인터페이스로 제공함. 

 

익명함수의 매개변수 자료형에 함수 자료형을 지정 후 익명함수를 함수의 인자로 전달 -> 함수 내부에서 실행후 결과 반환

fun main() {

    // 변수로 Int값 반환 -> Int
    val res3 = (fun(x: Int): (Int) -> Int { // 외부 익명함수
        val inner = fun(y: Int): Int { // 내부 익명 함수
            println("res3의 y: " + y)
            return x + y
        }
        return inner
    })(10)(20) // (외부 익명함수 인자, 내부 익명함수 인자)

    // 바로 Int값 반환 ->Int
    val res4 = (fun(x: Int): (Int) -> Int {
        return fun(y: Int): Int {
            return x + y
        }
    })


    // 함수를 매개변수로 받음
    val res5 = fun(x: Int, y: Int, f: (Int, Int) -> Int): Int {
        return f(x, y)


    }
    println(res3)
    println(res4(10)(20))
    println(res5(100, 200, fun(x: Int, y: Int): Int { return x + y }))
}
//결과값
res3의 y: 20
30
30
300

 

🔶2.2 람다 표현식

익명함수처럼 람다 표현식도 정의하면 바로 사용가능하다.

보통 함수의 인자나 반환시에 많이 쓴다.

🐽람다표현식 표기법

- { } 으로 시작하며 이 중괄호 안에 매개변수와 표현식을 작성\\\\

- {매개변수 -> 표현식} 이때 매개변수에 괄호 처리를 하지 않는다.

- 매개변수가 없을때는 생략하고 표현식만 작성한다. ⪼코드...1번

- 람다표현식을 작성하고 바로 실행하려면 람다표현식 다음에 호출연산자인 괄호()를 사용해서 매개변수에 해당 인자를 전달 ⪼코트...2번

- 람다 표현식에서는 기본적으로 return문을 사용할 수 없지만, 인라인 함수로 사용할 때만 return문을 사용할 수 있다. 그 이유는 실제 람다표현식 내부 로직이 호출된 곳에 코드로 삽입되기 때문. 

 

+인라인 함수란?

더보기

인라인 함수는 호출될 때 일반적인 함수의 호출 과정을 거치지 않고, 함수의 모든 코드를 호출된 자리에 바로 삽입하는 방식의 함수입니다.

이 방식은 함수를 호출하는 데 걸리는 시간은 절약되나, 함수 호출 과정으로 생기는 여러 이점을 포기하게 됩니다.

따라서 코드가 매우 적은 함수만을 인라인 함수로 선언하는 것이 좋습니다.

 

c++코드지만 설명이 좋아서 첨부 합니당 

http://www.tcpschool.com/cpp/cpp_cppFunction_inlineFunction

 

 

+인라인 고차함수에서만 람다표현식에 rerun이 가능한 이유

더보기
fun findAndPrintTen(numbers: List<Int>) {
    numbers.forEachB {
        if (it == 10) {
            println("find 10!!")
            return  // compile error : 'return' is not allowed here
        }
    }
}

파라미터로 받은 람다의 경우 호출하는 쪽에 inline되지 않는다. 람다로 받은 함수는 컴파일러에 의해 아무런 처리가 되지 않고, 컴파일 된 이후 Function  타입의 객체가 된다.

람다가 결국 Function 타입의 객체라면, 이 람다를 변수에 저장할 수도 있다는 뜻인데 이런 현상 때문에 return이 금지되는 것이다(inline 함수의 람다는 객체가 아니므로 변수에 저장 불가). 만약 람다를 외부에 저장해놓고, 자신을 호출한 함수의 context를 벗어난 곳에서 함수가 실행 된다면 예상치 못한 버그가 일어날 수 있기 때문이다. 위 코드의 경우에는 findAndPrintTen 함수가 이미 끝난 상태에서 어딘가에 저장된 람다가 다시 호출 되는 경우라고 볼 수 있다.

https://wooooooak.github.io/kotlin/2022/01/11/%EB%9E%8C%EB%8B%A4%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C%EC%9D%98return/

 

람다표현식에서 중간에 함수를 빠져나갈 경우 레이블된 return을 사용할 수 있다. 이때는 반환값이 아니라 현재 처리하는 람다표현식을 종료하는 조건으로만 사용한다.

 

+람다에서 return사용하기

더보기
람다식 함수명 라벨이름@ {
	return@라벨이름 
}

 

암묵적 라벨을 사용하면 아래와 같이 위의 코드를 바꿀 수 있다.

fun lambdaParam(a: Int, b: Int, lambda: (Int, Int) -> Unit){
	lambda(a, b)
}

fun func(){
	println("start of func")
	lambdaParam(13, 3) { a, b ->
		val result = a+b
		if(result > 10) return@lambdaParam
		println("result: $result")
	}
	println("end of func") 
}

출처) https://velog.io/@cgw0519/kotlin-%EB%9E%8C%EB%8B%A4-%ED%95%A8%EC%88%98%EC%97%90%EC%84%9C%EC%9D%98-return-%EC%82%AC%EC%9A%A9

 

- 람다표현식에서 매개변수가 하나만 있을경우 별도의 매개변수를 지정하지 않고 하나의 매개변수라는 의미로 it을 제공한다.

fun main() {
    // 인자가 없는 경우, {} 람다식 시작의미
    { println("아무인자가 없다") }() //...1번

    // 인자가 하나 있는 경우
    println({ x: Int -> x * x }(10)) // ...2번
    // 인자가 두개 있는 경우
    println({ x: Int, y: Int -> x * y }(10, 20))

    // 재사용하려면 변수에 할당
    val a = { x: Int, y: Int -> x + y }
    println(a(100, 200))

    // 함수를 매개변수로 가짐: f
    fun func(x: Int, y: Int, f: (Int, Int) -> Int): Int {
        return f(x, y)
    }
    // 인자로 람다표현식 전달
    println(func(100, 200, { x, y -> x + y }))
}
// 결과값
아무인자가 없다
100
200
300
300

 

⚫ 람다 표현식과 익명함수 비교

 

익명함수는 람다표현식보다 내부코드가 많아지거나 재사용할때 사용한다.

 

⚫ 람다표현식 활용

fun main() {
    // 함수의 반환값을 람다로
    fun ret(): () -> Int {
        return { 100 }
    }

    val r = ret()
    println(r())

    val ll = listOf(1, 2, 3)
    println(ll.map { it * it })

    class LL(val action: (x: Int) -> Int)

    val l = LL({ it * 2 })
    print(l.action(10))

}
// 결과값
100
[1, 4, 9]
20

 

🔶2.3 클로저

내부함수는 외부함수의 지역변수를 사용할 수 있다. 반환된 내부함수가 실행될 동안 외부함수의 지역변수를 계속 사용한다. 이때 외부함수의 지역변수를 자유변수, 이런 환경을 클로저라고 한다.

 

fun main() {
    fun outer1(x: Int): Int { // 내부함수를 익명함수로 처리
        return (fun(y: Int): Int {
            return x + y
        })(10)
    }
    println(outer1(10))

    fun outer2(x: Int): Int {
        return { y: Int -> x + y }(10) // 내부함수를 람다처리후 즉시실행
    }
    println(outer2(10))
}
// 결과값
20
20

 

 

지역함수가 일반함수 일때는 바로 반환할 수 없다. 

1. 리플렉션 함수 참조사용하기 => 함수를 반환하는 것은 함수를 실행한 결과가 아닌 함수 자체를 반환하는 것이다.

fun main() {
    // 반환값이 함수 참조
    fun outer1(x: Int): (Int) -> Int {
        fun inner(y: Int): Int {
            println("y:" + y) // y:15
            return x + y
        }
        return ::inner
    }

    fun outer2(x: Int): (Int) -> Int {
        fun inner(y: Int) = x + y

        return ::inner
    }
    val inner1 = outer1(10)(15)
    val inner2 = outer2(10)(15)
    val inner3 = outer2(10)
    println(inner1) // 25
    println(inner2) // 25
    println(inner3(11)) // 21

}
fun main() {
    // 반환값이 람다 표현식
    fun outer4(x: Int): (Int) -> Int {
        return { y: Int -> x + y }
    }

    // 반환값이 익명함수
    fun outer5(x: Int): (Int) -> Int {
        return fun(y: Int) = x + y
    }
    println(outer4(10)(20)) // 30
    println(outer5(30)(40)) // 70

}

 

⚫ 렉시컬 스코핑

 

내부함수는 외부함수의 스코프를 참조할 수 있으므로 스코프 계층이 생긴다. 이런 계층에서 변수를 검색하는 방법을 렉시컬 스코핑이라고 한다.

- 렉시컬 스코프 처리 기준: 항상 자기 내부를 조회하고 없으면 외부 함수를 조회하고 없으면 전역이 패키지 변수를 찾는다.

- 보통 local -> global ->built-in 순으로 변수를 검색한다. 

 

아래 코드는 외부함수의 지역변수인 매개변수를 람다표현식 내부에서 사용하는 클로저환경이 구성되었다.

fun main() {
    fun Len(length: Int): (String) -> Boolean {
        return { input: String -> input.length == length }
    }

    val len1 = Len(4)
    println(len1("한여름 매우 덥다")) // false
    println(len1("스터디날")) // true

    println(Len(5)("요를레이요")) // true
}

 

3. 함수 자료형 알아보기

🔶 3.1 함수 자료형 정의 

- 변수에 자료형은 클래스와 인터페이스가 다 가능하다.

- 함수 자료형은 함수이름 매개변수를 제외한 함수 시그니처로 작성가능하다. 예) (Int)->Unit

(매개변수) -> 반환 자료형

- 반환자료형엔 () 사용x 

- 매개변수 자료형에 함수가 전달될때는 함수 자료형을 지정

- 보통 반환 자료형이 함수일 때도 함수 자료형을 지정 ->⚫ 내부함수를 반환하는 함수를 변수에 할당에서 다룸 

- 변수에 함수가 할당될때엔ㄴ 익명함수, 람다표현식, 함수 참조에서 정의된 것을 확인하고 타입추론이 가능하다.

(⚫ 변수에 함수 자료형 처리 코드참고 )

 

⚫ 변수에 함수 자료형 처리 

 

- 함수 자료형에 인자가 없을때도 괄호를 반드시 표기한다. -> 매개변수가 없다는 것을 명확히 표기하기 위해

fun main() {
    val a: () -> Unit = { println("함수") }
    val b: (Int) -> Int = { x -> x * 3 }
    val c: (Int, Int) -> Int = { x, y -> x + y }

    a() // 함수
    println(b(10)) // 30
    println(c(10, 20)) // 30
}

 

익명함수 사용시

fun main() {
    val a: () -> Unit = fun() { println("함수") }
    val b: (Int) -> Int = fun(x: Int): Int { return x * 3 }
    val c: (Int, Int) -> Int = fun(x:Int, y:Int): Int { return x + y }

    a() // 함수
    println(b(10)) // 30
    println(c(10, 20)) // 30
}

 

함수참조를 사용해 변수 할당

fun main() {
    fun Unit() {
        println("함수")
    }

    fun Unit2(x: Int): Int {
        return x * 3
    }

    fun Unit3(x: Int, y: Int): Int {
        return x + y
    }

    val a: () -> Unit = ::Unit
    val b: (Int) -> Int = ::Unit2
    val c: (Int, Int) -> Int = ::Unit3

    a() // 함수
    println(b(10)) // 30
    println(c(10, 20)) // 30
}

 

⚫ 내부함수를 반환하는 함수를 변수에 할당

 

함수의 반환값이 함수라서 함수 자료형을 지정

아래 코드에서 람다 표현식 내에 람다표현식이 정의 되어있다. 

즉 람다표현식으로 외부 함수를 정의하고 내부함수로 람다 표현식을 작성. -> 변수에 할당할때 반환자료형이 함수자료형으로 정의 되어야함.

첫번째 -> 의 오른쪽이 하나의 자료형으로 묶어서 처리

보통 타입 추론이 되어 변수에 자료형이 지정하지 않고 람다표현식을 정의해도 함수 자료형을 변수에 지정 가능.

 

 

fun main() {
    // {x->{x->x}}느낌
    val p1: () -> () -> Unit = {
        { println("함수전달") }
    }
    val p2 = { { println("함수전달2") } }

    fun f1(): () -> Unit {
        return { println("함수전달3") }
    }

    val p3: () -> () -> Unit = ::f1
    p1()()
    p2()()
    p3()()

}
//결과값 
함수전달
함수전달2
함수전달3

 

🔶 3.2 널이 가능한 함수 자료형 정의 

코틀린은 널이 불가능한 자료형을 확장해서 널이 가능한 자료형으로 만들 수 있다

함수자료형에 널이 가능한 자료형으로 지정해서 처리하는 방법 알아보기 

 

▪️ 널러블 함수의 자료형: (함수자료형)? 예) action: ( ()->Unit )?

▪️ 널러블 함수 자료형을 갖은 함수 호출 : 함수명?.invoke(인자)

널이 들어온 경우 안전호출을 처리하기 위해 invoke메서드로 처리한다. 

 

⚫ 매개변수에 Nullable함수 자료형

 

인자가 함수 자료형이 하나이거나 마지막 인자이면 람다표현식을 전달할 때 괄호()를 생략해서 처리할 수 있다. 코드에서...1번

다음의 코드는 함수 인자로 널, 람다표현식, 익명함수, 함수참조가 모두 처리되는 것을 볼 수 있다.

fun main() {
    // 함수 자료형도 널 자료형 가능 
    fun nullF(action: (() -> Unit)?): Long {
        val start = System.nanoTime()
        action?.invoke()
        return System.nanoTime() - start
    }

    println(nullF(null)) // null전달
    println(nullF { println("Hello World") }) // 람다함수 전달...1번 
    println(nullF(fun(): Unit { println("익명함수") })) // 익명함수 전달

    fun unitFunc() = println("함수처리")
    println(nullF(::unitFunc)) // 함수 참조 전달

}
// 결과값
400
Hello World
720901
익명함수
81301
함수처리
69100

 

 

⚫ 함수 반환을 널러블 함수 자료형 처리

 

함수의 반환 자료형을 널러블 함수자료형으로 정해 null반환 처리가 가능하다. 

 

🔶 3.3 호출메서드(invoke)

위에서는 nullable함수 자료형을 처리할 때 호출연산자 대신 invoke 메서드로 처리하는 방법을 알아봤고 

실제 호출가능한 함수, 생성자, 메서드 등은 이 invoke메서드를 사용한다. 

 

⚫ 함수에서 호출연산자 사용

 

일반함수를 정의하려면 호출연사자인 소괄호를 사용해서 처리한다. 하지만 함수참조로 함수를 조회하려면 호출 메서드인 invoke를 사용한다. 

아래 코드처럼 람다표현식과 익명함수로 정의해서 변수에 할당한 경우는 바로 호출메서드 invoke를 사용할 수 있다. 코드...1, 2 번

일반함수로 정의한 경우는 호출연산자만 사용할 수 있다. 코드...3번

일반함수도 함수참조로 가져와 변수에 할당한 다음 호출 메서드인 invoke를 사용할 수 있다.  코드...4번

fun main() {
    val toUpperCase = { str: String -> str.uppercase() } // ...1번
    println(toUpperCase.invoke("summer"))

    val upper = fun(str: String): String { // ...2번
        return str.uppercase()
    }
    println(upper.invoke("fall in love"))

    fun toLowerCase(str: String): String { // ...3번
        return str.lowercase()
    }
    println(toLowerCase("SUMMER"))

    val lower = ::toLowerCase // ...4번
    println(lower.invoke("Summer"))
}
// 결과값
SUMMER
FALL IN LOVE
summer
summer

 

⚫ 클래스에 연산자 오버로딩

 

아래 코드에서 함수 자료형과 Function<Unit>인터페이스를 받아서 클래스를 정의했다. 항상 호출 메서드 invoke를 재정의해야한다.

인터페이스로 재정의할 때는 인터페이스에 invoke메서드가 정의 되어있지 않아 override 없이 정의했다. 객체를 만들어서 실행하면 함수처럼 호출되는 객체가 만들어진다. 

 

 ▪️함수자료형 구분하기

- 함수 자료형 표기법 : 예) (Int) -> Unit 

-함수 인터페이스 표기법 : 코틀린 패키지 내부에 함수 자료형과 일치하는 인터페이스를 제공

예) Function<Int, Int> 매개변수1개와 반환값 1개를 꺽쇠 내부에 표시 

 

fun main() {
    class MyFunction : () -> Unit {
        override fun invoke() {
            println("실행연산자 실행1")
        }
    }

    val function = MyFunction()
    function()

    class A : Function<Unit> {
        operator fun invoke() {
            println("실행연산자 실행2")
        }
    }

    val functionA = A()
    functionA()
}
//결과값 
실행연산자 실행1
실행연산자 실행2

 

⚫ object 정의와 표현식으로 호출연산자 처리

 

object로 정의되면 하나의 객체만 만들므로 클래스에서 정의하는 것보다 더 많이 사용된다. 

아래 코드에서 첫 번째 object는함수자료형을 상속받아 invoke메서드를 재정의했다.

두 번째는object 정의에 함수 자료형을 받고 invoke 메서드를 재정의했다.

 

object Fun : (Int, Int) -> Int {
    override fun invoke(x: Int, y: Int): Int {
        println("객체 invoke호출2")
        return x * y
    }
}

fun main() {
    val a = object : (Int, Int) -> Int {
        override fun invoke(x: Int, y: Int): Int {
            println("객체 invoke호출 1")
            return x + y
        }
    }

    val x = a(10, 20)
    println(x)

    val y = Fun(10, 20)
    println(y)
}
// 결과값
객체 invoke호출 1
30
객체 invoke호출2
200

 

🔶 3.4 함수 오버로딩  

함수 이름, 매개변수 개수, 매개변수(들)의 자료형이 하나라 다른경우 재정의 가능하다. 

 

fun main(){
    fun text1(a: String, b:String?=null){
        println("test1")
    }
    fun text2(a: String, b:String?=null){ // 이름만 다름
        println("test2")
    }
    fun text1(a: String, b:String){ // 자료형 다름
        println("test3")
    }
    text1("a")
    text1("a","b")
    text2("a")
}
// 결과값
test1
test3
test2

 

⚫ 초깃값과 가변인자로 함수 오버로딩 줄이기

 

함수 오버로딩을 줄이는 방법으로는 함수를 정의할 때 초깃값을 사용하거나

매개변수를 가변인자(여러개의 매개변수가 같은 자료형일때)로 변경해서 작성하면 된다.

fun main() {
    fun test3(a: Any): String = "매개변수하나"
    fun test3(a: String, b: String = "hello"): String = "매개변수 두개"
    fun test3(vararg a: String): String = "가변매개변수"

    println(test3("a", "b", "c"))
    println(test3("a", "b"))
    println(test3("100"))
    println(test3(100))
}
// 결과값 
가변매개변수
매개변수 두개
매개변수 두개
매개변수하나