프로그래밍 언어/C#

[C#] 8.0 새로운 기능

ShovelingLife 2023. 10. 4. 13:49

1. 디폴트 인터페이스 멤버(Default Inteface Members)

이전 버전에서는 인터페이스를 한번 배포한 후 수정하면, 기존에 구현된 모든 타입들을 수정하지 않는 한 타입 오류를 발생시켰다. 더구나 그 인터페이스를 외부에서 사용한다면, 수정은 거의 불가능하였다. C# 8.0에서는 인터페이스에 새로운 멤버를 추가하고 새로운 멤버의 Body 구현 부분을 추가할 수 있게 되었다. 이렇게 새로 추가된 인터페이스 멤버는 디폴트로 사용되기 때문에 기존 구현된 타입들이 새 멤버를 추가적으로 구현되지 않을 경우 이 디폴트 구현을 사용하게 된다.

  • 새로 구현하는 클래스는 디폴트 멤버 구현을 사용하지 않고 재정의할 수 있다.
  • 인터페이스의 디폴트 멤버 구현을 액세스 하기 위해서는 인터페이스로 캐스팅된 변수를 사용해야 한다.
// ILogger v1.0
public interface ILogger
{
    void Log(string message);
}

// ILogger v2.0
public interface ILogger
{
    void Log(string message);

    // 추가된 멤버
    void Log(Exception ex) => Log(ex.Message);
    void Log(string logType, string msg)
    {
        if (logType == "Error" || logType == "Warning" || logType == "Info")
        {
            Log($"{logType}: {msg}");
        }
        else
        {
            throw new ApplicationException("Invalid LogType");
        }
    }
}

class MyLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
        Debug.WriteLine(message);
    }

    // 디폴트 구현을 사용하지 않고 새로 정의함
    public void Log(Exception ex)
    {
        Debug.WriteLine(ex.ToString());
    }
}

2. 패턴 매칭

1) switch expression

기존 switch 문(switch statement)은 case 별로 값을 체크하여 분기하지만, switch 식(switch expression)은 기존의 case 블록들을 보다 간결하게 식으로 표현한 것이다.

  • switch 식은 switch 문과 달리 switch 식 앞에 변수 명을 적고 각 case 블록은 case, break, default 등을 쓰지 않고 (패턴/값) => (수식)과 같은 식으로 표현한다.
static double GetArea(Shape shape)
{
    // switch expression
    double area = shape switch 
    {
        null        => 0, // null 체크
        Line _      => 0,
        Rectangle r => r.Width * r.Height,
        Circle c    => Math.PI * c.Radius * c.Radius,
        _           => throw new ArgumentException() // default와 같은 기능
    };
    return area;
}

2) 속성 패턴(Property Pattern)

속성 패턴(Property Pattern)은 객체의 속성을 사용하여 패턴 매칭을 할 수 있도록 한 것이다. 속성 패턴을 사용하면 복잡한 switch 문을 보다 간결하게 switch 식으로 표현할 수 있다.

public decimal CalcFee(Customer cust)
{
    // Property Pattern
    decimal fee = cust switch
    {
        { IsSenior: true } => 10,
        { IsVeteran: true } => 12,
        { Level: "VIP" } => 5,
        { Level: "A", IsMinor: false} => 10,
        _ => 20
    };
    return fee;
}

3) 튜플 패턴(Tuple Pattern)

튜플 패턴(Tuple Pattern)은 하나의 변수가 아닌 여러 개의 변수들에 기반한 패턴 매칭을 말한다.

static int GetCreditLimit(int creditScore, int debtLevel)
{
    // Tuple Pattern
    var credit = (creditScore, debtLevel) switch
    {
        (850, 0) => 200,
        var (c, d) when c > 700 => 100,
        var (c, d) when c > 600 && d < 50 => 80,
        var (c, d) when c > 600 && d >= 50 => 60,
        _ => 40
    };
    return credit;
}

static void Main(string[] args)
{
    int creditPct = GetCreditLimit(650, 30);
    Console.WriteLine(creditPct);
}

4) 위치 패턴(Positional pattern)

만약 어떤 타입이 Deconstructor를 가지고 있다면, Deconstructor로부터 리턴되는 속성들을 그 위치에 따라 패턴 매칭에 사용할 수 있는데, 이를 위치 패턴(Positional pattern)이라 한다.

class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) =>
        (x, y) = (X, Y);
}

static string 사분면(Point point)
{
    // Positional pattern
    string quad = point switch
    {
        (0, 0) => "원점",
        var (x, y) when x > 0 && y > 0 => "1사분면",
        var (x, y) when x < 0 && y > 0 => "2사분면",
        var (x, y) when x < 0 && y < 0 => "3사분면",
        var (x, y) when x > 0 && y < 0 => "4사분면",
        var (_, _) => "X/Y축",
        _ => null
    };
    return quad;
}

static void Main(string[] args)
{
    var p = new Point(-5, -2);
    string q = 사분면(p);
    Console.WriteLine(q); // 3사분면
}

5) 재귀 패턴(Recursive pattern)

패턴은 다른 서브 패턴(sub pattern)들을 포함할 수 있고 한 서브 패턴은 내부에 또 다른 서브 패턴들을 포함할 수 있는데, 이러한 것을 재귀 패턴(Recursive pattern)이라 한다.

IEnumerable<string> GetStudents(List<Person> people)
{
    foreach (var p in people)
    {
        // Recursive pattern
        if (p is Student { Graduated: false, Name: string name })
        {
            yield return name;
        }
    }
}

3. Nullable Reference Type

이전 버전에서는 reference 타입에 null을 할당할 수 있어 Null Reference Exception이 자주 발생되곤 했다. 그래서 C# 8.0에서는 reference 타입에 null을 할당하면 컴파일러가 경고하는 기능을 추가되었다.

  • reference 타입은 기본적으로 null을 넣을 수 없는 Non-nullable Reference Type이 되고, NULL을 허용하기 위해서는 레퍼런스 타입 뒤에 물음표(?)를 붙여 Nullable Reference Type임을 표시해야 한다.
  • Nullable Reference Type 기능은 디폴트로 Disable 되어 있으며 사용하기 위해서는 프로젝트 레벨이나 파일 레벨, 혹은 소스코드 내의 임의의 위치에서 Enable 해야 한다.
static void Main(string[] args)
{
    // nullable enable
    string s1 = null; // Warning: Converting null literal or possible null value to non-nullable type
    if (s1 == null) return;

    string? s2 = null;
    if (s2 == null) return;

    // nullable disable
    string s3 = null; // No Warning
    if (s3 == null) return;
}

4. 인덱싱과 슬라이싱

1) 인덱싱

System.Index 구조체는 시퀀스의 시작 또는 끝으로부터 인덱싱을 표현하는 데 사용된다.

  • ^ prefix 연산자를 사용해 시퀀스를 뒤에서부터 인덱싱할 수 있다. 시퀀스의 끝 인덱스를 ^1, 끝에서 2번째는 ^2, 끝에서 3번째는 ^3과 같이 표시한다.
string s = "Hello World";

// System.Index
Index idx = ^2;
ch = s[idx]; // l

// 예
char ch1 = s[0];  // H
char ch1 = s[1];  // e
char ch2 = s[^1]; // d
char ch2 = s[^2]; // l

2) 슬라이싱

System.Range 구조체는 시작과 마지막 인덱스를 함께 가지며 범위를 표현할 때 사용된다.

  • .. 범위 연산자를 사용해 시퀀스의 부분 인덱스를 표시할 수 있다.
  • 마지막 인덱스는 실제 범위의 마지막 다음 요소이다. 즉, Range 가 1..4이면 1부터 3까지가 실제 범위가 된다.
string s = "Hello World";

// System.Range
Range r1 = 1..4;
string str1 = s[r1]; // ell

Index start = r1.Start;
bool b = start.IsFromEnd; // false
int v1 = start.Value;  // 1
int v2 = r1.End.Value; // 4

// 예
var s1 = s[1..4];   // ell
var s2 = s[^5..^2]; // Wor
var s3 = s[..];  // Hello World
var s4 = s[..3]; // Hel
var s5 = s[3..]; // lo World

Range rng = 1..^0;
var s6 = s[rng];  // ello World

5. using 선언

using 선언은 using 뒤에 있는 변수가 using을 둘러싼 범위를 벗어날 경우 Dispose 하도록 컴파일러에게 지시하게 된다.

  • Dispose는 메모리 관리를 위해 사용되며 더 이상 이 오브젝트를 쓰지 않고, 관련 리소스를 정리한다는 뜻이다.
  • 메서드가 끝날 때 Dispose를 자동 호출한다.
private void GetDataCS8()
{ 
    using var reader = new StreamReader("src.txt");
    string data = reader.ReadToEnd();
    Debug.WriteLine(data);
    
    // 여기서 Dispose() 호출됨
}

6. 널 병합 할당자(Null Coalescing Assignment)

흔히 NULL을 먼저 체크하고 NULL이면 어떤 값을 할당하는 코드를 많이 작성해 왔다.

if (list == null)
   list = new List<int>();

C# 8.0에서는 더욱 간결하게 널 병합 할당자(Null Coalescing Assignment)인 ??= 연산자를 써서 표현할 수 있게 되었다.

static List<int> AddData(List<int> list, int? a, int? b)
{
    list ??= new List<int>();
    list.Add(a ??= 1);
    list.Add(b ??= 2);

    return list;
}

7. 구조체 읽기 전용 멤버

이전 버전에서는 구조체(struct) 전체를 readonly로 만들 수 있었는데, C# 8.0부터는 구조체의 각 멤버에 대해 개별적으로 readonly로 정의할 수 있게 되었다. 만약 구조체의 메서드나 속성이 구조체의 상태를 변경하지 않는다면 readonly로 적용할 수 있고, readonly 멤버가 다른 non-readonly 멤버를 액세스 하면 컴파일러가 Warning을 표시한다.

 

[C# 8.0] 새로운 기능 (1) - 디폴트 인터페이스 멤버, 패턴 매칭 — 💡번뜩💡 (tistory.com)

[C# 8.0] 새로운 기능 (2) - Nullable Reference Type, 인덱싱과 슬라이싱 — 💡번뜩💡 (tistory.com)

[C# 8.0] 새로운 기능 (3) - using 선언, 널 병합 할당자, 구조체 읽기 전용 멤버 — 💡번뜩💡 (tistory.com)