-
아이템2 - 변수의 스코프를 최소화하라Kotlin/Effective Kotlin 요약 2022. 12. 29. 00:01728x90
변수의 스코프란?
코틀린의 스코프는 기본적으로 중괄호로 만들어집니다.
내부 스코프에서 외부 스코프에 있는 요소에만 접근할 수 있습니다.
val a = 1 fun fizz(){ val b = 2 print(a + b) } //이 위치에서는 a를 사용할 수 있지만, b는 사용할 수 없음
스코프를 최소화하면 장점
- 코드를 분석할 때 추적 관리에 용이합니다. (mutable 보다 immutable 프로퍼티를 선호하는 이유와 동일)
- 스코프의 범위가 너무 넓으면 다른 개발자에 의해 변수가 잘못 사용될 수 있습니다.
이런 관점에서 변수를 정의할 때 초기화하는 것이 좋습니다.
여러 프로퍼티를 한꺼번에 설정해야 하는 경우에는 구조 분해 선언을 활용하는 것이 좋습니다.
fun main() { updateWeather(3) } fun updateWeather(degrees: Int){ val (description, color) = when{ degrees < 5 -> "cold" to Color.BLUE else -> "hot" to Color.RED } println(description) println(color) } //출력 cold java.awt.Color[r=0,g=0,b=255]
캡처링
코루틴을 교육할 때 시퀀스 빌더를 사용하여 에라토스테네스의 체를 구현해보라는 문제를 많이 냅니다.
fun main() { var numbers = (2..100).toList() val primes = mutableListOf<Int>() while (numbers.isNotEmpty()) { val prime = numbers.first() primes.add(prime) numbers = numbers.filter { it % prime != 0 } } print(primes) }
이를 시퀀스로 바꾼다면 어떻게 될까요?
fun main() { val primes: Sequence<Int> = sequence{ var numbers = generateSequence(2) { it + 1} while(true){ val prime = numbers.first() yield(prime) numbers = numbers.drop(1).filter { it % prime != 0} } } print(primes.take(10).toList()) } //출력 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
시퀀스란?
순차적인 컬렉션으로 요소의 크기를 특정하지 않고, 나중에 결정할 수 있는 특수한 컬렉션
Collection vs Sequence
Eager로 처리하는가 Lazy로 처리하는가
Collection은 map, filter와 같은 중간 연산의 결과를 즉시 생성합니다.
Sequence는 중간 연산 결과를 저장하지 않습니다.
원소가 많은 컬렉션에 대한 연산이 연쇄적으로 있는 경우에는 시퀀스를 사용하는 것이 좋습니다.
generateSequence(2) {it + 1}는 첫번째 element를 특정 값(2)으로 지정하고 1씩 증가하도록 시퀀스를 정의합니다.
추후 take(10)을 통해 10개의 요소가 저장됩니다.
yield()는 1개의 element를 argument로 가지고 Iterator에게 제공합니다.
val oddNumbers = sequence { yield(1) yieldAll(listOf(3, 5)) yieldAll(generateSequence(7) { it + 2 }) } println(oddNumbers.take(5).toList()) // output: // [1, 3, 5, 7, 9]
drop(1)은 맨앞 하나의 요소를 제거합니다.
만약 위의 코드를 이렇게 작성하면 실행 결과가 이상하게 나오게 됩니다.
fun main() { val primes: Sequence<Int> = sequence{ var numbers = generateSequence(2) { it + 1} var prime: Int while(true){ prime = numbers.first() yield(prime) numbers = numbers.drop(1).filter { it % prime != 0} } } print(primes.take(10).toList()) } //출력 [2, 3, 5, 6, 7, 8, 9, 10, 11, 12]
람다 캡처링이란?
람다 본문 블록 내에서 외부 함수의 로컬 변수나 글로벌 변수 등을 활용할 수 있는데, 이것을 람다 캡처링이라 합니다
자바와 달리 코틀린 람다에서는 final 변수가 아닌 변수에 접근이 가능하며 변경도 가능합니다.
final로 선언된 경우 자바에서와 동일하게 값을 복사하여 람다 내부에서 사용할 수 있고
final이 아니라면 특별한 wrapper 클래스로 감싸 그 wrapper를 final 변수에 담아 람다 내에서 사용한다.
무엇이 잘못되었을까요?
prime이라는 변수를 캡처했기 때문입니다.
반복문 내부에서 filter를 활용하여 prime으로 나눌 수 있는 숫자를 필터링합니다.
하지만 필터링은 지연되고 최종적인 prime 값으로만 필터링되게 됩니다.
prime이 2로 설정되어 있을 때 필터링된 4를 제외하면 DROP만 동작하므로 그냥 연속된 숫자가 나와 버립니다.
시퀀스는 위와같이 filter, drop과 같은 중간 연산에서는 바로 연산을 수행하지 않고 종단 연산인 first를 만날 때까지 연산을 누적해서 기다립니다.
실제로 filter와 drop메서드를 타고가보면 Sequence를 반환하는 중간 연산이며, first메서드는 T를 반환하는 종단 연산입니다.
변수의 스코프는 항상 좋게 활용하며, var보다는 val을 사용하며, 람다에서 변수를 캡처한다는 사실을 꼭 기억해야 합니다.
참고자료
https://unluckyjung.github.io/kotlin/2022/04/08/kotlin-collections-vs-sequence/
https://iosroid.tistory.com/79
https://bottom-to-top.tistory.com/64
https://mangbaam.github.io/book/2022/02/28/effective_kotlin-2.html
'Kotlin > Effective Kotlin 요약' 카테고리의 다른 글
6장 - 사용자 정의 오류보다는 표준 오류를 사용하라 (0) 2023.01.21 아이템5 - 예외를 활용해 코드에 제한을 걸어라 (0) 2023.01.04 아이템4 - inferred 타입으로 리턴하지 말라 (0) 2023.01.03 아이템3 - 최대한 플랫폼 타입을 사용하지 말라 (0) 2022.12.31 아이템1 - 가변성을 제한하라 (0) 2022.12.26