프로그래밍/C#

[C#] Boxing / Unboxing (2)

갓똥 2020. 2. 11. 17:31
728x90
반응형

1. 핵심 정리

using System;

class Program {
    static void Main() {
        Console.WriteLine(10 < 20);  // true
        
        Console.WriteLine(10.CompareTo(20));  // -1
        Console.WriteLine(10.CompareTo(10));  // 0
        Console.WriteLine(10.CompareTo(5));   // 1
        
        string s1 = "AAA";
        string s2 = "BBB";
        
        Console.WriteLine(s1 < s2);
        
        Console.WriteLine(s1.CompareTo(s2));  // -1
    }
}

 ① 객체의 크기를 비교하는 방법

C#에서 객체의 크기를 비교하는 방법은 관계연산자와 CompareTo메소드가 있다.
관계연산자는 bool타입의 true, false를 반환하고
CompareTo는 int타입으로 앞의 객체가 작다면 -1, 같다면 0, 크다면 1을 반환한다.
위의 값타입들은 문제가 없다. 이번엔 string을 한 번 보자.
14번째라인에서 string으로 관계연산자를 이용한 비교를 해보고 있다.
근데 문제는 이러면 에러가 나는 것을 볼 수 있다. string은 관계연산자를 쓸 수 없다.
string은 비교를 하려면 CompareTo메소드를 사용해야 한다.
비교 자체는 ASCII코드를 이용하여 한다.

 


2. CompareTo() #1

using System;

class Point {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

Point 클래스를 만들고 Main에서 두 개의 객체를 만든 후 크기비교를 하는 코드이다.
관계연산자로 제공해도 되지만 C#에서 일반적인건 CompareTo이므로 CompareTo를 만들어보자.
CompareTo의 동작방식은 아래와 같다.

 ① A.CompareTo(B)

using System;

class Point {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(Point other) {
        if(x > other.x) return 1;
        else if(x == other.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

위와 같이 만들어 사용할 수 있다. 간단하게 x만을 비교하고 있는데 비교정책은 맘대로 바꾸면 된다.
이제 Point로 만든 객체도 CompareTo를 사용할 수 있게 되었다.
그렇다면 이런 가정을 한 번 해보자.
C#의 대부분의 class들이 CompareTo를 사용하는데 실수로 compareTo로 만들었다고 해보자.
사실 이렇게 만들어도 쓸 때 compareTo로 사용하면 되지만 일관성이 없다.
그렇다면 CompareTo를 만들 땐 모두 같은 이름으로 쓰자 라고 약속하면 좋을텐데
이러한 약속이 객체지향의 기본 문법에서 함수 이름을 약속하는 문법은 interface가 있다.
그렇다면 아래와 같이 바꿀 수 있다.
using System;

// 비교하는 메소드의 이름을 약속하자.
interface IComparable {
    int CompareTo(Point other);
}

// 모든 비교 가능한 객체는 IComparable 인터페이스를 구현해야 한다.
class Point : IComparable {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(Point other) {
        if(x > other.x) return 1;
        else if(x == other.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

이제 모든 비교 가능한 객체를 만들 땐 IComparable 인터페이스를 상속하여
CompareTo에 대한 이름의 일관성을 지켰다.
근데 문제가 있다. 사실 IComparable을 만든 의미대로 Point만 쓸게 아닌 다른 모든 객체에서 쓸 수 있어야 한다.
Point를 object로 바꿈으로 해결할 수 있다. 아래에서 해결법을 보자.
using System;

interface IComparable {
    int CompareTo(object other);
}

class Point : IComparable {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(object other) {
        Point pt = other as Point;
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

마지막으로 IComparable은 사실 우리가 만드는게 아닌 이미 C#에 있다.
따라서 지워도 아무문제 없다.
정의로 이동해보면 int CompareTo(object obj)가 있는걸 볼 수 있다.

 ② IComparable 인터페이스

    => CompareTo 메소드에 대한 규칙을 담은 인터페이스

    => 크기 비교가 가능한 타입은 IComparable 인터페이스를 구현해야 한다.

    => C#은 대부분의 메소드의 규칙을 인터페이스로 제공하고 있다.

 

다음으로 위의 코드의 문제점을 살펴보고 개선해보자.

3. CompareTo() #2

using System;

class Point : IComparable {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(object other) {
        Point pt = other as Point;
        
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

자 이제 문제점을 살펴보자.
위의 포인트는 class로 만들었다. 하지만 포인트가 struct로 만들었다고 가정해보자.
그럼 아래와 같이 코드를 수정해서 문제없이 사용할 수 있다.
using System;

struct Point : IComparable {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(object other) {
        // Point pt = other as Point;
        Point pt = (Point)other;
        
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

casting만 바꾸면 struct로 바꾸어도 아무 문제가 없다.
하지만 Main에서 Point는 현재 값 타입이다. 근데 CompareTo에서 인자로 받는건 object로 받고 있다.
object는 레퍼런스 타입이므로 인자가 넘어가며 Boxing이 발생한다. 
또 CompareTo 내부에서 캐스팅할 때 Unboxing이 발생한다.
이는 인자로 넘어갈 때, 캐스팅 될 때 모두 복사본이 만들어지므로 성능저하가 발생할 수 있다.
해결책은 object가 아닌 Point로 받으면 되지만 그러면 interface를 쓸 수 없다.
using System;

// C# 1.0
// interface IComparable {
//     int CompareTo(object other);
// }

// C# 2.0 : generic interface
interface IComparable<T> {
    int CompareTo(T other);
}

struct Point : IComparable<Point> {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(Point pt) {        
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 

C#초기에는 generic이 없었다.
그 후 C#이 발전되며 2.0에서 generic이 생겨났다.
따라서 위와 같이 수정할 수 있으며 더이상 Boxing, Unboxing이 발생하지 않게 되었다.
위와 마찬가지로 IComparable<T>는 이미 구현되어 있어 지워도 문제 없다.

 ① IComparable vs IComparable<T>

마지막으로 작성한 코드도 완벽한 해결책은 아니다.
만약 Main에서 객체를 생성할 때
Point p2 = new Point(2, 2); 가 아닌
object p2 = new Point(2, 2); 로 만들었다고 한다면
CompareTo(p2)에서 인자를 보낼 수가 없다.
따라서 최고의 해결책은 두 가지를 모두 만드는 것이다.
using System;

struct Point : IComparable<Point>, IComparable {
    private int x;
    private int y;
    public Point(int xPos, int yPos) {
        x = xPos;
        y = yPos;
    }
    public int CompareTo(Point pt) {        
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
    public int CompareTo(object other) {
        Point pt = (Point)other;
        
        if(x > pt.x) return 1;
        else if(x == pt.x) return 0;
        return -1;
    }
}

class Program {
    static void Main() {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);
        
        Console.WriteLine(p1.CompareTo(p2));
    }
}

 ② 권장 사항

    => IComparable 과 IComparable<T> 인터페이스를 모두 구현하는 것이 좋다.

728x90
반응형