본문 바로가기
C#

C# 9.0] init keyword, record, with-expression

by Fastlane 2023. 9. 5.
728x90
반응형

Init-only properties

Object initializer는 사용자에게 객체 생성을 위한 유연하고 가독성있는 형식의 타입을 제공한다. 특히, 객체의 nested object 생성에 편리하다. 

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

한계 중 하나는, properties가 mutable이어야 한다는 것이다. Init-only properties로 해결할 수 있다. 

init accessor는 객체 초기화 시에만 호출되는 set accessor의 변형이다. 

public class Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}
var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK
person.LastName = "Torgersen"; // ERROR!
//Init-only property or indexer 'Person.LastName' can only be assigned 
//in an object initializer, or on 'this' or 'base' in an instance constructor 
//or an 'init' accessor.

Init accessors and readonly fields

init accessors 는 초기화 시에만 호출되기 때문에, enclosing class의 readonly fields 값을 수정할 수 있다. 

public class Person
{
    private readonly string firstName = "<unknown>";
    private readonly string lastName = "<unknown>";
    
    public string FirstName 
    { 
        get => firstName; 
        set => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName))); //ERROR!!
        //A readonly field cannot be assigned to 
        //(except in a constructor or init-only setter of the type 
        //in which the field is defined or a variable initializer)
    }
    
    public string LastName 
    { 
        get => lastName; 
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

Records

classic oop의 core에는 object는 strong identity가 있고, mutable state를 encapsulate한다는 생각이 있다. C#은 기본으로 oop를 지향하기 때문에, object 전체가 immutable하고 value처럼 행동하게 하려면 record를 선언하는 것을 고려해볼 수 있다. 

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

record는 class이지만 record keyword 때문에 몇가지 추가적인 value-like 행동을 한다. records는 identity가 아닌, contents로 판단된다. 이 점에서, records는 structs에 가깝지만, 여전히 reference types이다. 

with-expressions

immutable data를 다루는 데, 공통 패턴은 존재하는 object로부터 새로운 value를 만다는 것이다. 예를들어, person의 last name 변경은 변경된 last name과 함께 old one을 복사하는 new object로 나타낼 수 있다. 이 기술을 non-destructive mutation이라 한다. 이 기술을 위해 records는 with-expression 이라는 새로운 종류의 expression을 허용한다. 

public class Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" }; //ERROR!!
//The receiver type 'Person' is not a valid record type and is not a struct type.

public record PersonRecord
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}
       
var personRecord = new PersonRecord { FirstName = "Mads", LastName = "Nielsen" };
var otherPersonRecord = personRecord with { LastName = "Torgersen" };

with-expression을 사용하려면 properties는 init 또는 set accessor을 갖고있어야 한다. 

Value-based equality

모든 object는 object class로부터 virtual Equals(object) 함수를 상속받는다. value-based로 override하였다. 

public record PersonRecord
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}
       
var personRecord = new PersonRecord { FirstName = "Mads", LastName = "Nielsen" };
var otherPersonRecord = personRecord with { LastName = "Torgersen" };
var originPersonRecord = otherPersonRecord with { LastName = "Nielsen" };

수정된 object의 last name을 다시 되돌리면, ReferenceEquals(person, originalPerson) = false 이고 Equals(person, originalPerson) = true 이다. value-based Equals과 마찬가지로, value-based GetHashCode() override도 동일하게 동작한다. 

Inheritance

Records는 다른 records를 상속받을 수 있다. 

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

public record Student : Person
{
    public int ID;
}

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };
var otherStudent = student with { LastName = "Torgersen" };
WriteLine(otherStudent is Student); // true
Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };
WriteLine(student != similarStudent); //true, since ID's are different

Positional records

record 사용 시, positional 접근이 유용할 때가 있다. constructor arguments를 통해 contents가 주어진 곳에서 positional deconstruction으로 추출될 수 있다. record에서 your own constructor와 deconstructor를 명시할 수 있다. 

public record Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; }
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

동일하지만, 훨씬 간단한 표기방법이 있다. 

public record Person(string FirstName, string LastName);

이것은 public init-only auto-properties와 constructor, deconstructor를 선언한다. 따라서 아래와 같이 작성이 가능하다. 

var person = new Person("Mads", "Torgersen"); // positional construction
var (f, l) = person;                        // positional deconstruction

만약에 generated auto-property가 싫으면, 직접 작성할 수 있다. 대신 동일한 이름을 사용해야 한다. 예를들어, FirstName을 protected property로 하려면 아래와 같이 작성한다. 

public record Person(string FirstName, string LastName)
{
    protected string FirstName { get; init; } = FirstName; 
}

 

 

3 Different Ways to Implement Value Object in C# 10 (trycatchblog.tech)

 

3 Different Ways to Implement Value Object in C# 10

Let’s find out how to implement value objects and map them to a database table with EF Core 6.0.

trycatchblog.tech

 

 

728x90
반응형

댓글