-
3장 - 함수 정의와 호출Kotlin/코틀린인액션요약 2022. 9. 11. 00:01728x90
3장에서 다루는 내용
- 컬렉션, 문자열, 정규식을 다루기 위한 함수
- 이름 붙인 인자, 디폴트 파라미터 값, 중위 호출 문법 사용
- 확장 함수와 확장 프로퍼티를 사용해 자바 라이브러리 적용
- 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화
코틀린에서 컬렉션 만들기
var set = hashSetOf(1,7,53) // java.util.HashSet var list = arrayListOf(1,7,53) // java.util.ArrayList var map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") // java.util.HashMap val Strings = listOf("first","second","fourteenth"); println(strings.last()) //출력 : fourteenth val numbers = setOf(1,14,2) println(umbers.max()) //출력: 14
이는 코틀린이 자신만의 컬렉션 기능을 제공하지 않는다는 뜻입니다.
하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있습니다. (last, max 등)
코틀린이 자체 컬렉션을 제공하지 않는 이유는?
표준 자바 컬렉션을 사용하면 자바 코드와 상호작용하기 훨씬 더 쉽습니다.
자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 컬렉션을 서로 변환할 필요가 없습니다.
함수 호출 변환시키기
자바 컬렉션에는 디폴트 toString 구현이 들어있습니다.
,로 인자를 구분해서 출력해주는 것으로 고정되어 있습니다.
하지만 디폴트 구현과 달리 원소 사이를 ;으로 구분하고 싶다면 어떻게 해야 할까요?
자바에서는 구아바나 아파치 커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야 합니다.
하지만 코틀린에서는 이런 요구사항을 간단하게 만들 수 있습니다.
fun main(){ var set = hashSetOf(1,7,53) // java.util.HashSet println(joinToString(set, "; ","(" ,")")) } fun <T> joinToString( collection: Collection<T>, separator: String, prefix: String, postfix: String ) : String{ var result = StringBuffer(prefix) for((index, element) in collection.withIndex()){ if(index >0) result.append(separator) result.append(element) } result.append(postfix) return result.toString() }
하지만 이렇게 함수를 호출하게 되면 인자로 어떤 값이 들어가는지 헷갈리게 됩니다.
이런 이유로 자바에서는 빌더패턴을 사용합니다.
하지만 코틀린에서는 다음과 같이 사용할 수 있습니다.
println(joinToString(collection = set, separator = "; ", prefix = "(" , postfix = ")"))
디폴트 파라미터 값
자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있습니다.
예를 들어 java.lang.Thread에는 8가지 생성자가 있습니다.
코틀린에서는 함수 선언에서 파라미터 디폴트 값을 지정하여 이런 오버로드 중 상당수를 피할 수 있습니다.
fun main(){ var set = hashSetOf(1,7,53) // java.util.HashSet println(joinToString(set)) println(joinToString(set,"; ")) } fun <T> joinToString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = "" ) : String{ var result = StringBuffer(prefix) for((index, element) in collection.withIndex()){ if(index >0) result.append(separator) result.append(element) } result.append(postfix) return result.toString() }
자바에서는 디폴트 파라미터 값이라는 개념이 없어 코틀린 함수를 자바에서 호출해야 하는 경우에는 모든 인자를 명시해주어야 합니다.
자바에서 코틀린 함수를 자주 호출한다면 @JvmOverloads 애노테이션을 추가하면 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 생성자를 만들어줍니다.
최상위 프로퍼티
어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지 않지만 가끔 유용할 때가 있습니다.
const를 사용하여 public static final처럼 사용할 수 있습니다.
const val UNIX_LINE_SEPARATOR = "\n"
확장 함수와 확장 프로퍼티
어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수입니다.
다음은 어떤 문자열의 마지막 문자를 돌려주는 메서드입니다.
package strings fun String.lastChar(): Char = this.get(this.length - 1) println("Kotlin".lastChar())
확장 함수를 만들기 위해서는 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 됩니다.
매우 편리해 보이지만 확장 함수가 private 멤버나 protected 멤버에 접근할 수 없습니다.
확장 함수의 충돌
import strings.lastChar as last val c = "Kotlin".last()
as 키워드를 사용하여 import 한 클래스나 함수를 다른 이름으로 부를 수 있습니다.
한 파일 안에서 여러 패키지에 속해있는 이름이 같은 함수를 불러와야 할 때 유용하게 사용됩니다.
자바에서 확장 함수 호출
StringUtil.kt 파일에 확장 함수를 정의했다면 다음과 같이 호출할 수 있습니다.
char c = StringUtilKt.lastChar("Java");
joinToString()을 확장으로 정의하기
fun main(){ var set = hashSetOf(1,7,53) // java.util.HashSet println(set.joinToString()) println(set.joinToString("; ")) } fun <T> Collection<T>.joinToString( separator: String = ", ", prefix: String = "", postfix: String = "" ) : String{ var result = StringBuffer(prefix) for((index, element) in this.withIndex()){ if(index >0) result.append(separator) result.append(element) } result.append(postfix) return result.toString() }
이제 Collection 클래스의 멤버인 것처럼 호출할 수 있습니다.
확장 함수는 오버라이드 할 수 없다.
정적 메서드와 같은 특징을 가지기 때문에 오버라이드 할 수 없습니다.
확장 함수는 클래스의 일부가 아니라 밖에 선언됩니다.
따라서 이름과 파라미터가 완전히 동일한 확장 함수를 정의해도 실제로 확장 함수를 호출할 때 수신 객체로 지정된 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되며 동적으로 결정되지 않습니다.
fun View.showOff() = println("i'm a view") fun Button.showOff() = println("i'm a button") val view: View = Button() //확장 함수는 정적으로 결정된다. view.showOff(); //결과 : i'm a view
확장 프로퍼티
기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있습니다.
프로퍼티라는 이름으로 불리지만 상태를 저장할 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없습니다.
그럼에도 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 있습니다.
var StringBuilder.lastChar: Char get() = get(length -1) //프로퍼티 게터 set(value: Char){ this.setCharAt(length -1, value) //프로퍼티 세터 } fun main() { val sb = StringBuilder("Kotlin?") println(sb.lastChar) sb.lastChar = '!' println(sb) } //출력 //? //Kotlin!
필드가 존재하지 않기 때문에 게터는 꼭 정의해야 합니다.
컬렉션 처리 : 가변 길이 인자, 중위 함수 호출, 라이브러리
자바 컬렉션 API가 확장될 수 있는 이유 중 하나는 바로 위에서 다루었던 확장 함수 때문에 가능해집니다.
가변 인자 함수
자바의 가변 길이 인자처럼 메서드를 호출할 때 원하는 개수만큼 값을 넘기면 됩니다.
자바와 차이점은 타입 뒤에 ...을 붙이는 대신 코틀린에서는 파라미터 앞에 vararg 변경자를 붙이면 됩니다.
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 배열 앞에 *를 붙여줘야 하는 차이점이 있습니다.
중위 호출
중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣어줍니다.
1.to("one") //to 메서드를 일반적인 방식으로 호출 1 to "one" //to 메서드를 중위 호출
문자열과 정규식 다루기
코틀린의 문자열을 자바와 동일하며 단순히 다양한 확장 함수를 제공할 뿐입니다.
자바 개발자라면 String의 split 메서드를 잘 알고 있습니다.
자바의 split 메서드를 사용하면 "."을 구분자로 이용해서 문자열을 분리할 수 없습니다.
왜냐면 .은 정규식에서 모든 문자를 나타내는 것입니다.
따라서 코틀린은 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받아서 이를 해결합니다.
정규식을 명시적으로 만들어 줍니다.
println("12.345-6.A".split("\\.|-".toRegex()))
또한 간단한 경우에 굳이 정규식을 쓸 필요가 없습니다
println("12.345-6.A".split(".","-"))
확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있습니다.
또한 삼중따옴표(""")를 활용한 정규식 사용과 확장 함수를 사용하여 문자열 파싱을 수행할 수 있습니다.
로컬 함수와 확장
Don't Repeat Yourself 원칙은 개발자들이 중요하게 생각합니다.
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있습니다.
필드를 검증하는 부분이 중복된 코드
class User(val id: Int, val name: String, val address: String) fun saveUser(user : User){ if(user.name.isEmpty()){ throw IllegalArgumentException("") } if(user.address.isEmpty()){ throw IllegalArgumentException("") } //user를 DB에 저장 }
로컬 함수를 사용해 코드 중복 줄이기
class User(val id: Int, val name: String, val address: String) fun saveUser(user : User){ fun validate(user: User, value : String, filedName: String){ if(value.isEmpty()){ throw IllegalArgumentException("empty ${filedName}") } } validate(user, user.name, "Name") validate(user, user.address, "Address") //user를 DB에 저장 }
하지만 User 객체를 로컬 함수에게 하나하나 전달하는 것이 아쉽습니다.
로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있는 특성이 있습니다.
로컬 함수에서 바깥 함수의 파라미터 접근하기
class User(val id: Int, val name: String, val address: String) fun saveUser(user : User){ fun validate(value: String, filedName: String){ if(value.isEmpty()){ throw IllegalArgumentException("${user.id} empty ${filedName}") } } validate(user.name, "Name") validate(user.address, "Address") //user를 DB에 저장 }
여기서 조금 더 개선하고 싶다면 검증 로직을 User 클래스를 확장한 함수로 만들 수 있습니다.
검증 로직을 확장 함수로 추출하기
class User(val id: Int, val name: String, val address: String) fun User.validateBeforeSave() { fun validate(value: String, filedName: String) { if (value.isEmpty()) { throw IllegalArgumentException("${id} empty ${filedName}") } } validate(name, "Name") validate(address, "Address") } //user를 DB에 저장 fun saveUser(user: User) { user.validateBeforeSave() }
User는 라이브러리에 있는 클래스가 아니라 사용자가 만든 클래스입니다.
이 경우 검증 로직은 User를 사용하는 다른 곳에서는 쓰이지 않습니다.
따라서 User를 간결하게 유지하면 더 쉽게 코드를 파악할 수 있습니다.
하지만 중첩이 깊어지면 가독성이 안 좋아지므로 일반적으로 한 단계만 함수를 중첩시키기를 권장합니다.
728x90'Kotlin > 코틀린인액션요약' 카테고리의 다른 글
6장 - 코틀린 타입 시스템 (0) 2022.09.16 5장 - 람다로 프로그래밍 (0) 2022.09.13 4장 - 클래스, 객체, 인터페이스 (1) 2022.09.12 2장 - 코틀린 기초 (1) 2022.09.10 1장 - 코틀린소개 (0) 2022.09.08