C# & .NET

[C# 10.0] C#의 편리한 구문들 모음 (Syntax Sugar)

카루-R 2022. 2. 23. 21:21
반응형

환영합니다, Rolling Ress의 카루입니다.

 

오늘은 C# 의 여러 편리한 문법들을 소개해드리려고 합니다. 처음에는 C# 코드들이 상당히 번잡하다는 느낌이 들었는데, 8.0부터 점점 간결해지더니 10.0 현재는 오히려 다른 언어들에 비해서도 상당히 깔끔해졌어요. 하나씩 살펴봅시다.

 

** C# 10.0에만 한정된 내용은 아닙니다. 7.x부터 9.0까지의 내용이 섞여있습니다.

 

1. 최상위문(Top-level statements) (C# 9.0)

사실 이건 쓸 일이 많지 않아보이는데, 그래도 간단한 테스트용으로는 충분히 요긴하게 쓸 수 있으니까 가져와봤습니다. C# 9.0 이상 환경에서 동작합니다. 기타 프로젝트에서는 <LangVersion>을 Preview로 설정해주세요.

using System;

namespace Application
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
 

기존에는 문장 하나 출력하는데도 저렇게 많은 코드를 사용해야 했죠. 이제는 그럴 필요가 없습니다. 마치 파이썬을 보는 듯한 간결함.

Console.WriteLine("Hello World!");
 

Console은 using System을 사용하지 않아도 알아서 불러옵니다. 참고로 최상위문과 전통적인 Main() 메서드를 동시에 사용할 경우, Main()은 무시되고 최상위문이 entry point로 사용됩니다. args는 따로 선언하지 않아도 지역변수처럼 작동합니다. 클래스를 정의할 수도 있는데, 반드시 코드의 최하위에서 사용해야 합니다. 그 위에서 하면 오류납니다.

 

2. 파일 범위 네임스페이스 선언 (C# 10.0)

File-Scoped Namespace Declaration입니다. 실질적으로는 최상위 문보다 이쪽을 더 자주 쓰지 않을까 싶어요. 기존 코드가 이렇게 생겼다고 가정합시다.

// Source1.cs
using System;

namespace Application
{
    class Class1
    {
        object? field = null;
        void Method1()
        {
        }

        void Method2()
        {
        }
    }

    record Record1
    {
        string property { get; init; } = string.Empty;
        void Method1()
        {
        }
    }
}
 

namespace Application에 속하는 걸 아는데, 굳이 이렇게 써야 하나 싶죠. 제가 C# 의 문법 중 가장 싫어했던 게 이거였습니다. 쓸데없이 길어지고, 무엇보다 들여쓰기가 강제된다는 점에서요. C# 10.0부터 이러한 문제점이 개선되었습니다.

namespace Application;
using System;

class Class1
{
    object? field = null;
    void Method1()
    {
    }

    void Method2()
    {
    }
}

record Record1
{
    string property { get; init; } = string.Empty;
    void Method1()
    {
    }
}
 

namespace 네임스페이스; 이렇게 세미콜론으로 끝내주면 소스코드 전체가 해당 네임스페이스에 귀속됩니다. 덕분에 상당히 간결해지겠죠. 근데 오히려 이게 뇌이징이...ㅋㅋㅋㅋㅋㅋ아, 익숙해지는데 시간 좀 걸릴 듯 합니다. 참고로 VS 2022, C# 10.0 이상 환경에선 네임스페이스 옆에 ';'을 붙이면 알아서 중괄호를 삭제해줍니다. 그러지 않는다면, 중첩 네임스페이스가 있는 경우. 그럴 때는 이 문법을 사용할 수 없습니다.

 

3. 전역 Using문 (Global Using statements, C# 10.0)

using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
 

사실 특정 앱을 개발하다보면 네임스페이스를 비슷비슷한 걸로 사용하기 마련이죠. 그때마다 모든 코드 파일에 저걸 추가하긴 너무 번거롭습니다. IDE 차원에서 지원해준다고 해도, 지저분하고 보기 싫은 건 어쩔 수 없어요. 그래서, 새로운 문법을 지원합니다. global using이라는 건데, 한 파일에서 global using을 선언하면 모든 파일에서 using이 적용됩니다.

// Global Using statements

// .NET default namespaces
global using System;
global using System.Collections.Generic;

// UWP namespaces
global using Windows.UI.Xaml;
global using Windows.UI.Xaml.Controls;

// My namespaces
global using RollingRess;

// static & alias
global using static RollingRess.StaticClass;
global using muxc = Microsoft.UI.Xaml.Controls;
 

저는 GlobalUsing.cs 라는 파일에 따로 담아서 사용중입니다. 이렇게 하면 using에 주석 달기도 깔끔해지고, 각각의 파일들도 군더더기가 줄어든다는 장점이 있어요. 물론, 꼭 필요한 것들만 담는 게 좋겠지요.

 

4. 사용자 정의 형식의 Tuple Deconstruct (C# 7.x)

C# 7.x에 들어서면서 ValueTuple을 이용한 다중 반환이 가능해졌습니다. 뿐만 아니라 클래스들도 Deconstruct를 지원하죠. 만약 사용자 정의 클래스나 구조체에서 이 기능을 이용하고 싶다면, Deconstruct 메서드를 정의해주면 됩니다. Tuple이 아니라 void를 반환함을 주의하세요.

public void Deconstruct(out *타입* *인스턴스*, out *타입* *인스턴스*, ...)
 

이렇게 해서 클래스 내부의 인스턴스를 out 매개변수에 대입해주기만 하면 됩니다. 그럼 클래스 외부에서는 클래스 인스턴스를 바로 분해할 수 있습니다.

 

5. 비파괴적 변경 (with 표현식)

record와 함께 with 구문이 도입되었는데, 꼭 record가 아니어도 사용할 수 있습니다. 값을 분해하거나 새로 정의할 필요 없이, 기존 값을 복사하여 특정 원소만 바꿀 때 사용합니다.

// Windows.UI.Color
public struct Color : IFormattable
{
    public byte A, R, G, B;
}

// 사용자 설정
Color color = Colors.Azure; // 해당 색상의 ARGB값 대입됨

// 투명도 조절
Color semiTransparent = color with { A = 120 };
 

이렇게 되면 semiTransparent는 color의 R, G, B값을 동일하게 가져가지만 A값은 120을 갖습니다.

 

6. in 매개변수와 ref readonly

C++에서는 이름 없는 임시 객체로 인한 성능 손실을 최소화하기 위해 이동 생성자 및 이동 대입 연산자를 정의해서 사용했죠. 특히 R-Value reference란 개념이 자주 쓰였습니다. 늦게나마 C언어에서도 Compound Literals가 나왔고요. C#은 로우레벨 언어가 아니라서 좀 늦어진 감이 있는데, 이제 복사대입으로 인한 성능 손실을 최소화할 수 있습니다. 기본적인 개념은 똑같아요. "복사가 아니라, 상수형 참조를 하자."

// in을 쓰나 쓰지 않으나 동일하게 작동. (생략 가능)
PrintDate(in DateTime.Now);

// 여기서의 in은 ref readonly처럼 작동함
void PrintDate(in DateTime date)
{
    Console.WriteLine(date);
}
 

함수의 매개변수로 사용할 때는 ref readonly 대신 in을 사용합니다. 그냥 ref readonly로 쓰면 컴파일 오류납니다.

Class cls = new();
ref readonly DateTime date = ref cls.GetDateTime();

class Class
{
    private DateTime dt = DateTime.Now;

    public ref readonly DateTime GetDateTime()
        => ref dt;
}
 

이 코드도 정말 별로죠. 여러분 상황에 맞게 쓰시길 바랍니다. 이게 꽤 중요한데, 클래스 멤버를 ref readonly로 반환할 때예요. 무턱대고 통으로 반환하면 (특히 값 형식) 대량의 복사가 일어나게 되죠. 그럴 때 ref readonly 반환을 사용하면 참조 형식으로, 수정은 못하게 받을 수 있습니다. 이때는 ref를 네 군대 사용해야 하니 위치를 확인해주세요. (함수 반환형, 함수 반환문, 반환받는 변수, 대입하는 값)

 

7. 간결한 Using문 (C# 8.0)

try
{
    Resource resource = new();
    resource.DoAction();
}
finally
{
    resource.Dispose();
}
 

자원 해제는 중요합니다. 자칫 락걸리면 다른 곳에서 사용을 못하게 되니까요. 예외가 발생해도 자원 해제를 안전하게 할 수 있도록 try~finally 조합을 사용하곤 합니다. IDisposable 인터페이스를 상속받으면 Dispose() 메서드가 있음을 보장해주죠. 그럼 끝에서 Dispose()를 호출하면 됩니다. 그런데, 너무 길어요.

using (Resource resource = new())
{
    resource.DoAction();
}
 

많이 줄어든 것 같아요. 그런데 여전히 using 블럭이 좀 눈에 걸립니다. 그래서, 아래와 같이 축약할 수 있습니다.

using Resource resource = new();
resource.DoAction();
 

C# 이 워낙 중괄호를 많이 사용하다보니, 슬슬 블록을 없애는 방향으로 발전하는(?) 것 같아요 이렇게 using을 타입 앞에 붙인 경우, Dispose가 호출되는 시점은 해당 블록이 끝나는 시점입니다. 만약 그 전에 Dispose를 호출해야 한다면, 기존처럼 using 블록으로 감싸주세요.

 

int[] array = new int[] { 1, 2, 3, 4, 5 };
const bool largest = true;
ref int refVar = ref ((largest) ? ref array[4] : ref array[0]);
 

8. Null 관련 연산자 추가 (C# 8.0)

C# 8.0에서 Nullable과 관련해 새로운 기능이 대폭 생겨났죠. 그 기능들의 일부입니다. Nullable을 활성화 시켰을 때 컴파일러가 '아, 이거 null인지 아닌지 모르겠네' 싶은 것들은 죄다 경고를 때립니다. Null이 아님이 확실할 때, 표기를 해주는 방법이 생겼어요.

string s = value.ToString()!;
 

인스턴스 뒤에 ! 을 붙이면 해당 인스턴스는 null이 아닌 것으로 간주합니다. 또한, 대입연산자도 추가되었어요.

// 오래전
instance = instance == null ? value : instance;

// 기존
instance = instance ?? value;

// 현재
instance ??= value;
 

??= 연산자인데, 작동 방식은 위와 같습니다. 사실 이것도 간편 표기법에 지나지 않는 터라, A ??= B라 하면 A가 null일 때만 B값을 대입하는 겁니다. 이런 거 보면 C# 이 null은 참 잘 챙겨요.

 

9. stackalloc으로 스택에 변수 할당 (C# 8.0)

// 기존
unsafe
{
    int* numbers = stackalloc int[5] { 1, 2, 3, 4, 5 };
    for (int i = 0; i < 5; i++)
        Console.WriteLine(numbers[i]);
}

// C# 8.0
Span<int> numbers = stackalloc int[5] { 1, 2, 3, 4, 5};
foreach (int number in numbers)
    Console.WriteLine(number);
 

스택에 변수를 할당할 수 있게 해주는 stackalloc 키워드가 있죠. 정확히 말하면, 값 형식의 배열을 스택에 만들어주는 겁니다. 그런데 기존에는 포인터 연산만을 사용했기에 활용성이 떨어졌어요. unsafe 블록 사용하는 걸 그닥 달가워하지 않으실 겁니다. 그런데 C# 8.0부터 Span이 도입되어 활용성이 올라갔어요. 이제 더 이상 unsafe를 사용하지 않아도 됩니다.

 

10. 기타 편리한 구문 모음

// C# 9.0 인스턴스 선언시 타입 생략
MyClass instance = new(); // MyClass instance = new MyClass();

// out과 동시에 변수 선언
GetVariables(out var result);

// C# 6.0 Auto property
public string Name { get; set; } = "Karu";

// C# 10.0 자유로워진 Tuple unpack
int a;
(a, int b) = (3, 5);

// C# 9.0 Record
public record Pos(double Y, double Y);
Pos pos = new(30, 20);

// C# 8.0 인덱스 연산
string str = "Rolling Ress";
var last = str[^1]; // 's'
var sub = str[0..4]; // "Roll", 구간은 [0, 4)
 

 

 

반응형