본문 바로가기

코딩 이야기/안드로이드&코틀린 공부

[안드로이드&코틀린 공부] Extension, Enum, Data class

반응형

(코틀린 공식 도큐먼트 참고하여 작성하였습니다.)

Extension

코틀린은 클래스나 인터페이스를 상속받거나 데코레이터와 같은 디자인 패턴을 사용하지 않고 새로운 기능을 추가하는 능력을 제공한다고 합니다.

이는 서드파티 라이브러리의 클래스에 새로운 함수를 작성할 수 있으며, 이를 원래 그 안에 선언되어있던 함수처럼 사용할 수 있습니다.

class a{
    fun saymyname(){
        println("I am 우식이에요")
    }
}

fun a.nexttime(){
    println("next time 같이 체스 두실 분 구해요")
}

fun main(){
	var A = a()
    A.saymyname()
    A.nexttime()
}

class a 에는 saymyname 이라는 함수 밖에 없지만 extension을 통해 nexttime이라는 함수를 추가해주었습니다.

단순히 같은 코드 내에서 extension을 사용할 때는 뭐야… 그냥 저거 그대로 클래스 아넹 적으면 되잖아? 라고 생각하실 수도 있지만, 코드가 분할되거나 외부 라이브러리를 사용할 때 extenstion을 은 더욱 강력해집니다.

extension은 실제 해당 클래스를 수정하거나 그 안에 함수를 실제로 추가하진 않습니다.

해당 타입의 변수에 대해 도트 표기법을 사용하여 호출할 수 있는 새로운 함수를 정의할 뿐입니다.

코틀린 공식 도큐먼트의 코드가 너무 적절하여 한번 가져와봤습니다.

open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

fun main(){
	printClassName(Rectangle()) //출력 : Shape
}

클래스 Rectangle이 Shape라는 클래스를 부모로 상속받고 있고, 두 클래스 다 getName()이라는 함수를 extension을 통해 정의해주었습니다. 전 이 코드를 봤을 때, 자식 클래스에서 오버라이딩이 되서 Rectangle 아닐까? 했는데 printClassName의 파라미터가 Shape의 getName이 호출되서 Shape가 출력됩니다.

물론 printClassName의 파라미터를 Rectangle로 바꾸면 Rectangle이 출력이 됩니다.

또한 원래 있던 멤버함수와 같은 이름과 같은 argument 로 확장을 통해 함수를 선언하면, main함수에서 호출했을 때 멤버함수가 이기게 됩니다.

class Rectangle{
	fun getName() : Int{
        return 3
    }
}

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Rectangle) {
    println(s.getName())
}

fun main(){
	printClassName(Rectangle()) //출력 : 3
}

extension을 통해 getName을 정의하였음에도 멤버함수의 3이 출력됩니다.

Null reciever

class Rectangle

fun Rectangle?.getName(){
    if(this == null){
        println("null")
    }else{
				println("Rectangle")
    }
}

fun main(){
    var A : Rectangle? = null
    A.getName()
}

이런식으로 this를 사용해서 extension이 호출받은 객체, 대상이 null인지 확인하는 것도 가능합니다.

fun Any?.toString(): String {
    if (this == null) return "null"
    // After the null check, 'this' is autocast to a non-nullable type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

(코틀린 공식 문서에서 가져온 코드입니다)

이런 식으로 원래 있던 코드에 null 체크를 추가하는 것도 가능합니다.

Extension Property

프로퍼티를 extension 하는 것도 가능합니다.

다만 이도 extension 함수와 마찬가지로 실제로 클래스에 추가되거나 하는 것은 아니기에 getter, setter를 만들어서 다뤄야합니다

class Rectangle{
	var size : Int = 3
}

val Rectangle.area : Int
	get() = size*size

fun main(){
	var r = Rectangle()
    println(r.area)
}

extension 프로퍼티도 함수처럼 기존의 클래스를 해치지 않고, 프로퍼티를 추가한다는 장점이 있지만 아쉽게도 val만 사용가능한 것이 단점입니다.

Declaring extensions as members

클래스 안에서 다른 클래스를 위한 extensions 를 클래스의 멤버로써 선언할 수 있습니다.

extension이 선언된 클래스의 인스턴스를 dispatch receiver 라 하며 extension method 의 receiver type 의 인스턴스는 extension receiver라고 합니다.

class Rectangle{
	var size : Int = 3
    fun getArea() : Int = size*size
}

class Star(var rectangle : Rectangle){
    fun Rectangle.drawStar(){
        val area = rectangle.getArea()
        for(i in 1..area){
            print("*")
            if(i%3 == 0){
                println("")
            }
        }
    }
    fun drawStar(){
        rectangle.drawStar()
        println("it's square star!")
    }
}

fun main(){
	var r = Rectangle()
  var s = Star(r)
  s.drawStar()
// 출력 : ***
//       ***
//       ***
//       it's square star!
}

Enum Class

enum은 enumerated type. 열거형의 준말입니다.

enum 클래스 내에 상태를 나타내기 위한 여러개의 객체들을 생성해두고 그 중 하나의 상태를 선택하여 나타내기 위한 클래스입니다.

enum의 객체들은 고유한 속성을 가집니다. enum 클래스의 생성자에서 속성을 받을 때 객체들도 이에 영향을 받습니다. 이외에 일반 클래스처럼 함수를 추가할 수도 있습니다.

enum class player(var Name : String, var HP : Int){
    WOOSIKSTATE("woosik", 100),
    VILLAINSTATE("villaiin", 100);
    
    fun Attack(){
        if(this.Name == WOOSIKSTATE.Name){
            WOOSIKSTATE.HP -= 10
        }else{
            VILLAINSTATE.HP -= 10
        }
    }
    
}

fun main(){
    var Playerwoosik = player.WOOSIKSTATE
    var Playervillain = player.VILLAINSTATE
    Playerwoosik.Attack()
    Playervillain.Attack()
    Playervillain.Attack()
    println(Playerwoosik.HP)
    println(Playervillain.HP)
}

enum을 왜 사용할까?

인스턴스 생성과 상속을 방지, 상수값의 타입 안정성을 보장합니다.

추상함수, 공통함수 만들어서 사용하기

class Task{
	var state : ProtocolState = ProtocolState.WAITING
    fun getState() = state.signal()
    enum class ProtocolState {
        WAITING {
            override fun signal(){println("WAITING")}
        },

        TALKING {
            override fun signal(){println("TALKING")}
        };

        abstract fun signal()
    }
}

fun main(){
    val task = Task()
    
	task.getState() // 출력 : WAITING
    
    task.state = Task.ProtocolState.TALKING
    
    task.getState() // 출력 : TALKING
}

이런 식으로 enum class 에 추상함수를 넣게 되면, enum class 안에 있는 객체들에서 이 함수를 정의하는 것을 강제하게 됩니다. 이렇게 되면 특정 상수 값에 대한 처리를 빼먹는 것을 막을 수 있고, 이는 공통 함수의 예제이기도 합니다. 변수에 따라 같은 함수임에도 출력이 변화하게 됩니다.

Data class

데이터클래스는 데이터를 다루는데에 특화된 클래스입니다.

데이터 클래스는 생성 시 내부에서 5가지를 내부에서 자동적으로 생성해줍니다

  • equals()
  • toString()
  • hashcode()
  • copy()
  • componentX()

equals()

내용의 동일성을 판단합니다.

일단 이전에 자바와 코틀린에서 값이나 메모리 상 동일한 객체인지 확인하는 방법에서 다뤄봐야합니다.

동일한 값인지 비교 Java : equals Kotlin : ==

메모리 상 동일한 객체인지 비교 Java : == Kotlin : ===

직접 equlals()를 써서 사용해도 되지만, ==와 ===로 연산자 오버로딩이 되서 사용가능합니다.

즉 데이터 클래스로 만든 객체에 == 와 ===를 사용해도 동일하게 작동합니다

제가 자바로 안드로이드를 하다와서 코틀린에 대한 지식이 빈약해서 한번 일단 객체는 정말 ==이 안되는지 해봤습니다.

class A(var cnt : Int, var name : String)

fun main(){
	var a = A(22,"woosik")
	var b = A(22,"woosik")

   	if(a == b) println("a == b")
    if(a === b) println("a === b")
}

뭐든 다 이유가 있는 법이였습니다.

toString()

toString 함수는 생성한 데이터 클래스의 객체의 프로퍼티들을 리턴해줍니다.

그래서 객체 안에 프로퍼티를 다루기 매우 편해집니다.

data class A(var cnt : Int, var name : String)

fun main(){
	var a = A(20,"woosik")
	println(a) // 출력 : A(cnt=20, name=woosik)
}
data class A(var cnt : Int, var name : String)

fun main(){
	var a = A(20,"woosik")
	println(a.toString()) // 출력 : A(cnt=20, name=woosik)
}

hashCode()

hash에 대해서 간단하게 설명을 하자면 한 데이터를 해시 함수에 대입하여 단방향적으로 정해진 길이의 값을 매핑하는 함수입니다. 즉 아무리 큰 값을 넣어도 정해진 길이의 값이 나와야하며, 넣는 값이 서로 같다면 함수의 결과 값도 같습니다.

data class A(var cnt : Int, var name : String)

fun main(){
	var a = A(20,"woosik")
  var b = A(20,"woosik")
	println(a.hashCode()) //출력 : -782166198
  println(b.hashCode()) //출력 : -782166198
}

서로 다른 객체이지만 값 자체는 같기 때문에 hashCode()함수의 리턴 값은 같습니다.

data class A(var cnt : Int, var name : String)

fun main(){
	var a = A(22,"woosik")
  var b = A(20,"woosik")
	println(a.hashCode()) //출력 : -782166136
  println(b.hashCode()) //출력 : -782166198
}

그렇기에 객체의 값을 바꿔버리면 리턴값도 바뀌게 됩니다.

println(a)과 println(a.toString())의 출력값이 같다고 해서

println(a.hashCode())과 println(a.toString()hashCode())의 출력값이 같을까요?

data class A(var cnt : Int, var name : String)

fun main(){
	var a = A(22,"woosik")
	println(a.hashCode()) // 출력 : -782166136
  println(a.toString().hashCode()) // 출력 : -239237144

}

당연히 틀립니다. 그냥 a는 객체이고 a.toString()은 문자열이기 때문입니다.

copy()

copy()는 이름 그대로 값을 복사합니다. 매우 유용한 기능이긴하지만.

문제는 ‘값’을 복사는데에 있습니다.

얕은 복사, 깊은 복사

data class A(var cnt : Int, var name : String, var hobbys: MutableList<String>)

fun main() {
    val hobbys = mutableListOf("guitar", "chess", "coding")
    
    val woosik = A(20, "woosik", hobbys)
    
    val woosikver2 = woosik.copy()
    
    woosik.hobbys[2] = "game"

    println(woosik) //출력 : A(cnt=20, name=woosik, hobbys=[guitar, chess, game])
    println(woosikver2) //출력 : A(cnt=20, name=woosik, hobbys=[guitar, chess, game])
}

어째서 woosik의 hobbys[2] 값만 바꿨는데 woosikver2도 변경이 될까요?

이는 MutableList가 동적할당으로 작동하는 리스트이기 때문입니다. 그렇기에 메모리 상의 리스트를 가르키는 주소만을 복사하여서 두 객체 둘다 같은 MutableList를 참조하게 되어서 발생한 문제입니다.

copy()는 이러한 문제점을 가지고 있습니다. 얕은 복사만을 지원합니다.

해결하려면 그때 새로 MutableList 선언해서 안에 값을 복사 후 넣어주는 깊은 복사를 해야합니다.

componentN()

데이터 클래스의 프로퍼티를 쉽게 가져오는 것을 돕는 함수입니다.

 

data class A(var cnt : Int, var name : String, var hobbys: MutableList<String>)

fun main() {
    val hobbys = mutableListOf("guitar", "chess", "coding")
    
    val woosik = A(20, "woosik", hobbys)
    println(woosik.component1()) //출력 : 20
    println(woosik.component2()) //출력 : woosik
    println(woosik.component3()) //출력 : [guitar, chess, coding]
}

 

반응형