NORU검색
카테고리Programming

C# 9.0 : Record 타입

C# 9.0의 record 타입을 정의 방법, 값 기반 같음, with 복사, 상속과 제네릭 제약 조건 중심으로 정리합니다.

태그
이 글의 목차

.NET 5 이상에서 사용 가능한 C# 9.0 버전에 추가된 내용 중 가장 낯선 내용은 레코드라는 것이 추가되었다는 것이었습니다. 이번엔 레코드에 관해 조사한 내용을 정리 및 공유하려고 합니다.

개요

기존 C#에서는 struct로 대표되는 Value 타입과 class로 대표되는 Reference 타입으로 분류가 가능했습니다. 새로 추가된 레코드의 경우 class와 같은 Reference 타입입니다. 하지만, 두 레코드 변수에 대한 비교를 진행할 때 레코드 정의와 모든 필드의 값이 같은지 비교하는 '값 기반 같음'을 사용한다는 점에서 class와 구분됩니다.

반대로 struct와 비교를 하게 되면, struct로도 충분히 값 기반 같음을 제공하고, 아래에서 설명할 불변 데이터 중심의 디자인이 가능합니다. 하지만 struct는 상속을 지원하지 않고, 값 기반 같음을 구현했을 때 레코드에 비해 효율이 낮다고 합니다. 값 형식의 ValueType.Equals 메소드는 리플렉션을 통해 모든 필드를 찾지만, 레코드는 컴파일러가 Equals 메소드를 생성합니다.

레코드 정의하기

레코드를 정의하기 위해선 record라는 키워드를 사용합니다. 기존의 struct나 class를 정의하듯이 정의한다면 다음과 같이 사용할 수 있습니다.

public record Test
{
    public int ID { get; init; } = default;
    public string Value { get; init; } = default;
};

처음 보는 키워드가 또 있네요. init은 프로퍼티의 set 대신 사용할 수 있는 키워드로, 초기화 이후에는 변경이 불가능한 프로퍼티를 만들 수 있습니다. 이를 초기화 전용 프로퍼티라고 합니다. 레코드의 경우 기존처럼 (set을 사용하는) 변경 가능한 값을 가지는 데이터로 사용할 수도 있지만, 주로 (init을 사용하는) 변경 불가능한 값을 가지는 데이터를 지원하기 위한 목적이라고 합니다.

더 간단하게 정의하기

레코드가 변경 불가능한, 불변 데이터를 다루는 것에 주로 목적을 두고 있다고 했습니다. 이를 증명하는 것처럼 레코드의 경우 초기화 전용 프로퍼티를 가지는 레코드를 더욱 간단하게 정의할 수 있습니다.

public record Test(int ID, string Value);

위 코드에서 int ID나 string Value를 위치 매개 변수라고 부릅니다. 이렇게 정의했을 때, 컴파일러는 각 위치 매개 변수에 대해 초기화 전용 프로퍼티를 생성하고, 위 선언에 맞게 기본 생성자를 제공합니다. 또한 둘 이상의 위치 매개 변수가 있다면 Deconstruct 메소드를 제공해 정의된 레코드를 분해할 수 있게 됩니다.

값 기반 같음

런타임 형식이 일치하고, 모든 프로퍼티와 필드 값이 일치할 경우 레코드 형식의 두 변수가 같다고 판단하는 방식입니다. 기존의 Reference 타입은 두 변수가 같다고 판단하는 기준은 ID이므로, 두 변수가 같은 개체를 참조해야 두 변수를 같다고 판단하는 점에서 레코드와 class의 차이가 있습니다.

using static System.Console;

public class Program
{
	public record Test1(int ID, string Value);
	public class Test2
	{
		public int ID { get; set; }
		public string Value { get; set; }

		public Test2(int ID, string Value)
		{
			this.ID = ID;
			this.Value = Value;
		}
	}

	public static void Main()
	{
		// record : 값 기반 비교
		Test1 test1_1 = new(1, "aaa");
		Test1 test1_2 = new(1, "aaa");
		WriteLine(test1_1.Equals(test1_2)); 			// output: True
		WriteLine(ReferenceEquals(test1_1, test1_2)); 	// output: False

		// class : 참조하는 개체 동일 여부 비교
		Test2 test2_1 = new(1, "aaa");
		Test2 test2_2 = new(1, "aaa");
		WriteLine(test2_1.Equals(test2_2)); 			// output: False
		WriteLine(ReferenceEquals(test2_1, test2_2)); 	// output: False
	}
}

아니 레코드가 복사가 된다고? 아니 레코드가 복사가 된다고?

레코드의 경우 불변 데이터를 변경해야하는 경우 등 기존의 레코드 변수를 복제해야하는 상황일 때 사용할 수 있는 키워드가 있습니다. 바로 with인데요, 아래의 코드처럼 사용할 수 있습니다.

using static System.Console;

public class Program
{
	public record Test(int ID, string Value);

	public static void Main()
	{
		Test test1 = new(1, "aaa");
		WriteLine(test1);
		// output : Test { ID = 1, Value = aaa }

		Test test2 = test1 with
		{
			ID = 2
		};
		WriteLine(test2);
		// output : Test { ID = 2, Value = aaa }

		Test test3 = test1 with
		{
			Value = "bbb"
		};
		WriteLine(test3);
		// output : Test { ID = 1, Value = bbb }

		Test test4 = test1 with { };
		WriteLine(test4);
		// output : Test { ID = 1, Value = aaa }

		// test4는 test1을 복제해 같은 값을 갖지만 다른 개체입니다
		WriteLine(test1 == test4);					// output : True
		WriteLine(ReferenceEquals(test1, test4));	// output : False
	}
}

상속

레코드는 다른 레코드를 상속할 수 있습니다. 하지만, 레코드는 클래스를 상속받을 수 없고, 클래스도 레코드를 상속받을 수 없습니다.

using static System.Console;

public class Program
{
	public record Base(int ID, string Value);
	public record Test1(int ID, string Value, bool Other) : Base(ID, Value);
	public record Test2(int ID, string Value, bool Other) : Base(ID, Value);

	public static void Main()
	{
		Test1 test1 = new(1, "aaa", true);
		Test2 test2 = new(1, "aaa", true);
		WriteLine(test1 == test2); // output : False
	}
}

위 코드에서 test1과 test2는 동일한 프로퍼티와 값을 갖지만, 런타임 형식이 다르기 때문에 두 값을 비교했을 때 다르다는 결과를 받게 됩니다.

제네릭 제약 조건

제네릭 선언 시에 형식을 레코드로 제약하는 별도의 조건은 없지만, 레코드는 class 제약 조건을 만족하기 때문에 class를 사용하시면 됩니다.

using static System.Console;

public class Program
{
	public record Test(int ID, string Value);

	public static void Print<T>(T val) where T : class
	{
		WriteLine(val);
	}

	public static void Main()
	{
		Test test = new(1, "aaa");
		Print(test);
		// output : Test { ID = 1, Value = aaa }
	}
}

참조