Kotlin provides many types for representing arrays: IntArray, LongArray, DoubleArray, FloatArray, CharArray, ShortArray, ByteArray, BooleanArray. Each array store elements of the corresponding type (Int, Long, Double and so on). Note, there is no StringArray.
Function 또한 일정한 타입으로 볼 수 있는데... ex) 3 -> Int , 3L -> Long
fun sum(a: Int, b: Int): Int = a + b
sum has a type of (Int, Int) -> Int.
fun sayHello() {
println("Hello")
}
printHello has a type of () -> Unit
이런식이다.
코틀린은 함수 또한 오브젝트처럼 다룰 수 있다.
Var mul = {a: Int, b:Int -> a + b}
이러면 mul 변수가함수를가리키는 “변수” 다.
즉, 함수를 가리키는 "변수" 를 지정할 수 있다.
mul 변수의 타입을 "명시적" 으로 표현해 볼까 그럼?
val mul: (Int, Int) -> Int = {a: Int, b: Int -> a + b}
요런식으로 표현이 된다.
그럼 함수를 mul 이라는 변수에 담았을 때는 저런 식으로, "참조" 할 수 있다 치고...
fun sum(a: Int, b:Int): Int = a + b
이렇게이름이부여된함수를넘기고싶다면?
fun sum(a: Int, b: Int): Int = a + b
val mul2 = { a: Int, b: Int -> a * b }
val increment = placeArgument(1, ::sum)
val triple = placeArgument(3, mul2)
:: 를 함수 이름 앞에 붙여서 넘기면 된다.
함수를 리턴하는 함수를 보면서, 함수의 타입에 대한 이해를 마치도록 하자.
fun getRealGrade(x: Double) = x
fun getGradeWithPenalty(x: Double) = x - 1
fun getScoringFunction(isCheater: Boolean): (Double) -> Double {
if (isCheater) {
return ::getGradeWithPenalty
}
return ::getRealGrade
}
이름이 없는 함수
이름이 없는 함수는 크게 두 가지로 나뉜다.
하나는익명, 하나는람다.
fun(arguments): ReturnType { body } – this one is commonly called an "anonymous function".
{ arguments -> body } – this one is commonly called a "lambda expression".
익명 함수가 이미 이름이 없는 함수인데 이게 뭔 개 소리인가 싶을 것이다. 그런데 어쩔 수없다. 영어 표현과 한국어 표현의 차이다.
영어로 표현하면 function which has no name. 요딴식이겠지.
하여간, 이름이 없는 함수는 => 어노니머스 함수 와 람다 함수. 이렇게 둘로 나뉜다.
(헷갈리니까 어노니머스 함수 라고 하자.)
fun(a: Int, b: Int): Int {
return a * b
}
{ a: Int, b: Int -> a * b }
위의 두 표현은 "같은 표현" 이다.
즉
val mul1 = fun(a: Int, b: Int): Int {
return a * b
}
val mul2 = { a: Int, b: Int -> a * b }
이렇게 하면, 사실상 mul1 과 mul2 는 완전 똑같은 행위를 한다.
람다 함수의 경우, 매겨변수가 없을 때는
{ body }
요런 식으로 몸통만 써줘야 한다. -> 화살표는 반드시 생략한다.
람다를 매개변수로 받는 쪽은 여러 함축 단계를 거칠 수 있다. 매우 귀찮으니..
Lambdas and syntactic sugar
람다와, 시태틱 슈가
There are ways to make the code more readable for human without changing the code logic. If such a way is provided by the programming language and relates to syntax, the way is called syntactic sugar.
코드를 짜는 논리를 바꾸지 않으면서도, 코드를 좀 더 읽기 쉽게 하는 여러 방법이 있다. 만약 그런 것이 프로그래밍 언어 차원에서 지원될 때
우리는 이런걸 "syntactic sugar" 라고 부른다.
Kotlin promotes Functional Programming so there is syntactic sugar for it.
코틀린은 함수형 프로그래밍을 지향(?) 혹은 지원하며 이를 위한 문법적인 지원이 있다.
Let's recall the following example of passing the function as an argument:
fun isNotDot(c: Char): Boolean = c != '.'
val originalText = "I don't know... what to say..."
val textWithoutDots = originalText.filter(::isNotDot)
Rewrite it to pass a lambda:
val originalText = "I don't know... what to say..."
val textWithoutDots = originalText.filter({ c: Char -> c != '.' })
이런식으로 람다 함수를 쓰면, isNotDot 함수를 미리 선언할 필요가 없어진다.
Now we act! First of all, Kotlin infers types of many objects, and here specifying the c type isn't necessary:
originalText.filter({ c -> c != '.' })
그런데 솔직히 타입은 필요 없잖아? Char 를 굳이 써 줘야해? 생략하자.
Secondly, there are situations when the lambda is passed as the last argument. This is the case. Kotlin provides a way to eliminate these bracket sequences ({ }), allowing to write the lambda outside the parentheses:
originalText.filter() { c -> c != '.' }
그런데 함수의 매개변수로 넘어가는 람다가, "마지막 변수" 면, 사실 () 뒤쪽으로 빼내는게 좀더 보기 좋지 않냐? 그러니까 뺄 수 있어.
If the parentheses are left empty after this operation, they can be removed:
originalText.filter { c -> c != '.' }
아니 빼고 나니까 () 가 조금 거슬리네? 아무 변수도 없는데 저게 남아있잖아. 아 그럼, 아무것도 없는 괄호는 그냥 없앨 수 있게 하자.
() 안이 비어있으면 생략 가능함.
Finally, when there is a single parameter in a lambda, there is an opportunity to skip it. The parameter is available under the it name. The final version of the code removing dots is the following:
val originalText = "I don't know... what to say..."
val textWithoutDots = originalText.filter { it != '.' }
음... 다 좋은데. 저 c 가 마음에 안들어. 아니, 어차피 매개변수가 딱 하나인데... 굳이 c 요런 식으로 하나를 써 줘야해? 매개변수가 하나밖에 없을 때는 그냥 c 같은 건 생략할 수 있게 하는 건 어떨까?
람다는, 매개변수 없으면 -> 없어지잖아. 그 하나뿐인 매개변수는 it 으로 표현할 수 있게 하자..
여기서 getClousre 함수가아니라, 이안에서뱉어지는 function() {return text; }
가클로저야. 이 클로저는 말 그대로 "문을 닫는 느낌" 이지. text 배열을 자기 방 안에 넣고, 문을 닫아버리는 거야.
--
if 와 when
코틀린에서는
if 문이문, 이아니라표현이다.
표현은반드시반환값을갖는것이며, 문은반환값을갖는다기보다, 값들에변화를일으키는
것들을일컫는다.
만약 if가표현으로서즉값을반환하는용도로쓰인다면반드시 else 가있어야한다.
또 switch 대신 when 이 있다.
When 또한 Expression 으로쓸수있는데
이때는 if 처럼반드시 else 가있어야함.
val result = when (op) {
"+" -> a + b
"-" -> a - b
"*" -> a * b
else -> "Unknown operator"
}
println(result)
보면, when(op)이런식으로 ( ) 안에변수가하나들어가고
이 op 에해당하는값이있으면 -> 분기를타는방식이다.
when (n) {
0 -> println("n is zero")
in 1..10 -> println("n is between 1 and 10 (inclusive)")
in 25..30 -> println("n is between 25 and 30 (inclusive)")
else -> println("n is outside a range")
}
만약 when 뒤에아무런변수도없다? 그냥모두 boolean 조건이필요함.
when {
n == 0 -> println("n is zero")
n in 100..200 -> println("n is between 100 and 200")
n > 300 -> println("n is greater than 300")
n < 0 -> println("n is negative")
// else-branch is optional here
}
이런 식으로...
Switch 랑다르게, 한번에한분기만탈수있다. (여러 expression 실행불가)
when(str) {
“Hello”,”world” -> {
}
}
이런 식으로는 쓸 수 있다. 이렇게 한 분기.
반복문
repeat(n) 이라는함수가있다. Argument 로 int 로값을받아 n 회실행한다.
While 문은여타 while 문과같음.
scanner.hasNext() for strings and scanner.hasNextInt()
scanner의 hasNext() 를 활용할 수 있다.
라벨 개념
loop@for (i in 1..3) {
for (j in 1..3) {
println("i = $i, j = $j")
if (j == 3) break@loop
}
}
귀찮으니 간단히 정리하고 넘어가겠다.
val fileName = "src/reading.txt"
val lines = File(fileName).readText()
print(lines)
이렇게가져오면, 파일의모든값을한번에가져옴.
메모리 제한은 2GB 정도다.
val fileName = "src/reading.txt"
val lines = File(fileName).readLines()
for (line in lines){
println(line)
}
readLine 도 2GB. 사실상 전부 불러오고, List<String> 안에 한 줄씩 넣어놨을 뿐임.
readBytes() – may be helpful if you need to store file contents as an array of bytes:
이 것도, 결국 readText() 를 구현하기 위해 사용하는 세부 함수라서 2GB 메모리 제한임.
그 이상의 파일은 다음과 같은 방법을 사용해보라.
File(fileName).forEachLine { print(it) }
일단 넘어가자. 파일은 나중에 쓸 일이 있을 때 한번 더 훑어봐도 늦지 않는다.
클래스
Properties 는반드시초기화돼야함.
Also, a property has a strict type. The type can be any. It can be a standard type like a number or a string or a custom type. So a property type can be your own class and even the same class where the property is declared.
Constructors are class members that initialize a new object of the class.
class Size {
var width: Int = 0
var height: Int = 0
constructor(_width: Int, _height: Int) {
width = _width
height = _height
}
}
이런식의생성자를 “Secondary constructor” 라고한다. 차순위생성자?
대체생성자?
Omitting default values
Classes in Koltin have useful feature: you don't have to declare a default property value if the value is assigned in the constructor of the class:
Primary Constructor
주생성자
존나코틀린의새로운문법중하나인데
Class Size constructor(width: Int, height: Int) {}
요런식으로 constructor를선언하는거임.
class Size constructor(width: Int, height: Int) {
val width: Int = width
val height: Int = height
val area: Int = width * height
}
Primary constructor 의 constructor 키워드는생략이되며,
심지어 primary constructor 에서는변수선언도가능하다.
class Size(val width: Int, height: Int) {
val height: Int = height
val area: Int = width * height
}
위와같이. Width 는없던변수를선언해놓는것이다.
위의클래스선언은사실상첫번째것과다를게없다.
class Size(val width: Int, val height: Int)
이런식으로,{} 를생략하고아주간단한 Single line class를만들수도있다.
Any Kotlin class has a constructor to initialize its objects.
A constructor is available for invocation under the class name.
There are different ways to declare a constructor but a primary constructor is the most concise.
If a class has no custom constructors, then there is an implicit default constructor with no arguments and it does nothing.
모든코틀린클래스는오브젝트를초기화하기위한생성자를갖고있다.
컨스트럭트는클래스이름아래에있을수있다.
커스텀컨스트럭터가없다면, 기본생성자가암시적으로존재한다.
enumclass Rainbow(val color: String) {
RED("Red"),
ORANGE("Orange"),
YELLOW("Yellow"),
GREEN("Green"),
BLUE("Blue"),
INDIGO("Indigo"),
VIOLET("Violet")
}
Enum
enumclass Rainbow(val color: String, val rgb: String) {
RED("Red", "#FF0000"),
ORANGE("Orange", "#FF7F00"),
YELLOW("Yellow", "#FFFF00"),
GREEN("Green", "#00FF00"),
BLUE("Blue", "#0000FF"),
INDIGO("Indigo", "#4B0082"),
VIOLET("Violet", "#8B00FF");
fun printFullInfo() {
println("Color - $color, rgb - $rgb")
}
}
Enum 의기본속성들
Name -> 그냥이름 Rainbow.RED -> RED
Ordinal -> enum의순서?Rainbow.GREEN.ordinal -> 3
Values ()-> enum instances 들을반환.
fun isRainbow(color: String) : Boolean {
for (enumin Rainbow.values()) {
if (color.toUpperCase() == enum.name) returntrue
}
returnfalse
}
반복문은위와같이 enum 을씀.
valueOf() - returns an instance of Enum by its name with String type and case sensitivity
If you want to extend your Enum but with static context, then you need to wrap your method with companionobject keywords, let's modify our Rainbow in order to find an instance by RGB parameter:
Companion object 가여기서처음등장한다.
Static 으로 class 에 member를추가할때사용한다.
enumclass Rainbow(val color: String, val rgb: String) {
RED("Red", "#FF0000"),
ORANGE("Orange", "#FF7F00"),
YELLOW("Yellow", "#FFFF00"),
GREEN("Green", "#00FF00"),
BLUE("Blue", "#0000FF"),
INDIGO("Indigo", "#4B0082"),
VIOLET("Violet", "#8B00FF"),
NULL("", "");
companionobject {
fun findByRgb(rgb: String): Rainbow {
for (enumin Rainbow.values()) {
if (rgb == enum.rgb) returnenum
}
return NULL
}
}
fun printFullInfo() {
println("Color - $color, rgb - $rgb")
}
}
이렇게쓴다.
Member function
자바에서는클래스안에있는함수를 “메서드” 라고표현하곤하는데,
여기서는 Member function 이라고쓴다.
코틀린은클래스에 function 을 “확장선언” 하는기능을제공한다.
이를우린 “확장함수” Extension Function 이라고부른다.
fun String.repeated(): String = this + this
Fun 을붙이고, “클래스”.”함수이름” 을하고 “내용을선언” 하면끝.
"ha".repeated() => “haha”
The syntax to define an extension function is like defining a top-level function. Just write the name of a class which you would like to extend and a dot before the function name. The class to be extended is called a receiver type.
이때, “클래스” 를 “리시버타입 “ 이라고칭한다.
This 키워드를사용하면, 확장함수를사용하는 “인스턴스” 를참조할수있다.
So the issue is solved: Kotlin developers are able to add any functions to any classes they want.
클래스 내에서 함수를 override 할 때는 operator 키워드를 붙여줄 필요는 없다.
Comparable 을 상속해서 , compareTo 함수를 오버라이딩 해주면 끝.
이 compareTo 는 operator 에서 < == > 등을 비교하는데 쓰인다.
2) In Range
이 문제는 보자마자 뭔 개소리지?? 하는 느낌을 주었다.
애초에 자바에 in 같은 키워드가 없기 때문이다. (적어도 난 안쓴다.)
쿼리로 따지면 Between 같은 느낌의 연산자이다.
"연산자" 이다. < > == 같은 연사자.
따라서 연산자 오버라이딩을 하면 된다.
Range
A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values which are both included in the range. Ranges are defined for comparable types: having an order, you can define whether an arbitrary instance is in the range between two given instances. The main operation on ranges iscontains,which is usually used in the form ofinand!inoperators.
To create a range for your class, call therangeTo()function on the range start value and provide the end value as an argument.rangeTo()is often called in its operator form...
class DateRange(val start: MyDate, val endInclusive: MyDate){
operator fun contains(item: MyDate): Boolean =
item >= start && item <= endInclusive
}
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
return date in DateRange(first, last)
}
이 문제는 이런 식으로 풀면 된다.
DateRange 라는 클래스를 만들고, 이 클래스의 main constructor 는 start 와 endinclusive 를 받는다.
그리고 자기가 갖고 있는 start와 endEnclusive 로 넘어오는 MyDate instance 가 범위 안에 속하는지 아닌지를 반환하는 contains 함수를 정의 해준다.
여기서 알 수 있는 것은, "operator" 키워드를 사용해서, 타 클래스의 연산에 관여할 수 있는 클래스를 만들 수 있다는 것이다.
또한 MyDate는 이 전 문제 (1번) 에서 재 정의한 compareTo 함수를 갖고 있다는 것을 명심하자.
1) compareTo 로 대소를 배교할 수 있는 클래스(MyDate) 를 만들게 됨.
2) 이제 대소를 비교할 수 있으니 contains 함수도 만들 수 있음. in 연산자를 오버라이딩 한 클래스가 대신 이것을 처리 해 줌.
3) RangeTo
확장 함수로 연산자 오버로딩을 할 수 있다.
이 전 2번 문제에서는
date in DateRange(first,last) ... 방식으로 범위 안에 속하는지를 검사 했지만.
date in first..last 형식으로 이를 바꾸는 것이다.
date 도 first 도 last 도 모두 MyDate 클래스의 인스턴스이다.
.. 을 사용하기 위해서는 rangeTo operator 를 만들어 줘야 한다.
operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
class DateRange(override val start: MyDate, override val endInclusive: MyDate): ClosedRange<MyDate>
위와 같이, operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
로 만들면 된다.
2단원 부터 갑자기 난이도가 올라가는 면이 있는데, 대부분이 오버라이딩에 관련된 문제인데 함수의 원형을 알기 힘들기 때문이다.
in 은 단순히 대소 비교를 통해, 범위 안에 값이 들어가는지 확인만 하는 것은 아니다. (아닌가?_
for 문 안에서도 사용될 수 있다.
예를 들면
for(date in firstDate..secondDate) {
}
와 같이 사용된다.
하지만 여기서는 같은 in 키워드여도 다른 것이 필요하다. 단순한 ClosedRange 가 아닌, Iterable 을 상속해야 하며.
Iterabe 인터페이스를 구현하면, Iterator를 반환해야 하고.
이 Iterator를 반환하기 위해 새로운 클래스를 만들어야 한다.
그냥 Iterator를 anonymous object로 전달하자 무한 루프에 빠져 버렸다. (다른 요인일 수도 있다.)
anonymous object로 만들지 말고, 클래스를 하나 만들고, 인스턴스로 Iterator를 하나 만들어서 뱉어주니 잘 작동했다.
class DateRange(val start: MyDate, val end: MyDate): Iterable<MyDate>{
override fun iterator(): Iterator<MyDate> = DateIterator(this)
}
class DateIterator(val dateRange:DateRange) : Iterator<MyDate> {
var current: MyDate = dateRange.start
override fun next(): MyDate {
val result = current
current = current.nextDay()
return result
}
override fun hasNext(): Boolean = current <= dateRange.end
}
fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
for (date in firstDate..secondDate) {
handler(date)
}
}
이런 식으로 작서 ㅇ되는데...
firstDate..secondDate 를 보면 알 수 있듯이, rangeTo 연산자를 오버라이딩 했다.
rangeTo 함수는 이전 3번처럼 DateRange 인스턴스를 반환하다. DateRange 는 Iterable 을 구현한 클래스이다.
Iterator 는 next 와 hasNext 함수를 상속해줘야 한다. 살짝 IntelliJ의 자동 완성 기능의 도움을 받으면 쉽게 오버라이딩 할 함수의 틀을 만들 수 있다.
5) Operator overloading
오버로딩과 오버라이딩의 차이는 잘 알 것이다.
보통 부모의 함수를 상속받아, 자기 껄로 대체 하는것을 오버라이딩이라고 하고.
함수 이름은 같은데 매개변수가 다른 여러 함수를 만드는 것을 오버로딩이라고 한다.
오버라이딩은 런타임 시에, 오버로딩은 컴파일 시에 정의 된다...
간략히 여기 까지만 설명 하고.
연산자 오버라이딩에서 왜 갑자기 연산자 오버로딩이 제목이 된 걸까? 이를 활용하는 것이 이번 문제의 핵심이자. 정말 재밌는 문제다.
난 못 풀었다.
6) Destructing declarations.
자바스크립트에 "구조 분해 할당" 이란 것이 있다.
그거랑 비슷 한건데,
fun isLeapDay(date: MyDate): Boolean {
val (year, month, dayOfMonth) = date
// 29 February of a leap year return year % 4 == 0 && month == 2 && dayOfMonth == 29 }
이렇게 있을 때 MyDate 의 필드 값들이 year, month, dayOfMonth 에 분해돼 들어가는 느낌을 주는...
이것 또한 연산자로 지정해 줘야 한단다...
다음과 같다.
class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) {
operator fun component1(): Int {
return year
}
operator fun component2(): Int {
return month
}
operator fun component3(): Int {
return dayOfMonth
}
}
componentN() 을 정의해주면 된다.
N 에 1~N 까지으 숫자가 들어간다.
6) Invokable
별 거 없는 문제인데 한 번 기억해두면 좋을 요소가 하나는 있다.
class Invokable {
var numberOfInvocations: Int = 0
private set
operator fun invoke(): Invokable {
numberOfInvocations++
return this
}
}
fun invokeTwice(invokable: Invokable) = invokable()()
짜잔.
invoke 라는 연산자를 오버라이딩하면..
인스턴스에다가 () 를 붙여서 함수처럼 쓸 수 있다... 위에 invokeTwice 는 말 그대로 invoke 두번 한거다.
여기서 private set 부분을 한번 보고 넘어가자.
Kotlin 에서는 클래스 안에 var a = 3 이런 식으로 "필드" 를 선언해 놓으면 자동으로
get 과 set 함수가 생긴다.
자바에서는 아마 @Getter @Setter 등을 붙이거나. (lombok 사용 시에) 직접 만들어야 했을 것이다.
만약 상수 선언을 의미하는 val 을 쓴다면? setter 가 생기지 않는다.
위에서 필드 선언 부분 아래에서 private set 부분을 확인할 수 있다.
set 함수를 private으로 만든 것이다. var 로 선언했기 때문에 값이 변할 수는 있지만, 이 값을 외부에서 직접 특정 값으로 수정하지는 못하게 한 것이다.
"코틀린은 함수 선언시에 기본 값을 지정할 수 있다. 또한 함수 호출 시에 파라미터 이름과 함께 넘길 수 있다."
1.4 Default Arguemnts
위에 설명한, 기본 값 지정에 대한 문제이다. 나처럼 풀면 이렇게 된다.
fun foo(name: String, number: Int, toUpperCase: Boolean) = (if (toUpperCase) name.toUpperCase() else name) + number
fun foo(name: String, number: Int) = foo(name, number, false)
fun foo(name: String) = foo(name, 42)
fun foo(name: String, toUpperCase: Boolean) = foo(name, 42, toUpperCase)
fun useFoo() = listOf( foo("a"), foo("b", number = 1), foo("c", toUpperCase = true), foo(name = "d", number = 2, toUpperCase = true) )
하지만 코틀린은 "기본 값을 지정" 할 수 있다.
다음과 같이 간단히 작성 할 수 있다.
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false) = (if (toUpperCase) name.toUpperCase() else name) + number
fun useFoo() = listOf( foo("a"), foo("b", number = 1), foo("c", toUpperCase = true), foo(name = "d", number = 2, toUpperCase = true) )
1.5 Lambdas
람다는 자바에서도 8 버전에서부터 지원하는 것으로 알고 있다.
코틀린 또한 당연히 람다를 지원한다.
함수를 파라미터로 받는 함수를, Higher order Function이라고 한다.
이런 함수에, 파라미터를 넘겨줄 때 람다를 쓰면 아주 깨끗한 코드를 만들 수 있다. (사람마다 코드의 미적 기준은 다를 수 있다.)
아래가 그 예시이다.
// Lambdas are code blocks enclosed in curly braces. items.fold(0, { // When a lambda has parameters, they go first, followed by '->' acc: Int, i: Int -> print("acc = $acc, i = $i, ") val result = acc + i println("result = $result") // The last expression in a lambda is considered the return value: result })
함수 타입은 다음과 같이 나타낸다.
(R, T) -> R
코틀린은 변수를 쓸 때 var a: Int = 1 이런 식으로 쓸 수 있다.
함수 타입이 (R, T) -> R 과 같은 방식이란 말은
var sampleFunction: (Int, Int) -> String = { a: Int, b: Int -> "the result is $a + $b" } print(sampleFunction(1, 2))
결과: the result is 3
요런 식으로 쓸 수 있다는 말이다.
코틀린은 특이한 문법이 있다.
가장 마지막에 들어가는 "함수 인자" 는 parenthesis 밖으로 뺄 수 있다. (paraenthesis = ( or ) )
이게 무슨 개소린가 싶기도 한데...
fun higherOrderFunctionDefault(a: Int, b: Int, f: (Int, Int) -> Int): Int { return f(a, b) }
var result = higherOrderFunctionDefault(1,2, {a: Int, b: Int -> a + b})
따라서 위와 같은 경우 init 이 먼저 시작된다. secondary constructor 보다는 init 이 우선시 된다.
main constructor 가 없다? 없을 수는 없다. 사실 암시적으로 갖고 있는 것이다. 마치 자바의 기본 생성자가 있듯이... 만약 main constructor 가 없다면, 암시적으로 호출하고 있다고 생각해야 한다.
그렇다면 순서대로라는 것은 무슨 의미인가?
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
위와 같은 경우 반드시 순서대로 시작된다. (위의 also 는 kotlin 의 확장 함수 중 하나이다. 자기 자신을 매개변수로 넘기고, 자기 자신을 return 한다.
val person: Person = getPerson().also {
print(it.name)
print(it.age)
}
마지막엔 자동으로 자기 자신을 return 한다.
5. main constructor 에는 연산을 넣을 수 없다. 따라서 연산을 위해서는 클래스 안에 init 을 사용하도록 해라.
6. class 는 클래스 : 부모클래스() 와 같이 상속. interface 는 클래스: 언터페이스 와 같이 상속한다.
7. 상속 하려면 open 키워드를 붙여야 한다.
다른게 무지 많다. 자세히 알아보지도 않고 대충 정리만 해놨다. 무엇보다 1번이 가장 애매한데... 초기화 안하면 컴파일러에서 빽빽돼서 저리 적어 놓았다.
1.8 Nullable Type
꽤나 중요한 문제이다.
fun sendMessageToClient(
client: Client?, message: String?, mailer: Mailer
){
val email = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
위가 정답이다. 깔끔하고 멋있다. kotlin 의 ? 의 힘이다.
자료형에 ? 가 붙어있는 것을 볼 수 있는데, null 이 들어갈 수도 있는 자료형을 나타낸다.
인스턴스에 ? 를 붙인뒤, 인스턴스의 자료 혹은 함수에 접근하려고 하면, null 을 자동으로 체크해준다.
만약 접근하려는 놈이 null 일 경우 그냥 null 을 반환한다. 그래서 마지막에 email != null 을 체크하는 것이다.
굳이 이 흉한 코드를 올려놓은 이유는, !! 또한 키워드 중 하나이기 때문이다. ? 자료형의 변수에, !! 를 뒤에 붙여주면.
"이 값은 null 일리가 없다!" 라는 것을 의미한다. 따라서 String? 자료형의 변수는 String 으로 변해서 파라미터로 전달되는 효과가 있다.
1.9 smart casts
제목 그대로 "똑똑한 형변환" 을 의미한다. 일단 kotlin 에서는 if 문과 when 문을 사용해서 변수에 값을 할당할 수 있다는 것을 알고 넘어가자.
var a = 3;
var b = if(a == 3) 4 else 5
이런 식으로 쓸 수 있다는 말이다. (많이 신기함..)
위에서는 If문과 When 문이라고 했지만.. Kotlin 에서의 If 는 "Expression" 이다.
이게 뭔 의미냐면, 반환형이 있다는 말이다.
If 표현의 마지막 문장은 자동으로 "반환 값" 이 된다.
따라서 Kotlin 에는 ? a : b 표현이 없다. If 로 충분하기 때문이다.
when 은 Java에서의 Switch 랑 비슷한데 여러모로 다른 점이 있다.
fun eval(expr: Expr): Int =
when (expr) {
is Num -> expr.value
is Sum -> eval(expr.left) + eval(expr.right)
else -> throw IllegalArgumentException("Unknown expression")
}
위를 보면 알 수 있듯이, when 은 is 로 타입을 확인 할 수 있다. 자바로 따지면 instance of 같은 느낌이다.
왜 "똑똑한 형변환" 일까?
위에서 Expr 이 인터페이스라고 하자. 인터페이스를 구현한 클래스 A : Num , B : Sum 이 있다.
그럼 eval 함수에는 A 도, B 도 들어갈 수 있겠지?
이 때 when 의 is 로 타입을 구분해서, 사용할 수 있다는 말이다.
잘 보면 expr.value, expr.left expr.right 라고 저녛 다른 것들을 사용 하는 것을 볼 수 있다.
interface 가 when 절의 is 를 통과했을 때, "자동으로" is 에서 비교한 타입으로 형변환 해서 넘어간다.
따라서 is Num 을 통과한 녀석은 Num 자료형으로 자동으로 변경돼서 들어간다.
똑똑하다는 말이다.
when의 is 는 타입을 비교할 때 쓴다, 보통은 왼쪽에 "조건식" 이 들어간다.
when (num) {
1 -> "하나"
2 -> "둘"
else -> "둘 초과"
}
이런 식으로 사용할 수 있다.
만약 when 에 아무런 인자도 넘어가지 않는다면, 그냥 "순수한 조건식" 만을 이용해서 사용할 수도 있다.
var a = 3
when {
a == 3 -> "a는 3이 맞어"
else -> "a 는 3이 아니여"
}
이런 식으로 말이다.
물론 위와 같이 쓸 필요는 없다. 위와 같으 2개의 분기로 나눠 질때는 if를 쓰면 된다.
(Kotlin 에서의 Any 는 Java에서의 Object와 비슷한 맥락이지만. 절대 같지는 않다.)
class C {
// Private function, so the return type is the anonymous object type
private fun foo() = object {
val x: String = "x"
}
// Public function, so the return type is Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // Works
val x2 = publicFoo().x // ERROR: Unresolved reference 'x'
}
}
1.11 SAM Conversion
SAM Conversions
Just like Java 8, Kotlin supports SAM conversions. This means that Kotlin function literals can be automatically converted into implementations of Java interfaces with a single non-default method
1.12 Extension On Collection
문제 자체는 아주 간단하다.
제목 그대로 Collection을 어떻게 확장해서 사용하고 있는지를 알려준다.
차트를 보면 3단원에 제대로 Collection을 다룰 예정인듯 하니 기다리도록 하자.
이렇게 1단원을 끝냈다.
굳이 정리할 필요는 없겠지만, 기본 문법이니 만큼 적어두는 것이 좋다고 생각했다.
이 포스트의 내용 만으로는 완벽히 Kotlin을 이해하기 힘들다.
글을 쓰고 있는 사람부터가 이해하고 쓰는게 아니고, 이해하기 위해, 혹은 이해하고 나서 훗날 기억하기 위해 적고 있기 때문이다.