티스토리 뷰

KOTLIN

코틀린 기본 문법 정리

야라바 2022. 12. 1. 17:07
728x90

2011년 처음 등장한 코틀린(KOTLIN) 언어는 자바 가상 머신(JVM)과 완벽히 호환되도록 개발되었으며 범용 언어라는 특징답게 JVM 환경에서 돌아가는 앱(Kotlin/JVM)을 비롯하여 iOS와 안드로이드 모두에서 수행되는 모바일 앱 개발(Kotlin/Native), 웹 응용 개발(Kotlin/JS)에도 사용할 수 있다. 코틀린이란 이름은 개발자의 의도에 따라 핀란드만에 있는 러시아의 섬 이름 코틀린에서 따온 것이라 한다. 구글은 2019년부터 안드로이드 개발 환경의 공식 언어로 코틀린을 채택하였다. 물론 여전히 자바 언어도 사용할 수 있다. 코틀린과 자바가 자연스럽게 호환되기 때문이다.

 

일단 코틀린으로 작성한 소스코드는 위의 그림처럼 *.kt로 저장한다. 스크립트의 경우 *. kts이다. 

 

@AndroidEntryPoint
class MainActivity : NordicActivity() {

    @Inject
    lateinit var activitySignals: ActivitySignals

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NordicTheme {
                Surface(
                    color = MaterialTheme.colorScheme.surface,
                    modifier = Modifier.fillMaxSize()
                ) {
                    NavigationView(HomeDestinations + ProfileDestinations)
                }

                AnalyticsPermissionRequestDialog()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        activitySignals.onResume()
    }
}

코틀린 코드를 받아보면 우선 눈에 들어오는 특징은 문장 끝에 세미콜론(;)을 붙이지 않는다는 것이다. 또 한 가지는 타입 뒤에 물음표(?)가 붙는 것을 자주 목격하게 되는데 코틀린은 기본적으로 변수에 널(NULL)을 대입할 수 없지만 타입 뒤에 물음표(?)를 널 값을 대입할 수 있음을 지정하는 것이다. 중괄호 {}의 사용법에 주의할 필요가 있는데 when과 if 구분을 한 줄에 기술하는 경우를 제외하면 if, for, when, do, while 구문에서 반드시 중괄호를 사용해야 한다. 빈 구문이라도 {}가 아니라 줄을 나누어야 한다.

 

■ 패키지와 임포트

package no.nordicsemi.android.prx.view
import no.nordicsemi.android.material.*
import no.nordicsemi.android.prx.R
import no.nordicsemi.android.theme.R as themeR

소스코드는 통상 package와 import 문장으로 시작한다. package는 소스 코드 내에 있는 클래스나 함수들이 포함될 꾸러미를 지정하는 것으로 내부 함수나 클래스의 전체 경로는 "패키지 이름.함수"이 된다. 패키지와 디렉터리 경로가 반드시 일치할 필요는 없다. 패키지를 지정하지 않으면 소스 코드의 내용들은 이름이 없는 기본 패키지에 담기게 된다. 임포트는 포함시킬 꾸러미를 지정하는 것으로 위의 예제처럼.*로 지정 스코프(Scope) 내의 모든 패키지, 클래스, 함수 등을 포함시킬 수도 있고 패키지 이름이 충돌할 경우에는 예제처럼 "as"로 이름을 지정해 줄 수 있다. import 구문을 기술하지 않아도 기본적으로 포함되는 패키지들이 있는데 다음과 같다.

kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.*
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*
java.lang.*
kotlin.jvm.*

 

■ 함수 

    override fun log(priority: Int, message: String) {
        logger.log(priority, message)
    }

    override fun getMinLogPriority(): Int {
        return Log.VERBOSE
    }

코틀린은 자바나 C언어와 달리 함수 선언 시 fun이라는 키워드를 앞에 붙이고 인수 목록 뒤에 리턴 타입을 지정한다. 리턴 값이 없을 때는 다른 언어의 void와 같이 Unit이라는 타입을 사용할 수 있지만 예제의 첫 번째 함수처럼 생략할 수 있다. 함수 인사의 경우에도 인수의 타입은 인수의 이름과 콜론(:) 다음에 기술한다. 

 

fun BluetoothGattService.getCharacteristic(
    uuid: UUID,
    requiredProperties: Int = 0,
    instanceId: Int? = null,
): BluetoothGattCharacteristic? = characteristics
    .firstOrNull { it.uuid == uuid && (instanceId == null || it.instanceId == instanceId) }
    ?.takeIf {
        it.properties and requiredProperties == requiredProperties
    }
    
fun String.toAnnotatedString() = buildAnnotatedString {
    append(this@toAnnotatedString)
}

위의 함수 예제는 "Int = 0"으로 함수 인수에 기본값을 적용한 예제이며 함수 몸체를 중괄호 {}로 묶지 않고 " = 단일 문장"으로 기술할 수도 있음을 확인할 수도 있다. 예제의 두 번째 함수처럼  " = 단일 문장"으로 함수 몸체를 기술하면서 리턴 타입을 지정하지 않으면 리턴 값의 타입은 추론하여 자동 반영된다.

 

private fun AppBar(state: CSCViewState, navigateUp: () -> Unit, viewModel: CSCViewModel) {
    val toolbarName = (state.cscManagerState as? WorkingState)?.let {
        (it.result as? DeviceHolder)?.deviceName()
    }

    if (toolbarName == null) {
        BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp)
    } else {
        LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(OnDisconnectButtonClick) }) {
            viewModel.onEvent(OpenLogger)
        }
    }
}

//......
val navigateUp = { viewModel.onEvent(NavigateUp) }
AppBar(state, navigateUp, viewModel)

위의 예제에서 navigateUp 인수는 람다(Lambda)를 인수로 받고 있다. 람다는 정식 함수로 선언된 것은 아니지만 C의 함수 포인터처럼 인수로 전달할 수 있는 특성이 있다. "val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }"처럼 람다의 정식 형식은 인수와 리턴 타입을 모두 기술하고 -> 다음에 몸체를 기술하는 것이지만,  위의 예제나 "val sum = { x: Int, y: Int -> x + y }"과 같이 타입 지정 부분을 생략할 수 있다.

 

람다를 마지막 인수로 정의한 경우에는 위의 예제처럼 일반적으로 사용할 수도 있지만 함수를 호출하면서 괄호 바깥에 람다를 몸체를 기술하거나 괄호 없이 람다를 기술할 수도 있다. 위의 예제에서 AppBar() 함수의 인수가 navigateUp하나뿐이거나 navigateUp 인수가 마지막이고 다른 인수에 기본값이 설정되어 있다면 함수 호출 시 다음과 같은 호출 방식이 모두 가능하다는 이야기이다.

AppBar(navigateUp = { viewModel.onEvent(NavigateUp) })
AppBar() { viewModel.onEvent(NavigateUp) } 
AppBar { viewModel.onEvent(NavigateUp) }

 

■ 변수와 상수

private val TAG = "BLE-CONNECTION"
private val _status = MutableStateFlow<BleManagerResult<T>>(IdleResult())
val status = _status.asStateFlow()
private var lastValue: T? = null

다른 언어에서 const나 final을 붙여서 상수를 선언하는 것처럼 코틀린에서는 val 키워드를 사용하면 변경할 수 없는 상수로 var 키워드를 사용하면 변경 가능한 변수로 취급한다. 콜론(:) 다음에 타입을 지정하지 않으면 지정하는 값으로 타입을 추론하여 자동 지정한다.

 

■ 클래스

internal class isPrimeTest {
    @Test
    fun testPrime1(){
        assert(2.isPrime()==true)
    }
}

class TestGithub {
    open class BaseClass(val name: String)

    class DerivedClass(name: String) : BaseClass(name)
}

코틀린의 클래스 선언은 다른 언어와 마찬가지로 class 키워드를 사용한다. 큰 차이점 하나는 위의 예제처럼 open을 붙이지 않은 클래스는 상속할 수 없다. 클래스 몸체는 선언 시 지정하지 않을 수 있다. internal을 붙이면 모듈 내에서만 접근할 수 있고, public은 모든 곳에서, private는 클래스 내부에서만 접근할 수 있으며 protected를 붙이면 클래스를 상속받은 곳에서만 접근할 수 있다. 또 한 가지 차이점은 클래스 선언 자체를 생성자로 사용한다는 것이고 클래스 선언과 다른 인수가 필요한 생성자는 constructor()로 추가할 수 있다.

 

■ 각종 이름과 주석 사용

코틀린에서 사용하는 각종 이름은 영문 대소문자와 숫자 및 밑줄로 기술할 수 있다. 호환성이나 가독성을 몇 가지 가이드를 제시하고 있는데 다음과 같다.

- 패키지 이름 : 밑줄 없이 소문자로만 기술한다. package com.fasterxml.jackson.module.kotlin.test.github

- 클래스 이름 : 대문자로 시작하는 파스칼 케이스 class TestGithub {......

- 함수 및 속성 이름 : 소문자로 시작하는 카멜 케이스 val testInstance = HealthStatusMap

 

다른 언어처럼 단일문을 //로 주석 처리하고 여러 문장은 /*... */로 주석 처리한다.

 

단순 주석이 아니라 주석과 동식에 자동 문서화 처리(KDoc)를 하는 것이 좋다. 위의 그림처럼 /**로 시작하고 */ 로 끝내는데 이렇게 기술해 놓으면 안드로이드 스튜디오 등에서 자동으로 해당 함수나 클래스에 대한 힌트를 보여주기도 한다. 단일 문장도 /**...... */ 사이에 기술하면 된다.

 

■ 문자열과 템플릿

val text: String = it.getStringValue(0) ?: String.EMPTY
log(10, "\"$text\" received")
val messages = data.value.messages + UARTRecord(text, UARTRecordType.OUTPUT)

코틀린에서 문자열 리터럴은 두 가지로 기술할 수 있는데 하나는 "....."로 기술하는 에스케이프 문자열이고 다른 하나는 """...... """로 기술하는 단순 문자열로 줄 나눔과 다양한 문자를 자유롭게 표현할 수 있다. String은 배열 인덱스로 개별 문자를 참조할 수 있다. 개별 문자는 'a'와 같이 표현하면 되고 Char 타입을 가진다. 에스케이프 문자는 다음과 같다.

 

\t : 탭, \b : 백스페이스, \n : LF, \r : CR, \' : 작은따옴표, \" : 큰 따옴표, \\ : 백슬래시, \$ : 달러부호, \u16진수 : UTF문자

 

달러 부호도 에스케이프에 속하는데 이유는 바로 템플릿 기능 때문이다. 문자열 내에서 $변수명으로 기술하면 해당 변숫값으로 대치시켜준다. ${......}식으로 기술하면 변수뿐만 아니라 표현식을 사용할 수도 있다. 단순 변수가 아니라 변수 내의 메서드나 속성을 참조하는 경우에는 ${a.attr}처럼 기술해야 한다. """로 기술하는 단순 스트링도 템플릿으로 사용할 수 있는데 에스케이프가 적용되지 않으므로 달러 부호는 ${'$'}로 기술한다.

 

■ 조건문과 루프

val errorText = if (isError.value) {
    stringResource(id = R.string.uart_name_empty)
} else {
    String.EMPTY
}

다른 언어에 있는 삼항 연산자를 지원하지 않고 위의 예제처럼 if 문의 표현식 자체를 대입문에 사용할 수 있다. 단일문으로 한 줄에 if 문을 기술하는 경우를 제외하고 반드시 중괄호 { }를 사용해야 한다. if 문을 대입문에 적용하는데 중괄호 내에 여러 문장이 있다면 마지막 문장이 대입문에 적용된다.

 

internal fun CSCData.displayTotalDistance(speedUnit: SpeedUnit): String {
    return when (speedUnit) {
        SpeedUnit.M_S -> String.format("%.2f m", totalDistance)
        SpeedUnit.KM_H -> String.format("%.2f km", totalDistance.toKilometers())
        SpeedUnit.MPH -> String.format("%.2f mile", totalDistance.toMiles())
    }
}

internal fun String.toSpeedUnit(): SpeedUnit {
    return when (this) {
        DISPLAY_KM_H -> SpeedUnit.KM_H
        DISPLAY_M_S -> SpeedUnit.M_S
        DISPLAY_MPH -> SpeedUnit.MPH
        else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $this")
    }
}

fun CGMScreen() {
    val viewModel: CGMViewModel = hiltViewModel()
    val state = viewModel.state.collectAsState().value

    Column {
        val navigateUp = { viewModel.onEvent(NavigateUp) }

        AppBar(state, navigateUp, viewModel)

        Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
            when (state) {
                NoDeviceState -> NoDeviceView()
                is WorkingState -> when (state.result) {
                    is IdleResult,
                    is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
                    is ConnectedResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
                    is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
                    is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
                    is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
                    is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN, navigateUp)
                    is SuccessResult -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
                }
            }.exhaustive
        }
    }
}

다른 언어에서 switch... case로 표현하는 다중 분기를 코틀린에서는 when 문으로 기술한다. case 대신 "값 또는 조건 ->"의 형식으로 기술한다. 각 조건에 대하여 단일 문장이 아니면 중괄호로 묶어야 하고 조건이 여러 개인 경우에는 " is IdleResult, is ConnectingResult -> "처럼 콤마(,)로 연속해서 기술한다. 다른 언어에서는 break를 기술하지 않으면 아래 case로 흐르지만 코틀린은 해당 문장만 수행한다. 

 

어떤 조건에도 해당하지 않는 경우는 다른 언어는 default로 기술하지만 코틀린은 "else ->"로 기술한다. 열거형 클래스(enum), 부울형(Boolean), 봉인된 클래스(Sealed)를 제외한 경우에는 반드시 기술해야 한다. 단, else를 생략할 수 있는 경우에도 가능한 모든 경우를 기술한 상태에서만 else를 생략할 수 있다.

 

when 문은 if 문처럼 대입문에도 사용할 수 있다. -> 앞에 리터럴 값이 아니라 표현식이 올 수도 있다. 위의 예제에서는 특정한 타입이 맞는지는 검사하기 위해 is를 사용했는데 반대로 !is를 사용할 수도 있다. 값이 특정한 범위나 군집에 포함되어 있는지 여부를 "in 1..10 ->"처럼 in 이나 !in으로 검사할 수도 있다.

 

for (l in 0..(j - i) / 2) {
    if (string[l + i] != string[j - l]) {
        return false
    }
}

for (k in i until j) {
    val temp: Int = palindromePartition(string, i, k) + palindromePartition(string, k + 1, j) + 1
    if (temp < mn) {
        mn = temp
    }
}

for (i in n - 1 downTo 0 step 2) {
    if (jobs[i].finish <= jobs[n].start) {
        return i
    }
}	

for (i in array.indices) {
    if (array[i].compareTo(key) == 0) {
        return i
    }
}

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

for (i in (1..4).reversed()) print(i)

for문은 C#의 foreach와 유사하게 in 키워드를 상용하여 기술한다. 전통적인 for() 문장처럼 사용하려면 위의 예제처럼 범위 연산자(.., until, downTo, step)를 사용해야 한다. downTo 대신 reversed()를 사용해도 역방향 루프를 돌릴 수 있다. 배열이나 리스트의 첨자로 루프를 돌리려면 indices()를 사용하고 첨자와 값을 동시에 받으려면 withIndex()를 사용한다.

 

while (i < p.size) {
    m[i][i] = 0
    i++
}

while() 루프와 do while 루프는 위의 예제처럼 전통적인 언어들과 유사하게 사용할 수 있다. 또한 for 및 while 루프는 다른 언어들처럼 break나 continue로 루프를 조정할 수 있다.

 

■ 집합

fun testForBasicCases(){
	assert(findMaxProfit(listOf(Job(0,1,30),Job(0,2,10),Job(0,3,50),Job(1,4,60),Job(2,3,120),Job(2,5,90),Job(3,6,40),Job(4,7,70)))==220)
}

internal fun SparseArray<CGMRecord>.toList(): List<CGMRecord> {
    val list = mutableListOf<CGMRecord>()
    this.keyIterator().forEach {
        list.add(get(it))
    }
    return list.sortedBy { it.sequenceNumber }.toList()
}

val numbers = setOf(1, 2, 3, 4)
val numbersBackwards = setOf(4, 3, 2, 1)
println("The sets are equal: ${numbers == numbersBackwards}")

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

코틀린은 List, Set, Map 세 가지의 집합을 제공한다. Map은 C#이나 파이썬의 사전 타입에 해당한다. ListOf, setOf, mapOf로 집합 객체를 생성할 수 있다. 또한 각 집합 타입별로 mutableListOf, mutablesetOf, mutablemapOf로 요소 추가, 삭제 등 변경 가능한 집합을 생성할 수도 있다. 예제에서 mutableListOf로 생성한 리스트에 대하여 요소 추가, 정렬 등의 작업을 수행하는 것을 확인할 수 있다. 리스트는 동일한 내용이 들어갈 수도 있지만 Set과 Map은 각 요소가 유일해야 한다. Set과 Map은 내부 배열 순서도 별 관계가 없다. 위의 예제에서 배열 순서가 서로 다른 두 개의 Set이 동일한지 == 연산자로 확인하면 true를 리턴한다

 

728x90