Tianea2160 / kotlin-in-action-study

코틀린 인 액션 책을 완독하기 위한 스터디 레포지토리 입니다.
MIT License
0 stars 0 forks source link

3주차 Chapter 5,6 #3

Open Tianea2160 opened 1 year ago

kim-sung-jee commented 1 year ago

!! 사용을 최대한 자제해야 하는 이유

자세히

저는 자바에서의 것처럼 런타임시 NPE를 허용하는 것이기 때문이라고 이해했습니다.

null-safe calls 이나 Elvis operator 같은 다른 것들을 선택하는게 좋다고 합니다 https://www.baeldung.com/kotlin/not-null-assertion

다음코드를 개선

class Foo(val name: String) val list = listOf(Foo("Big"), null, Foo("Small")) List<Foo?> list.filter { it != null } .map { it!!.name.toUpperCase() }

자세히 **class Foo(val name: String) val list = listOf(Foo("Big"), null, Foo("Small")) List list.filterNotNull() .map { it.name.toUpperCase() }**

immutable collection를 default로 채택한 이유

자세히


문제 1.https://www.acmicpc.net/problem/1068 2.https://www.acmicpc.net/problem/1092 3.https://www.acmicpc.net/problem/2805

hweyoung commented 1 year ago

코틀린에서 자바와 달리 람다 밖 함수에 있는 final이 아닌 변수에 접근/변경이 가능한 이유

answer
코틀린에서는 자바와 달리 람다 밖 함수에 있는 final이 아닌 변수에 접근/변경이 가능하다. val(final 변수) : 자바와 마찬가지로 변수의 값이 복사된다. var(final이 아닌 변수) : 변수를 Ref 클래스 인스턴스에 넣는다. 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장한다. → 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다.

sequence를 사용하는 이유와, sequence를 사용후 다시 컬렉션으로 돌리는 이유

answer
- 시퀀스를 사용하면 모든 연산이 각 원소에 대해 순차적으로 적용된다. - 컬력션에 들어있는 원소가 많으면 중간 원소를 재배열 하는 비용이 커지기 때문에 sequence를 사용하는게 낫다. ```kotlin fun main(args: Array) { listOf(1, 2, 3, 4).asSequence() .map { print("map($it) "); it * it } .filter { print("filter($it) "); it % 2 == 0 } } ``` - 위 코드를 실행하면 아무 내용도 출력되지 않는다. map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을때 적용되기 때문이다. ```kotlin fun main(args: Array) { listOf(1, 2, 3, 4).asSequence() .map { print("map($it) "); it * it } .filter { print("filter($it) "); it % 2 == 0 } .toList() } // 결과 >>> map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) ``` - 컬렉션보다 시퀀스가 훨씬 더 낫다면 그냥 시퀀스를 쓰는 편이 나을수도 있다. 하지만 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메소드가 필요하다면 시퀀스를 리스트로 변환해야 한다.

코틀린에서 읽기 전용 컬렉션을 나눠놓은 이유?

answer
- 코틀린은 읽기 전용 컬렉션과 읽고 쓸 수 있는 컬렉션으로 구분됨 - 읽기 전용 : Iterable, Collection, Set, List 인터페이스 - 읽고 쓸 수 있는 : MutableInterable, MutableCollection, MutableSet, MutableList 인터페이스 - 읽기 전용 컬렉션이 내부의 값을 변경할 수 없다는 의미는 아니고 대부분의 경우에는 변경할 수 있지만 읽기 전용 인터페이스가 이를 지원하지 않으므로 변경할 수 없음 그렇기 때문에 읽기 전용 파라미터를 선언 후 컬렉션을 변경하는 자바 함수를 호출 시 컬렉션 변경이 가능 - 즉 읽기 전용 컬렉션을 mutable로 다운 캐스팅을 하는 것은 굉장히 좋지 않고 **만약 변경이 필요하다면 copy를 통해 새로운 mutable 컬렉션을 만들어서 활용**하는 것이 좋음 이렇게 나눠서 사용하는 이유가 있는지 궁금해서 더 찾아보고 있었는데, 그냥 코틀린에서 가변성을 제한하기 위해 사용한건가 생각합니당ㅇ...

코테문제 https://www.acmicpc.net/problem/1931 https://www.acmicpc.net/problem/1456 https://www.acmicpc.net/problem/1325

Tianea2160 commented 1 year ago

질문

  1. 아래와 같은 자료구조가 있을 때 해당 리스트 안에있는 원소를 first와 second를 기준으로 각각 내림차순 정렬하시오.
    val list: ArrayList<Pair<String, Int>> = arrayListOf("hello" to 1, "world" to 2, "jhj" to 3)
    answer
import java.util.ArrayList

fun main() {
    // second를 기준으로 내림차순으로 정렬하시오.
    val list: ArrayList<Pair<String, Int>> = arrayListOf("hello" to 1, "world" to 2, "jhj" to 3)
    list.sortBy { -it.second }
    list.sortByDescending { it.second }
    println(list.joinToString(","))

    // first를 기준으로 내림차순으로 정렬하시오.
    list.sortByDescending { it.first }
    println(list.joinToString(","))
}

  1. 아래의 코드가 있을 때 문제가 발생할 수 있는 코드가 있다면 찾고 해당 코드가 정상적으로 동작하기 위해서 코드를 추가해보시오.
    
    class Point(
    val a: Double,
    val b: Double
    ) {
    operator fun times(value: Double): Point = Point(a * value, b * value)
    override fun toString(): String = "Point(a=$a, b=$b)"
    }

fun main() { val p1 = Point(10.0, 20.0) println(p1) val p2 = p1 2.0 val p3 = 2.0 p1 println(p2) println(p3) }


<details>
<summary>answer</summary>

```
class Point(
    val a: Double,
    val b: Double
) {
    operator fun times(value: Double): Point = Point(a * value, b * value)
    override fun toString(): String = "Point(a=$a, b=$b)"
}

operator fun Double.times(p: Point) = Point(p.a * absoluteValue, p.b * absoluteValue)

fun main() {
    val p1 = Point(10.0, 20.0)
    println(p1)
    val p2 = p1 * 2.0
    val p3 = 2.0 * p1
    println(p2)
    println(p3)
}
```

</details>
3. by lazy 키워드와 lateinit 키워드의 차이점과 각각 언제 사용하면 좋은지 이야기하시오.
<details>
<summary>answer</summary>
lateinit은 말그대로 현재는 아무런 값도 넣지 않지만 나중에 반드시 넣겠다는 의미입니다. 따라서 추후에 값을 초기화하지 않으면 예외가 발생하며 개인적으로 개발자의 실수가 용인될 수 있기 때문에 사용을 피해야한다고 생각합니다.
lazy by는 이미 무엇을 선언할 지 모두 결정되었지만 해당 객체의 생성만 뒤로 미루는 키워드입니다. 따라서 처음 프로그램을 동작시킬때 너무 많은 부하가 걸린다면 해당 키워드를 이용해서 부하를 늦출 수 있습니다. 마치 프록시의 역할을 언어차원에서 어느정도 대신하다고 볼 수 있습니다.
구체적인 사용예시로 안드로이드 어플을 만들때 여러 객체를 화면 생성시에 만드는 경우가 발생하는데 이때에 lazy by를 써서 핸드폰의 부하를 줄일 수 있습니다.
</details>

### 코테 문제
https://www.acmicpc.net/problem/2108
https://www.acmicpc.net/problem/2563
https://www.acmicpc.net/problem/2738

### 토이 프로젝트 주제 
- 기숙사 공지 모니터링 서버
yangdongjue5510 commented 1 year ago
  1. 코틀린의 시퀀스는 컬렉션 함수를 사용할 때와 어떻게 다른지 설명해보세요. 그리고 자바의 Stream과는 어떤 차이점이 있는지, Stream에 비하면 어떤 장점이 있는지 설명해보세요
  2. 람다와 익명 클래스 인스턴스 내부의 this 키워드 사용의 차이점에 대해 설명해보세요.
  3. 코틀린에서 널 가능성이 없는 원소를 갖는 컬렉션 혹은 읽기 전용 컬렉션을 자바에서 Collection 매개변수로 받아 사용할 때 주의해야 할 점을 설명해보세요.

내 생각

  1. 시퀀스는 중간 연산과 최종 연산이 있다. 시퀀스는 최종 연산이 되기 전까지 원소들에게 연산을 적용하지 않는다. 반면 컬렉션 함수는 매번 연산이 있을 때마다 모든 원소에 해당 연산을 적용해서 새로운 컬렉션을 만든다. 자바 스트림은 시퀀스와 달리 병렬적으로 진행할 수 있는 기능이 있다. 반면 시퀀스는 자바 8버전 이하여도 적용할 수 있어서 호환성이 좋다.
  2. 람다는 컴파일 타임에 아직 인스턴스를 만들어내지 않는다. 컴파일러가 내부적으로 익명 클래스를 통해 인스턴스화 하므로 람다에서 this를 하는 경우 해당 람다를 감싼 외부 클래스의 객체를 참조한다. 반면 명시적으로 익명 클래스 인스턴스에서 this를 사용하는 경우 해당 인스턴스를 참조하게된다.
  3. 자바에서는 Collection 매개변수로 받아오면, 코틀린의 널 가능성이나 읽기 전용 컬렉션을 고려하지 않고 모두 가능해진다. 이때 자바에서 널을 집어넣거나 수정하는 경우 코틀린에서 가정이 무너지게 되므로 자바에서 코틀린 컬렉션을 사용할 때 제약조건을 확인하고 사용하자.

이번주는 개인적으로 바빠서 코테는 못풀었습니다 ㅠㅠ 차차 추가할게요. 토이 프로젝트 주제는 멤버십 포인트 적립 서비스 서버

ccppoo commented 1 year ago

타입과 람다의 방

1.

fun myFunction(){
    println("no error")
}

val yourFunction = {
    println("your function")
}

Section 1

// 1-1. 빌드가 된다 (O/X)
fun test_1() = (::myFunction)()

// 1-2. 빌드가 된다 (O/X)
val test_2 = ::myFunction

// 1-3. 빌드가 된다 (O/X)
val test_3 = run(myFunction)

// 1-4. 빌드가 된다 (O/X)
val test_4 = if (::myFunction is KFunction<*>) ::myFunction else ::yourFunction

// 1-5. 빌드가 된다 (O/X)
val test_5 = if (::myFunction is () -> Unit) ::myFunction else ::yourFunction
정답 1. O - 최상위에 선언된 함수 `myFunction`를 참조하고 (`::myFunction`)
- 인자 없이 호출하는 함수로 정의 됩니다 (`(::myFunction)()`) 2. O - 최상위에 선언된 함수를 참조한 값을 저장한 변수가 됩니다 - 타입은 ` KFunction0` 으로 인자 X , 반환 X(void == Unit) 형태를 의미합니다 3. X - 참조가 아닌 함수 이름만 작성한 경우 작동(Invoke) 해야하는 형태로 사용해야 합니다 `run`은 다음과 같으며 ```kt public inline fun T.run(block: T.() -> R) : R ``` `T`는 호출(Invoke)가능한 (Callable) 수식 객체가 되어야하기 때문에 컴파일이 되기 위해서는
함수의 참조형(함수 포인터)을 전달해야합니다. 따라서 실행이 되기 위해서는 아래와 같이 작성되어야 합니다. ```kt val test_3 = run(::myFunction) ``` 4. O 위에 설명한 것과 같이 코틀린에서 함수를 변수처럼 다루기 위해선 참조형태 `::function`로 전달해야합니다. ```kt val test_4 = if (::myFunction is KFunction<*>) ::yourFunction else ::myFunction ``` 이 경우 `myFunction`은 함수형이 맞으므로 `True`를 반환하고 `if ... else ...`는 값을 반환하는 **expression** 이므로 **True** 조건인 `::yourFunction`를 반환합니다. `test_4`는 정상적으로 `::yourFunction` 참조를 갖게 되므로 문제가 발생하지 않습니다. 5. O ```kt val test_5 = if (::myFunction is () -> Unit) ::myFunction else ::yourFunction ``` 4번 문제와 마찬가지로 `()->Unit`은 인자가 없는 함수가 아무것도 반환을 하지 않는(void) 타입을 의미한다. 따라서 True를 반환하고 ```kt val test_5 = ::myFunction ``` 와 같은 의미를 지닌다.

Section 2

코드는 위 Section 1에 계속 이어서...

fun main(){
    // 2-1. 실행이 된다 (O/X)
    println(test_4)

    // 2-2. 실행이 된다  (O/X)
    test_4()

    // 2-3. 실행이 된다  (O/X)
    run(test_5)

    // 2-4. 실행이 된다 (O/X)
    run(::test_5)
}
정답 1. O ```kt fun main(){ println(test_4) } ``` 앞서 test_4가 함수 참조를 정상적으로 받고, 이를 출력하면 다음과 같이 나온다 `function myFunction` 즉, 실행하지 않는 함수의 이니셜을 보여준다고 보면 된다 2. X ```kt fun main(){ println(test_4()) } ``` 앞서 test_4가 함수 참조를 정상적으로 받고, 실행하면 당연히 될 줄 알았나? 이건 위 `if ... else ...` expression이 반환하는 값이 `::myFunction`, `::yourFunction` 이고 단순히 `println(...)` 만 있는 함수와 람다라서 타입도 똑같으니깐 당연히 똑똑한 코틀린이 타입 추론을 통해서 `test_4`를 함수로 취급하는거 아닌가? 라는 생각을 할 수 있다. `if ... else ...`는 결국 함수와 같이 값을 반환하는 **expression** 이므로 값을 받는 인자의 타입을 지정하지 않는 이상 `Any` 타입으로 받게 된다. 따라서 `test_4`의 타입은 `Any` 이고, 우리가 일반적으로 함수를 실행하듯이 호출하면 코틀린은 `test_4`가 함수인지 뭔지 확실하지 않으므로(`Callable` 인터페이스를 가지고 있는지 확실하지 않은 상황) 아래와 같은 Exception을 던진다 ```shell Expression 'test_4' of type 'Any' cannot be invoked as a function. The function 'invoke()' is not found ``` 이를 정상적으로 실행시키기 위해서 타입을 **정확히 캐스팅하고** (Null check 빡세게 하는 것과 유사함) 실행하면 된다 ```kt val castedFunction = test_4 as () -> Unit castedFunction() // or (test_4 as () -> Unit)() ``` `as`는 값을 반환하는 `operator` 이므로 [참고](https://kotlinlang.org/docs/typecasts.html#unsafe-cast-operator) 아래처럼 작성해도 된다. 3. X 위의 2번 문제와 같은 이유로 안된다. (Section 1의 3번 문제 해설 참고) 4. O **근데...** ```kt run(::test_5) // 아무것도 출력이 안됨 run(test_5 as () -> Unit) // myFunction이 실핼된 것처럼 정상적으로 my Function이 출력이 됨 왜일까? ```

덧,

https://ktor.io/

이걸로 학교 공지 사진이랑 내용 API처럼 반환해주는거 하고 싶은 나중에 쓸 일 많을거 같음

오늘 참석 못함

누나 해외 출장 귀국했다고 + 설 앞둔 기념으로 예약 잡아놨다고 시간 못뺌 ㅈㅅㅎㅎ