Posts [Kotlin] Chapter3. 함수 정의와 호출 총정리
Post
Cancel

[Kotlin] Chapter3. 함수 정의와 호출 총정리

Kotlin IN ACTION(출판사: 에이콘) 책을 통해 Kotlin을 배워보자

Chapter3. 함수 정의와 호출

코틀린 컬렉션

1
2
3
4
5
6
7
8
9
val set = hashSetOf(1, 7, 53)
val list = listOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

fun main(args: Array<String>) {
    println(set.javaClass)	//class java.util.HashSet
    println(list.javaClass)  //class java.util.ArrayList
    println(map.javaClass)  //class java.util.HashMap
}
  • 자바 컬렉션을 활용한 코틀린 컬렉션

  • 자바보다 더 많은 기능을 쓸 수 있음

    1
    2
    3
    4
    
    val strings = listOf("first", "second", "fourteenth")
    println(strings.last())	//fourteenth
    val numbers = setOf(1, 14, 2)
    println(numbers.max())	//14
    
    • last(), max() 는 코틀린에서 편의를 위해 추가한 확장함수

직관적인 함수 호출

  • toString()함수 응용한 함수

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    fun <T> joinToString(
            collection: Collection<T>,
            separator: String,
            prefix: String,
            postfix: String
    ): String {
      
        val result = StringBuilder(prefix)
      
        for ((index, element) in collection.withIndex()) {
            if (index > 0) result.append(separator)
            result.append(element)
        }
      
        result.append(postfix)
        return result.toString()
    }
      
    fun main(args: Array<String>) {
        val list = listOf(1, 2, 3)
        val alphabets = listOf("a", "b", "c")
        println(joinToString(list, separator = "; ", prefix = "(", postfix = ")"))
        println(joinToString(alphabets, separator = "; ", prefix = "(", postfix = ")"))
        //(1; 2; 3)
        //(a; b; c)
    }
    
    • 인자 이름 & 기본 파라미터 값

      • 인자의 이름을 명시 가능

        Java

        1
        
        joinToString(collection, /* separator */ "", /* prefix */ "", /*postfix */ ".");
        

        Kotlin

        1
        
        joinToString(collection, prefix = "(", separator = ";", postfix = ")")
        

        차이점? 파라미터 이름이 같다면 순서를 바꿔도 가능!

        주의! 자바에서 작성한 코드를 코틀린에서 호출한 경우에는 인자 이름 명시 불가능!

        ​ 함수 파라미터에 정보를 넣는 것은 자바 8이후, 코틀린은 JDK 6과 호환!

      • 기본 파라미터 값 초기화

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        
        fun <T> joinToString(
            	collection : Collection<T>,
                separator: String = ", ",
                prefix: String = "",
                postfix: String = ""
        ): String {
           ...
        }
              
        fun main(args: Array<String>) {
            val list = listOf(1, 2, 3)
            println(joinToString(list))	
            //1, 2, 3
            println(joinToString(list, separator = ";"))	
            //1;2;3
            println(joinToString(list, separator = "; ", prefix = "("))	
            //(1; 2; 3
            println(joinToString(list, separator = "; ", prefix = "(", postfix = ")"))
            //(1; 2; 3;)
        }
        
        • 값을 지정하지 않은 모든 인자는 디폴트 값을 적용

      주의! 자바에는 디폴트 파라미터 값이라는 개념이 없기때문에 코틀린 함수를 자바에서 호출하는 경우

      ​ @JvmOverloads 애노테이션을 추가하면 컴파일러가 자동으로 오버로딩 함수 생성

최상위 함수와 프로퍼티

  • 최상위 함수

    • Java

      1
      2
      3
      4
      5
      
      package strings;
          
      public class JoinKt{
      	public static String joinToString(...){...}
      }
      
    • Kotlin

      1
      2
      3
      
      package strings
          
      fun joinToString(...){...}
      
      • 자바에서 함수 호출

        1
        2
        3
        4
        
        import strings.JointKt; 
        ... 
              
        JoinKt.joinToStirng(list,",","","");
        
      • 파일에 대응하는 클래스 이름 변경

        1
        2
        3
        
        //코틀린
        @file:JvmName("StringFunctions")	//패키지명 선언보다 이전에 위치
        package strings
        
        1
        2
        3
        
        //자바에서 호출
        import strings.StringFunctions;
        StringFunctions.joinToString(list, ",", "", "");
        

UtilClass를 대신해서 사용할 수 있다는 장점이 있다고 했는데, Util이 더 편하지않나…?

  • 최상위 프로퍼티

    1
    2
    3
    4
    5
    6
    
    var cnt;		//최상위 프로퍼티
      
    fun performOperation(){	//최상위 함수
    	cnt++
    }
    ...
    
    • 프로퍼티를 클래스보다 더 이전에 위치
    • 최상위 프로퍼티도 접근자 메소드를 통해 접근가능
    • const 변경자를 추가하면 public static final 필드로 만들 수 있다.

확장함수와 확장 프로퍼티

  • 클래스의 멤버 메서드처럼 호출되지만 클래스 밖에 호출되는 함수

  • 확장함수를 만들기 위해서는 수신객체타입(Receiver Type)과 수신객체(Receiver Object)가 필요

    1
    2
    3
    
    fun String.lastChar() : Char = this.get(this.length - 1)
    println("Kotlin".lastChar())
    //n
    
    • 수신객체타입(Receiver Type): 확장이 정의될 클래스 ex) String

    • 수신객체(Receiver Object): 위 클래스의 인스턴스 객체 ex)”Kotlin”, this

    • 확장함수 안에서는 privateprotected 접근 불가능, 해당 클래스의 public함수에만 접근 가능

  • 확장함수의 Import

    • 확장함수를 사용하기 위해서는 import를 통해서 사용

      1
      2
      3
      
      import strings.lastChar		
      //import strings.*
      val c = "Kotlin".lastChar()
      

      주의! : 한 클래스 내의 같은 이름의 확장함수가 둘 이상일 경우 이름이 충돌할 수 있으니 as를 사용

      1
      2
      
      import strings.lastChar as last
      val c = "Kotlin".last()
      
  • joinToString함수를 이용한 확장함수

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    fun <T> Collection<T>.joinToString(
            separator: String = ", ",
            prefix: String = "",
            postfix: String = ""
    ): String {
        val result = StringBuilder(prefix)
      
        for ((index, element) in this.withIndex()) {	//this는 수신객체
            if (index > 0) result.append(separator)
            result.append(element)
        }
      
        result.append(postfix)
        return result.toString()
    }
      
    //문자열의 컬렉션만 받음
    fun Collection<String>.join(
            separator: String = ", ",
            prefix: String = "",
            postfix: String = ""
    ) = joinToString(separator, prefix, postfix)
      
    fun main(args: Array<String>) {
        println(listOf("one", "two", "eight").join(" "))	//one two eight
    }
    
    • 내부적으로 확장함수는 수신객체첫 번째 인자로 받음
    • 자바에서 호출시 수신객체를 첫 번째 인자로 넘겨줘야함

확장함수의 오버라이드

  • 멤버함수 오버라이드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    open class View {
        open fun click() = println("View clicked")	//멤버함수
    }
      
    class Button: View() {
        override fun click() = println("Button clicked")
    }
      
    fun main(args: Array<String>) {
        val view: View = Button()
        view.click()	//Button Clicked
    }
    
  • 확장 함수가 정적 메소드와 같은 특징을 가지므로 확장함수를 하위 클래스에서 오버라이드 할 수 없다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    open class View {
        open fun click() = println("View clicked")	//멤버함수
    }
      
    class Button: View() {
        override fun click() = println("Button clicked")
    }
      
    fun View.showOff() = println("I'm a view!")		//확장함수
    fun Button.showOff() = println("I'm a button!")	//확장함수
      
    fun main(args: Array<String>) {
        val view: View = Button()
        view.showOff()	//I'm a view!
    }
    
    • 확장 함수는 클래스의 밖에 선언되며 확장함수 호출 시, 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장함수가 올 지 결정되는 것이지, 변수의 객체에 저장된 동적인 타입에 의해 확장 함수가 결정되지 않는다.

      <그림 3.2 참고>

    TODO - 알고가자!

    1. open class : 코틀린에서의 모든 클래스는 암시적으로 상위 클래스로 Any클래스를 가지는데,

    ​ 명시적으로 상위클래스를 지정하기 위해서는 open을 명시

    1. 멤버메서드? 정적메서드? 확장메서드?

    2. 특정 클래스에 추가된 확장함수가 멤버함수와 이름이 같다면 우선순위는 멤버함수 > 확장함수

확장 프로퍼티

1
2
3
4
5
6
7
8
val String.lastChar : Char
	get() = get(length - 1)		//getter만

var StringBuilder.lastChar : Char
	get() = get(length - 1)
	set(value : Char){
        this.setChartAt(length - 1, value)
    }
  • 일반 프로퍼티와 같은데 대신 수신객체클래스가 추가 되었을 뿐
  • 확장 프로퍼티는 backing field를 가지고 있지않으니 최소한 getter()는 구현해야 함.

컬렉션 처리

  • 가변인자함수

    1
    2
    3
    4
    
    val numbers = listOf(1,2,3,4,5)
    val strings = listOf("1", "2", "3")
      
    fun listOf<T>(vararg var: T) : List<T>{...}
    
    • vararg를 변경자로 선언타입 앞에 붙임

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      fun printNumbers(vararg numbers: Int) {
          for (number in numbers) {
              println(number)
          }
      }
          
      val numbers = intArrayOf(1, 2, 3)
      printNumbers(*numbers)
      printNumbers(10, 20, *numbers, 30, 40)
      
      • 자바의 스프레드 연산자: ... / 코틀린의 스프레드 연산자 *

      • 배열(Array)을 함수에 전달할 수 있다.

  • 중위 호출

    1
    2
    
    1 to "one"		//중위함수 호출
    1.to("one")		//확장함수 호출
    
    • 중위함수 to

      1
      
      infix fun Any.to(other : Any) = Pair(this, other)    // to는 중위함수이자 확장함수
      
  • 구조 분해 선언(7장에서 자세히 다룸)

    • data class 형태로 나오는 여러 값을 동시에 반환할 수 있다.
    • 자동으로 componentN함수가 생성됨.
    1
    2
    3
    4
    5
    
    for ((index, element) in this.withIndex()) {	//this는 수신객체 Collection<Int> / (1, 2, 3)
         println("$index: $element")	//0: 1
        							  //1: 2
        							  //2: 3
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    data class Size(val width : Int, val height : Int)
      
    fun main(){
        val size = Size(10, 20)
        val (w, h) = size
        val a = size.component1()
        val b = size.component2()
          
        println(a) // 10
        println(b) // 20
    }
    

문자열과 정규식

  • 문자열 나누기 / split()

    1
    2
    3
    
    //String.kt
    public inline fun CharSequence.split(regex: Regex, limit: Int = 0): List<String> = regex.split(this, limit)
    public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {...}
    
    1
    2
    3
    4
    
    println("12.345-6.A".split("\\.|-".toRegex()))		//정규식을 파라미터로 받는 함수
    //[12, 345, 6, A]
    println("12.345-6.A".split(".", "-"))			    //String을 파라미터로 받는 함수
    //[12, 345, 6, A]
    
    • 마침표 . 를 문자로 읽기위해 \\.을 사용하여 escape 시킴

    • 코틀린에서는 split()의 확장함수를 제공함으로써 Java의 와일크카드 문자(?)로 인한 혼동을 피함.
    • 정규식 표현 https://regexr.com/, https://ihateregex.io/playground에서 찾으면 쉽다.
  • 정규식과 3중 따옴표

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    fun parsePath(path: String) {
        val regex = """(.+)/(.+)\.(.+)""".toRegex()
        //group#1: /Users/yole/kotlin-book
        //group#2: chapter
        //group#3: adoc
        val matchResult = regex.matchEntire(path)
        if (matchResult != null) {
            val (directory, filename, extension) = matchResult.destructured
            // val directory = matchResult.destructured.component1()
            // val filename = matchResult.destructured.component2()
            // val extension = matchResult.destructured.component3()
            println("Dir: $directory, name: $filename, ext: $extension")
        }
    }
      
    fun main(args: Array<String>) {
        parsePath("/Users/yole/kotlin-book/chapter.adoc")
    }
    
    • 3중 따옴표를 사용하면, \, \n등을 포함한 어떤 문자도 따로 escape 시킬 필요가 없다.

    주의! : 역슬래쉬(\)나 줄바꿈(\n)등을 3중 따옴표 안에 넣으면 문자로 인식해서 그냥 출력된다.

중첩함수(Local method)

  • 함수에서 추출한 함수를 원 함수 내부에 중첩시키는 것을 말함.

  • 코드중복을 보여주는 예제 //아직 중첩함수 X

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    class User(val id: Int, val name: String, val address: String)
      
    fun saveUser(user: User) {
        if (user.name.isEmpty()) {					//-----------검증 중복
            throw IllegalArgumentException(
                "Can't save user ${user.id}: empty Name")
        }
      
        if (user.address.isEmpty()) {				//------------검증 중복
            throw IllegalArgumentException(
                "Can't save user ${user.id}: empty Address")
        }
      
        // Save user to the database
    }
      
    fun main(args: Array<String>) {
        saveUser(User(1, "", ""))
        //Can't save user ${user.id}: empty Name
    }	
    
    • 중첩함수를 이용한 중복 줄이기

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      class User(val id: Int, val name: String, val address: String)
          
      fun saveUser(user: User) {
          fun validate(user: User,				//중첩함수 validate(...)선언
                       value: String,
                       fieldName: String) {
              if (value.isEmpty()) {
                  throw IllegalArgumentException(
                      "Can't save user ${user.id}: empty $fieldName")
              }
          }
          
          validate(user, user.name, "Name")
          validate(user, user.address, "Address")
          
          // Save user to the database
      }
          
      fun main(args: Array<String>) {
          saveUser(User(1, "", ""))
      }
      
      • 중첩함수는 자신이 속한 바깥함수의 모든 파라미터와 변수를 참조 가능

      • 따라서 validate의 파라미터인 user까지 제거

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        
        class User(val id: Int, val name: String, val address: String)
              
        fun saveUser(user: User) {
            fun validate(value: String, fieldName: String) {	//불필요한 user파라미터 제거
                if (value.isEmpty()) {
                    throw IllegalArgumentException(
                        "Can't save user ${user.id}: " +
                            "empty $fieldName")
                }
            }
              
            validate(user.name, "Name")							//user.name만 전달
            validate(user.address, "Address")					//user.address만 전달
              
            // Save user to the database
        }
              
        fun main(args: Array<String>) {
            saveUser(User(1, "", ""))
        }
        
        • 확장함수를 이용해서 검증로직 개선

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          
          class User(val id: Int, val name: String, val address: String)
                  
          fun User.validateBeforeSave() {			//User클래스의 확장함수
              fun validate(value: String, fieldName: String) {	//중첩함수
                  if (value.isEmpty()) {
                      throw IllegalArgumentException(
                         "Can't save user $id: empty $fieldName")	//{$user.id}를 쓸 필요 X, $id O 
                  }
              }
                  
              validate(name, "Name")
              validate(address, "Address")
          }
                  
          fun saveUser(user: User) {
              user.validateBeforeSave()
                  
              // Save user to the database
          }
                  
          fun main(args: Array<String>) {
              saveUser(User(1, "2", ""))
          }
          

          TODO - 생각해보자!

          좋은 구조인가??

          과유불급이라고 중첩된 함수의 깊이가 깊어지면 깊어질수록 코드 읽기가 어려우니

          일반적으로는 한 단계만 함수를 중첩시키는 것을 권장한다.

참조
  • Kotlin IN Action / 출판사: 에이콘
출처
This post is licensed under CC BY 4.0 by the author.