Skip to content

C# 치트시트

.NET, 웹, 게임을 위한 현대 객체지향 언어.

01

시작하기

Hello World

C# 프로그램은 Main()에서 시작합니다. using System;은 System 네임스페이스(Console, Math 등)를 가져옵니다. C# 9+는 최상위 문을 지원합니다: Console.WriteLine("Hello");만 있는 파일도 유효한 프로그램입니다. 컴파일러가 클래스와 Main을 자동으로 생성합니다.

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

변수와 타입

C#은 정적 타입 언어입니다. var는 컴파일러가 타입을 추론하게 합니다(컴파일 타임에 여전히 타입 안전). decimal(m 접미사)은 정확한 정밀도의 금융 계산용입니다. const는 컴파일 타임 상수이고; readonly는 생성자에서 설정하는 런타임 상수입니다.

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

문자열 보간

문자열 보간($"...")은 중괄호 안에 표현식을 삽입합니다. : 뒤의 형식 지정자가 출력을 제어합니다(F2는 소수점 2자리, yyyy-MM-dd는 날짜). 축어적 문자열(@"")은 백슬래시를 문자 그대로 처리합니다—파일 경로와 정규식에 유용합니다. 둘을 결합: $@"...".

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

입출력

Console.ReadLine()은 한 줄 전체를 문자열로 읽습니다. int.Parse는 변환하지만 잘못된 입력 시 예외를 던집니다; int.TryParse가 더 안전합니다—bool을 반환하고 out 매개변수를 사용합니다. 잘못된 데이터로 인한 예외를 피하기 위해 사용자 입력에는 항상 TryParse를 사용하세요.

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Nullable 타입

int?는 nullable 값 타입(Nullable<int>)입니다. ?. 연산자(null 조건부)는 멤버에 안전하게 접근합니다—예외를 던지는 대신 null을 반환합니다. ?? 연산자(null 병합)는 null에 대한 기본값을 제공합니다. C# 8+ nullable 참조 타입은 컴파일 타임에 잠재적 null에 대해 경고합니다.

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

문자열

문자열 메서드

C# 문자열은 불변입니다—메서드는 원본을 수정하는 대신 새 문자열을 반환합니다. 일반적인 메서드: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. 무거운 문자열 조작에는 많은 중간 문자열 생성을 피하기 위해 StringBuilder를 사용하세요.

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder는 가변적이며 루프에서 문자열을 만드는 데 효율적입니다. 문자열 연결(+)은 매번 새 문자열을 생성하므로 반복 연결은 O(n^2)입니다. StringBuilder는 O(n)으로 분할 상환됩니다. 몇 번 이상의 연결이 있을 때 StringBuilder를 사용하세요.

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

형식화와 파싱

string.Format은 {0}, {1} 자리 표시자를 사용합니다. 형식 지정자: F(고정), N(구분 기호가 있는 숫자), P(퍼센트), X(16진수). 파싱은 문화권에 민감합니다—소수점 구분 기호가 로케일마다 다릅니다. 명시적 제어를 위해 CultureInfo를 사용하거나, 안전을 위해 TryParse를 사용하세요.

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

문자열 비교

대소문자 구분 없는 비교에는 StringComparison.OrdinalIgnoreCase를 사용하세요—ToLower() 후 == 보다 명확하고 빠릅니다. 문화권에 민감한 비교에는 == 를 피하세요; CultureInfo와 함께 string.Compare를 사용하세요. 사전 키에는 StringComparer.OrdinalIgnoreCase를 사용하세요.

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

정규 표현식

Regex(System.Text.RegularExpressions에서)는 패턴 매칭을 제공합니다. 백슬래시를 이중 이스케이프할 필요가 없도록 @"" 축어적 문자열을 사용하세요. 반복 사용을 위해 Regex 인스턴스를 한 번 컴파일하고 재사용하세요(RegexOptions.Compiled) 더 나은 성능을 위해.

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

숫자와 수학

숫자 타입

C#은 고정 크기 타입을 가집니다: int(32비트), long(64비트), double(64비트 float), decimal(128비트, 금융용). 밑줄(9_000_000)은 가독성을 개선합니다. 돈에는 decimal을 사용하세요—부동소수점 반올림 오류를 피합니다. 접미사: L(long), f(float), m(decimal).

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Math 클래스

Math는 일반적인 연산을 위한 정적 메서드를 제공합니다. Math.Round는 기본적으로 은행가 반올림(짝수로 반올림)을 사용합니다—학교식 반올림에는 MidpointRounding.AwayFromZero를 사용하세요. Math.BigMul은 오버플로우를 피하기 위해 long 곱셈을 처리합니다.

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

난수

Random은 의사 난수를 생성합니다. 한 인스턴스를 만들어 재사용하세요(루프에서 많이 만들면 시간 기반 시드로 인해 중복이 발생할 수 있습니다). 암호학적 난수에는 System.Security.Cryptography.RandomNumberGenerator를 사용하세요. C# 8+는 스레드 안전을 위해 Random.Shared를 제공합니다.

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

타입 변환

데이터 손실이 없을 때 암시적 변환이 자동으로 일어납니다(int에서 double). 정밀도가 손실될 수 있을 때 명시적 캐스트((type))가 필요합니다. Convert.ToInt32는 많은 타입을 처리하고 반올림합니다(잘라내는 캐스트와 달리). 문자열-숫자 변환에는 예외를 피하기 위해 항상 TryParse를 선호하세요.

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

정수 오버플로우와 checked

기본적으로 정수 오버플로우는 조용히 감싸집니다(unchecked). checked 블록은 오버플로우 시 OverflowException을 던집니다—안전이 중요한 코드에 사용하세요. 진정으로 큰 숫자에는 BigInteger(System.Numerics)가 오버플로우 없이 임의 정밀도 정수를 처리합니다.

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

제어 흐름

If / Else

if/else if/else가 표준입니다. 삼항 연산자(condition ? a : b)는 간결한 if/else 표현식입니다. C#은 boolean 조건을 요구합니다—C와 달리 정수는 bool로 암시적 변환되지 않습니다. 유지 보수 버그를 방지하기 위해 단일 문장에도 중괄호를 사용하세요.

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch와 패턴 매칭

C# 8+ switch 표현식(=>)은 간결하고 값을 반환합니다. _ 패턴이 기본 케이스입니다. 패턴 매칭(is, switch)은 타입 패턴, 속성 패턴, 관계형 패턴(> 18)을 지원합니다. 이는 전통적인 switch보다 강력하고 보일러플레이트를 줄입니다.

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

루프

for는 카운트 반복용; foreach는 컬렉션(배열, 리스트, IEnumerable) 순회; while은 조건이 거짓일 때까지 반복합니다. foreach는 읽기 전용입니다—반복 중 컬렉션을 수정할 수 없습니다. 인덱스가 필요하거나 요소를 수정해야 할 때 for를 사용하세요.

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break, Continue, goto

break는 가장 가까운 루프나 switch를 종료합니다; continue는 다음 반복으로 건너뜁니다. C#에는 중첩 루프를 위한 레이블 break가 없습니다—플래그를 사용하거나, return이 있는 메서드로 추출하거나, LINQ를 사용하세요. goto는 switch의 폴스루에 유효하지만 그 외에는 권장되지 않습니다.

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

반복자와 yield

yield return은 메서드를 반복자로 변환하여 값을 지연 생성합니다—요청 시 한 번에 하나씩. 이는 크거나 무한 시퀀스에 메모리 효율적입니다. 컴파일러가 상태 기계를 생성합니다. yield break는 반복을 조기 종료합니다. LINQ는 반복자를 광범위하게 사용합니다.

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

메서드와 대리자

메서드 정의

메서드는 한 줄짜리에 대해 식 본문(=>)을 가질 수 있습니다. 기본 매개변수는 인수를 선택적으로 만듭니다. 명명된 인수(name:)는 많은 매개변수가 있는 호출의 가독성을 개선하고 선택적 인수를 건너뛸 수 있게 합니다. C#은 반환 타입만 다른 메서드 오버로딩을 지원하지 않습니다.

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out, ref, params

out 매개변수는 메서드에서 할당해야 합니다(호출자가 초기화할 필요 없음). ref 매개변수는 호출자가 초기화해야 하며 수정될 수 있습니다. params는 가변 개수의 인수를 허용합니다. C# 7+는 인라인 out var 선언을 허용합니다: TryParse(s, out var result).

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

대리자와 이벤트

대리자는 타입 안전 함수 포인터입니다. Func<T,TResult>는 입력을 받아 값을 반환; Action<T>는 void 반환; Predicate<T>는 bool 반환. 이벤트는 +=/-= 구독과 안전한 발생(?.Invoke, null 검사)이 있는 대리자입니다. 옵저버 패턴(pub/sub)에 이벤트를 사용하세요.

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

람다 표현식

람다는 =>를 사용하는 익명 메서드입니다. 둘러싼 스코프에서 변수를 캡처합니다(클로저). 캡처된 변수는 람다가 생성될 때가 아니라 실행될 때 평가됩니다—루프에서 이를 주의하세요. LINQ, 이벤트, 콜백과 함께 람다를 광범위하게 사용하세요.

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

확장 메서드

확장 메서드는 기존 타입을 수정하지 않고 메서드를 추가합니다. 첫 번째 매개변수 앞의 this 키워드가 확장 메서드로 표시합니다. 정적 클래스에 있어야 합니다. LINQ는 전적으로 IEnumerable의 확장 메서드로 구현됩니다. 이는 유틸리티 메서드를 추가하는 강력한 방법입니다.

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

컬렉션

List<T>

List<T>는 동적 배열입니다(C++의 vector나 Java의 ArrayList처럼). Add/Remove는 분할 상환 O(1) / O(n)입니다. Count는 요소 수를 줍니다(용량이 아님). Find/FindAll은 술어를 사용합니다. 중간에 잦은 삽입/삭제에는 LinkedList<T>가 나을 수 있습니다.

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary는 O(1) 평균 조회의 해시 테이블입니다. TryGetValue가 인덱서(KeyNotFoundException 던짐)보다 안전합니다. 대소문자 구분 없는 문자열 키에는 생성자에 StringComparer.OrdinalIgnoreCase를 전달하세요. Dictionary는 삽입 순서를 보존하지 않습니다(필요하면 OrderedDictionary 사용).

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T>와 SortedSet<T>

HashSet<T>은 O(1) 조회로 고유 요소를 저장합니다—중복 제거와 멤버십 테스트에 사용. 집합 연산(Union, Intersect, Except)을 지원합니다. SortedSet<T>(레드-블랙 트리)은 O(log n) 연산으로 요소를 정렬된 상태로 유지합니다. 순서가 중요하지 않을 때 HashSet, 중요할 때 SortedSet을 사용하세요.

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T>와 Stack<T>

Queue<T>는 FIFO(Enqueue/Dequeue)입니다—작업 스케줄링, BFS에 사용. Stack<T>는 LIFO(Push/Pop)입니다—실행 취소/다시 실행, DFS, 표현식 평가에 사용. 둘 다 핵심 연산이 O(1)입니다. 동시성 시나리오에는 System.Collections.Concurrent의 ConcurrentQueue와 ConcurrentStack을 사용하세요.

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

배열

배열은 고정 크기입니다. Array.Sort는 제자리에서 정렬합니다. 다차원 배열([,])은 직사각형(균일한 열)입니다. 가변 배열([][])은 배열의 배열입니다(행마다 길이가 다를 수 있음). 동적 크기에는 List<T>를; 고정 크기, 성능이 중요한 데이터에는 배열을 사용하세요.

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

클래스와 OOP

클래스와 속성

자동 속성({ get; set; })은 지원 필드를 자동 생성합니다. 식 본문 속성(=>)은 접근 시 계산합니다. C# 9+ init 전용 속성({ get; init; })은 생성 중에만 설정 가능하여 불변성을 가능하게 합니다. 객체 이니셜라이저: new Person { Name = "X" }.

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

상속과 virtual

virtual은 재정의를 위한 메서드를 표시; 하위 클래스의 override가 새 구현을 제공합니다. C#은 명시적 override를 요구합니다(Java와 달리). sealed는 클래스가 상속되거나 메서드가 재정의되는 것을 방지합니다. 모든 클래스는 암묵적으로 object(System.Object)에서 상속됩니다.

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

인터페이스

인터페이스는 구현 없이 계약을 정의합니다. 클래스는 여러 인터페이스를 구현할 수 있습니다(단일 클래스 상속과 달리). C# 8+는 기본 인터페이스 메서드를 허용합니다. 다형성과 의존성 주입에 인터페이스를 사용하세요. 명명 규칙: I 접두사(IShape, ILogger).

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

추상 클래스

추상 클래스는 인스턴스화할 수 없으며 추상(재정의 필요)과 구체적(virtual) 멤버를 모두 가질 수 있습니다. 공유 구현이 있을 때 추상 클래스를; 순수 계약에는 인터페이스를 사용하세요. 클래스는 하나의 추상 클래스만 상속할 수 있지만 많은 인터페이스를 구현할 수 있습니다.

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

레코드와 구조체

레코드(C# 9+)는 값 기반 동등성, 불변성, 간결한 구문을 제공합니다—DTO와 데이터 모델에 이상적. with 표현식은 수정된 속성으로 복사본을 만듭니다. 구조체는 값 타입입니다(대입 시 복사, 스택 할당)—작고 가벼운 데이터에 사용. 클래스는 참조 타입입니다(힙 할당).

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where와 Select

LINQ(Language Integrated Query)는 컬렉션에 SQL 유사 쿼리를 제공합니다. Where는 필터링, Select는 변환(map). LINQ는 지연됩니다—쿼리는 열거될 때(예: ToList() via)만 실행. 이는 중간 컬렉션 없이 효율적인 체이닝을 가능하게 합니다. 메서드 구문(위)이 가장 일반적; 쿼리 구문도 사용 가능.

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

정렬과 그룹화

OrderBy/OrderByDescending 정렬; ThenBy는 보조 정렬 기준 추가. GroupBy는 키로 요소를 클러스터링하여 IGrouping<key, element> 그룹을 반환. 각 그룹은 Key를 가지며 멤버의 IEnumerable 자체입니다. 이는 그룹화를 위한 수동 루프와 사전을 대체합니다.

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

집계

LINQ 집계 메서드(Sum, Average, Min, Max, Count)는 컬렉션에서 단일 값을 계산. Aggregate는 가장 일반적입니다—누적적으로 함수를 적용하는 fold/reduce. 빈 시퀀스에서 예외를 던집니다; *OrDefault 변형을 사용하거나 먼저 비어 있는지 확인하세요.

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First, Single, ElementAt

First는 첫 번째 요소 반환(빈 경우 예외); FirstOrDefault는 찾지 못하면 기본값(int는 0, 참조 타입은 null) 반환. Single은 정확히 하나의 요소 필요(그렇지 않으면 예외)—검증에 사용. Any/All은 전체 컬렉션을 열거하지 않고 bool 반환(단락).

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join과 Zip

Join은 내부 조인(SQL처럼)을 수행하여 키로 요소 매칭. Zip은 위치별로 두 시퀀스의 요소를 짝지음. 둘 다 새 시퀀스를 생성. LINQ는 GroupJoin(그룹화가 있는 왼쪽 조인)도 지원. 여러 소스에서 관련 데이터를 결합하는 데 강력합니다.

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

오류 처리

Try / Catch / Finally

먼저 특정 예외를 잡고, 마지막에 일반 Exception(가장 특정에서 가장 덜 특정으로). finally는 항상 실행—정리(파일 닫기, 자원 해제)에 사용. 제어 흐름으로 catch(Exception)를 피하세요; 처리할 수 있는 것만 잡으세요. 스택 추적을 보존하기 위해 throw e;가 아닌 throw;를 사용하세요.

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

사용자 정의 예외

Exception(또는 더 특정한 기반)에서 사용자 정의 예외를 파생. 디버깅에 도움이 되는 컨텍스트 필드를 추가. 항상 메시지와 함께 기반 생성자 호출. 관례적으로 예외 클래스 이름은 Exception으로 끝남. 호출자가 처리할 수 있는 도메인별 오류 조건에 사용자 정의 예외를 사용하세요.

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using과 IDisposable

using은 예외가 발생해도 Dispose()가 호출되도록 보장합니다—이것이 자원 관리를 위한 C#의 RAII 동등물. using 선언(C# 8+)이 더 깔끔함—Dispose가 스코프 끝에 호출. 비관리 자원(파일 핸들, 데이터베이스 연결)을 보유한 클래스에 IDisposable을 구현하세요.

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Null 처리

C# 8+ nullable 참조 타입(string?)은 컴파일 타임 null 안전을 가능하게. ?.는 NullReferenceException 대신 null 반환. ??는 폴백 제공. throw 표현식(?? throw)은 검증을 위한 간결한 형태. 프로젝트 전체 null 검사를 위해 .csproj에 <Nullable>enable</Nullable>을 활성화하세요.

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

예외 필터

예외 필터(when)는 catch 블록에 조건을 추가합니다—필터가 참인 경우에만 예외가 잡힘. 이는 스택 추적을 보존하기 때문에 잡고 다시 던지는 것보다 강력. 로깅 패턴(when이 false 반환)으로 예외를 처리하지 않고 관찰할 수 있습니다.

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

파일 I/O와 비동기

File 클래스(빠른 I/O)

File은 빠른 일회성 파일 작업을 위한 정적 메서드를 제공합니다—단순하지만 전체 파일을 메모리에 로드. 대용량 파일의 경우 한 줄씩 처리하기 위해 StreamReader/StreamWriter를 사용. File.Exists는 존재를 확인하지만 TOCTOU 경쟁이 있습니다—파일 작업의 예외를 항상 처리하세요.

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

스트림(StreamReader/Writer)

StreamReader/StreamWriter는 모든 것을 메모리에 로드하지 않고 한 줄씩 처리—대용량 파일에 사용. using 문은 예외가 발생해도 파일이 닫히도록 보장. StreamWriter는 데이터를 버퍼링; 즉시 쓰려면 Flush()를 호출하거나 Dispose가 처리하게 두세요.

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await는 비차단 비동기 작업을 가능하게. 메서드는 Task<T>(또는 void의 경우 Task)를 반환. await는 스레드를 차단하지 않고 메서드를 일시 중지—스레드가 다른 작업에 해방. UI와 웹 애플리케이션의 I/O 바운드 작업(파일, 네트워크, 데이터베이스)에 필수.

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task와 Parallel

Task.Run은 스레드 풀에서 작업을 스케줄. Task.WhenAll은 여러 작업을 동시에 대기(CPU 바운드 작업에 async/await를 사용하지 마세요—Task.Run 사용). Parallel.For는 CPU 바운드 병렬 루프용. PLINQ(AsParallel)는 LINQ 쿼리를 병렬화. I/O에는 async를, CPU에는 Parallel/PLINQ를 사용하세요.

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

JSON 직렬화

System.Text.Json은 현대적이고 고성능 JSON 직렬화기입니다(대부분의 경우 Newtonsoft.Json 대체). Serialize/Deserialize가 object<->JSON을 처리. 형식화(들여쓰기, camelCase)를 위해 JsonSerializerOptions를 사용. 비동기를 위해 대용량 JSON을 메모리에 로드하지 않도록 스트림 기반 메서드를 사용하세요.

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

LINQ 심층 분석

쿼리 구문 vs 메서드 구문

LINQ는 두 가지 동등한 구문을 가집니다. 쿼리 구문은 SQL 유사하고 복잡한 조인/그룹화에 더 가독성이 좋음. 메서드 구문(fluent)이 더 일반적이고, 모든 연산자를 지원하며, 자연스럽게 체이닝. 같은 IL로 컴파일됩니다. 복잡한 다중 절 쿼리에는 쿼리 구문을; 단순한 체인에는 메서드 구문을 사용하세요. 둘 다 지연 평가(지연 실행).

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

지연 실행 vs 즉시 실행

LINQ는 지연 실행을 사용—Where/Select/OrderBy는 쿼리를 빌드하기만; 열거될 때 실행. 이는 결과가 쿼리 생성 시가 아닌 열거 시점의 소스 상태를 반영함을 의미. ToList/ToArray/Count 등이 즉시 실행을 강제하여 스냅샷을 캡처. 주의: 지연 쿼리를 두 번 열거하면 두 번 실행. 안정적인 결과가 필요하면 ToList로 캐시하세요.

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

그룹화와 조인

GroupBy는 키로 요소를 클러스터링하여 IGrouping<key, element> 시퀀스 반환. Join은 내부 조인(키 매칭) 수행. 그룹 조인(join...into)은 계층적 결과를 만들고 DefaultIfEmpty()로 왼쪽 외부 조인을 가능하게. 이 연산들은 데이터 분석에 강력. 대용량 데이터셋의 경우 반복 조회를 위해 ToLookup을 고려(사전 계산된 그룹화).

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

집계와 수량자

LINQ는 표준 집계(Sum, Min, Max, Average, Count)와 사용자 정의 감소를 위한 범용 Aggregate(JavaScript reduce처럼)를 제공. 수량자(Any, All, Contains)는 boolean을 반환하고 단락—Any는 첫 번째 일치에서 중지, All은 첫 번째 비일치에서 중지. 존재 확인에 Count() > 0이 아닌 Any()를 사용—더 효율적이고 가독성 좋음.

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable과 IQueryable

IEnumerable<T>는 메모리 내 컬렉션용(LINQ to Objects)—대리자를 사용, 로컬에서 실행. IQueryable<T>는 원격 소스용(Entity Framework, LINQ to SQL)—표현식 트리를 사용, 소스의 쿼리 언어(SQL)로 변환. IQueryable은 서버 측 쿼리를 효율적으로 조합. 변환할 수 없는 클라이언트 측 로직이 필요할 때 AsEnumerable()으로 IEnumerable로 전환.

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Async/Await 심층 분석

Task와 Task<T>

Task는 결과 없는 비동기 작업을 나타냄; Task<T>는 T를 반환. ValueTask<T>(C# 7)는 결과가 종종 동기적으로 사용 가능한 경우(캐싱 시나리오) 힙 할당을 피합니다—但 한 번만 대기 가능. 비동기 API에서 동기 결과를 위해 Task.FromResult를 사용. 비동기 메서드에 void보다 Task를 선호(void는 이벤트 핸들러 전용).

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken은 협력적 취소를 가능하게. 비동기 메서드에 전달하고 루프에서 ThrowIfCancellationRequested()를 확인. 호출자가 CancellationTokenSource.Cancel()로 취소; 메서드가 언제/어떻게 응답할지 결정. 비동기 API(특히 라이브러리 코드)에서 항상 CancellationToken 매개변수를 받으세요. 타임아웃을 위해 TimeSpan과 함께 CancellationTokenSource를 사용. 취소는 OperationCanceledException을 던집니다.

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll vs WhenAny

Task.WhenAll은 여러 작업을 동시에 대기하고 모두 완료되면 계속(병렬성). Task.WhenAny는 첫 번째 작업이 완료되면 계속(경쟁/중복). WhenAll은 기본적으로 첫 번째 예외만 던짐; 모든 실패를 보려면 각 작업의 IsFaulted/Exception을 검사. 발사 후 망각의 경우 Task.Run을 사용하고 내부에서 예외를 처리하여 미관찰 예외를 피하세요.

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

비동기 스트림(IAsyncEnumerable)

IAsyncEnumerable<T>(C# 8)은 비동기 스트림입니다—항목이 사용 가능해지면 생성하고, yield 사이에 대기. await foreach로 소비. 모든 것을 메모리에 버퍼링하지 않고 대용량 데이터셋이나 실시간 데이터를 스트리밍하는 데 완벽. [EnumeratorCancellation] 속성은 WithCancellation으로 스트림이 소비될 때 취소 토큰이 올바르게 흐르도록 보장.

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

비동기 함정(교착 상태)

1번 비동기 함정: SynchronizationContext가 있을 때(UI 앱, 레거시 ASP.NET) Task에서 .Result나 .Wait()를 호출하면 교착 상태 발생 가능. await가 컨텍스트를 캡처; .Result가 연속이 필요한 스레드를 차단. 해결: 메서드를 끝까지 async로 만들거나, 라이브러리 코드에서 ConfigureAwait(false) 사용. async void는 위험—예외를 잡을 수 없고 대기 불가. async Task를 대신 사용하세요.

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

이벤트와 대리자

대리자 기초

대리자는 타입 안전 함수 포인터입니다. 사용자 정의 대리자(delegate int MathOp(int,int))는 주로 내장 Action(void 반환)과 Func<T>(T 반환)로 대체. 대리자는 멀티캐스트—여러 메서드를 보유 가능(+로 결합). 호출 시 모든 메서드 실행. 멀티캐스트 대리자의 반환 값은 마지막 메서드의 결과. 대부분의 현대 코드에 Action/Func를 사용하세요.

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

이벤트(발행자/구독자)

이벤트는 대리자의 제한된 형태—선언 클래스만 호출 가능하지만 외부 코드는 구독/구독 해제(+=/-=) 가능. 표준 패턴을 위해 EventHandler<TArgs>를 사용. 구독자가 없는 이벤트는 null이므로 항상 null 조건부 연산자(Click?.Invoke)를 사용. 메모리 누수를 방지하기 위해 항상 구독 해제(이벤트가 핸들러에 강한 참조를 보유).

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged(데이터 바인딩)

INotifyPropertyChanged는 WPF, MAUI, WinForms에서 데이터 바인딩의 표준 패턴. 속성이 변경되면 바인딩된 UI가 업데이트되도록 PropertyChanged 발생. [CallerMemberName](C# 5)이 호출 속성의 이름을 자동으로 전달—매직 문자열 없음. 검사(if != value)가 불필요한 알림을 방지. ViewModel이 이를 많이 사용; 소스 생성기(CommunityToolkit.Mvvm)가 이 보일러플레이트를 자동 생성.

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

이벤트 vs 대리자

주요 차이: public 대리자 필드는 누구나 호출, 재할당, 삭제 가능. 이벤트는 외부 접근을 +=(구독)과 -=(구독 해제)로만 제한—선언 클래스만 호출하거나 삭제 가능. 이 캡슐화가 옵저버 패턴에 이벤트가 표준인 이유. 이벤트는 또한 스레드 안전한 add/remove 접근자를 생성하고 디자이너 도구와 통합.

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

약한 이벤트(메모리 누수 방지)

이벤트는 구독자에 강한 참조를 보유하여, 구독자가 가비지 수집되어야 하지만 발행자가 오래 살아남으면 메모리 누수 발생. 해결: 명시적 구독 해제(최선), WeakEventManager(WPF) 사용, 또는 WeakReference로 약한 이벤트 패턴 구현. 장기 실행 앱(서버, 데스크톱 앱)에서 흔한 누수源. 구독자의 수명이 끝날 때 항상 Dispose에서 구독 해제하세요.

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

리플렉션

타입 정보

리플렉션은 런타임에 타입 메타데이터를 검사. typeof(T)는 컴파일 타임에 Type을 얻음; obj.GetType()은 런타임에 얻음. Type은 Name, IsClass, IsValueType, 멤버 열거 메서드(GetMethods, GetProperties, GetFields)를 제공. BindingFlags가 반환되는 것을 제어(NonPublic, Instance, Static, Public). 리플렉션은 강력하지만 느림—가능하면 결과를 캐시하세요.

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

인스턴스화와 호출

Activator.CreateInstance가 객체를 동적으로 생성. MethodInfo.Invoke는 리플렉션으로 메서드를 호출(인수 박싱과 보안 검사로 느림). 반복 호출의 경우 CreateDelegate가 훨씬 빠름—리플렉션 오버헤드를 우회하는 강력한 타입 대리자 생성. 리플렉션은 컴파일 타임 타입 안전을 깨고 직접 호출보다 느림; 프레임워크(직렬화, DI, ORM)와 캐싱 시나리오에 사용.

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

특성

특성은 코드 요소(클래스, 메서드, 속성)에 메타데이터를 추가. [AttributeUsage(...)]로 유효한 대상을 지정하여 정의. GetCustomAttributes<T>()(제네릭, 현대적) 또는 Attribute.GetCustomAttribute로 검색. 내장 특성: [Obsolete], [Serializable], [DllImport], [Conditional]. 프레임워크는 특성을 광범위하게 사용: [HttpGet](ASP.NET), [JsonProperty](JSON), [Table](EF Core).

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit과 동적 코드 생성

Reflection.Emit은 동적 시나리오(표현식 컴파일, 직렬화기, 목 프레임워크)에서 최대 성능을 위해 런타임에 IL을 생성. 타입/메서드를 빌드하고 IL opcode를 직접 emit. 고급임—더 단순한 동적 코드를 위해 표현식 트리(Expression.Compile)를 사용. Emit은 DLR(동적 언어), ORM, 직렬화 라이브러리가 런타임에 빠른 타입화된 코드를 생성하는 데 사용.

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

표현식 트리

표현식 트리는 코드를 데이터(Expression 노드의 트리)로 표현. IQueryable의 기반(LINQ 제공자가 이를 SQL 등으로 변환). Compile()이 표현식을 실행 가능한 대리자로 변환. 동적 코드 생성을 위해 수동으로 표현식을 빌드 가능(Reflection.Emit보다 가독성 좋음). 표현식 트리는 ORM, 규칙 엔진, 동적 LINQ를 가능하게.

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

확장 메서드와 패턴

확장 메서드

확장 메서드는 기존 타입을 수정하지 않고 메서드를 '추가'하게 함. 첫 번째 매개변수에 'this'가 있는 정적 클래스에서 정의. 구문 설탕—컴파일러가 email.IsEmail()을 StringExtensions.IsEmail(email)로 재작성. LINQ는 전적으로 IEnumerable의 확장 메서드. 봉인된 클래스, 인터페이스, 또는 타사 타입에 유틸리티 메서드를 추가하는 데 사용. using으로 스코프에 가져오세요.

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

플루언트 인터페이스

플루언트 인터페이스는 'this'를 반환하여 메서드 체이닝을 가능하게, 문장처럼 읽히는 가독성 좋은 코드 생성. 빌더(QueryBuilder), 설정(ASP.NET, EF), DSL에서 흔함. 확장 메서드는 기본 타입에 플루언트 메서드를 추가 가능(5.Seconds()). 불변 플루언트 API를 위해 메서드를 순수하게(새 인스턴스 반환) 유지하거나, 빌더를 위해 this를 변이하고 반환.

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

반복자 메서드(yield)

yield return은 메서드를 반복자로 변환—컴파일러가 상태 기계 생성. 실행은 지연: 항목이 끌어당겨질 때만 코드 실행. 무한 시퀀스와 효율적인 스트리밍을 가능하게(버퍼링 없이 한 번에 한 항목 처리). yield break가 반복 종료. 메서드는 IEnumerable<T> 또는 IEnumerator<T>를 반환해야. LINQ 연산자는 yield에 구축. 재열거는 메서드 재실행.

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

의존성 주입

의존성 주입(DI)은 내부적으로 생성하는 대신 생성자를 통해 의존성을 전달. 이점: 시험 가능성(테스트에서 구현 교체), 느슨한 결합, 단일 책임. .NET의 내장 DI 컨테이너(Microsoft.Extensions.DependencyInjection)가 등록과 수명(AddTransient, AddScoped, AddSingleton)을 처리. 유연성을 위해 항상 구체적 클래스가 아닌 인터페이스에 의존하세요.

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

팩토리와 빌더 패턴

팩토리 패턴은 객체 생성을 중앙화하여 호출자로부터 구체적 클래스를 숨김(생성이 복잡하거나 타입이 런타임에 선택될 때 유용). 빌더 패턴은 플루언트 메서드로 복잡한 객체를 단계별로 구성하여 '망원경 생성자'(많은 매개변수)를 피함. 둘 다 가독성과 유지보수성 개선. C# 레코드의 'with' 표현식이 불변 데이터를 위해 빌더를 대체하는 경우가 많음.

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

패턴 매칭(C# 7-10+)

타입 패턴과 is

패턴 매칭(C# 7+)은 한 단계로 타입 검사와 변수 바인딩을 결합: 'o is string s'가 검사하고 할당. 속성 패턴({ Length: > 3 })이 객체 속성 매칭. C# 9는 'not', 'and', 'or' 결합자 추가. switch 표현식(C# 8)은 값을 반환하고 패턴 사용—if-else 체인보다 훨씬 깔끔. _는 버리기 패턴(기본 케이스).

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

속성과 위치 패턴

속성 패턴은 객체 속성 매칭: { Prop: pattern }. 위치 패턴은 Deconstruct 메서드를 통해 분해(레코드는 자동으로 가짐). 패턴은 조합: 중첩과 관계형 연산자(<, > 등) 결합 가능. C# 10은 중첩 속성 약식 허용(Name.Length 대신 Name: { Length: }). 패턴 매칭은 복잡한 조건 논리를 선언적이고 가독성 좋게 만듦.

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

관계형과 논리 패턴(C# 9)

C# 9은 관계형 패턴(<, >, <=, >=)과 패턴을 위한 논리 결합자(and, or, not)를 도입. 이는 범위 검사와 카테고리 논리를 위해 switch 표현식을 표현력 있게 만듦. 'or'은 패턴 결합(둘 중 하나라도 일치); 'and'는 둘 다 요구; 'not'은 부정. 괄호가 우선순위 그룹화. 이는 장황한 if-else 체인을 선언적이고 철저한(컴파일러 검사) 논리로 대체.

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

리스트 패턴(C# 11)

리스트 패턴(C# 11)은 배열과 리스트의 형태 매칭: 빈 [], 단일 요소 [x], 정확히 [a, b], 또는 슬라이스 [first, .., last]. 슬라이스 패턴(..)은 중간 요소 0개 이상을 변수로 캡처. var와 가드(when)와 결합하여 시퀀스에 대한 표현력 있는 매칭 가능. 파싱, 검증, 알고리즘 구현에 유용.

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

switch 표현식

switch 표현식(C# 8)은 switch 문과 달리 값을 반환하는 표현식. 패턴 매칭(=>)을 사용하고 더 간결. 튜플 전환((a, b) switch)이 여러 값 처리. 컴파일러가 enum에 대해 철저성 검사(케이스 누락 시 경고). 런타임 예외를 원하지 않는 한 항상 기본(_)을 포함하세요. 값을 반환하는 논리에는 switch 문보다 표현식이 선호.

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

레코드와 불변성

레코드 기초(C# 9)

레코드(C# 9)는 값 기반 동등성을 가진 참조 타입—두 레코드는 데이터가 같으면 동등(참조 동등성을 사용하는 클래스와 달리). 컴파일러가 Equals, GetHashCode, ToString, Deconstruct를 자동 생성. 'with' 표현식이 수정된 속성으로 복사본 생성(비파괴적 변이). 레코드는 DTO, 값 객체, 불변 데이터 모델에 이상적.

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

레코드 구조체(C# 10)

C# 10은 레코드 구조체(레코드 기능이 있는 값 타입)와 읽기 전용 레코드 구조체(불변 값 타입)를 추가. 선택: 레코드 클래스(참조 타입, 더 큰/공유 데이터), 레코드 구조체(값 타입, 작은 데이터, 힙 할당 회피), 읽기 전용 레코드 구조체(불변 값 타입, 가장 안전). 레코드 구조체는 레코드 클래스처럼 값 동등성과 'with' 지원. Point, Money 같은 작은 불변 값에는 읽기 전용 레코드 구조체를 사용하세요.

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

init 전용 setter

init 전용 setter(C# 9)는 객체 초기화 중에만 설정 허용—생성자 보일러플레이트 없이 생성 후 불변성. 'required'(C# 11)는 호출자가 이니셜라이저에서 속성을 설정하도록 강제(컴파일 타임 검사). 레코드는 기본적으로 init setter 사용. 이는 생성자 매개변수보다 가독성 좋은 객체 이니셜라이저 구문으로 불변 객체 패턴을 가능하게.

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

레코드 상속

레코드는 상속을 지원: 파생 레코드는 생성자에 기반 레코드 매개변수를 포함. 'with' 표현식은 런타임 타입을 보존(Animal이 아닌 Dog에서 Dog 생성). 동등성은 런타임 타입을 검사—두 레코드는 같은 타입이고 데이터가 같을 때만 동등. 이는 순진한 값 동등성과 달리 레코드가 다형적 컬렉션에서 올바르게 작동하게 함.

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

레코드 vs 클래스 vs 구조체

클래스는 참조 동등성(==가 참조 비교); 레코드는 값 동등성(데이터 비교); 구조체는 기본적으로 값 동등성이지만 값 타입(대입 시 복사). 값 의미론이 있는 불변 데이터(DTO, 값 객체, 메시지)에는 레코드를 사용. 식별성이 있는 가변 엔티티(User, Order)와 상속 계층에는 클래스를. 작은(≤16바이트) 복사되어야 하는 값(Point, Color, Money)에는 구조체를 사용하세요.

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

패턴 매칭 심층

속성 패턴

속성 패턴은 객체 속성을 인라인으로 매칭. switch 표현식이 값을 직접 반환. 결합 패턴(and/or)이 유연한 조건 생성. null과 _(버리기)가 엣지 케이스 처리. 이는 장황한 if-else 체인을 선언적이고 가독성 좋은 코드로 대체.

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

리스트 패턴

리스트 패턴(C# 11)은 배열과 인덱스 가능 시퀀스를 매칭. []는 빈, [single]은 한 요소, [first, .. rest]는 슬라이스 패턴으로 나머지 요소 캡처. 명령 파싱, 시퀀스 검증, 재귀 알고리즘에 유용.

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

튜플 패턴

튜플 패턴은 여러 값을 동시에 매칭. 각 케이스가 값의 튜플을 테스트. 관계형 패턴(>, <, >=)과 논리 패턴(and, or, not)과 결합하여 중첩 if 문 없이 강력한 다차원 매칭 생성.

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

타입 패턴과 가드

타입 패턴은 런타임 타입 매칭. when 절이 추가 가드. var는 모든 것( null 포함) 매칭. 순서가 중요: 더 특정한 패턴이 일반적인 것보다 먼저 와야. 이는 현대 C#에서 타입 기반 디스패치의 관용적 방법.

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

중첩 패턴

패턴은 임의로 깊이 중첩. 패턴 내 var가 가드나 표현식 본문에서 사용하기 위해 매칭된 값 캡처. 이 예는 Line 레코드 내 Point 속성을 매칭. 중첩 패턴은 복잡한 객체 그래프를 간결하게 검증하는 데 강력.

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

레코드와 with 표현식

레코드 값 동등성

레코드는 기본적으로 값 기반 동등성을 제공: 같은 데이터를 가진 두 인스턴스가 동등. 컴파일러가 Equals, GetHashCode, ToString, == 연산자 생성. 레코드는 참조 타입이지만 불변성을 위해 설계. DTO, 값 객체, 내용으로 비교되어야 하는 데이터에 사용하세요.

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

with 표현식

with 표현식은 수정된 속성으로 레코드의 복사본 생성. 원본은 변경되지 않음(비파괴적 변이). 이것이 불변 데이터를 업데이트하는 관용적 방법. 내부적으로 컴파일러는 보호된 복사 생성자와 init 전용 setter를 사용.

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

위치 vs init 전용

위치 레코드는 기본 생성자 구문을 사용하고 deconstruct 메서드를 생성. init 전용 레코드는 객체 이니셜라이저 구문을 사용, 기본값과 required 수정자 허용. 단순 값 타입에는 위치를; 선택적이거나 계산된 속성이 있는 복잡한 레코드에는 init 전용을 선택하세요.

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

레코드 구조체와 상속

레코드 구조체는 값 타입(대입 시 복사)이며 다른 레코드 구조체에서 상속 불가. 참조 타입 레코드는 상속 지원. readonly record struct는 변이 방지. 작은 불변 값에는 레코드 구조체를; 참조 의미론과 상속의 이점이 있는 더 큰 객체에는 레코드 클래스를 사용하세요.

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct와 매칭

위치 레코드는 Deconstruct 메서드를 자동 생성하여 튜플 분해 가능. 이는 패턴 매칭과 원활하게 통합: 레코드 속성을 위치별 또는 이름으로 매칭 가능. 분해는 var 패턴, 튜플 패턴, switch 표현식에서 작동.

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

소스 생성기

기본 소스 생성기

소스 생성기는 컴파일 타임에 실행되어 컴파일에 C# 소스 파일을 추가. IIncrementalGenerator가 현대 API(C# 9+). RegisterPostInitializationOutput는 사용자 코드 분석 없이 정적 코드 추가. 생성기는 읽기 전용: 파일을 추가할 수 있지만 기존 코드를 수정할 수 없음.

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

특성에서 생성

ForAttributeWithMetadataName는 특정 특성으로 표시된 타입을 찾음. 파이프라인: 구문 제공자가 노드 필터링, 변환이 데이터 추출, Collect가 결과 일괄 처리, RegisterSourceOutput이 코드 방출. 이 증분 파이프라인은 결과를 캐시하고 입력이 변경될 때만 재생성하여 빌드를 빠르게 유지.

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

StringBuilder 출력

생성기는 문자열로 소스를 생성. StringBuilder가 여러 줄 출력에 효율적. 대상 클래스는 생성기가 멤버를 추가할 수 있도록 partial여야 함. 생성된 파일은 IDE의 Dependencies > Analyzers 아래에 나타남. 관례상 생성된 파일을 구분하기 위해 .g.cs 접미사를 사용하세요.

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json은 직렬화 로직을 사전 컴파일하는 내장 소스 생성기를 포함, 런타임 리플렉션을 제거. 이는 성능을 개선하고 AOT 컴파일(Native AOT)을 지원. JsonSerializable로 타입을 선언하고, 생성된 컨텍스트를 serialize/deserialize 호출에 사용하세요.

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

일반적인 사용 사례

인기 있는 생성기 사용 사례: DTO 생성, DI 등록, 강력한 타입 설정, AutoMapper 프로필, 로깅 템플릿, JSON 직렬화. MediatR, AutoMapper, Microsoft.Extensions.Logging 같은 라이브러리가 보일러플레이트를 줄이고 AOT 호환성을 개선하기 위해 생성기 사용.

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span과 Memory

Span 기초

Span<T>는 복사 없이 연속 메모리에 대한 타입 안전하고 메모리 안전한 뷰를 제공. 배열, stackalloc 메모리, 비관리 메모리, 문자열을 감쌀 수 있음. Span은 ref struct(스택 전용)이므로 박싱, 필드에 저장, 람다로 캡처 불가. 고성능 슬라이싱과 파싱에 사용하세요.

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

비동기를 위한 Memory

Memory<T>는 Span<T>의 힙 안전한 대응물. 필드에 저장, 람다로 캡처, await 경계를 넘어 사용 가능. 실제 작업을 할 때 .Span으로 Span<T>로 변환. 비동기 API에는 Memory를, 동기 핫 패스에는 Span을 사용하세요.

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

할당 회피

Span은 파싱과 슬라이싱 중 중간 문자열 할당을 제거. 대용량 입력을 처리하는 핫 패스(HTTP 파싱, JSON, 로그 파일)의 경우 GC 압력을 극적으로 줄일 수 있음. int.TryParse와 다른 기본 타입은 Span 오버로드를 가짐. 문자열이 실제로 필요할 때만 ToString()을 호출하세요.

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

stackalloc과 비관리

stackalloc은 호출 스택에 할당(자동 해제, GC 압력 없음)하지만 작은 크기로 제한. 큰 버퍼의 경우 NativeMemory.Alloc/Free(C# 7+) 또는 Marshal.AllocHGlobal을 사용. 비관리 메모리를 항상 Span으로 감싸고 누수를 방지하기 위해 finally 블록에서 해제하세요.

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

Span 친화적 API

버퍼에 대해 Span<T>나 ReadOnlySpan<T>를 받도록 API를 설계, 배열 할당 회피. 호출자가 stackalloc, 배열, 또는 비관리 메모리 중 무엇을 사용할지 결정. Span.TryWrite(C# 10+)는 문자열 할당 없이 스팬에 직접 형식화, 로깅과 직렬화에 유용.

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

보간 문자열 심층

형식 지정자

문자열 보간($"...")은 중괄호 안에 표현식을 삽입. 콜론 뒤의 형식 지정자가 출력 제어: C는 통화, F는 고정 소수점, X는 16진수, D는 패딩이 있는 숫자, P는 퍼센트, yyyy-MM-dd는 날짜. 컴파일러가 이를 string.Format이나 FormattableString으로 변환.

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

원시 문자열 보간

축어적 보간 문자열($@)은 백슬래시를 문자 그대로 보존하고 여러 줄 내용 허용. 원시 문자열 보간(C# 11)은 삼중 따옴표를 사용하고 모든 이스케이프를 제거: 중괄호나 따옴표를 이스케이프할 필요 없음. $ 기호의 수가 보간에 필요한 중괄호 수를 제어.

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString과 문화권

FormattableString은 보간 구조를 보존하여 특정 문화권으로 지연 형식화 가능. 현재 문화권에 의존하지 않아야 하는 라이브러리에 중요. FormattableString.Invariant는 고정 문화권을 강제, 직렬화와 로깅에서 로케일별 문제를 방지.

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

보간 문자열 핸들러

보간 문자열 핸들러(C# 10)는 컴파일러가 보간 구성 요소를 메서드에 직접 전달하게 하여, 결과가 필요하지 않을 때 문자열 할당을 회피. 이것이 고성능 로깅의 작동 방식: 로그 레벨이 비활성화면 문자열이 빌드되지 않음. 핸들러는 리터럴과 형식화된 값을 개별적으로 받음.

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Span 기반 형식화

Span.TryWrite는 보간 문자열을 스택 할당된 버퍼에 직접 형식화, 힙 할당을 완전히 회피. 보간 문자열과 문화권으로 string.Create는 중간 할당 없이 문화권 인식 형식화를 제공. 이들은 고처리량 애플리케이션의 핫 패스에 필수적인 기술.

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

파일 스코프 네임스페이스

파일 스코프 구문

파일 스코프 네임스페이스(C# 10)는 중괄호 대신 세미콜론으로 전체 파일에 대한 네임스페이스를 선언. 이는 들여쓰기 수준을 하나 제거하여 코드를 더 평평하고 읽기 쉽게 만듦. 파일당 하나의 파일 스코프 네임스페이스만 허용. 파일의 모든 타입이 해당 네임스페이스에 속함.

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

전역 using

전역 using(C# 10)은 한 파일에 선언하면 프로젝트의 모든 파일에 적용. 깔끔하고 최소한의 보일러플레이트를 위해 파일 스코프 네임스페이스와 결합. .NET SDK 프로젝트는 일반 네임스페이스에 대한 전역 using을 자동 생성(ImplicitUsings). 순서: 전역 using, 파일 using, 네임스페이스.

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

중첩 네임스페이스

파일 스코프 네임스페이스는 계층을 위해 점으로 된 이름을 사용합니다: MyApp.Services.Authentication은 중첩된 네임스페이스 블록과 동등합니다. 여러 파일이 같은 네임스페이스를 공유할 수 있습니다. 같은 파일 내에서 다른 네임스페이스에 타입을 중첩할 수 없습니다; 대신 별도의 파일을 사용하세요.

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

프로젝트 설정

ImplicitUsings는 프로젝트 파일에서 일반 System 네임스페이스에 대한 전역 using을 자동 생성. 파일 스코프 네임스페이스와 결합하면 대부분의 보일러플레이트를 제거. Nullable은 nullable 참조 타입을 활성화. 이들은 새로운 .NET 6+ 프로젝트의 기본값입니다.

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

마이그레이션과 모범 사례

.editorconfig를 사용하여 파일 스코프 네임스페이스(IDE0161)를 강제. IDE는 전통적에서 파일 스코프로의 원클릭 마이그레이션을 제공. 모범 사례: 파일당 하나의 public 타입, 파일 이름을 타입 이름과 일치. 이는 파일을 작게 유지하고 탐색을 쉽게 합니다.

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

필수 멤버

필수 속성

required 수정자(C# 11)는 호출자가 객체 생성 중에 속성을 초기화하도록 강제. 필수 멤버가 설정되지 않으면 컴파일러가 CS9035를 방출. 이것이 필수 속성을 위한 생성자 검증을 대체. init 전용 setter와 객체 이니셜라이저와 함께 작동, 컴파일 타임 보장을 제공.

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

SetsRequiredMembers 특성은 생성자가 모든 필수 멤버를 초기화한다고 컴파일러에게 알림, 호출자가 객체 이니셜라이저 없이 생성자를 사용 가능. 이는 필수 속성과 전통적 생성자 사이의 간극을 연결. 이 특성은 런타임에 검증하지 않음; 컴파일 타임 약속입니다.

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

레코드와 필수

필수 멤버는 레코드와 작동. 위치 레코드의 경우, 필수 속성은 기본 생성자가 아닌 본문에 선언. with 표현식은 필수 값을 자동으로 보존. 기본값(OpenedAt처럼)이 required 수정자 없이 속성을 선택적으로 만듭니다.

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

init vs required vs set

init는 속성을 생성 중에만 설정 가능하게(이후 불변). required는 초기화를 강제. 함께(required + init) 사용하면 필수 불변 속성을 생성. required + set은 생성 후 수정 허용. 값이 생성 후 변경되어야 하는지에 따라 선택하세요.

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

JSON 직렬화

System.Text.Json은 required 수정자를 존중: JSON에 필수 속성이 누락되면 역직렬화가 JsonException을 던짐. 이는 컴파일 타임 계약과 일치하는 런타임 검증을 제공. init와 결합하여 모든 필수 필드가 채워진 것이 보장되는 불변 DTO를 만드세요.

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

컬렉션 표현식

컬렉션 표현식 구문

컬렉션 표현식(C# 12)은 모든 컬렉션 타입을 초기화하기 위한 통일된 구문 [..]을 제공. 컴파일러가 타입을 추론하고 적절한 빌더를 사용. 장황한 new List<int> { 1, 2, 3 } 구문을 대체. 배열, List<T>, Span<T>, IEnumerable<T>, 그리고 CollectionBuilder 특성이 있는 사용자 정의 컬렉션과 작동.

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

스프레드 연산자

스프레드 연산자(..)는 컬렉션을 둘러싼 컬렉션 표현식으로 평탄화. 이는 Concat, AddRange, 수동 루프를 대체. 개별 요소와 스프레드를 혼합 가능. 스프레드는 모든 IEnumerable<T>와 작동, 여러 데이터 소스를 간결하게 결합하기 쉽게 만듦.

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

반환과 매개변수 패턴

컬렉션 표현식은 return 문, 인수, 대입에서 작동. 컴파일러가 대상 타입으로 자동 변환. 조건부 스프레드(..가 있는 삼항)로 요소를 선택적으로 포함 가능. 이는 여러 선택적 소스에서 컬렉션을 빌드하는 것을 깔끔하고 가독성 좋게 만듦.

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

사용자 정의 컬렉션 빌더

CollectionBuilder 특성은 컬렉션 타입을 팩토리 메서드에 연결, 컬렉션 표현식 지원을 활성화. Create 메서드는 요소의 ReadOnlySpan<T>를 받음. 이는 타사 컬렉션이 통일된 [..] 구문에 참여할 수 있게 함. 빌더 패턴이 초기화를 효율적으로 유지.

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

params 컬렉션

C# 12는 배열뿐 아닌 모든 컬렉션 타입과 함께 params를 허용. 컬렉션 표현식을 params 메서드에 전달 가능. 대상이 Span<T>일 때, 컴파일러는 작은 컬렉션에 스택 할당을 사용, 힙 할당을 완전히 회피. 이는 가변 인수 메서드를 인체공학적이고 효율적으로 만듦.

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.