모던 C#

  • 14 minutes to read

C#은 2002년 처음 1.0 버전이 나온 후 계속해서 발전하여 현재 13.0 버전까지 이르렀습니다. 이 강의를 보는 시점에서는 그 이후의 버전이 나올 수도 있겠네요. 이번 강의에서는 비교적 가장 최신의 C# 문법에 대해 정리해보겠습니다.

> // 모던 C#: 검색 엔진에서 "C# New Features" 등으로 검색하여 Microsoft Learn의 자료 참조

C#의 새로운 기능

C#은 20년 넘게 계속 업데이트되었고 앞으로도 계속 업데이트될 예정입니다. 박용준 강사는 C# 베타 버전 때부터 C#을 사용해오고 있습니다. 그래서 운이 좋아 이렇게 독자분들과도 만날 수 있는 것 같습니다.

1.0 버전부터 5.0 버전까지는 큼지막한 기능들이 추가되는 형태로 발전해왔습니다. 그렇지만 그 이후로는 큰 기능보다는 작지만 프로그래밍에 도움을 주는 소소한 기능들이 추가되는 형태로 발전해왔습니다. 이러한 내용을 편의 구문(Syntax sugar)라고 합니다.

이 강의의 진행 시점인 현재는 최신 버전이 13.0 버전입니다. 그래서 여기서는 비교적 최신 버전인 8.0 버전부터 13.0 버전에 대한 몇 가지 정보를 추가로 정리할 것입니다.

노트: C# 13.0 이후 버전

앞으로 어떤 버전이 나올지는 아무도 모릅니다. 다행인 것은 마이크로소프트는 C#의 새로운 기능이 출시되면 Microsoft Learn의 다음 경로를 통해서 가이드를 제공해주고 있으니, 최신 버전의 C#에 대해 궁금하신 독자분들은 다음 경로를 참고하면 됩니다.

간편 구문(Syntactic Sugar)

프로그래밍 관련 해외 자료를 찾다보면 Syntactic sugar 또는 Syntax sugar 표현이 나옵니다.

이를 어떻게 번역할까 고민하다가, 마이크로소프트 언어 포털에도 나와 있지 않아서 검색하다가, 그나마 차선책으로 얻은 정보는 간편 구문(Syntactic Sugar)과 편의 문법으로 결정하였습니다.

간편 구문은 여러 줄 또는 복잡하게 처리해야하는 어떤 코드를 간편하게 처리할 수 있도록 도와주는 구문을 의미합니다.

모던 C#은 굉장히 많은 간편 구문을 추가해 나가고 있습니다.

급작스레 모든 내용이 변경되는 것이 아니기에 C# 7.0 이상의 문법은 조급하게 학습할 필요없이 필요할 때마다 하나씩 적용해나가는 점진적인 학습 방법을 적용해 보면 좋습니다.

[실습] C# 8.0의 기능을 테스트 프로젝트에서 실행하기

소개

C# 8.0의 최신 기능을 .NET Core 테스트 프로젝트에서 테스트해 보겠습니다.

따라하기 1: .NET Core 테스트 프로젝트 생성

(1) DotNetCore.Tests 이름으로 [C# + MSTest + .NET 7.0] 기반의 테스트 프로젝트를 새롭게 생성하거나 DotNet 솔루션에 추가합니다.

(2) 기본으로 생성된 cs 파일은 삭제합니다.

따라하기 2: 널가능 참조 형식(Nullable Reference Types) 테스트

(1) C# 8.0으로 들어서는 널에 대한 좀 더 엄격한 제약을 제공합니다. Visual Studio 또는 Visual Studio Code를 사용하여 null 값을 다룰 때 경고 수준이 이전 버전보다 더 강력합니다.

(2) DotNetCore.Tests 프로젝트에 마우스 오른쪽 버튼을 클릭 후 [Edit Project File] 메뉴를 선택하여 csproj 파일의 편집 화면으로 들어갑니다.

(3) DotNetCore.Tests.csproj 파일의 여러 항목 중에서 <PropertyGroup> 섹션에 <Nullable> 항목이 enable로 되어 있는지 확인 또는 없으면 다음 샘플 코드와 같이 추가합니다.

코드: DotNetCore.Tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

(4) DotNetCore.Tests 프로젝트에 NullableReferenceTypeTest.cs란 이름으로 클래스 파일을 만들고 다음과 같이 테스트 클래스와 테스트 메서드를 작성합니다. 비주얼 스튜디오의 C# 컴파일러는 Name 속성에 밑줄을 표시하고 null 값이 들어올 가능성이 있다고 작은 경고를 띄울 것입니다. 무시합니다.

코드: NullableReferenceTypeTest.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace DotNetCore.Tests
{
    class Person
    {
        public string Name { get; set; } 
    }

    [TestClass]
    public class NullableReferenceTypeTest
    {
        [TestMethod]
        public void NullableEnableTest()
        {
            Console.WriteLine((new Person()).Name);
        }
    }
}

이 경고를 없애려면 Name 속성을 다음 코드와 같이 널 가능 참조 형식으로 변경하면 됩니다.

public string? Name { get; set; }

따라하기 3: 튜플 리터럴에 default 키워드 사용하기

(1) DotNetCore.Tests 프로젝트에 DefaultDeconstructionTest.cs 이름으로 클래스 파일을 작성하고 다음과 같이 테스트 코드를 작성합니다.

코드: DefaultDeconstructionTest.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DotNetCore.Tests
{
    [TestClass]
    public class DefaultDeconstructionTest
    {
        [TestMethod]
        public void DefaultTest()
        {
            (int number, string? name) = (default, default);

            Assert.AreEqual(0, number);
            Assert.IsNull(name);
        }
    }
}

튜플 리터럴에 default 키워드를 사용하면 해당 변수는 해당 데이터 형식의 기본값으로 초기화됩니다.

마무리

우선 짧은 실습 예제로 C# 8.0의 새로운 기능을 테스트해 보았습니다.

C# 8.0의 새로운 기능 10가지 소개

소개

이번 실습 예제에서는 하나의 .NET Core 기반의 C# 프로젝트를 만들고 C# 8.0의 주요 특징 10가지를 모두 적용하는 예제를 만들어보겠습니다.

따라하기 0: .NET Core 7.0 기반의 C# 콘솔 프로젝트 생성하기

(1) SeeSharp.Eight 이름으로 .NET Core 7.0 이상으로 C# 콘솔 앱 프로젝트를 생성합니다.

(2) SeeSharp.Eight 프로젝트에 마우스 오른쪽 버튼을 클릭 후 [Edit Project File] 메뉴를 선택하여 csproj 파일의 편집 화면으로 들어갑니다.

(3) SeeSharp.Eight 파일의 여러 항목 중에서 <PropertyGroup> 섹션에 <Nullable> 항목이 enable로 되어 있는지 확인 또는 없으면 다음 샘플 코드와 같이 추가합니다.

코드: SeeSharp.Eight.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

따라하기 1: C# 8.0의 새로운 기능 10가지가 적용된 코드 작성

200 줄 가까이되는 다음 코드는 작성 후 실행해보세요

좀 더 빠른 이해를 위해서 다음 경로에 미리보기 동영상을 올려두었습니다.

코드: DotNet\SeeSharp.Eight\Program.cs

//[?] C# 8.0의 새로운 기능 10가지
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SeeSharp.Eight
{
    // Interface
    public interface IEmployee
    {
        public string Name { get; }
        public decimal Salary { get; }
        //[!] C# 8.0: Default interface members
        public string Id { get => $"{Name}[{this.GetHashCode()}]"; }
    }

    // Class
    public class Person
    {
#nullable disable
        public string Name { get; }
        public Person(string name) => Name = name;

        //[!]
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        public string LastName { get; set; }
#nullable enable

        public Person(string first, string last)
        {
            FirstName = first;
            MiddleName = null;
            LastName = last;
        }

        public Person(string first, string middle, string last)
        {
            FirstName = first;
            MiddleName = middle;
            LastName = last;
        }
    }

    // Abstract Class
    public abstract class Employee : Person, IEmployee
    {
        public Employee(string name, decimal salary)
            : base(name) => Salary = salary;
        public decimal Salary { get; protected set; }
    }

    public class Professor : Employee, IEmployee
    {
        public string Topic { get; }
        public Professor(string name, decimal salary, string topic)
            : base(name, salary) => Topic = topic;

        // TODO: Deconstruct() Method => C# 7.0 User-Defined Types Deconstructing
        public void Deconstruct(out string name, out string topic)
            => (name, topic) = (Name, Topic);

        //[?] Indices and ranges
        //public string Id => $"{Name}[{Topic[0..3]}]";
        public string Id => $"{Name}[{Topic[..3]}~{Topic[^3..^0]}]";
    }

    public class Administrator : Employee
    {
        public string Department { get; }
        public Administrator(string name, decimal salary, string department)
            : base(name, salary) => Department = department;
    }

    public static class Service
    {
#nullable disable
        static Person[] people = null;
#nullable enable

        static Service()
        {
            //[?] Null Coalescing Assignment Operator: ??= 
            people ??= new Person[]
            {
                new Professor("RedPlus", 1______000, "Computer Science"),
                new Administrator("Taeyo", 2_000, "ABC"),
                new Professor("Itist", 3_000, "Computer Science")
            };
        }

        public static IEnumerable<IEmployee> GetEmployees()
        {
            foreach (var person in people)
            {
                if (person is IEmployee employee)
                {
                    yield return employee;
                }
            }
        }

        //[?] C# 8.0 Asynchronous streams
        public static async IAsyncEnumerable<IEmployee> GetEmployeesAsync()
        {
            foreach (var person in people)
            {
                await Task.Delay(500);
                if (person is IEmployee employee) yield return employee;
            }
        }
    }

    class Program
    {
        static async Task Main(string[] args)
        {
            //[?] C# 8.0 - Static Local Function
            static void Print(string message) => Console.WriteLine(message);

            //[A] Synchronous 
            foreach (var employee in Service.GetEmployees())
            {
                Print($"Name: {employee.Name}");
            }
            Print("========================================");
            foreach (var employee in Service.GetEmployees())
            {
                //[?] Pattern Matching: C# 7.0 Type Pattern
                if (employee is Administrator administrator
                    && administrator.Department is "ABC")
                {
                    Print($"Administrator: {administrator.Name}");
                }
            }
            Print("========================================");

            //[B] Asynchronous
            await foreach (var employee in Service.GetEmployeesAsync())
            {
                //[?] Pattern Matching: C# 8.0 Property Pattern, Var Pattern 
                if (employee is Professor
                {
                    Topic: "Computer Science", Name: var name
                } professor)
                {
                    Print($"Professor: {name} ({professor.Id})");
                }
            }
            await foreach (var employee in Service.GetEmployeesAsync())
            {
                //[?] Pattern Matching: C# 8.0 Location Pattern
                if (employee is Professor(var name, "Computer Science") professor)
                {
                    Print($"Professor: {name} ({professor.Id})");
                }
            }

            //[?] C# 8.0 - Nullable Reference Type
            var red = new Person("YJ", "Park");
            var length = GetMiddleNameLength(red);
            Console.WriteLine(length); // 0 

            //[?] Switch Expression
            Print("========================================");
            await foreach (var employee in Service.GetEmployeesAsync())
            {
                Console.WriteLine(GetDetails(employee));
            }

            static string GetDetails(IEmployee person)
            {
                return person switch
                {
                    Professor p when p.Salary > 1_000 => $"{p.Name}-{p.Topic}-Big",
                    Professor p => @$"{p.Name} - {p.Topic}",
                    Administrator a => $"{a.Name} - {a.Department}",
                    _ => $@"Who are you?"
                };
            }
        }

        static int GetMiddleNameLength(Person? person)
        {
            //[?] is somthing
            if (person?.MiddleName is { } middle) return middle.Length;
            return 0; // is null 
        }
    }
}
Name: RedPlus
Name: Taeyo
Name: Itist
========================================
Administrator: Taeyo
========================================
Professor: RedPlus (RedPlus[Com~nce])
Professor: Itist (Itist[Com~nce])
Professor: RedPlus (RedPlus[Com~nce])
Professor: Itist (Itist[Com~nce])
0
========================================
RedPlus - Computer Science
Taeyo - ABC
Itist-Computer Science-Big

C:\DotNet\DotNet\DotNet\SeeSharp.Eight\bin\Debug\netcoreapp3.0\SeeSharp.Eight.exe (process 30252) exited with code 0.
Press any key to close this window . . .

C# 8.0

널 허용 연산자

C# 9.0

최상위 문

init 키워드

레코드 형식

C# 10.0

Global Usings

파일 범위 네임스페이스

Inferred Delegate Type

C# 11.0

required 키워드

C# 11에서 도입된 required 키워드는 개체 초기화 시 특정 속성이 반드시 설정되도록 강제하는 기능을 제공합니다. 이를 사용하면 생성자를 정의하지 않아도 필수 속성을 강제할 수 있습니다.

코드: RequiredDemo.cs

using System;

public class PersonRequired
{
    public required string Name { get; set; } // 필수 속성
    public required int Age { get; set; } // 필수 속성
    public string? Address { get; set; } // 선택적 속성
}

class RequiredDemo
{
    static void Main()
    {
        // 필수 속성을 포함한 올바른 개체 초기화
        var person = new PersonRequired { Name = "홍길동", Age = 30 };
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

        // 필수 속성이 누락되면 컴파일 오류가 발생함
        // var invalidPerson = new PersonRequired { Name = "백두산" }; // Age 속성 없음 → 오류 발생
    }
}
Name: 홍길동, Age: 30

이 예제에서 PersonRequired 클래스는 NameAge 속성을 required로 설정하여, 개체 생성 시 반드시 값을 설정해야 합니다. 반면 Address 속성은 선택적 속성이므로 초기화 없이도 개체를 생성할 수 있습니다. RequiredDemo 클래스의 Main 메서드에서 PersonRequired 개체를 올바르게 초기화하면 정상적으로 실행됩니다. 하지만 required 키워드를 적용한 속성이 개체 초기화에서 빠질 경우 컴파일 오류가 발생하여, 필수 값이 설정되도록 강제됩니다.

C# 12.0

기본 생성자

컬렉션 표현식

스프레드 연산자

Experimental 특성 사용하기

C# 13.0

부분 속성

새로운 이스케이프 시퀀스

\e를 사용하여 새롭게 이스케이프 시퀀스를 구현할 수 있습니다.

params 컬렉션

Params 컬렉션

ref struct interfaces

field 키워드

장 요약

C# 8.0에 처음 소개된 기능 및 앞으로 추가되는 최신 기술들은 6.0버전부터의 정책에 기초하여 개발자들을 도와주는 자그마한 기능들을 계속해서 추가하고 있습니다. 이 강의와 책이 출간된 이후로도 계속해서 새로운 기능들이 추가될 예정인데요. 이 강의와 책에서 다 담지못한 C#의 기능들은 Microsoft Learn 사이트의 C# 카테고리를 참고하기 바랍니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com