연산자 오버로딩확장 함수와 속성을 이용한 인젝팅확장 함수를 이용한 메소드 인젝팅확장 속성을 이용한 속성 인젝팅서드파티 클래스 인젝팅Static 메서드 인젝팅클래스 내부에서 인젝팅infix 를 이용한 중위표기법Any 객체를 이용한 자연스러운 코드Scope Functions함수 선택Context object : this or itapply를 이용한 반복 참조 제거run 을 이용한 결과 얻기let을 이용해 객체를 아규먼트로 넘기기also를 사용한 void 함수 체이닝암시적 리시버리시버 전달리시버를 이용한 멀티플 스코프
연산자 오버로딩
operator fun Pair<Int, Int>.plus(other: Pair<Int, Int>) = Pair(first + other.first, second + other.second)
data class Complex(val real:Int, val imaginary: Int) { operator fun times(other: Complex) = Complex(real * other.real - imaginary * other.imaginary, real * other.imaginary + imaginary * other.real) private fun sign() = if (imaginary < 0 ) "-" else "+" override fun toString() = "$real ${sign()} ${abs(imaginary)}i" }
추천사항
- 절제하여 사용하라
- 코드를 읽는 사람 입장에서 당연하게 받아들여질 경우에만 사용하라
- 오버로딩된 연산자는 일반적인 연산자의 동작이어야 한다.
- 변수이름을 의미있게 만들어라. 그래야 오버로딩의 문맥을 파악하기 좋다
확장 함수와 속성을 이용한 인젝팅
[ Kotlin docs ] Extensions
- 상속이 불가능한 클래스 역시 확장에는 열려있다.
- 클래스에 이미 존재하는 메소드를 확장 함수로 만들면 안된다. 충돌이 있는 경우에 클래스의 멤버 함수가 항상 확장 함수를 이긴다.
확장 함수를 이용한 메소드 인젝팅
data class Point(val x: Int, val y: Int) data class Circle(val cx: Int, val cy: Int, val radius: Int) fun Circle.contains(point: Point) = (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) < radius * radius val circle = Circle(100, 100, 25) val point1 = Point(110, 110) val point2 = Point(10, 100) println(circle.contains(point1)) println(circle.contains(point2))
- Circle 클래스의 인스턴스에서 해당 메서드를 호출할 수 있다.
- 코틀린의 확장 함수는 패키지의 static 메서드로 만들어진다. 그리고 컨텍스트 객체(예제에서 Circle)를 함수의 첫번째 파라미터로 전달하고, 이어서 실제 파라미터를 전달함
- 확장 함수를 사용할 때 메서드 호출로 보이는 과정은 사실은 static 메서드를 호출하는 과정과 동일함
- 확장함수의 한계
- 인스턴스 메서드와 같은 이름을 갖고 있으면 항상 인스턴스 메서드가 실행됨
- 인스턴스의 캡슐화된 부분(private 메서드 혹은 속성)에 접근할 수 있는 인스턴스 메서드와 달리, 확장 함수는 정의된 패키지 안에서 객체에 보이는 부분(public 메서드 혹은 속성)에만 접근 가능함
확장 속성을 이용한 속성 인젝팅
val Circle.area : Double get() = kotlin.math.PI * radius * radius val circle = Circle(100, 100, 25) println("Area is ${circle.area}")
- 확장 속성은 클래스 내부에 존재하는 것이 아니기 때문에 백킹 필드를 가질 수 없다. 즉, 확장 속성은 field에 접근할 수 없다는 이야기
- 확장 속성은 클래스의 다른 속성이나 메서드를 이용해 작업을 완료할 수 있다.
서드파티 클래스 인젝팅
fun String.isPalindrome(): Boolean { return reversed() == this }
- 우리는 확장 함수를 서드파티 클래스에 추가할 수도 있고, 이미 존재하는 메서드로 확장 함수를 라우팅할 수도 있다.
Static 메서드 인젝팅
fun String.Companion.toUrl(link: String) = java.net.URL(link) val url: java.net.URL = String.toUrl("https://pragprog.com")
클래스 내부에서 인젝팅
class Point(x: Int, y: Int) { private val pair = Pair(x, y) private val firstsign = if (pair.first < 0) "" else "+" private val secondsign = if (pair.second < 0) "" else "+" override fun toString() = pair.point2String() fun Pair<Int, Int>.point2String() = "(${firstsign}${first}, ${this@Point.secondsign}${this.second})" }
- 확장 함수가 클래스 내부에서 생성되었기 때문에 확장 함수에는 this와 this@Point 두개의 리시버를 갖고 있음
- extension receiver : 확장 함수가 실행되는 객체 (위 예시에서
Pair
). 즉, 확장 함수를 리시브 하는 객체 - dispatch receiver : 확장 함수를 만들어 추가한 클래스의 인스턴스( 위 예시에서
Point
). 즉, 메서드 인젝션이 된 클래스
- 프로퍼티와 메서드 바인딩을 할 때 extension receiver가 우선순위를 가짐
- dispatch receiver를 바로 참조하고 싶다면
this@Outer
(위 예시에서this@Point
) 문법을 사용하면 됨
infix 를 이용한 중위표기법
//Java if(obj instanceof String) {
- 위의 코드를
if(obj.instanceOf(String) {
이라고 작성해야 한다고 상상하면 꽤나 복잡하다
- 이렇게 연산자가 중간에 있거나 피연산자 사이에 있는 것을 중위표기법(
infix notation
)이라 부름
- Java에서는 이런 표현 방법이 이미 정의된 연산자에서만 제한적으로 사용할 수 있다. 그러나 코틀린에서는 코드에 중위표기법을 사용할 수 있다.
- 코틀린에서 연산자는 항상 자동으로 중위표기법을 사용한다.
operator infix fun Circle.contains(point: Point) = (point.x - cx) * (point.x - cx) + (point.y - cy ) * (point.y - cy ) < radius * radius println(circle.contains(point1)) // true println(circle contains point1)
- method에 infix 어노테이션을 사용하면 코틀린은 점과 괄호를 제거하는 것을 허용해준다.
- 코틀린은 infix를 이용해 함수에 유연성을 제공함
- 하지만 한계 역시 존재
- infix 메소드는 정확히 하나의 파라미터만 받아야 함
- vararg도 사용할 수 없고, 기본 파라미터도 사용 불가
Any 객체를 이용한 자연스러운 코드
[ Kotlin docs ] Scope Functions
비록 scope function이 코드를 간결하게 만들어 줄 지라도, 너무 남용하지는 말라
이것은 코드를 읽기 어렵게 만들고 에러를 유발할 수 있다.
scope function을 중첩해서 사용하는 것도 피하는 것을 추천하고, 체이닝하여 사용할 때는 context object가
this
인지 it
인지 헷갈릴 수 있기에 주의해서 사용해야 한다. Scope Functions
- 기본적으로 이 함수들은 동일한 작업을 수행함 ( object에 대해 코드 블럭을 실행)
- 차이점은 코드블럭 내부에서 object 가 어떻게 이용가능해지는지, 모든 표현식의 결과가 무엇인지 가 다른점임
함수 선택
Function | Object Reference | Return Value | Is extension function |
let | it | Lambda result | Yes |
run | this | Lambda result | Yes |
run | - | Lambda result | No: called without the context object |
with | this | Lambda result | No: takes the context object as an argument |
apply | this | Context object | Yes |
also | it | Context object | Yes |
- Executing a lambda on non-nullable objects:
let
- Introducing an expression as a variable in local scope:
let
- Object configuration:
apply
- Object configuration and computing the result:
run
- Running statements where an expression is required: non-extension
run
- Additional effects:
also
- Grouping function calls on an object:
with
let()
과run()
,with()
은 람다를 실행시키고 람다의 결과를 호출한 곳으로 리턴
also()
와apply()
는 람다의 결과를 무시하고 컨텍스트 객체를 호출한 곳으로 리턴
Context object : this or it
각각의 scope 함수는 context object를 참조하는 두가지 방법 중 하나를 사용함 (
this
or it
)fun main() { val str = "Hello" // this str.run { println("The string's length: $length") //println("The string's length: ${this.length}") // does the same } // it str.let { println("The string's length is ${it.length}") } }
run
, with
, apply
는 context object를 lambda receiver 로써 참조함 (to denote current receiver, use this
keyword)
- receiver. 수신자. 어떠한 멤버 함수를 호출하는 주체가 되는 애를 receiver 라고 하는 듯함 [참고 Function literals with receiver ]
- lambda receiver라 하면 lambda 내부에서 함수를 호출할때의 주체인데(this 를 통해 참조) 그것이 context object 인것
class HTML { fun body() { ... } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // create the receiver object html.init() // pass the receiver object to the lambda (제공된 lambda에 html을 넘김) return html } html { // lambda with receiver begins here body() // calling a method on the receiver object }
let
과also
는 context object를 lambda argument 를 통해 참조함- context object를 function call 의 argument로 쓸 때는
it
으로 쓰는게 더 나음
Method | Argument | Receiver | Return | Result |
let | context | lexical | RESULT | RESULT |
also | context | lexical | RESULT | context |
run | N/A | context | RESULT | RESULT |
apply | N/A | context | RESULT | context |
class Mailer { val details = StringBuilder() fun from(addr: String) = details.append("from $addr ... \n") fun to(addr:String) = details.append("to $addr ... \n") fun subject(line:String) = details.append("subject $line...\n") fun body(message: String)= details.append("body $message...\n") fun send() = "...sending...\n$details" } val mailer = Mailer() mailer.from("builder@agiledeveloper.com") mailer.to("venkats@agiledeveloper.com") mailer.subject("your code sucks") mailer.body("...details...") val result = mailer.send() println(result)
apply를 이용한 반복 참조 제거
val mailer = Mailer() .apply { from("builder@agiledeveloper.com") } .apply { to("venkats@agiledeveloper.com") } .apply { subject("your code sucks") } .apply { body("...details...") } val result = mailer.send() println(result)
- apply() 호출은 Mailer의 인스턴스에서 호출하고 같은 인스턴스를 리턴함 → 체인을 사용할 수 있도록 해줌
- apply() 메서드는 apply() 를 마지막으로 호출한 객체의 컨텍스트에서 람다를 실행시킴. 그래서 우리는 apply()에 전달하는 람다에서 Mailer에 여러 번의 메서드 호출을 사용할 수 있음
val mailer = Mailer().apply { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") } val result = mailer.send() println(result)
run 을 이용한 결과 얻기
val result = Mailer().run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") send() } println(result)
- run() 호출은 Mailer의 인스턴스에서 호출, 람다의 결과를 리턴
- send() 메서드의 결과가 return 되기에 이후에 Mailer 인스턴스 참조로 메서드 호출이 불가함
let을 이용해 객체를 아규먼트로 넘기기
- let() 호출은 argument가 caller가 넘어오고, Receiver는 lexical 임. 람다의 결과를 리턴
let()
에 아규먼트로 전달한 람다의 결과를 사용하길 원한다면 let() 이 좋은 선택이다. 하지만 let() 을 호출한 타깃 객체에서 뭔가 작업을 계속 하길 원한다면also()
를 사용해야 한다.
fun createMailer() = Mailer() fun prepareAndSend(mailer: Mailer) = mailer.run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") send() }
val mailer = createMailer() val result = prepareAndSend(mailer) println(result) // 첫번째 변경 val result = prepareAndSend(createMailer()) // let() 사용 val result = createMailer().let { mailer -> prepareAndSend(mailer) } // 암시적 파라미터 it 사용 val result = createMailer().let { prepareAndSend(it) } // 메서드 참조 사용 val result = createMailer().let(::prepareAndSend)
also를 사용한 void 함수 체이닝
- also() 메서드는 체이닝을 사용할 수 없는 void 함수를 체이닝 하려고 할 때 유용하다
fun prepareMailer(mailer: Mailer): Unit { mailer.run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") } } fun sendMailer(mailer: Mailer): Unit { mailer.send() println("Mail sent") } val mailer = createMailer() prepareMailer(mailer) sendMailer(mailer) // also()를 활용해 메서드 체이닝 적용 createMailer() .also(::prepareMailer) .also(::sendMailer)
암시적 리시버
리시버 전달
var length = 100 val printIt: (Int) -> Unit = { n: Int -> println("n is $n, length is $length") } printIt(6) // n is 6, length is 100
- 람다 내부 스코프에 length가 없으니 lexical scoped에서 length를 찾음
var length = 100 val printIt: String.(Int) -> Unit = { n: Int -> println("n is $n, length is $length") } printIt("Hello", 6) // n is 6, length is 5
- 람다의 시그니처 정의에서 (Int) → Unit 대신 String.(Int) → Unit 을 사용함으로써 receiver를 String 타입으로 한정할 수 있음
- 호출 시, 추가적인 아규먼트를 전달해주어야 함. 내부에서 this 에 바운딩 될 컨텍스트 또는 리시버가 필요
- 람다를 리시버의 멤버함수처럼 사용할 수 있음
"Hello".printIt(6)
- 람다는 리시버의 확장 함수처럼 동작한다. 사실 이 말이 작동 방식을 가장 완벽하게 표현함
리시버를 이용한 멀티플 스코프
람다 표현식은 다른 람다 표현식에 중첩될 수 있다. 이때 내부의 람다 표현식은 멀티플 리시버를 가진 것처럼 보일 수 있다.
중첩 클래스와 이너클래스에서 봤던 이너 클래스의 리시버와도 유사함
fun top(func: String.() -> Unit) = "hello".func() fun nested(func: Int.() -> Unit) = (-2).func() top { println("In outer lambda $this and $length") nested { println("in inner lambda $this and ${toDouble()}") println("from inner through receiver of outer : ${length}") println("from inner to outer receiver ${this@top}") } } /* In outer lambda hello and 5 in inner lambda -2 and -2.0 from inner through receiver of outer : 5 nested에 length 가 없으므로 위의 스코프로 넘어감 from inner to outer receiver hello */