C# 9.0: 레코드(record) 타입
C# 9.0 에서 가장 두드러진 변화는 record 타입을 새로 도입한 것이다. 지금까지의 C#/.NET에서는 struct를 사용하는 Value Type(값 타입)과 class를 사용하는 Reference Type(레퍼런스 타입)이 있었는데, C# 9에서 Immutable 값 데이타를 갖는 record 타입을 추가하였다. 새로 도입된 record 타입은 객체 내의 멤버가 변하지 않는 Immutable Reference Type을 만들기 위한 것이다. C# 9에서는 이를 위해 record 라는 새로운 키워드를 도입했으며, class 키워드를 사용해서 클래스를 정의하는 것이 아니라, 이 record라는 키워드를 통해 Immutable Type을 정의하게 된다.
예제
// 예제(1)
public record Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
=> (Name, Age) = (name, age);
}
class Program
{
static void Main(string[] args)
{
Person p = new Person("Tom", 30);
string s = p.Name;
}
}
public record Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
=> (Name, Age) = (name, age);
}
class Program
{
static void Main(string[] args)
{
Person p = new Person("Tom", 30);
string s = p.Name;
}
}
C# 9.0: init only setter
C# 9.0에서 새롭게 추가된 키워드로 init 이라는 키워드가 있다. init은 속성을 정의할 때 쓰는 set 대신에 사용하는데, set으로 정의된 속성이 흔히 객체 생성 이후에 속성을 변경하는데 사용되는 것이라면, init은 객체가 처음 초기화될 때만 속성을 변경할 수 있다. init 키워드는 해당 속성을 Immutable(불변) 속성으로 만드는 역활을 하게 된다. init이 하나의 속성을 Immutable로 만드는 것이라면, record 타입은 객체 전체를 Immutable로 만드는 것으로 볼 수 있다. record 타입을 정의할 때 위 예제(1)과 같이 생성자를 쓰는 대신, 아래와 같이 init과 객체 초기자(Object Initializer)를 사용할 수 있는데 만약 필드/속성의 수가 많다면, 아래 표현이 더 간결할 수 있다.
예제
// 예제(2)
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
class Program
{
static void Main(string[] args)
{
Person p = new Person
{
Name = "Tom",
Age = 30
};
}
}
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
class Program
{
static void Main(string[] args)
{
Person p = new Person
{
Name = "Tom",
Age = 30
};
}
}
C# 9.0: with 표현식
record 타입은 객체 전체가 Immutable인 타입인데, 만약 객체 중 일부만 변경하여 새로운 record 객체를 만들고 싶다면, C# 9에서 새로 도입된 with 키워드를 사용할 수 있다. 예를 들어, 아래 예제에서 Person 타입의 객체로 tom1을 만들었을 때, tom1 객체 중 나이만 변경하고 나머지 데이타는 동일하게 tom2 객체를 만들고 싶다면, 아래와 같이 with 키워드를 사용할 수 있다.
예제
// 예제(3)
Person tom1 = new Person
{
Name = "Tom",
Age = 30
};
Person tom2 = tom1 with { Age = 40 };
Person tom1 = new Person
{
Name = "Tom",
Age = 30
};
Person tom2 = tom1 with { Age = 40 };
C# 9.0: record 객체 비교
record 타입은 Immutable인 타입으로 두 record 객체를 비교할 때, 객체 내의 값들이 동일한 지를 비교하는 경우가 더 많을 것이다. 이러한 맥락에서 record 타입의 Equals() 메서드는 record 객체의 데이타 값들이 동일한 지를 체크하고 데이타가 동일하면 참을 리턴한다. 아래 예제에서 두 객체의 레퍼런스는 다를 것이므로 ReferenceEquals()은 false를 리턴하고, 두 객체의 데이타가 동일하므로 Equals()은 true를 리턴한다.
예제
// 예제(4)
Person p1 = new Person
{
Name = "Tom",
Age = 30
};
Person p2 = new Person
{
Name = "Tom",
Age = 30
};
bool same = p1.Equals(p2); // true
bool b = ReferenceEquals(p1, p2); //false
Person p1 = new Person
{
Name = "Tom",
Age = 30
};
Person p2 = new Person
{
Name = "Tom",
Age = 30
};
bool same = p1.Equals(p2); // true
bool b = ReferenceEquals(p1, p2); //false
C# 9.0: Positional record
record 타입은 아래 예제(5)에서 보는 바와 같이 생성자(constructor)와 Deconstructor를 사용할 수 있는데, 이들은 Positional record를 사용하면 생성자(constructor)와 Deconstruct를 정의할 필요 없이 아래 예제(6)과 같이 간결하게 표현할 수 있다. Positional record는 말 그대로 멤버의 위치가 중요하면, 그 위치대로 데이타는 넣거나 가져올 수 있다. 예제(7)은 Constructor와 Deconstructor를 사용하는 예이다.
예제
// 예제(5) 레코드에 Constructor/Deconstructor 사용
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string name, int age)
=> (Name, Age) = (name, age);
public void Deconstruct(out string name, out int age)
=> (name, age) = (Name, Age);
}
// 예제(6) Positional record
public record Person(string Name, int Age);
// 예제(7)
class Program
{
static void Main(string[] args)
{
// Constructor 사용
Person tom = new Person("Tom", 30);
// Deconstructor 사용
var (name, age) = tom;
Console.WriteLine($"{name}, {age}");
}
}
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string name, int age)
=> (Name, Age) = (name, age);
public void Deconstruct(out string name, out int age)
=> (name, age) = (Name, Age);
}
// 예제(6) Positional record
public record Person(string Name, int Age);
// 예제(7)
class Program
{
static void Main(string[] args)
{
// Constructor 사용
Person tom = new Person("Tom", 30);
// Deconstructor 사용
var (name, age) = tom;
Console.WriteLine($"{name}, {age}");
}
}
C# 9.0: 파생 record
record 타입은 상속을 지원하며, 파생 클래스를 정의하는 것과 동일한 방식으로 파생 record를 만들 수 있다. 파생 record 타입에서도 with 표현식과 Equals 객체 비교 등이 동일하게 적용된다.
예제
// 예제(8) 파생 record
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
public record Employee : Person
{
public int Id { get; init; }
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Employee
{
Id = 1001,
Name = "Tom",
Age = 30
};
}
}
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
public record Employee : Person
{
public int Id { get; init; }
}
class Program
{
static void Main(string[] args)
{
Person p1 = new Employee
{
Id = 1001,
Name = "Tom",
Age = 30
};
}
}