글 작성자: Doublsb

최근 코드를 작성하던 중 C# 11에 다양한 기능이 추가되었다는 걸 알게 되어 이것저것 해 보았다.

 

  • 어트리뷰트를 상속받는 제네릭 클래스를 선언
  • 문자열 보간에서 줄 바꿈을 할 수 있다
  • 이스케이프 문자를 사용하지 않고 문자열을 선언할 수 있다
  • 구조체의 모든 필드가 기본값으로 초기화됨

 

다만 이 멋진 기능들을 당장 사용하는 건 불가능했는데, C# 11은 프리뷰 버전이며 Unity에서도 적용하고 있지 않기 때문이다.

그럼 Unity에서는 대체 C#의 어떤 버전을 사용하고 있단 말인가? 답은 공식 문서에서 찾을 수 있었다.
유니티 버전이 올라갈 때마다 C# 버전도 함께 상승하고 있으며, 현재 최신으로는 C# 9.0을 적용하고 있음을 알 수 있다.

 

유니티 버전(LTS 기준) 컴파일러 C# 언어 버전
2020.3 Roslyn 8.0
2021.3 Roslyn 9.0
2022.2 Roslyn 9.0

현재 실무 프로젝트는 2020 버전을 쓰고 있으므로, 업그레이드를 하지 않는 이상 8.0 이하의 기능만 사용할 수 있겠다.

그러면 8.0에는 무슨 기능이 추가되었던 것인지 정리해보는 글이 되겠다. 마이크로소프트 공식 문서를 참조했다.

 


 

읽기 전용 멤버

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public readonly double Distance => Math.Sqrt(X * X + Y * Y);

    public override readonly string ToString() =>
        $"({X}, {Y}) is {Distance} from the origin";
}

구조체 멤버에 readonly 한정자를 적용할 수 있다.
굳이 사용할 필요가 없어 보이지만, 컴파일러가 디자인 의도를 이해하고 최적화를 수행할 수 있다고 한다.

 

 

기본 인터페이스 메서드

이제 인터페이스에 멤버 선언 시 구현을 정의할 수 있다. 이름만 지정할 수 있었던 지난 날들 안녕.
이러면 추상 클래스랑 뭐가 달라? 실제로 스택오버플로우에서 싫어하는 사람들이 좀 있다.

 

 

패턴 일치 개선

 

스위치 표현식

public static RGBColor FromRainbow(Rainbow colorBand) =>
    colorBand switch
    {
        Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
        _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
    };
  • case, :=>로 대체되며 가독성 급상승
  • default_로 대체되며 가독성 급상승

 

속성 패턴

public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
    location switch
    {
        { State: "WA" } => salePrice * 0.06M,
        { State: "MN" } => salePrice * 0.075M,
        { State: "MI" } => salePrice * 0.05M,
        // other cases removed for brevity...
        _ => 0M
    };

객체의 특정 프로퍼티를 검사할 수 있다.
Address 객체의 State만 검사하도록 지정할 수 있으므로 명확하고 코드 짜기도 좋다.

 

튜플 패턴

public static string RockPaperScissors(string first, string second)
    => (first, second) switch
    {
        ("rock", "paper") => "rock is covered by paper. Paper wins.",
        ("rock", "scissors") => "rock breaks scissors. Rock wins.",
        ("paper", "rock") => "paper covers rock. Paper wins.",
        ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
        ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
        ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
        (_, _) => "tie"
    };

튜플 값에 따라 검사를 수행할 수 있다.
가독성이 좋아 단순한 구현 시 자주 사용하는 편인데, 스위치에서 튜플 패턴을 제공하니 감사할 따름.

 

위치 패턴

public 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 Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

Deconstruct 메서드를 사용하면 원하는 속성을 분해하여 전달할 수 있다.
그리고 분해 메서드를 포함한 클래스가 있다면, 해당 속성들로 스위치문을 만들 수 있다.

 

 

Using 선언

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        int skippedLines = 0;
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
        return skippedLines;
    } // file is disposed here
}

using문을 사용하면, 문의 끝줄에 도달할 시 using문에서 선언한 변수를 삭제하라고 알아서 명령을 내린다.
네트워크에서 이미 너무 많이 본 구현 방식이라 별 감흥은 없지만.

 

 

정적 로컬 함수

int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);

    static int Add(int left, int right) => left + right;
}

함수 안에 함수를 선언하는 로컬 함수를 사용할 때, static을 넣을 수 있다.

 

 

삭제 가능한 ref struct

ref로 선언한 struct는 인터페이스를 구현할 수 없으므로 조심해야 한다. IDisposable을 구현하지 못하니까.
그러므로 액세스 가능한 Dispose()를 항상 가지고 있어야 한다. 신…기능…이라고 할 수 있나?

 

 

nullable 참조 형식

int a = null; // compile error

int? b = null; // OK

string c = null; // OK

string? d = null; // compile error on C# 7.0

int, char와 같은 밸류 타입은 값이 null일 수 없다. 하지만 이걸 물음표가 가능하게 만들어준다.
아주 잘 쓰고 있음.

 

 

비동기 스트림

public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

스트림을 비동기식으로 만들고 사용할 수 있다. 오우… 스트리밍을 할 수 있었단 말인가.
async 한정자를 사용해야 하며, IAsyncEnumberable을 반환한다.
연속적 요소의 반환이 필요하므로 yield return을 포함해야 한다.

 

 

비동기 삭제 가능

IAsyncDisposable 인터페이스를 구현하는 객체는 이제 비동기 삭제 가능 형식을 지원한다.

 

 

인덱스 및 범위

var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0

Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"

var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"

시퀀스에서 범위를 지정하여 요소들을 간결한 표현으로 가져올 수 있다. 이럴수가… 몰랐음…
단 List<T>의 경우 특정 요소를 가져오는 색인은 가능하지만 여러 개를 가져오는 범위는 지원하지 않는다고 한다.

 

 

null 병합 할당

List<int> numbers = null;
int? i = null;

numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers));  // output: 17 17
Console.WriteLine(i);  // output: 17

병합 할당 연산자 ??=가 도입되었다. 왼쪽이 null인 경우에만 오른쪽 연산이 대입된다.
추가로 ??의 경우 null이 아닌 경우 왼쪽을 반환하고, 아니라면 오른쪽을 반환한다.
예전에는 당최 의미를 알 수 없는 문구라 지나쳤는데, 내용은 간단했다.

 

 

관리되지 않는 생성 형식

관리 자원은 가비지 콜렉터가 관리한다. 그러나 일부 형식은 가비지 콜렉터가 관리하지 않는다.
sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool 같은 형식은 비관리형에 포함된다.
이전에는 Coords<int>같은 생성 형식은 int가 비관리형이라 만들 수 없었는데 8.0부터는 된다. ^^;

 

 

중첩 식의 stackalloc / 보간된 약어 문자열의 향상된 기능

사용하지 않거나 크게 들여다 볼 필요 없으므로 스킵.

반응형