Kotlin-In-Action | #5. 람다로 프로그래밍

보람·2022년 5월 4일
0

Kotlin-In-Action

목록 보기
6/12

람다

  • 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.
  • 컬렉션 처리에 자주 사용

람다 식과 멤버 참조

코드 블록을 함수 인자로 넘기기

  • 람다를 메서드가 하나뿐인 무명 객체 대신 사용 가능
button.setOnclickListener { /*클릭시 수행 동작*/ }

람다와 컬렉션

  • 자바 8 이전에는 컬렉션 라이브러리가 적어서 필요한 기능을 직접 작성
    • 코드가 굉장히 길어지고 실수가 생기면 결과가 달라질 가능성이 크다.

컬렉션 검색

println(people.maxBy { it.age }) //람다 사용
println(people.maxBy { p -> p.age })
println(people.maxBy(Person::age)) //멤버 참조 -> 이해하기 쉬움

멤버참조

  • 위 예제의 3번째 라인처럼 ::를 사용하는 식을 멤버 참조라고 부른다.

    Person::age
    클래스 멤버

최상위 함수 참조
fun salute() = println("Salute!") //최상위 함수 

>> run(::salute) //>> Salute!
확장 함수도 멤버함수와 동일하게 참조 가능
fun Person.isAdult() = age >= 21 //확장함수 isAdult()
val predicate = Person::isAdult

람다 식의 문법

{ x : Int, y : Int -> x + y }
<--파라미터---><-본문->

  • 항상 중괄호로 둘러싸임
  • 람다가 저장된 변수를 일반 함수처럼 사용 가능
val sum = { x: Int, y: Int -> x+y }
println(sum(1,2)) //변수에 저장된 람다를 함수처럼 호출

run 사용

run { println(42) }
  • 위 예제는 람다 본문에 있는 코드를 변수에 저장 없이 실행한다.
    • 코드의 일부분을 블록으로 둘러싸 실행할 필요 X -> run 사용

람다 파라미터 타입 제거

people.maxBy { p:Person -> p.age } //파라미터 타입 명시
people.maxBy { p -> p.age } //파라미터 타입 생략(컴파일러가 추론)
people.maxBy { it.age } //단일파라미터라면 타입생략&it사용
  • 처음에는 타입을 쓰지 않고 람다를 작성후 컴파일러가 타입을 모르겠다고 하는 경우에만 타입을 명시하는 것이 좋다.
  • 람다식의 파라미터가 하나뿐이라면 디폴트 파라미터 이름인 it 사용 가능

여러줄 람다 가능

val sum = { x:Int, y:Int ->
	println("sum of $x and $y.....")
    x+y //실제 람다 결과
}
  • 결과는 마지막 줄

현재 영역에 있는 변수 접근

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
	var cnt = 0;
    messages.forEach { //각 원소에 대해 수행할 작업을 람다로 받는다.
        cnt++ //람다 밖에서 선언된 변수까지 변경 가능 -> 람다가 포획한 변수
        println("$prefix $it") //람다 안에서 함수의 "prefix" 파라미터 사용 가능!!
    }
}
  • 람다를 함수 안에 정의 하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 모두 사용 가능
  • 람다 바깥의 변수도 변경 가능 -> 람다가 포획한 변수

컬렉션 함수형 API

  • 함수형 프로그래밍(코틀린🌝) 스타일을 사용하면 컬렉션을 다룰 때 편리
  • 대부분 작업에 라이브러리 함수를 활용 -> 간결한 코드 가능

여러가지 유용한 함수를 소개합니다!

filter & map 은 필수

val people = listOf(Person("Alice", 29), Person("Bob", 31))

filter : 컬렉션에서 원치 않는 원소 제거, 원소 변환 불가능

println(people.filter{it.age>30})
//[Person(name=Bob, age=31)]

map : 새 컬렉션을 만듦

원하는 값으로 원소 변경

val list = listOf(1, 2, 3, 4)
println(list.map { it * it })  //제곱구하기
//[1, 4, 9, 16]

리스트 변환 가능

println(people.map{it.name}) 
//[Alice, Bob] //사람의 리스트 -> 이름의 리스트로 
//people.map(Person::name) 도 가능

filter & map -> filter가 우선적인게 더 좋음

people.filter { it.age > 30 }.map(Person::name)
//30살 이상인 사람의 이름 출력
  • map을 먼저 사용한 후 filter를 사용하면 굳이 구하지 않아도 되는 값의 map을 해야 해서 불합리한 계산식이 될 때가 있음
  • filterKeys, mapKeys : 키 걸러내거나 변환
  • filterValues, mapValues : 값을 걸러내거나 변환

all, any, count, find

  • 컬렉션에 술어 적용
  • all : 컬렉션 모든 값이 조건을 만족하니?
  • any : 컬렉션중 조건을 만족하는 값이 하나라도 있니?
  • count : 조건을 만족하는 컬렉션 수
    • size를 사용하게 되면 중간컬렉션이 생기기 때문에 count가 훨씬 더 효율적
  • find : 컬렉션 중 조건을 만족하는 가장 첫번째 값(firstOrNull)

groupBy

  • 리스트를 여러 그룹으로 이뤄진 맵으로 변경
val list = listOf("a", "ab", "b")
println(list.groupBy(String::first) //확장함수 first 참조 
//{a=[a, ab], b=[b]} //문자의 첫 문자열을 기준으로 그룹

flatMap, flattern : 중첩된 컬렉션 안의 원소 처리

flatMap

  • 컬렉션의 모든 객체를 map 하고 얻어지는 여러 리스트를 한 리스트로 모음
/**
 * class Book
 * 책 이름 
 * 책을 쓴 저자는 1명 이상
 */
class Book(val title: String, val authors: List<String>)

fun main(args: Array<String>) {
    val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
                       Book("Mort", listOf("Terry Pratchett")),
                       Book("Good Omens", listOf("Terry Pratchett",
                                                 "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet()) //books 컬렉션에 있는 책을 쓴 모든 저자의 집합
    //toSet 은 중복 제거
}

flatten : 변환X, 리스트의 리스트 펼치기

    val ary = arrayOf(
        arrayOf(1),
        arrayOf(2, 3),
        arrayOf(4, 5, 6)
    )

    println(ary.flatten()) // [1, 2, 3, 4, 5, 6] 있는 내용 펼치기

지연계산(lazy) 컬렉션 연산

asSequence

  • map&filter는 결과 컬렉션을 즉시 생성 (매단계마다 중간결과컬렉션 존재)
  • 시퀀스는 중간 임시 컬렉션 사용X & 연산 연쇄 가능
    • 시퀀스 원소는 중간저장X, 필요시 계산
    • 큰 컬렉션 계산시 시퀀스 사용 권장
      • 원소가 많을 수록 중간 저장 비용이 커짐
listOf(1, 2, 3, 4).asSequence() //원본 컬렉션을 시퀀스로 변환
        .map { print("map($it) "); it * it } //시퀀스도 컬렉션과 똑같은 API 제공
        .filter { print("filter($it) "); it % 2 == 0 }
        .toList() //결과 시퀀스를 다시 리스트로 변환

시퀀스 연산 실행 : 중간 연산, 최종연산

sequence.map{}.filter{}.toList()
중간연산-------------->최종연산

  • 중간 연산 : 다른 시퀀스 변환
    • 항상 지연 계산
    • 최종 연산 호출시 적용
  • 최종 연산 : 결과 반환
println(listOf(1,2,3,4).asSequence().map{it*it}.find{it>3})
//1*1 = 1
//2*2 = 4 > 3 --> 연산 종료(지연계산임)
  • 위 예제에서는 3,4는 아예 실행되지 않는다.

수신 객체 지정 람다

with

  • 수신객체를 지정해서 연산에 사용
  • 어떤 객체에 대한 참조를 반복해서 언급X, 그 객체의 메서드를 호출 가능
fun alphabet() = with(StringBuilder()) {//메서드를 호출하려는 수신 객체를 지정
    for (letter in 'A'..'Z') {
        append(letter) //this.append(letter) 과 동일한 문장
    }
    append("\nNow I know the alphabet!") //여기도 this 생략
    toString() //여기도 this 생략 //실제 리턴 값
}

apply

  • 항상 자신에게 전달된 수신객체를 반환한다.
  • 어떤 객체라도 빌더 스타일의 API를 사용해 생성하고 초기화 가능
  • 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 하는 경우 유용
fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}
  • buildString함수는 StringBuilder를 활용해 String을 만드는 경우 사용할 수 있는 방법
profile
백엔드 개발자

0개의 댓글