티스토리 뷰

Visual Studio

Welcome to C# 9.0

Connor Park 2020. 11. 24. 21:25

C# 9.0이 릴리즈 되었습니다. 어떤 기능이 추가되었는지 블로그 포스트를 참고해서 간단하게 작성해 보았습니다.

 

C# 9.0 on the record | .NET Blog (microsoft.com)

 

C# 9.0 on the record | .NET Blog

C# 9.0 on the record It’s official: C# 9.0 is out! Back in May I blogged about the C# 9.0 plans, and the following is an updated version of that post to match what we actually ended up shipping. With every new version of C# we strive for greater clarity

devblogs.microsoft.com

Init-only properties

일반적으로 객체를 생성하는 간단한 방법이 있습니다. 특히 이 방법은 전체 객체 트리를 한번에 생성할 수 있어서 매우 유용합니다.

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

객체를 만들 클래스는 몇개의 간단한 속성을 가지고 있습니다

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

그런데, 여기에는 큰 제한이 있습니다. 객체 생성시 데이터를 입력하기 위해서는 프로퍼티의 속성이 변경 가능해야 한다는 것입니다. 그래서, 초기화 전용 속성인 init를 추가했습니다.

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 accessors and readonly fields

init 접근자는 초기화 중에만 호출 할 수 있기 때문에 클래스의 읽기 전용 필드를 변경할 수 있습니다.

public class Person
{
    private readonly string firstName = "<unknown>";
    private readonly string lastName = "<unknown>";
    
    public string FirstName 
    { 
        get => firstName; 
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
    }
    public string LastName 
    { 
        get => lastName; 
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

Records

고전적인 객체 지향 프로그래밍의 핵심은 객체가 강력한 정체성을 가지고 있으며, 시간이 지남에 따라 진화하는 가변 상태를 캡슐화한다는 아이디어입니다. C#은 항상 이를 잘 수행했지만, 때로는 정반대를 원할 때가 있습니다. 여기서 C#의 기본값은 작업을 방해하는 경향이있어, 작업이 매우 힘들어졌습니다. 전체 객체가 불변하고 값처럼 동작하기를 원하면 이를 Records로 선언하는 것을 고려해야 합니다.

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

record는 여전히 클래스이지만, record 키워드는 몇가지 추가 값 유사 동작을 포함합니다. 일반적으로 기록은 identity가 아닌 value로 정의됩니다. 이와 관련하여 record는 구조체에 훨씬 더 가깝지만 record는 여전히 reference type입니다.

record는 변경 가능하지만 주로 변경 불가능한 데이터 모델을 더 잘 지원합니다.

With-expressions

불변 데이터로 작업 할 때 일반적인 패턴은 기존 값에서 새 값을 만들어 새 상태를 나타내는 것입니다. 예를 들어, 우리가 성을 변경하는 경우 다른 성이있는 경우를 제외하고 이전 개체의 복사 본을 만들어 새 개체를 사용합니다. 이 기술을 종종 비파괴 돌연변이라고 합니다. 시간이 지남에 따라 사람을 나타내는 대신 record는 주어진 시간에 사람의 상태를 나타냅니다. 이러한 스타일의 프로그래밍을 돕기 위해 record는 새로운 종류의 표현인 with를 허용합니다.

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

With-expression은 객체 초기화 구분을 사용하여 이전 객체와 새 객체의 차이점을 표현합니다. 여러 속성을 지정할 수도 있습니다.

 

With-expression은 실제로 이전 객체의 전체 상태를 새 객체에 복사 한 다음 객체 초기화를 하는 방식으로 작동합니다. 즉, 프로퍼티에는 with-expression에서 변경 될수 있는 init 또는 set 접근자가 있어야 합니다.

Value-based equality

모든 객체는 객체 클래스에서 가상 Equals(object) 메서드를 상속합니다. 두 매개 변수가 모두 null이 아닌 경우 Object.Equals(object, object) 정적 메서드의 기반으로 사용됩니다. 구조체는 "값 기반 같음"을 가지도록 재정의하고 Equals를 재귀적으로 호출하여 구조체의 각 필드를 비교합니다. record도 마찬가지로 동작합니다. 이는 "value-ness"에 따라 두 record 객체가 동일한 객체가 아니어도 서로 동일 할 수 있을을 의미 합니다. 예를 들어 수정 된 사람의 성을 다시 수정하는 경우 :

var originalPerson = otherPerson with { LastName = "Nielsen" };

이제 ReferenceEquals(person, originalPerson) = false이지만 Equals(person, originalPerson) = true가 됩니다. 값 기반 Equals와 함께 값 기반 GetHashCode() 재정의도 있습니다. 또한 record는 IEquatable<T>를 구현하고 == 및 != 연산자를 오버로드하므로 값 기반 동작이 모든 서로 다른 같음 메커니즘에서 일관되게 표시됩니다.

Inheritance

Record는 다른 record에서 상속 할 수 있습니다.

public record Student : Person
{
    public int ID;
}

With-expression 및 value equality는 정적으로 알려진 유형 뿐만 아니라 전체 런타임 객체를 고려한다는 점에서 record 상속과 잘 작동합니다. Student를 생성하지만 Person 변수에 저장한다고 가정합니다.

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

With-expression은 여전히 전체 객체를 복사하고 런타임 유형을 유지합니다.

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에서 자체 생성자와 해체자를 지정하는 것은 완벽하게 가능합니다.

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);

이것은 공용 초기화 전용 자동 속성과 생성자 및 해체자를 선언하므로 다음과 같이 사용할 수 있습니다.

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

생성 된 자동 속성이 마음에 들지 않으면 같은 이름의 고유 한 속성을 대신 정의 할 수 있으며, 생성 된 생성자와 해체자는 해당 속성을 사용합니다. 이 경우 매개 변수는 초기화에 사용할 수 있는 범위에 있습니다. 예를 들어 FirstName을 보호 된 속성으로 사용하고 싶다고 가정 해 보겠습니다.

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

Positional record는 다음과 같은 기본 생성자를 호출 할 수 있습니다.

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

 

Top-level programs

C#으로 간단한 프로그램을 작성하려면, 상당한 양의 상용구 코드가 필요합니다.

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

이것은 언어 초보자에게 어렵고 느끼게하고, 코드를 복잡하게 만드는 들여 쓰기 수준을 추가하게 됩니다. 그래서, C# 9.0에서는 Top-level 에서 기본 프로그램을 작성할 수 있습니다.

using System;

Console.WriteLine("Hello World!");

모든 코드가 허용됩니다. 프로그램은 using 후와 파일의 유형 또는 네임 스페이스 선언 전에 작성되어야하며, Main 메서드가 하나만 있을 수 있는 것처럼 하나의 파일에서만 그 작업을 수행 할 수 있습니다. 상태코드를 반환하려면, 그렇게 할 수 있습니다. 작업을 기다리고 싶다면, 그렇게 할 수 있습니다. 명령 줄 인수에 액서스하려면 args를 사용할 수 있습니다.

using static System.Console;
using System.Threading.Tasks;

WriteLine(args[0]);
await Task.Delay(1000);
return 0;

로컬 함수는 명령문의 한 형태이며 Top-level 프로그램에서도 허용됩니다. 대신 외부에서 호출하는 것은 오류를 발생합니다.

 

Improved pattern matching

몇 가지 새로운 종류의 패턴이 C#9.0에 추가되었습니다. 패턴 일치 튜토리얼의 이 코드를 살펴 보도록 하겠습니다. 자세한 사항은 여기를 참고하세요.

public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
       ...
       
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

Simple type patterns

이전에는 형식 패턴이 형식이 일치 할 때 식별자를 선언해야 했습니다. 위의 DeliveryTruck _에서와 같이 해당 식별자가 폐기 _ 인 경우에도 마찬가지입니다. 그러나 이제는 아래와 같이 사용할 수 있습니다.

DeliveryTruck => 10.00m,

Relational patterns

C#9.0에는 관계 연산자 <, <= 등에 해당하는 패턴이 도입되었습니다. 따라서 이제 위 패턴의 DeliveryTruck 부분을 중첩 된 스위치 표현식으로 작성할 수 있습니다.

DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},

Logical patterns

마지막으로, 식에 사용되는 연산자와의 혼동을 피하기 위해 패턴을 논리 연산자와 결합 할 수 있습니다. 예를 들어, 위의 중첩 스위치의 경우는 다음과 같이 오름차순으로 배치 될 수 있습니다.

DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))

또한 다루기 힘든 이중 괄호 대신 is-expression을 포함하는 if 조건에서 편리하게 사용할 수 있습니다.

if (!(e is Customer)) { ... }

상당히 불편하게 느꼈던 부분을 아래 처럼 변경할 수 있습니다.

if (e is not Customer) { ... }

실제로 is not expression은 Customer와 같은 type을 비교할 때도 사용할 수 있습니다.

if (e is not Customer c) { throw ... } // if this branch throws or returns...
var n = c.FirstName; // ... c is definitely assigned here

 

Target-typed new expressions

"Target-typing"은 표현식이 사용되는 컨텍스트에서 유형을 가져올 때 사용하는 용어입니다. 예를 들어 null 및 람다식은 항상 대상 형식입니다. C#의 새 식에는 항상 형식을 지정해야합니다. C# 9.0에서는 표현식이 할당되는 명확한 유형이 있는 경우 유형을 생략 할 수 있습니다.

Point p = new (3, 5);

이것은 배열이나 객체 초기화에서 반복이 많은 경우 매우 좋습니다.

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) }; 

Covariant returns

파생 클래스의 메서드 재정의에 기본 형식의 선언 보다 더 구체적인 반환 형식이 있음을 나타내는 것이 유용할 때가 있습니다. C# 9.0에서는 다음이 가능합니다.

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}

And much more…

C# 9.0 기능의 전체 세트를 확인하기 위해 가장 좋은 곳은 "C# 9.0의 새로운 기능" 문서 페이지 입니다.

 

댓글
댓글쓰기 폼