5.0 람다식
- 람다식 (람다) : 다른 함수에 넘길 수 있는 작은 코드 조각
- 람다의 특징
- 쉽게 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있음
- 코틀린 표준 라이브러리에서 많이 사용함
- 컬렉션 처리 대치
- 수신 객체 지정 람다 : 람다 선언을 둘러싸고 있는 환경과는 다른 상황에서 람다 본문을 실행할 수 있음
5.1 람다 식과 멤버 참조
5.1.1 람다 소개 : 코드 블록을 함수 인자로 넘기기
- 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야하는 경우
- 자바에서는 무명 내부 클래스 이용
- ⇒ 단점 : 번거로움
- ⇒ 예제) 무명 내부 클래스로 리스너 구현하기
-
/*자바*/ button.setOnClickListener(new onClickListener(){ @Override public void onClic(View view){ //클릭시 수행할 동작 } });
- 함수형 프로그래밍에서는 함수를 값처럼 다뤄서 해결
- ⇒ 람다식을 사용하면 코드블록을 직접 함수의 인자로 전달 할 수 있음
- ⇒ 예제) 람다로 리스너 구현하기
-
button.setOnClickListener{/*클릭시 수행할 동작*/}
- 메소드가 하나 뿐인 무명 객체 대신 람다를 사용할 수 있음
- 자바에서는 무명 내부 클래스 이용
5.1.2 람다와 컬렉션
- 사람의 이름과 나이를 저장하는 Person 클래스 예제
- 사람들로 이뤄진 리스트, 그중 연장자 찾기
- 람다 없이 구현 ⇒ 반복문 사용
data class Person(val name: String, val age:Int)
fun main() { val people = listOf(Person("Alice", 29), Person("Bob", 31)) findTheOldest(people) } fun findTheOldest(people: List<People>) { var maxAge = 0; var theOldest: Person? = null for(person in people){ if(person.age > maxAge){ maxAge = person.age theOldest = person } } println(theOldest) }
- 더 좋은 방법 : 라이브러리 함수 활용
- 모든 컬렉션에 대해 maxBy 함수 호출 가능
- maxBy : 가장 큰 원소를 찾기 위해, 인자로 비교에 사용할 값을 반환하는 함수를 받음
- {it.age} : maxBy에서 비교에 사용할 값을 반환하는 함수 (it은 컬렉션의 원소)
- 예제에서 컬렉션의 원소 : Person 객체 ⇒ 함수가 반환하는 값은 Person 객체의 age 필드에 저장된 나이 정보
val people = listOf(Person("Alice", 29), Person("Bob", 31)) println(people.maxBy{it.age})//멤버 참조로 바꾸어 사용할 수 있음 //출력 결과 : Person(name=Bob, age = 31)
- 멤버 참조를 사용해 컬렉션 검색하기
people.maxBy(Person::age)
5.1.3 람다식의 문법
- 람다 : 값처럼 여기저기 전달할 수 있는 동작의 모음
- 람다식 문법
- 항상 중괄호 사이에 위치
- 매개변수 목록 주변에 괄호가 없음
- → : 매개변수 목록과 본문을 구분해줌
-
{x: Int, y: Int -> x + y}
- 람다식 변수에 저장
-
val sum = {x: Int, y: Int -> x+y} println(sum(1,2)) //람다식 직접 호출 //{println(42)}()
-
- run : 인자로 받은 람다를 실행해주는 라이브러리 함수
-
run{println(42)}
-
❗매개변수 (parameter) : 함수 정의에 나열되어있는 변수
❗전달 인자, 인자 (argument) : 함수 호출시 전달되는 값
- 연장자 예제 정식으로 람다식 작성해보기
- p라는 Person 객체를 파라미터로 받아서 p의 age를 반환해줌
-
val people = listOf(Person("Alice", 29), Person("Bob", 31)) //println(people.maxBy{it.age}) people.maxBy({p:Person->p.age}) //출력 결과 : Person(name=Bob, age = 31)
- 위의 코드 개선시키기
- 코틀린에서 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면 그 람다를 괄호 밖으로 빼낼 수 있다. (문법 관습)
- 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출시 빈 괄호를 없앨 수 있음
- 둘 이상의 람다를 인자로 받는 함수라도 인자 목록의 맨 마지막 람다만 밖으로 빼낼 수 있음
- 컴파일러가 람다 파라미터의 타입 추론 가능함 ⇒ 파라미터 타입 생략 가능
- 람다를 변수에 저장할 때는 타입 추론 불가능 ⇒ 파라미터 타입 명시
-
val getAge = {p: People -> p.age} people.maxBy(getAge)
- 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 it을 바로 쓸 수 있음
- people.maxBy({p:Person->p.age}) //람다가 유일한 인자이며 마지막 인자다.
- people.maxBy(){p:Person->p.age} //람다를 괄호 밖으로 빼냄
- people.maxBy{p:Person->p.age} //빈괄호 없앰
- people.maxBy{p ->p.age} //파라미터 타입 생략
- people.maxBy{it.age} //디폴트 파라미터 이름 it 사용
- 무조건 간단해진다고 가독성이 좋아지고 효율적인 코드가 되는 것은 아님
- 아래 코드에서 윗줄이 더 간결하지만 람다의 용도를 알기 어려움
- 상황에 따라 적절하게 람다를 괄호 밖으로 빼내야 함
people.joinToString(" "){p: Person -> p.name} //people.joinToString("", {p:Person->p.name})
5.1.4. 현재 영역에 있는 변수에 접근
- 람다를 함수 안에서 정의하면 함수의 파라미터와 로컬 변수를 람다에서 모두 사용 가능
- 함수 파라미터를 람다 안에서 사용하기
-
fun printMessagesWithPrefix(messages:Collection<String>, prefix: String){ message.forEach{ // 각 원소에 대해 수행할 작업을 람다로 받음 println("$prefix $it") // 람다 안에서 함수의 파라미터 사용 } }
-
- 람다 안에서 바깥 함수의 로컬 변수 변경하기
-
fun printProblemCounts(responses: Collection<String>){ var clientErrors = 0 var serverErrors = 0 responses.forEach { if(it.startsWith("4")){ clientErrors++ // 람다 안에서 람다 밖의 변수 변경 } else if(it.startWith("5")){ serverErrors++ // 람다 안에서 람다 밖의 변수 변경 } } println("$clientErrors client errors, $serverErros server errors") }
-
5.1.5. 멤버 참조
- 이중 콜론(::)을 사용하는 식
- 프로퍼티나 메소드를 단 하나만 호출하는 함수값을 만들어줌
- 이중 콜론(::)은 클래스 이름과 참조하려는 멤버 이름 사이에 위치함
- 멤버 참조 뒤에는 괄호를 넣으면 안된다. (참조대상이 함수인지 프로퍼티인지와 관계 x )
Person::age
//Person은 클래스 , age는 참조하려는 멤버
//val getAge = {person: Person -> person.age} //멤버 참조와 같은 역할을 하는 코드
5.2 컬렉션 함수형 API
5.2.1. 필수 적인 함수: filter와 map
- Person 클래스
-
data class Person(val name: String, val age: Int)
-
- filter 함수
- 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
- 컬렉션에서 원치않는 원소를 제거함
val list = listOf(1,2,3,4) println(list.filter{it%2 == 0})
- map 함수
- 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만듦
- 원소를 변환할 수 있음
val list = listOf(1,2,3,4) println(list.map{it*it})
5.2.2. all, any, count, find: 컬렉션에 술어 적용
- all 함수 : 모든 원소가 해당 조건에 만족하면 true, 아니면 false를 반환함
- any 함수 : 조건에 만족하는 원소가 하나라도 있다면 true 반환 아니라면 false 반환함
- count 함수 : 조건을 만족하는 원소의 개수를 반환함
- find 함수 : 조건을 만족하는 첫 번째 원소를 반환함
5.2.3. groupBy : 리스틀르 여러그룹으로 이뤄진 맵으로 변경
- groupBy : 특성을 파라미터로 전달하면 컬렉션을 구분해주는 함수
-
val people = listOf(Person("Alice", 31), ... Person("Bob", 29), Person("Carol", 31)) println(people.groupBy{it.age})
-
- groupBy 연산 결과 : 원소를 구분하는 특성이 key, key 값에 따른 각 그룹이 값인 맵
5.2.4. flatMap과 flatten: 중첩된 컬렉션 안의 원소 처리
- flatMap : 모든 원소로 이루어진 단일 리스트를 반환, 내용의 변환이 필요할 때 사용
- flatten : 모든 원소로 이루어진 단일 리스트를 반환, 내용의 변환이 필요없을 때 사용
5.3 지연 계산(lazy) 컬렉션 연산
- 앞서 배운 컬렉션 함수들은 리스트를 반환함 == 연산할 때 새로운 리스트가 생겨남
-
people.map(Person::name).filter{ it.startsWith("A") } //각 연산이 컬렉션을 직접 사용 //즉, 두개의 리스트(임시 컬렉션)가 새로 생겨남
-
- 시퀀스를 사용하면 계산 중간에 임시 컬렉션을 사용하지 않고 컬렉션 연산을 여러번 이어 할 수 있음
- 결과는 같지만 성능이 더 좋아짐
people.asSequence() //원본 컬렉션을 시퀀스로 변환 .map(Person::name) .filter{ it.startsWith("A")} // 시퀀스도 컬렉션과 같은 API 제공함 .toList() //시퀀스를 다시 리스트로 변환
- Sequence 참고 링크
-
data class Person(val name: String, val age: Int) fun main() { val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Haeun", 23)) println(people.asSequence().map(Person::name) .filter{ it.startsWith("A")}.toList()) }
- 시퀀스 : 순차적인 컬렉션으로 요소의 크기를 특정하지 않고 나중에 결정할 수 있는 특수한 컬렉션, iterator를 통해 원소에 접근해야함
- asSequence 확장함수를 호출해서 어떤 컬렉션이든 시퀀스로 바꿀 수 있음
- toList() : 시퀀스를 리스트로 만들 때 사용하는 함수
- 지연 계산 후 Sequence를 List로 다시 바꾸어 주는 이유 : List는 인덱스로 원소 접근하는 등 다른 api 메소드를 활용할 수 있어서
- Sequence는 iterator로 원소 접근 vs List는 인덱스로 원소 접근 가능
- ⇒ 원소를 차례로 이터레이션 해야 할 경우 : Sequence 그대로 사용해도 됨
5.3.1. 시퀀스 연산 실행: 중간 연산과 최종 연산
- 중간 연산
- 다른 시퀀스를 반환
- 항상 지연 계산됨
- 최종 연산
- 결과를 반환
- 결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체다.
sequence.map{...}.filter{...}.toList()
//증간 연산 : map{...} filter{...}
//최종 연산 : toList()
- 최종 연산이 없는 예제
-
listOf(1,2,3,4).asSequence() .map{print("map($it)"); it * it} .filter{print("filter($it)"); it%2 == 0} //실행시키면 아무것도 출력되지 않음
- map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때 (즉 최종 연산이 호출될 때) 적용됨
-
- 최종 연산 있는 예제
fun main(){ val nums = listOf(1,2,3,4).asSequence() .map{print("map($it)"); it * it} .filter{print("filter($it)"); it%2 == 0} .toList() println() print(nums) }
- 연산 순서
- 컬렉션에 대한 연산 : map 함수를 각원소에 대해 먼저 수행해 새 컬렉션을 얻고 그 컬렉션에 대해 다시 필터를 수행함
- 시퀀스에 대한 연산 : 모든 연산은 각 원소에 대해 순차적으로 적용됨. 첫번째 원소가 변환된 다음에 걸러지면서 처리되고 다시 두번째 원소가 처리되며 이런 처리가 모든 원소에 적용
-
/* 첫번째 원소 1 : map(1) 출력 → 1*1 저장됨 → filter(1) 출력 → 1%2 ≠0 이라 걸러짐 두번째 원소 2 : map(2) 출력→ 2*2 저장됨 → filter(4) 출력 → 4%2 == 0 이라 안걸러짐 세번째 원소 3: map(3) 출력 → 3*3 저장됨 → filter(9) 출력 → 9%2 ≠0 이라 걸러짐 네번째 원소 4: map(4) 출력 → 4*4 저장됨 → filter(16) 출력 → 16%2 == 0 이라 안걸러짐 ⇒ 4, 16만 남음 */
'3-1기 스터디 > Kotlin' 카테고리의 다른 글
[11주차] 코틀린 타입 시스템(1) (0) | 2022.01.21 |
---|---|
[10주차] 람다로 프로그래밍(4) (0) | 2022.01.16 |
[8주차] 람다로 프로그래밍(2) (0) | 2022.01.03 |
[7주차] 람다로 프로그래밍(1) (0) | 2021.12.26 |
[5주차] 클래스, 객체, 인터페이스(2) (0) | 2021.11.28 |
댓글