개발자/kotlin 코틀린

Kotlin 핵심 개념 알아보기 2

지구빵집 2022. 2. 8. 09:42
반응형

 

 

 

Kotlin 핵심 개념 알아보기 1 에 이어 나머지 핵심 개념을 알아보자. 여기에서는 앞 편에 더해 제어부, 함수, 클래스에 대한 설명을 추가한다.  

 

 

스마트 형 변환

 

스마트 형 변환은 한 변수 형식을 다른 형식으로 변환하지만, 안전 형변환과는 다르게 암시적으로 변환을 수행한다. 일반적으로 스마트 형변환은 모든 불변 참조(val)과 로컬 가변 참조(var)에 사용할 수 있다. 다음과 같은 두 가지 스마트 형변환이 있다. 형식 스마트 형변환은 객체를 한 형식에서 다른 형식으로 변환한다. null 가능성 스마트 형변환은 null 허용 참조를 null 불허 참조로 변환한다.

 

명시적 타입 변환할 때 사용하는 as와 as?를 사용한다. (as, as? 연산자)

 

스마트 캐스트는 두 가지 경우에 자동으로 수행되는데 첫 번째로 변수의 값이 null 인지 확인할 때 두 번째로 is, !is 연산자로 변수 타입을 확인할 때 자동으로 수행된다.

 

먼저 null 인지 확인하는 예제를 보자.

 

fun main() 
{    
    val name : String? = "임태호"
    if (name != null) println(name.length)
}

 

실행 결과는 3

 

변수 name은 null이 가능한 String 타입이다. 하지만 if 문에서 변수 name이 null이 아님이 확인됐으므로 null이 불가능한 String 타입으로 변환해준다. 이것을 스마트 캐스트라고 한다. 하지만 if 문 블록 내부에서만 null이 불가능한 String 타입으로 사용할 수 있고 if 문 블록을 벗어나면 스마트 캐스트가 적용되지 않는다.

 

is, !is 연산자로 변수 타입을 확인하는 예제를 보자.

 

fun main() {
    smartCast(5)
    smartCast("문자열")

    val arr : IntArray = intArrayOf(1,2,3,4,5)
    smartCast(arr)
}

fun smartCast(e : Any){
    when (e){
        is Int -> println(e*2) //변수e가 Int이면 Int로 스마트 캐스트 후 e*2 처리
        is String -> println(e.length) //변수e가 String이면 String으로 스마트 캐스트 후 e.length 처리
        is IntArray -> println(e.sum()) //변수e가 IntArray이면 IntArray로 스마트 캐스트 후 e.sum() 처리
        else -> return
    }
}

 

실행 결과는

 

10
3
15

 

smartCast 함수가 매개변수로 Any 타입을 받는다. 여기서 Any타입은 자바에서 Object와 같은 최상위 클래스이다. 코틀린의 모든 클래스는 기본적으로 Any 클래스에서 상속받는다. 따라서 변수 타입을 Any로 지정하면 null이 아닌 어떤 타입의 값도 저장할 수 있다.

 

제어구조

 

Kotlin은 조건부 논리를 구현하기 위한 몇 가지 메커니즘을 제공합니다. 가장 일반적인 것은 if-else 문입니다. if 키워드 옆의 괄호 안에 포함된 표현식이 true로 평가되는 경우, 해당 분기 내 코드(즉, 중괄호 안에 포함된 바로 다음 코드)가 실행됩니다. 그렇지 않은 경우 else 분기 내 코드가 실행됩니다.

 

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

 

else if를 사용하여 여러 조건을 나타낼 수 있습니다. 즉, 다음 예와 같이 단일 조건문 내에서 보다 상세하고 복잡한 논리를 표현할 수 있습니다.

 

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

 

조건문은 스테이트풀(Stateful) 논리를 나타내는 데 유용하지만 작성 시 반복될 수 있습니다. 위 예에서는 각 분기에 String을 인쇄합니다. 이 반복을 피하기 위해 Kotlin은 조건식을 제공합니다. 마지막 예는 다음과 같이 다시 작성될 수 있습니다.

 

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

 

암시적으로 각 조건부 분기는 마지막 줄에 표현식의 결과를 반환하므로 return 키워드를 사용할 필요가 없습니다. 세 분기의 결과는 모두 String 유형이므로 if-else 표현식의 결과도 String 유형입니다. 이 예에서 answerString에는 if-else 표현식의 결과에서 초기 값이 할당됩니다. 유형 추론을 사용하여 answerString에 명시적 유형 선언을 생략할 수 있지만 명확히 하기 위해 유형 선언을 포함하는 것이 좋습니다.

 

참고: Kotlin에는 기존의 3항 연산자가 없으며 주로 조건식이 대신 사용됩니다.

 

아래 예와 같이 조건문의 복잡도가 증가하면 if-else 표현식을 when 표현식으로 교체할 것을 고려할 수 있습니다.

 

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

 

when 표현식의 각 분기는 조건, 화살표(->) 및 결과로 표시됩니다. 화살표의 왼쪽 조건이 true로 평가되면 오른쪽에 있는 표현식의 결과가 반환됩니다. 한 분기에서 다음 분기로 실행되지 않습니다. when 표현식 예의 코드는 이전 예의 코드와 기능적으로 동일하지만 쉽게 읽을 수 있습니다.

 

Kotlin의 조건부는 이 언어의 강력한 기능 중 하나인 스마트 변환을 강조합니다. 안전 호출 연산자 또는 null이 아닌 어설션 연산자를 사용하여 nullable 값을 처리하는 대신 아래 예와 같이 조건문을 사용하여 변수에 null 값 참조가 있는지 확인할 수 있습니다.

 

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

 

조건부 분기 내에서 languageName은 nullable이 아닌 것으로 간주될 수 있습니다. Kotlin에서는 분기 실행 조건에 따라 languageName은 null 값을 보유할 수 없으므로 분기 내에서 languageName을 nullable로 처리할 필요가 없습니다. 이 스마트 변환은 null 검사, 유형 검사 또는 컨트랙트를 충족하는 모든 조건에 적용됩니다.

 

함수

 

하나 이상의 표현식을 함수로 그룹화할 수 있습니다. 결과가 필요할 때마다 동일한 일련의 표현식을 반복하는 대신 함수에 표현식을 포함한 다음 함수를 호출할 수 있습니다.

 

함수를 선언하려면 fun 키워드 뒤에 함수 이름이 오도록 사용합니다. 그런 다음 함수에 사용되는 입력 유형(있는 경우)을 정의하고 함수에서 반환하는 출력 유형을 선언합니다. 함수의 본문에서는 함수를 호출할 때 호출되는 표현식을 정의합니다.

 

이전 예를 기반으로 완성된 Kotlin 함수는 다음과 같습니다.

 

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

 

위 예에서 함수의 이름은 generateAnswerString입니다. 입력 값은 받지 않으며, String 유형의 결과를 출력합니다. 함수를 호출하려면 함수의 이름 뒤에 호출 연산자(())를 사용합니다. 아래 예에서 answerString 변수는 generateAnswerString()의 결과에 따라 초기화됩니다.

 

val answerString = generateAnswerString()

 

아래 예와 같이 함수에서는 인수를 입력으로 사용할 수 있습니다.

 

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

 

함수를 선언할 때 인수의 개수와 유형을 지정할 수 있습니다. 위 예에서 generateAnswerString()은 Int 유형의 countThreshold 인수 한 개를 사용합니다. 함수 내에서 이름을 사용하여 인수를 참조할 수 있습니다.

 

이 함수를 호출할 때 함수 호출 괄호 안에 인수를 포함해야 합니다.

 

val answerString = generateAnswerString(42)

 

함수 선언 단순화

 

generateAnswerString()은 매우 간단한 함수입니다. 이 함수는 변수를 선언한 다음 즉시 반환합니다. 아래 예와 같이 단일 표현식의 결과가 함수에서 반환되는 경우, 함수에 포함된 if-else 표현식의 결과를 직접 반환하여 로컬 변수 선언을 건너뛸 수 있습니다.

 

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

 

반환 키워드를 할당 연산자로 바꿀 수도 있습니다.

 

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

 

익명 함수

 

모든 함수에 이름이 필요하지는 않습니다. 일부 함수는 입력과 출력에 의해 더 직접적으로 식별됩니다. 이러한 함수를 익명 함수라고 합니다. 이 참조를 사용하여 나중에 익명 함수를 호출하면 익명 함수 참조를 유지할 수 있습니다. 다른 참조 유형과 마찬가지로 애플리케이션에서 참조를 전달할 수도 있습니다.

 

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

 

이름이 지정된 함수와 마찬가지로 익명 함수는 표현식을 제한 없이 포함할 수 있습니다. 함수의 반환 값은 최종 표현식의 결과입니다.

 

위 예에서 stringLengthFunc는 String을 입력으로 사용하고 String 입력 길이를 Int 유형의 출력으로 반환하는 익명 함수 참조를 포함합니다. 따라서 함수의 유형은 (String) -> Int로 표시됩니다. 하지만 이 코드는 함수를 호출하지 않습니다. 함수의 결과를 가져오려면 이름이 지정된 함수처럼 호출해야 합니다. 아래 예와 같이 stringLengthFunc를 호출할 때 String을 공급해야 합니다.

 

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

 

고차 함수

 

함수는 다른 함수를 인수로 취할 수 있습니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 패턴은 자바에서 콜백 인터페이스를 사용할 때와 동일한 방식으로 구성요소 간에 통신하는 데 유용합니다.

 

다음은 고차 함수의 예입니다.

 

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

 

stringMapper() 함수는 전달된 String에서 Int 값을 파생하는 함수와 함께 String를 가져옵니다.

 

아래 예와 같이 다른 입력 매개변수를 충족하는 함수, 즉 String을 입력으로 사용하고 Int를 출력하는 함수와 String을 전달하여 stringMapper()를 호출할 수 있습니다.

 

stringMapper("Android", { input ->
    input.length
})

 

아래 예와 같이 익명 함수가 함수에 정의된 마지막 매개변수인 경우 함수를 호출하는 데 사용된 괄호 밖에서 함수를 전달할 수 있습니다.

 

stringMapper("Android") { input ->
    input.length
}

 

익명 함수는 Kotlin 표준 라이브러리 전체에서 찾을 수 있습니다. 자세한 내용은 고차 함수 및 람다를 참조하세요.

 

 

클래스

 

지금까지 언급된 모든 유형은 Kotlin 프로그래밍 언어에 내장되어 있습니다. 아래 예와 같이 맞춤 유형을 추가하려는 경우 class 키워드를 사용하여 클래스를 정의할 수 있습니다.

 

class Car

 

속성

 

클래스는 속성을 사용하여 상태를 나타냅니다. 속성은 getter, setter 및 backing 필드를 포함할 수 있는 클래스 수준 변수입니다. 아래 예와 같이 자동차를 운전하려면 바퀴가 필요하므로 Wheel 객체 목록을 Car의 속성으로 추가할 수 있습니다.

 

class Car {
    val wheels = listOf<Wheel>()
}

 

wheels는 public val입니다. 즉, Car 클래스 외부에서 wheels에 액세스 할 수 있지만 재할당할 수는 없습니다. Car의 인스턴스를 가져오려면 먼저 생성자를 호출해야 합니다. 그러면 액세스 가능한 모든 속성에 액세스할 수 있습니다.

 

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

 

바퀴를 맞춤 설정하려면 클래스 속성을 초기화하는 방법을 지정하는 맞춤 생성자를 정의하면 됩니다.

 

class Car(val wheels: List<Wheel>)

 

위 예에서 클래스 생성자는 List을 생성자 인수로 취하고 인수를 사용하여 wheels 속성을 초기화합니다.

 

 

클래스 함수 및 캡슐화

 

클래스는 함수를 사용하여 동작을 모델링합니다. 함수는 상태를 수정할 수 있으므로 노출하려는 데이터만 노출할 수 있습니다. 이 액세스 제어는 캡슐화라는 더 큰 객체 지향 개념의 일부입니다.

 

다음 예에서 doorLock 속성은 Car 클래스 외부의 모든 항목에서 비공개로 유지됩니다. 아래 예와 같이 자동차를 잠금 해제하려면 유효한 키를 전달하는 unlockDoor() 함수를 호출해야 합니다.

 

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

 

속성을 참조하는 방법을 맞춤설정하려면 맞춤 getter 및 setter를 제공하면 됩니다. 예를 들어 속성의 setter에 액세스 하는 것을 제한하면서 속성의 getter를 노출하려면 setter를 private으로 지정합니다.

 

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

 

속성과 함수를 조합하여 모든 유형의 객체를 모델링하는 클래스를 만들 수 있습니다.

 

상호운용성

 

Kotlin의 가장 중요한 기능 중 하나는 자바와의 유연한 상호운용성입니다. Kotlin 코드는 JVM 바이트 코드로 컴파일되기 때문에 Kotlin 코드는 자바 코드로 직접 호출될 수 있으며 그 반대의 경우도 마찬가지입니다. 즉, 기존 자바 라이브러리를 Kotlin에서 직접 활용할 수 있습니다. 또한 대부분의 Android API는 자바로 작성되어 Kotlin에서 바로 호출할 수 있습니다. 

 

 

Kotlin 핵심 개념 알아보기

 

 

 

 

반응형