Kotlin

[Kotlin] 💗 기본 문법 정리 - 코틀린 코테 준비

yujinius 2024. 7. 5. 22:27

💗 Kotlin 기본 문법 정리

💗 Hello World

fun main() {
    println("Hello, world!")
    // Hello, world!
}

💡 코틀린에서는

  • fun이 함수 선언에 사용된다.
  • main() 함수는 프로그램의 시작이다.
  • 함수의 body는 { }으로 감싼다.
  • println()과 print() 함수는 표준 출력으로 출력한다.

⭐ 변수

  • read-only 읽기 전용 : val
  • mutable 수정 가능 : var
  • 할당 : = 연산자 이용
val readOnlyVal = 5 // 읽기만 가능
var mutableVar = 10 // 수정 가능

// 수정 후 출력
mutableVar = 4
println(mutableVar) // 4

⭐ 문자열 템플릿(String templates)

  • 변수의 내용을 표준 출력하는 방법
  • 템플릿 표현식을 사용하면 변수 및 기타 객체에 저장된 데이터에 액세스하고 이를 문자열로 변환 가능하다!
  • 문자열은 “”으로 묶어서 표현한다.
  • 템플릿의 표현식은 항상 달러 기호($)로 시작한다.
  • 변수 타입은 Kotlin이 추론한다.
val count = 10
println("count는 $count 입니다.") // count는 10 입니다.
// 주의사항!! $count입니다. 라고 붙여 쓰면 'count입니다'가 변수로 인식됨!
println("count에 1을 더하면 ${count+1}입니다.") //count에 1을 더하면 11입니다.

💗 기본 타입(Basic types)

  • 데이터 타입은 컴파일러에게 해당 변수나 데이터 구조로 무엇을 할 수 있는지 알려주기 때문에 중요하다.
  • 코틀린의 모든 변수와 데이터 구조에는 데이터 유형이 있는데, 컴파일러에게 어떤 함수와 속성이 있는지 알려주게 되는 것.
  • 아래는 컴파일러가 Int타입으로 유추하여 연산 수행하는 예시이다.
var count = 10
// 변경
count = 4
count = count + 3 // 7
count /= 7 // 1
println(count) // 1

⭐ Kotlin의 기본 타입

  • Kotlin의 타입 같은 경우 Java에서 앞에만 대문자로 변경되는 것처럼 보인다.
  •  
범주 기본 유형
정수 Byte, Short, Int, Long
부호 없는 정수 UByte, UShort, UInt,ULong
부동 소수점 숫자 Float,Double
부울 Boolean
문자 Char
문자열 String
   
-  
  • 위의 변수 타입으로 변수를 선언하고 나중에 초기화를 할 수 있다.
  • Kotlin에서는 변수가 첫 번째 읽기 전에 초기화되기만 하면 이를 관리할 수 있다.
  • 즉, 초기화 없이 변수를 선언하려면 타입을 ‘:’와 함께 지정하고, 이후에 초기화 해서 사용할 수 있다는 뜻이다. 특히 읽기 전용인 val로 선언하면 이후 초기화만 해주면 읽을 수 있다는 뜻이다.
// 초기화 없이 변수 선언 (read-only는 val)
val sample : Int
// 변수 초기화 
sample = 3

// 명시적으로 타입과 함께 초기화 된 변수
val e : String  = "hello"

// 변수가 초기화 되어서 읽을 수 있음
println(sample) //3
println(e) //hello

💗 컬렉션 (Collections)

  • 프로그래밍할 때 나중에 처리할 수 있도록 데이터를 구조화해서 그룹화 하는 것이 유용하다.
  • Kotliin은 이를 위해 컬렉션을 제공한다.
  • Kotlin이 제공하는 컬렉션은 아래와 같다
컬렉션 타입 (Collection type) 정의
Lists 항목들이 순서대로 정렬된 컬렉션 (순서 O)
Sets 고유한 항목들로 이루어진 순서가 없는 컬렉션 (순서X)
Maps 키-값 쌍으로 이루어진 컬렉션,
  • 키는 고유하며 각 키는 하나의 값에 매핑됨
  • 각 컬렉션 타입은 변경 가능(mutable)하거나 읽기 전용(read-only)일 수 있다.

⭐ List

  • 항목이 추가된 순서대로 저장 (순서 O)
  • 중복 항목 허용

💫 리스트 생성

읽기 전용 리스트(List) 생성 : listOf("item1", "item2", …)

  • 읽기 전용 리스트를 생성하기 위해 lisfOf() 함수 사용

변경 가능한 리스트(MutableList) 생성 : mutableListOf("item1", "item2", …)

  • 변경 가능한 리스트를 만들기 위해 mutableListOf() 함수 사용

리스트 타입 명시적 선언

  • Kotlin은 저장된 항목의 타입으로 추론한다.
  • 타입을 명시적으로 선언하려면 리스트 선언 후 <> 안에 타입을 추가한다.
// 읽기 전용 리스트
val readOnlyShapes = listOf("triangle", "square", "circle")
println(readOnlyShapes)
// [triangle, square, circle]

// 변경 가능 리스트
val shapes: MutableList<String> = mutableListOf("triangle", "square", "circle")
println(shapes)
// [triangle, square, circle]

💫 Casting

Casting 으로 읽기 전용 뷰 만들기

  • 원하지 않는 수정으로부터 보호하기 위해, 변경 가능 리스트를 List로 할당해 읽기 전용 뷰를 얻을 수 있음
  • 이를 Casting이라고 부름
val shapes : MutableList<String> = mutableListOf("t", "s", "c")
val shapesLocked : List<String> = shapes

💫 리스트 사용하기

  • 값 접근: 리스트의 아이템은 [ ] 연산자로 인덱싱한다.
  • 처음 아이템은 .first() , 마지막 아이템은 .last()로 접근한다.
  • 아이템 개수 세기는 .count()
  • 리스트 안에 아이템이 있는지 확인하는 것은 in 연산자를 쓴다. true, false로 나온다.
  • mutable list에서 삽입은 .add() , 삭제는 .remove()를 쓴다.
val readOnlyShapes = listOf("t", "s", "c")
// 1. [ ]로 인덱싱
println("리스트의 첫 아이템 : ${readOnlyShapes[0]}") 
// 리스트의 첫 아이템 : t

// 2. 처음 아이템은 .first() , 마지막 아이템은 .last()로 접근
println("리스트의 첫 아이템 : ${readOnlyShapes.first()}") 
// 리스트의 첫 아이템 : t
println("리스트의 마지막 아이템 : ${readOnlyShapes.last()}") 
// 리스트의 마지막 아이템 : c

// 3. 아이템 개수 세기 .count()
println("리스트 아이템 개수 : ${readOnlyShapes.count()}") 
// 리스트 아이템 개수 : 3

// 4. in 연산자로 아이템 있는지 확인
println("c" in readOnlyShapes)
// true
println("k" in readOnlyShapes)
// false

// 5.  삽입은 .add() , 삭제는 .remove()
val shapes : MutableList<String> = mutableListOf("t", "s", "c")
// "p"를 리스트에 넣기
shapes.add("p")
println(shapes) 
// [t, s, c, p]
// "p"를 리스트에서 삭제하기 
shapes.remove("p") 
println(shapes) 
// [t, s, c]

⭐ Set

  • 순서 X
  • 고유한 항목만 저장 (중복 X)

💫 Set 생성

읽기 전용 Set 생성 : setOf("item1", "item2", …)

  • 읽기 전용 Set을 생성하기 위해 setOf() 함수 사용

변경 가능한 Set (MutableSet) 생성 : mutableSetOf("item1", "item2", …)

  • 변경 가능한 Set을 만들기 위해 mutableSetOf() 함수 사용
  • 아래 예시에서 Set은 고유한 항목만 포함해서 중복된 “cherry”는 제거됨
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
println(readOnlyFruit)
// 출력: [apple, banana, cherry]
val fruit: MutableSet<String> = mutableSetOf("apple", "banana", "cherry", "cherry")
println(fruit)
// 출력: [apple, banana, cherry]

읽기 전용 뷰 생성 (Casting)

  • 원하지 않는 수정으로부터 보호하기 위해서
  • MutableSetSet으로 할당해서 읽기 전용 뷰를 얻을 수 있다.
val fruit : MutableSet<String> = mutableSetOf("a", "b", "c", "c")
val fruitLocked : Set<String> = fruit

💫 Set 사용하기

  • Set은 순서가 없으므로 특정 인덱스 항목에 접근 불가
  • Set의 항목수 얻기는 .count() 사용
  • 항목 존재 여부 in 연산자
  • 항목 추가 및 제거는 .add().remove()
val readOnlyFruit = setOf("a", "b", "c", "c")
// 1. Set의 항목수 얻기는 .count() 사용
println("이 셋의 아이템 개수: ${readOnlyFruit.count()}")
// 이 셋의 아이템 개수: 3

// 2. 항목 존재 여부 in 연산자
println("b" in readOnlyFruit)
// true
println("k" in readOnlyFruit)
// false

// 3. 항목 추가 및 제거는 .add()와 .remove()
val fruit : MutableSet<String> = mutableSetOf("a", "b", "c", "c")
fruit.add("orange") // 셋에 orange 추가
println(fruit)
// [a, b, c, orange]
fruit.remove("orange") // 셋에서 orange 제거
println(fruit)
// [a, b, c]

⭐ Map

  • 항목을 키-값 쌍으로 저장
  • 값을 참조하려면 키를 사용
  • 맵은 리스트처럼 번호로 인덱싱하지 않고 키로 값을 조회할 때 유용
  • 모든 키는 고유해야 함
  • 각 키는 하나의 값에만 매핑
  • 중복 값은 허용

💫 Map 생성

읽기 전용 Map 생성 : mapOf(key1 to value1, key2 to value2, ...)

  • 읽기 전용 맵을 만들기 위해 mapOf() 함수 사용

변경 가능한 맵 (MutableMap) 생성 : mutableMapOf(key1 to value1, key2 to value2, ...)

  • 변경 가능한 맵을 만들기 위해 mutableMapOf() 함수 사용
val readOnlyMenu = mapOf("a" to 100, "b" to 20, "c" to 20)
println(readOnlyMenu)
// 
val menu : MutableMap<String, Int> = mutableMapOf("a" to 100, "b" to 20, "c" to 20)
println(menu)
//

읽기 전용 뷰 생성 (Casting)

  • 원하지 않는 수정으로부터 보호하기 위해서
  • MutablMapMap으로 할당해서 읽기 전용 뷰를 얻을 수 있다.
val juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
val juiceMenuLocked: Map<String, Int> = juiceMenu

💫 Map 사용하기

  • 값 접근 : 맵의 값을 참조하려면 인덱스 접근 연산자 [ ]와 함께 키를 사용 : myMap[key]
  • 항목 수 얻기 : .count()
  • 항목 추가 및 제거 : .put(key, value), .remove()
    • ※ 이미 key가 존재하는데 put을 할 경우 update 된다.
  • 특정 키 확인 : .containsKey() ⇒ true, false로 나온다.
  • 키와 값 컬렉션 얻기 : .keys, .values ⇒ List로 반환된다.
  • 키 또는 값 존재 여부 확인 : in 연산자 사용 ⇒ true, false 반환
    • key in myMap.keys
    • value in myMap.values
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
// 1. 값 접근: [key]
println("apple 주스의 값은: ${readOnlyJuiceMenu["apple"]}")
// 출력: apple 주스의 값은: 100

// 2. 항목 수 얻기 : `.count()`
println("이 맵에는 ${readOnlyJuiceMenu.count()}개의 키-값 쌍이 있습니다")
// 출력: 이 맵에는 3개의 키-값 쌍이 있습니다

// 3. 항목 추가 및 제거 : `.put()`, `.remove()`
val juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu.put("coconut", 780) // 맵에 키 "coconut"과 값 780 추가
juiceMenu.put("coconut", 150) // 맵에 키 "coconut"과 값 150 추가 => update됨
println(juiceMenu)
// 출력: {apple=100, kiwi=190, orange=100, coconut=150}
juiceMenu.remove("orange")    // 맵에서 키 "orange" 제거
println(juiceMenu)
// 출력: {apple=100, kiwi=190, coconut=150}

// 4. 특정 키 확인 : `.containsKey()` ⇒ true, false로 나온다.
println(readOnlyJuiceMenu.containsKey("kiwi"))
// 출력: true

// 5. 키와 값 컬렉션 얻기 : `.keys`, `.values`  ⇒ List로 반환된다.
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println(readOnlyJuiceMenu.keys)
// 출력: [apple, kiwi, orange]
println(readOnlyJuiceMenu.values)
// 출력: [100, 190, 100]

// 6. 키 또는 값 존재 여부 확인 : `in` 연산자 사용 ⇒ true, false 반환
println("orange" in readOnlyJuiceMenu.keys)
// 출력: true
println(200 in readOnlyJuiceMenu.values)
// 출력: false

💗 제어 흐름(Control flow)

  • 조건식 (Conditional expressions) : 참으로 평가되는지에 다라 결정 내림

⭐ 조건문 (Conditional expressions)

  • 코틀린은 조건식을 판별하기 위해 ifwhen을 사용

💫 if - else if - else

  • if를 사용하려면 조건식을 괄호 () 안에 추가하고, 결과가 참이면 수행할 작업을 중괄호 {} 안에 추가
val d: Int
val check = true
val anotherCheck = false

if (check) {
    d = 1
} else if (anotherCheck) {
    d = 2
} else {
    d = 3
}

println(d)  // 1
  • 대신 if를 표현식으로 사용할 수 있다.
  • 각 작업에 코드가 한 줄만 있는 경우 중괄호 {}는 선택 사항
val a = 1
val b = 2

println(if (a > b) a else b)  // 2

var max = a
if (a < b) max = b

// else와 함께 사용
if (a > b) {
    max = a
} else {
    max = b
}

// 표현식으로 사용
max = if (a > b) a else b

// 표현식에서 else if 사용
val maxLimit = 1
val maxOrLimit = if (maxLimit > a) maxLimit else if (a > b) a else b
  • if 표현식의 각 분기는 블록일 수 있다. 이 경우 블록의 마지막 표현식이 블록의 값이 된다.
  • if를 표현식으로 사용할 경우, 예를 들어 값을 반환하거나 변수에 할당하려는 경우, else 분기는 필수!!
  • val max = if (a > b) { print("Choose a") a } else { print("Choose b") b }

💫 when

  • 여러 분기 조건이 있는 조건식 사용할 때 사용
  • 다른 언어의 switch 문과 유사
  • when은 문장(statement)이나 표현식(expression)으로 사용 가능

❓ statment(문장)과 expression(표현식)이란? 오랜만에 개념 정리하기

🌟 Statement (문장)

  • statement는 프로그램이 수행해야 할 명령
  • statement보통 결과를 반환하지 않고, 프로그램의 상태를 변경하는 역할을 한다.
  • 예를 들어 변수 선언, 할당, 조건문, 반복문 등이 모두 statement 이다.

예시:

val x = 5  // 변수 선언 및 할당
println(x) // 함수 호출 (statement)
  • 위의 코드에서 val x = 5statement로, 변수 x를 선언하고 값을 할당한다.
  • println(x) 역시 statement로, x의 값을 출력한다.

🌟 Expression (표현식)

  • expression은 값을 계산하고 그 값을 반환하는 코드
  • expression항상 결과를 반환하며, 이 결과는 다른 expression이나 statement에서 사용할 수 있다.
  • 예를 들어, 수학 연산, 함수 호출, 변수 참조 등이 모두 expression입니다.

예시:

val sum = 1 + 2  // 1 + 2는 expression
val length = "hello".length  // "hello".length는 expression
  • 위의 코드에서 1 + 2"hello".length는 모두 expression으로, 각각 3과 5를 반환한다..
  • 이 값들은 변수 sumlength에 할당된다.

when을 문장(statement)으로 사용

  • 조건식을 괄호 () 안에 추가하고, 수행할 작업을 중괄호 {} 안에 추가
  • 각 분기에는 ->를 사용하여 조건과 작업을 구분
val obj = "Hello"

when (obj) {
    "1" -> println("One")
    "Hello" -> println("Greeting")
    else -> {
                println("Unknown")
                }
}
// Greeting

when을 표현식(expression)으로 사용

  • when 문법을 변수에 즉시 할당 가능
val obj = "Hello"

val result = when (obj) {
    "1" -> "One"
    "Hello" -> "Greeting"
    else -> "Unknown"
}
println(result)  // Greeting
  • when을 표현식으로 사용할 경우, else 분기는 필수
  • 단, 컴파일러가 모든 가능한 경우가 분기 조건에 의해 다루어지고 있음을 증명할 수 있는 경우(예: 열거형 클래스의 항목 및 클래스의 하위 유형)에는 예외
  • when 문에서는 다음 조건을 만족하는 경우 else 분기가 필수
    • when의 주제가 Boolean, enum 또는 sealed 타입이거나 그들의 nullable 버전인 경우.
    • when의 분기들이 주제의 모든 가능한 경우를 다루지 않는 경우.
    • enum class Color { RED, GREEN, BLUE } when (getColor()) { Color.RED -> println("red") Color.GREEN -> println("green") Color.BLUE -> println("blue") // 'else'는 필요하지 않습니다. 모든 경우가 다루어지기 때문입니다. } when (getColor()) { Color.RED -> println("red") // GREEN과 BLUE에 대한 분기가 없습니다. else -> println("not red") // 'else'는 필수입니다. }
  • when은 변수와 일치시키는 데 유용할 뿐만 아니라, Boolean 조건 체인을 확인할 때도 유용
val temp = 18

val description = when {
    temp < 0 -> "very cold"
    temp < 10 -> "a bit cold"
    temp < 20 -> "warm"
    else -> "hot"
}
println(description)  // warm
  • 여러 경우에 대해 공통 동작을 정의하려면 단일 줄에 조건을 쉼표로 결합
when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}
  • 분기 조건으로 상수뿐만 아니라 임의의 표현식을 사용 가능
when (x) {
    s.toInt() -> print("s encodes x")
    else -> print("s does not encode x")
}
  • 값이 범위나 컬렉션에 포함되는지 여부를 확인 가능
when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}
  • 특정 타입인지 여부를 확인 가능
  • 스마트 캐스트 덕분에 추가적인 검사 없이 타입의 메서드와 속성에 접근 가능
fun hasPrefix(x: Any) = when (x) {
    is String -> x.startsWith("prefix")
    else -> false
}

⭐ Ranges (범위)

  • 반복문에서 사용할 범위 만드는 방법

💫 Ranges

  • .. 연산자 (끝 포함) ⇒ 1..4는 1, 2, 3, 4
  • ..< 연산자 (끝 포함X) ⇒ 1..<4는 1, 2, 3
  • 역순으로 범위를 선언 downTo를 사용 ⇒ 4 downTo 1은 4, 3, 2, 1
  • 1이 아닌 다른 증가값을 가지는 범위를 선언하려면 step을 사용하고 원하는 증가값을 추가 ⇒ 1..5 step 2는 1, 3, 5
  • 문자(Char) 범위도 같은 방식으로 사용 가능
    • 'a'..'d''a', 'b', 'c', 'd'
    • 'z' downTo 's' step 2'z', 'x', 'v', 't'

⭐ Loops (반복문)

  • 프로그래밍에서 가장 일반적인 두 가지 반복 구조는 forwhile
  • for는 값의 범위를 반복하며 작업을 수행할 때 사용
  • while은 특정 조건이 만족될 때까지 작업을 계속할 때 사용

💫 for

  • for 루프는 반복자를 제공하는 모든 항목을 통해 반복
  • 다른 언어의 foreach 루프와 동일
  • 범위를 사용하여 작성
for (number in 1..5) {
    print(number)
}
// 12345
  • 컬렉션도 반복문으로 순회 가능
val cakes = listOf("carrot", "cheese", "chocolate")

for (cake in cakes) {
    println("Yummy, it's a $cake cake!")
}
// Yummy, it's a carrot cake!
// Yummy, it's a cheese cake!
// Yummy, it's a chocolate cake!
  • for는 반복자를 제공하는 모든 항목을 통해 반복 ⇒ 이것의 조건은?
    • iterator()를 반환하는 멤버 또는 확장 함수가 있으며, 이는 Iterator<>를 반환
    • next() 멤버 또는 확장 함수가 있음
    • Boolean을 반환하는 hasNext() 멤버 또는 확장 함수가 있음
    • 이 세 함수는 모두 operator로 표시되어야 함
  • 인덱스를 사용하여 배열이나 리스트를 반복
for (i in array.indices) {
    println(array[i])
}
  • withIndex 라이브러리 함수를 사용
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

💫 while

  • while은 두 가지 방식으로 사용할 수 있다.
    1. 조건식이 참인 동안 코드 블록을 실행 (while)
    2. 코드 블록을 먼저 실행한 다음 조건식을 확인 (do-while)

1. while 예제

  • 조건식이 참인 동안 코드 블록을 실행
var cakesEaten = 0
while (cakesEaten < 3) {
    println("Eat a cake")
    cakesEaten++
}
// Eat a cake
// Eat a cake
// Eat a cake

2. do-while 예제

  • 먼저 코드 블록을 실행한 다음 조건식을 확인
var cakesEaten = 0
var cakesBaked = 0

while (cakesEaten < 3) {
    println("Eat a cake")
    cakesEaten++
}
// cakesEaten이 3이 되어 있는 상태
do {
    println("Bake a cake")
    cakesBaked++
} while (cakesBaked < cakesEaten)
// Eat a cake
// Eat a cake
// Eat a cake
// Bake a cake // do에서 출력됨. 출력 후 cakeEaten은 3, cakesBaked는 1이 됨
// Bake a cake // 1 < 3 조건 통과로 출력된 것. cakesBaked는 2가 됨
// Bake a cake // 2 < 3 조건 통과로 출력된 것. cakesBaked는 3이 됨
  • return, break, countiue는 다른 언어들과 동일하게 사용된다.

Returns and jumps

  • Kotlin의 3가지 구조적 점프 표현식
    • return: 기본적으로 가장 가까운 함수 또는 익명 함수에서 반환함
    • break: 가장 가까운 루프를 종료
    • continue: 가장 가까운 루프의 다음 단계로 진행

💫 Break and continue labels

  • Kotlin에서는 모든 표현식에 라벨을 붙일 수 있다.
  • 라벨은 식별자 뒤에 @ 기호가 오는 형태 ⇒ 예를 들어 abc@ 또는 fooBar@ 처럼 생김
  • 표현식에 라벨을 붙이려면 해당 표현식 앞에 라벨을 추가하면 된다.
  • 라벨을 사용하여 break 또는 continue를 지정
loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (...) break@loop
    }
}
  • 라벨이 지정된 break는 해당 라벨로 표시된 루프 바로 다음 실행 지점으로 점프한다.
  • continue는 해당 라벨이 지정된 루프의 다음 반복으로 진행한다.

💫 Return to labels

  • Kotlin에서는 함수 리터럴, 로컬 함수, 객체 표현식을 사용하여 함수를 중첩할 수 있다.
  • 라벨이 지정된 반환은 외부 함수에서 반환할 수 있게 해준다.
  • 가장 중요한 사용 사례는 람다 표현식에서 반환하는 것
    • 이러한 비지역 반환은 인라인 함수에 전달된 람다 표현식에 대해서만 지원된다.
    • 람다 표현식에서 반환하려면 라벨을 붙이고 반환을 지정한다..
    • fun foo() { listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit // 람다의 호출자에게 로컬 반환 - forEach 루프 print(it) } print("명시적 라벨로 완료") }
    • 이러면 람다 표현식에서만 반환됨
  • fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return // foo()의 호출자에게 비지역 반환 print(it) } println("이 지점에는 도달하지 않습니다") }
  • 종종 암시적 라벨을 사용하는 것이 더 편리하다.
  • 이러한 라벨은 람다가 전달된 함수와 동일한 이름을 가진다.
  • fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@forEach // 람다의 호출자에게 로컬 반환 - forEach 루프 print(it) } print("암시적 라벨로 완료") }
  • 대안으로, 람다 표현식을 익명 함수로 대체할 수 있다. 익명 함수에서의 return 문은 익명 함수 자체에서 반환한다.
  • fun foo() { listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) { if (value == 3) return // 익명 함수의 호출자에게 로컬 반환 - forEach 루프 print(value) }) print("익명 함수로 완료") }
  • 위의 3가지 예제는 일반 루프에서 continue를 사용하는 것과 유사하다.

💗 Functions(함수)

  • Kotlin에서 함수는 fun 키워드를 사용하여 선언
  • 함수의 매개변수는 괄호 () 안에 작성
  • 각 매개변수는 타입을 가져야 함
  • 여러 매개변수는 쉼표로 구분
  • 반환 타입은 함수 괄호 뒤에 콜론 :으로 구분되어 작성
  • 함수의 본문은 중괄호 {} 안에 작성
  • 유효한 값을 반환하지 않는 함수의 반환 타입은 Unit으로, 이를 명시할 필요는 없다.
fun sum(x: Int, y: Int): Int {
    return x + y
}

fun justPrint(s : String) {
        println("그냥 프린트")
}
  • 코딩 규칙에서 함수 이름은 소문자로 시작하고 밑줄 없이 카멜 케이스(camel case)를 사용할 것을 권장

Named arguments

  • 코드의 가독성을 높이기 위해 함수 호출 시 매개변수 이름을 포함 가능
  • 이렇게 하면 매개변수를 어떤 순서로든 사용 가능
  • 다음 예는 문자열 템플릿($)을 사용하여 매개변수에 엑세스
fun printMessageWithPrefix(message: String, prefix: String) {
    println("[$prefix] $message")
}

fun main() {
    printMessageWithPrefix(prefix = "Log", message = "Hello")
    // [Log] Hello
}

Default parameter values

  • 함수 매개변수에 기본 값을 정의할 수 있다.
  • 기본 값이 있는 매개변수는 호출 시 생략할 수 있다.
fun printMessageWithPrefix(message: String = "test", prefix: String = "Info") {
    println("[$prefix] $message")
}

fun main() {
    printMessageWithPrefix("Hello", "Log") // [Log] Hello
    printMessageWithPrefix("Hello")        // [Info] Hello
    printMessageWithPrefix()        // [Info] test
}

Functions without return

  • 반환 값이 없는 함수의 반환 타입은 Unit이며, 명시할 필요는 없음
fun printMessage(message: String) {
    println(message)
}

fun main() {
    printMessage("Hello") // Hello
}

Single-expression functions

  • 단일 표현식 함수는 중괄호 대신 =을 사용하여 더 간결하게 작성할 수 있다.
  • 반환 타입은 함수 본문이 없는 경우에만 생략할 수 있다.
fun sum(x: Int, y: Int): Int {
    return x + y
}

fun main() {
    println(sum(1, 2))
    // 3
}
  • 아래는 변경한 것
fun sum(x: Int, y: Int) = x + y

fun main() {
    println(sum(1, 2)) // 3
}

Lambda expressions (람다 표현식)

  • Kotlin에서는 람다 표현식을 사용하여 코드를 더욱 간결하게 작성할 수 있다.
  • uppercasesString() 함수 예시
    • 위의 함수를 람다 표현식으로 아래와 같이 표현 가능하다.
    • fun main() { println({ text: String -> text.uppercase() }("hello")) // HELLO }
  • fun uppercaseString(text: String): String { return text.uppercase() } fun main() { println(uppercaseString("hello")) // HELLO }
  • 람다 표현식은 { } 안에 쓰여진다.
  • { 매개 변수 : 타입 -> 함수 본문 내용 }(매개변수)
  • 만약 매개변수가 없는 람다 표현식이라면 아래와 같이 ->를 사용하지 않고 작성해준다.
    • { println("Log message") }
  • 람다 표현식은 여러 방식으로 사용 가능
    • 람다를 변수에 할당한 후 나중에 호출할 수 있음
    • 람다 표현식을 다른 함수의 매개변수로 전달할 수 있음
    • 함수에서 람다 표현식을 반환할 수 있음
    • 람다 표현식을 자체적으로 호출할 수 있음

💫 Assign to variable(변수에 할당)

  • = 연산자로 변수에 람다 표현식 할당
fun main() {
    val upperCaseString = { text: String -> text.uppercase() }
    println(upperCaseString("hello"))
    // HELLO
}****

💫 Pass to another function

  • 컬렉션의 .filter() 함수에 전달하는 방법
val numbers = listOf(1, -2, 3, -4, 5, -6)
val positives = numbers.filter { x -> x > 0 }
val negatives = numbers.filter { x -> x < 0 }
println(positives)
// [1, 3, 5]
println(negatives)
// [-2, -4, -6]
  • .filter() 함수는 람다 표현식을 조건으로 받아 리스트의 각 요소를 검사하여 조건에 맞는 요소만 반환한다.
    • { x -> x > 0 }는 양수만 반환
    • { x -> x < 0 }는 음수만 반환
  • 람다가 함수의 유일한 매개변수인 경우, 함수 괄호를 생략할 수 있음
  • 또한, 컬렉션의 항목을 변형하는 .map() 함수의 예도 있음
val numbers = listOf(1, -2, 3, -4, 5, -6)
val doubled = numbers.map { x -> x * 2 }
val tripled = numbers.map { x -> x * 3 }
println(doubled)
// [2, -4, 6, -8, 10, -12]
println(tripled)
// [3, -6, 9, -12, 15, -18]
  • { x -> x * 2 }는 각 요소를 2배로 반환
  • { x -> x * 3 }는 각 요소를 3배로 반환

💫 Function types(함수 타입)

  • 기본 타입 외에도 함수 자체도 타입을 가짐
  • Kotlin의 타입 추론은 매개변수 타입에서 함수 타입을 추론할 수 있지만, 명시적으로 함수 타입을 지정해야 할 때도 있다.
  • 함수 타입의 구문은 다음과 같음
    • 각 매개변수의 타입은 괄호 () 안에 쉼표로 구분하여 작성
    • 반환 타입은 -> 뒤에 작성합
    • val 변수명 : (매개변수 타입) -> 변환 타입 = { 매개변수 -> 함수 본문 }
    • val upperCaseString: (String) -> String = { text -> text.uppercase() } fun main() { println(upperCaseString("hello")) // HELLO }
  • 매개변수가 없는 경우, 괄호 ()를 비워둔다. 예: () -> Unit
  • 매개변수와 반환 타입을 람다 표현식이나 함수 타입으로 선언해야 한다. 그렇지 않으면 컴파일러가 람다 표현식의 타입을 알 수 없기 때문이다.
    • 잘못된 예: val upperCaseString = { str -> str.uppercase() }

💫 Return from a function (함수에서 람다 표현식 반환)

  • 람다 표현식을 함수에서 반환하려면 함수 타입을 선언해야 한다.
  • 다음 예제에서 toSeconds() 함수는 (Int) -> Int 타입을 가지며, 항상 Int 타입의 매개변수를 받아 Int 값을 반환하는 람다 표현식을 반환한다.
  • toSeconds() 함수는 when 표현식을 사용하여 반환할 람다 표현식을 결정한다.
  • 예를 들어, "minute"을 인수로 받으면 value -> value * 60 람다를 반환한다.
fun toSeconds(time: String): (Int) -> Int = when (time) {
    "hour" -> { value -> value * 60 * 60 }
    "minute" -> { value -> value * 60 }
    "second" -> { value -> value }
    else -> { value -> value }
}

fun main() {
    val timesInMinutes = listOf(2, 10, 15, 1)
    val min2sec = toSeconds("minute")
    val totalTimeInSeconds = timesInMinutes.map(min2sec).sum()
    println("Total time is $totalTimeInSeconds secs")
    // Total time is 1680 secs
}

****

💫 Invoke separately (개별 호출)

  • 람다 표현식은 중괄호 {} 뒤에 괄호 ()를 추가하고, 해당 괄호 안에 매개변수를 포함하여 자체적으로 호출할 수 있다.
  • 람다 표현식 자체를 호출할 때, 필요한 매개변수를 괄호 안에 넣어준다.
println({ text: String -> text.uppercase() }("hello"))
// HELLO

****

💫 Trailing lambdas (후행 람다)

  • 람다 표현식이 함수의 유일한 매개변수인 경우 함수 괄호 ()를 생략할 수 있다.
  • 또한, 람다 표현식이 함수의 마지막 매개변수인 경우, 이 표현식을 함수 괄호 밖에 작성할 수 있으며. 이 구문을 후행 람다라고 한다.
println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 6

// 후행 람다 형태
println(listOf(1, 2, 3).fold(0) { x, item -> x + item })  // 6

💗 Classes (클래스)

  • Kotlin은 클래스와 객체를 사용하여 객체 지향 프로그램을 지원
  • 객체는 프로그램에서 데이터를 저장하는 데 유용
  • 클래스는 객체에 대한 일련의 특성을 선언할 수 있게 해줌
  • 클래스로부터 객체를 생성할 때 이러한 특성을 매번 선언할 필요가 없기 때문에 시간과 노력 절약 가능
  • 클래스 선언할 때 class키워드 사용
  • class Sample

⭐ Properties (속성)

  • 클래스 객체의 특성은 속성으로 선언 가능
  • 클래스 이름 뒤의 괄호 ( ) 안에 선언
    • 클래스의 생성자(constructor)의 매개변수로 속성 정의하는 방법
    • 인스턴스가 생성될 때 초기화 됨
class Contact(val id: Int, var email: String)
  • 중괄호 { }로 정의된 클래스 본문 안에 선언
    • 클래스 본문 안에 속성 정의하는 방법
    • 클래스 본문에 정의된 속성은 추가적인 초기화 로직 수행 가능
    • 초기화 블록(init )을 사용하여 더 복잡한 초기화 수행 가능
class Contact(val id: Int, var email: String) {
    val category: String = ""
}

class Person(val firstName: String, var lastName: String) {
    val fullName: String
        get() = "$firstName $lastName"
}

class Rectangle(val width: Double, val height: Double) {
    var area: Double
    var perimeter: Double

    init {
        area = width * height
        perimeter = 2 * (width + height)
        println("Rectangle initialized with area: $area and perimeter: $perimeter")
    }
}
  • 클래스의 인스턴스가 생성된 후 속성을 변경해야 하는 경ㅇ가 아니라면, 속성을 읽기 전용(val)으로 선언하는 것을 권장
  • 괄호 안에서 val, var 상관 없이 선언 가능하지만, 이러한 속성은 인스턴스 생성 후 접근 불가
  • 괄호 ( ) 안에 포함된 내용을 class header(클래스 헤더) 라고 함
  • 클래스 속성을 선언할 때 후행 쉼표를 사용할 수 있음
  • 함수 매개변수와 마찬가지로, 클래스 속성 기본값 가질 수 있음
class Contact(val id: Int, var email: String = "example@gmail.com") {
    val category: String = "work"
}

Create instance (인스턴스 생성)

  • 클래스에서 객체를 생성하기 위해 먼저 생성자(constructor)를 사용하여 클래스 인스턴스(instance)를 선언한다.
  • 예시
    • Contact 는 class
    • contactContact class의 instance
    • idemail은 properties(속성)
    • idemailcontact를 생성할 때 기본 생성자(constructor)로 사용됨
  • class Contact(val id: Int, var email: String) fun main() { val contact = Contact(1, "mary@gmail.com") }
  • Kotlin 클래스는 여러 생성자를 가질 수 있다.
  • 생성자는 직접 정의할 수 있다.
  • 여러 생성자를 선언하는 방법 생성자 참고 ▼

💫 Contructors (생성자) ⇒ 이후 정리하기

  • Kotlin에서 여러 생성자를 생성하는 방법에는 기본 생성자(primary constructor)와 1개 이상의 보조생성자(secondary constructors)를 사용하는 방법이 있다.
  • 기본 생성자(primary constructor)는 클래스 헤더에 정의
  • 보조생성자(secondary constructors)는 이후 constructor 키워드를 사용해 정의
class Person constructor(firstName: String) { /*...*/ }
  • 기본 생성자가 어떤 annottaionsvisibility modifers를 가지고 있지 않다면, constructor 키워드는 생략 가능
class Person(firstName: String) { /*...*/ }

Access properties (속성 접근)

  • 인스턴스 속성 접근은 인스턴스 이름 뒤 점(.)을 붙여 속성 이름 작성
class Contact(val id: Int, var email: String)

fun main() {
    val contact = Contact(1, "mary@gmail.com")

    // 속성 email의 값을 출력합니다.
    println(contact.email)           
    // mary@gmail.com

    // 속성 email의 값을 업데이트합니다.
    contact.email = "jane@gmail.com"

    // 속성 email의 새로운 값을 출력합니다.
    println(contact.email)           
    // jane@gmail.com
}
  • 속성의 값을 문자열의 일부로 연결하려면, 문자열 템플릿($)을 사용할 수 있다.
println("Their email address is: ${contact.email}")

Member functions (멤버 함수)

  • 객체의 동작을 정의하는 멤버 함수 정의
  • Kotlin에서 멤버 함수는 클래스 본문 내에 선언해야 한다.
  • 인스턴스에서 멤버 함수를 호출하려면, 인스턴스 이름 뒤 점(.)을 붙여 함수 이름 작성
class Contact(val id: Int, var email: String) {
    fun printId() {
        println(id)
    }
}

fun main() {
    val contact = Contact(1, "mary@gmail.com")
    // 멤버 함수 printId()를 호출합니다.
    contact.printId()           
    // 1
}

Data classes (데이터 클래스)

  • Kotlin에서는 데이터를 저장하는 데 특히 유용한 데이터 클래스가 있다.
  • 데이터 클래스는 일반 클래스와 동일한 기능을 가지지만, 추가적인 멤버 함수가 자동으로 제공된다.
  • 이들 멤버 함수는 인스턴스를 쉽게 출력할 수 있게 하거나, 클래스 인스턴스를 비교하고, 인스턴스를 복사하는 등의 작업을 수행할 수 있다. ⇒ 이러한 함수는 자동으로 자동으로 제공되므로 각 클래스에 대해 동일한 보일러플레이트 코드(boilerplate code)를 작성할 필요가 없다.

보일러플레이트 코드 (Boilerplate Code)란 무엇인가요?

보일러플레이트 코드 (Boilerplate Code)란 무엇인가요?

보일러플레이트 코드는 여러 장소에서 거의 동일하게 반복되는 코드 조각을 의미합니다. 주로 기본적인 구조나 설정을 포함하며, 실제 비즈니스 로직과는 직접적인 관련이 없는 코드를 지칭합니다. 이러한 코드는 일반적으로 필요하지만, 반복적이고 번거로울 수 있습니다.

예시

  1. 자바의 getter/setter 메소드:
    자바에서 객체의 속성에 접근하기 위해 자주 사용되는 getter와 setter 메소드들은 보일러플레이트 코드의 대표적인 예입니다. 이러한 메소드는 속성의 값을 읽고 설정하는데 사용되며, 대부분의 클래스에 반복적으로 포함됩니다.
  2. public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
  3. 로그 출력:
    로깅을 설정하고 로그 메시지를 출력하는 코드도 보일러플레이트 코드의 한 예입니다. 많은 프로그램에서 로깅은 필수적이지만, 설정과 사용법이 대부분 비슷합니다.
  4. =import java.util.logging.Logger; public class MyApp { private static final Logger LOGGER = Logger.getLogger(MyApp.class.getName()); public static void main(String[] args) { LOGGER.info("Application started"); // 기타 코드 } }
  5. 데이터 클래스:
    데이터 클래스는 주로 데이터를 저장하기 위한 클래스로, 생성자, toString(), equals(), hashCode(), copy() 메소드를 정의해야 하는 경우가 많습니다. 이런 메소드들은 대부분 동일한 패턴을 따르기 때문에 보일러플레이트 코드로 간주될 수 있습니다.

Kotlin의 보일러플레이트 코드 감소

Kotlin은 보일러플레이트 코드를 줄이기 위해 많은 언어적 특징을 제공합니다. 예를 들어, 데이터 클래스에서는 data 키워드를 사용하여 자동으로 toString(), equals(), hashCode(), copy() 메소드를 생성합니다.

data class User(val name: String, val id: Int)

이 한 줄의 선언으로, User 클래스는 모든 필요한 메소드를 자동으로 제공합니다. 이렇게 하면 개발자가 직접 반복적인 코드를 작성할 필요가 없어지며, 코드가 더 간결하고 유지보수하기 쉬워집니다.

요약

보일러플레이트 코드는 여러 곳에서 반복적으로 사용되는 기본적이고 구조적인 코드 조각입니다. Kotlin과 같은 현대 프로그래밍 언어는 이러한 코드를 줄이기 위한 다양한 기능을 제공하여 개발자의 생산성을 높이고 코드의 가독성을 향상시킵니다.

  • 데이터 클래스를 선언하려면 data 키워드를 사용한다.
  • data class User(val name: String, val id: Int)
  • 데이터 클래스의 가장 유용한 기본 제공 멤버 함수
| 함수 | 설명 |
| --- | --- |
| .toString() | 클래스 인스턴스와 그 속성의 읽기 쉬운 문자열을 출력 |
| .equals() 또는 == | 클래스 인스턴스를 비교 |
| .copy() | 다른 인스턴스를 복사하여 클래스 인스턴스를 생성 |
  • 각 함수 사용 예시는 다음과 같다.

💫 Print as string (문자열로 출력)

  • 클래스 인스턴스의 읽기 쉬운 문자열을 출력하려면, .toString() 함수를 명시적으로 호출하거나, println(), print()과 같은 출력 함수를 사용하면 자동으로 .toString() 함수가 호출된다.
val user = User("Alex", 1)

// 자동으로 toString() 함수를 사용하여 출력이 읽기 쉽게 됨
println(user)
// User(name=Alex, id=1)
  • 이 기능은 특히 디버깅이나 로그를 생성할 때 유용

💫 Compare instance (인스턴스 비교)

  • 데이터 클래스 인스턴스를 비교하려면, 동일 연산자 ==를 사용
val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)

// user와 secondUser를 비교합니다.
println("user == secondUser: ${user == secondUser}")
// user == secondUser: true

// user와 thirdUser를 비교합니다.
println("user == thirdUser: ${user == thirdUser}")
// user == thirdUser: false

💫 Copy instance (인스턴스 복사)

  • 데이터 클래스 인스턴스를 정확히 복사하려면, 인스턴스에서 .copy() 함수를 호출
  • 데이터 클래스 인스턴스를 복사하고 일부 속성을 변경하려면, 인스턴스에서 .copy() 함수를 호출하고 함수 매개변수로 속성에 대한 대체 값을 추가한다.
val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)

// user를 정확히 복사합니다.
println(user.copy())
// User(name=Alex, id=1)

// 이름을 "Max"로 설정하여 user를 복사합니다.
println(user.copy("Max"))
// User(name=Max, id=1)

// id를 3으로 설정하여 user를 복사합니다.
println(user.copy(id = 3))
// User(name=Alex, id=3)
  • 인스턴스를 복사하는 것은 원본 인스턴스를 수정하는 것보다 안전하다.
  • 원본 인스턴스에 의존하는 모든 코드가 복사본과 복사본으로 수행하는 작업에 영향을 받지 않기 때문이다!!

💗 Null safety (널 안전성)

  • Kotlin에서는 null 값을 가질 수 있다.
  • 프로그램에서 null 값으로 인한 문제를 방지하기 위해, Kotlin은 null 안전성을 제공한다.
  • null 안전성은 컴파일 시간에 잠재적인 null 값 문제를 감지하여 런타임 오류를 방지한다.
  • Null 안전성은 다음과 같은 기능들의 조합으로 이루어진다.
    • 프로그램에서 null 값을 허용할지 명시적으로 선언
    • null 값을 검사
    • null 값을 포함할 수 있는 속성이나 함수에 안전하게 접근
    • null 값이 감지되었을 때 수행할 작업을 선언

Nullable types (Nullalle 타입)

  • Kotlin은 nullable 타입을 지원하여 선언된 타입이 null 값을 가질 가능성을 허용
  • 기본적으로 타입은 null 값을 허용하지 않는다.
  • Nullable 타입은 타입 선언 뒤에 ? 을 추가하여 명시적으로 선언
fun main() {
    // neverNull은 String 타입입니다.
    var neverNull: String = "This can't be null"

    // 컴파일러 오류가 발생합니다.
    neverNull = null

    // nullable은 nullable String 타입입니다.
    var nullable: String? = "You can keep a null here"

    // 이는 허용됩니다.
    nullable = null

    // 기본적으로, null 값은 허용되지 않습니다.
    var inferredNonNull = "The compiler assumes non-nullable"

    // 컴파일러 오류가 발생합니다.
    inferredNonNull = null

    // notNull은 null 값을 허용하지 않습니다.
    fun strLength(notNull: String): Int {                 
        return notNull.length
    }

    println(strLength(neverNull)) // 18
    println(strLength(nullable))  // 컴파일러 오류가 발생합니다.

    var test : String = ""
    println(strLength(test)) // 0
}
  • length는 문자열 클래스의 속성으로, 문자열 내 문자 수를 포함

Check for null values (null 값 검사)

  • 조건문 내에서 null 값의 존재를 검사할 수 있다.
  • 다음 예제에서 describeString() 함수는 maybeString이 null이 아니고 길이가 0보다 큰지를 확인하는 if 문을 가지고 있다.
  • fun describeString(maybeString: String?): String { if (maybeString != null && maybeString.length > 0) { return "String of length ${maybeString.length}" } else { return "Empty or null string" } } fun main() { val nullString: String? = null println(describeString(nullString)) // Empty or null string }

Use safe calls (안전한 호출 사용)

  • null 값을 가질 수 있는 객체의 속성에 안전하게 접근하려면, 안전 호출 연산자 ?.를 사용
  • 안전 호출 연산자는 객체 또는 접근된 속성 중 하나가 null이면 null을 반환
  • 이는 코드에서 null 값이 오류를 발생시키는 것을 방지하는 데 유용하다.
  • 다음 예제에서 lengthString() 함수는 안전 호출을 사용하여 문자열의 길이 또는 null을 반환
  • fun lengthString(maybeString: String?): Int? = maybeString?.length fun main() { val nullString: String? = null println(lengthString(nullString)) // null }
  • 안전 호출은 체인으로 연결할 수 있다.
  • 안전 호출을 체인으로 연결 후 객체의 속성 중 하나라도 null이면 null을 반환하고 오류를 발생시키지 않는다.
  • 예를 들어:
  • person.company?.address?.country
  • 안전 호출 연산자는 확장 함수 또는 멤버 함수를 안전하게 호출하는 데도 사용할 수 있다.
  • 이 경우 null 검사 후에 함수가 호출된다.
  • null 값이 감지되면 호출이 생략되고 null이 반환된다.
  • 다음 예제에서 nullString은 null이므로 .uppercase() 호출이 생략되고 null이 반환된다.
  • fun main() { val nullString: String? = null println(nullString?.uppercase()) // null }

Use Elvis operator (엘비스 연산자 사용)

  • null 값이 감지되었을 때 반환할 기본 값을 제공하려면 엘비스 연산자 ?:를 사용한다.
  • 엘비스 연산자의 왼쪽에는 null 값을 검사할 대상을 작성하고, 오른쪽에는 null 값이 감지되었을 때 반환할 값을 작성한다.
  • 다음 예제에서 nullString은 null이므로 속성 접근에서 null이 반환된다. 따라서 엘비스 연산자가 0을 반환한다.
  • fun main() { val nullString: String? = null println(nullString?.length ?: 0) // 0 }