모던 C#
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#에 대해 궁금하신 독자분들은 다음 경로를 참고하면 됩니다.
- C#의 새로운 기능 정보 제공 사이트: https://learn.microsoft.com/ko-kr/dotnet/csharp/whats-new/index
간편 구문(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
C# 10.0
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
클래스는 Name
과 Age
속성을 required
로 설정하여, 개체 생성 시 반드시 값을 설정해야 합니다. 반면 Address
속성은 선택적 속성이므로 초기화 없이도 개체를 생성할 수 있습니다. RequiredDemo
클래스의 Main
메서드에서 PersonRequired
개체를 올바르게 초기화하면 정상적으로 실행됩니다. 하지만 required
키워드를 적용한 속성이 개체 초기화에서 빠질 경우 컴파일 오류가 발생하여, 필수 값이 설정되도록 강제됩니다.
C# 12.0
C# 13.0
새로운 이스케이프 시퀀스
\e
를 사용하여 새롭게 이스케이프 시퀀스를 구현할 수 있습니다.
params 컬렉션
ref struct interfaces
field 키워드
장 요약
C# 8.0에 처음 소개된 기능 및 앞으로 추가되는 최신 기술들은 6.0버전부터의 정책에 기초하여 개발자들을 도와주는 자그마한 기능들을 계속해서 추가하고 있습니다. 이 강의와 책이 출간된 이후로도 계속해서 새로운 기능들이 추가될 예정인데요. 이 강의와 책에서 다 담지못한 C#의 기능들은 Microsoft Learn 사이트의 C# 카테고리를 참고하기 바랍니다.