CS/C#

[C#] Record 와 Property

Mirab 2021. 12. 21. 23:04

프로퍼티 두 번째 시간입니다.

 

# 프로퍼티란?

레코드에 들어가기 전에 참조 형식과 값 형식에 대해서 잠깐 언급하고 가보겠습니다.

값 형식은 필드가 많으면 많을수록 복사 비용이 커지게 됩니다. 왜냐하면 깊은 복사로 새 객체가 기존에 가진 모든 필드를 1:1 복사하기 때문이죠.

그렇지만 참조 형식에서는 이런 오버헤드가 없습니다. 객체가 참조하는 메모리 주소만 복사하면 되기 때문입니다. 하지만 참조는 얕은 복사를 진행하며, 깊은 복사가 필요한 경우 따로 프로그래머가 구현해줘야 합니다.

 

더 들어가봅시다.

값 형식은 객체를 비교할 때에도 기본적으로 내용을 비교하는데 모든 필드를 1:1 비교합니다. 불변 객체에서 필요한 방법입니다.

참조 형식은 내용 비교를 할 수 있으려면 프로그래머가 직접 비교 코드를 작성해야 합니다. 이때 object로부터 상속하는 Equals() 메서드를 오버 라이딩해서 구현합니다.

 

정리하면 불변 객체를 참조 형식으로 선언하면 함수 호출 인수나 컬렉션 요소를 사용할 때 복사 비용을 줄일 수 있습니다. 즉, 값 형식의 좋은 점과 참조 형식의 좋은 점을 쏙 체리 픽해서 가져와 사용합니다.

 

이것을 지원하는 것이 바로 오늘 배울 레코드입니다.

 

레코드 형식은 값 형식처럼 다룰 수 있는 불변 참조 형식(내부 상태를 변경할 수 없는 객체 = Immutable)으로, 참조 형식의 비용 효율 + 값 형식이 주는 편리함 모두를 제공합니다.

 

# 레코드 선언하기

record RTransaction
{
    public string From { get; init; }
    public string To { get; init; }
    public int Amount { get; init; }
}

기존 프로퍼티 사용 방법과 동일하지만 record 키워드를 사용합니다.

이렇게 선언한 레코드로 인스턴스(객체)를 만들면, 불변 객체가 만들어집니다.

 

레코드는 어디서에 빛을 발휘할까요?

복사할 때, 비교할 때 유용합니다.

 

# 레코드 복사하기

RTransaction tr2 = tr1 with { To = "Charlie" };
RTransaction tr3 = tr2 with { From = "Dave", Amount = 30 };

C# 컴파일러는 레코드 형식을 위한 복사 생성자를 자동으로 작성합니다. 왜?? 복사하기 위해서죠.

그런데 이 복사 생성자는 protected로 선언되어 있기 때문에 명시적으로 호출할 수 없습니다.

하지만 with 식을 이용한다면 우측의 레코드를 좌측으로 복사할 수 있으며 { } 안에 적은 프로퍼티를 변경해서 저장할 수 있습니다.

 

with 식은 객체 상태(프로퍼티)가 다양할수록 유용합니다.

인스턴스를 새로 할당할 때 모든 프로퍼티를 입력하지 않아도 되기 때문입니다.

using System;

namespace WithExp
{
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    class Program
    {
        static void Main()
        {
            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = tr1 with { To = "Charlie" };
            RTransaction tr3 = tr2 with { From = "Dave", Amount = 30 };

            Console.WriteLine(tr1);
            Console.WriteLine(tr2);
            Console.WriteLine(tr3);
        }
    }
}
Alice      -> Bob        : $100
Alice      -> Charlie    : $100
Dave       -> Charlie    : $30

#레코드 객체 비교하기

C# 컴파일러는 레코드의 상태를 비교하는 Equals() 메서드를 자동으로 구현합니다.

기존 클래스 비교였다면 프로그래머가 직접 object로부터 상속받은 Equals() 메서드를 구현해야 했지만, 레코드는 C# 컴파일러가 자동으로 구현해주니까 단순이 선언만 하면 끝입니다.

 

아래의 코드는 프로그래머가 기존 클래스 비교에서 구현을 하지 않았다면 참조의 비교 형태와 레코드의 Equals()를 보여줍니다.

using System;

namespace RecordComp
{
    class CTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    class Program
    {
        static void Main()
        {
            CTransaction trA = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            CTransaction trB = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };

            Console.WriteLine(trA);
            Console.WriteLine(trB);
            // 기본적으로 Equals는 참조를 비교하므로 따로 정의하지 않았다면 참조 비교로 인해 false
            Console.WriteLine($"trA equals to trB : {trA.Equals(trB)}");

            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };

            Console.WriteLine(tr1);
            Console.WriteLine(tr2);
            Console.WriteLine($"tr1 equals to tr2 : {tr1.Equals(tr2)}");
        }
    }
}
Alice      -> Bob        : $100
Alice      -> Bob        : $100
trA equals to trB : False
Alice      -> Bob        : $100
Alice      -> Bob        : $100
tr1 equals to tr2 : True

클래스 비교에서 Equals()를 오버 라이딩해서 구현하지 않았다면 참조(주소 값)만 비교하기 때문에 false가 나오게 됩니다. 하지만 레코드의 비교는 컴파일러가 자동으로 구현해주기 때문에 참조 비교가 아닌 진짜 값을 비교하므로 true입니다.

 

오늘은 record 에서 배웠습니다.

record와 프로퍼티는 어떤 점이 달랐는지 record의 장점과 왜 사용하는지에 대해서도 배웠습니다.