본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

C# 치트시트 — 빠른 레퍼런스

C# 12(.NET 8 포함) 문법, OOP, LINQ 및 가장 일반적으로 사용되는 표준 라이브러리 치트시트로, 일상적인 시나리오의 약 80%를 다룹니다.

C#

C# C# 12 (with .NET 8)

.NET (Core / 5+ / 8) · OOP, 제네릭, 함수형, 동시성 · 정적, 강타입, 공칭 타입

추천 학습 경로

먼저 「Hello World 및 빌드 환경」(dotnet new console)을 실행 → 변수, 타입, 제어 흐름에 익숙해지기 → 컬렉션과 LINQ로 데이터 처리 → 객체 지향(class/interface) 이해 → async/await 비동기 프로그래밍 숙달 → 마지막으로 필요할 때 파일, 네트워크, 정규식, 빌드/디버그 참조. FAQ 섹션은 돌아와서 함정을 피하는 데 좋습니다.

1.Hello World 및 빌드 환경

.NET 프로그램을 처음부터 만들고 실행하고 구성합니다: 최상위 문, 프로젝트 파일, 네임스페이스, 명령줄 인자.

최소 프로그램

C# 9+는 최상위 문을 지원합니다. Program.cs에 직접 실행 코드를 작성할 수 있으며 class와 Main 보일러플레이트가 필요 없습니다. Console.WriteLine이 한 줄을 출력합니다.

1
2
3
// Program.cs
Console.WriteLine("Hello, world!");
// 최상위 문(토프레벨 스테이트먼트, C# 9+):컴파일러가 Main 진입점 자동 생성

생성과 실행

dotnet CLI는 .NET의 명령줄 진입점입니다. dotnet new로 프로젝트를 만들고, dotnet run으로 컴파일 후 실행하며, dotnet build는 컴파일만 합니다.

1
2
3
// $ dotnet new console -n app
// $ cd app && dotnet run
// $ dotnet build // 컴파일만, bin/Debug 생성

명령줄 인자

최상위 문에서 args는 string[] 명령줄 인자이며 args[0]부터 사용자 인자입니다. 전통적인 Main(string[] args)와 동등합니다.

1
2
3
4
5
6
7
// Program.cs
if (args.Length > 0) {
Console.WriteLine($"Hello, {args[0]}!");
} else {
Console.WriteLine("No args");
}
// $ dotnet run -- Nick

종료 코드

최상위 문은 return으로 종료 코드를 지정할 수 있으며 0은 성공, 0이 아닌 값은 오류 범주를 나타냅니다. Environment.ExitCode도 읽기/쓰기가 가능합니다.

1
2
3
// Program.cs
return 0; // 최상위 문에서 정수 직접 반환
// 또는 Environment.ExitCode = 1;

네임스페이스

namespace는 타입을 조직하여 이름 충돌을 피합니다. 파일 범위 네임스페이스(C# 10)는 세미콜론으로 선언하여 중괄호와 들여쓰기를 생략합니다.

1
2
3
4
5
6
namespace App.Utils; // 파일 스코프 네임스페이스(C# 10)
public class Helper {
public static int Twice(int x) => x * 2;
}
// 접근:App.Utils.Helper.Twice(3)

using 지시문

using 지시문은 네임스페이스를 가져와 완전한 한정 이름이 필요 없게 합니다. GlobalUsings.cs의 전역 using은 프로젝트 전체에서 가져오기를 공유합니다.

1
2
3
4
5
using System;
using System.Collections.Generic;
// GlobalUsings.cs
// global using System.Linq; // 전역 using(C# 10)
// global using static System.Console;

최상위 문 세부 사항

최상위 문은 Program 클래스의 Main 메서드로 컴파일됩니다. 프로젝트당 하나만 가능하며, 보조 메서드가 필요하면 로컬 함수를 사용합니다.

1
2
3
4
5
Console.WriteLine("start");
int total = Sum(1, 2);
Console.WriteLine(total);
static int Sum(int a, int b) => a + b; // 지역 함수

csproj 프로젝트 파일

csproj는 프로젝트 구성입니다. TargetFramework 대상 프레임워크, ImplicitUsings 암시적 using, Nullable null 허용 컨텍스트.

1
2
3
4
5
6
7
8
9
<!-- app.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

2.변수와 상수

타입 추론, const/readonly, nullable 타입, null 병합, nameof, 스코프.

var 타입 추론

var는 컴파일러가 초기화 식에서 타입을 추론하게 합니다. 로컬 변수에는 사용할 수 있지만 필드/속성에는 사용할 수 없습니다. 초기화 식의 타입이 분명할 때는 var를 우선 사용하면 간결합니다.

1
2
3
4
5
var count = 42; // int
var name = "Nick"; // string
var list = new List<int>(); // List<int>
var value = 3.14; // double
// var는 지역 변수에만 사용 가능, 필드는 명시적 타입 필수

const와 readonly

const는 컴파일 타임 상수(기본 타입)이고, readonly는 런타임 읽기 전용 필드(생성자에서 할당 가능)입니다. 정적 상수에는 보통 static readonly를 사용합니다.

1
2
3
4
public const int MaxRetry = 3; // 컴파일 타임 상수
public static readonly DateTime Epoch =
new DateTime(1970, 1, 1); // 런타임 읽기 전용
// readonly 필드는 생성자에서 한 번만 대입 가능

Nullable 타입

Nullable<T>(int? 등)는 값 타입에 null을 허용하며, null 허용 참조 타입(? 접미사 + Nullable enable)은 컴파일러가 null 참조를 정적으로 분석하게 합니다.

1
2
3
4
int? maybe = null; // 값 타입 null 허용
int real = maybe ?? 0; // 널 병합
string? name = FindName(); // null 허용 참조 타입
if (name is null) return; // 컴파일러는 이 뒤 name이 null 아님을 인지

Null 병합과 Null 조건

??는 왼쪽이 null이면 오른쪽을 반환하고, ?.는 왼쪽이 null이면 null로 단락(short-circuit)합니다. 연쇄 ?.로 깊은 접근을 null 안전하게 작성할 수 있습니다.

1
2
3
4
5
string? s = null;
var result = s ?? "default"; // "default"
int? len = s?.Length; // null(예외를 던지지 않음)
var first = s?[0] ?? '-'; // '-'
// ?. 와 ?? 결합:null 허용 체이닝 + 폴백

nameof 식

nameof은 심볼 이름을 문자열로 반환하며 이름 변경 시 자동으로 동기화됩니다. 매개변수 검증, 속성 알림, 로그 태그에 자주 사용됩니다.

1
2
3
4
5
public void SetAge(int age) {
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age));
}
var prop = nameof(Person.Name); // "Name"

스코프

C#는 중괄호로 블록 스코프를 정의합니다. 로컬 변수는 필드와 같은 이름을 가질 수 있지만 가리기는 피하세요. using 선언은 리소스 스코프를 블록 끝까지 제한합니다.

1
2
3
4
5
6
int x = 1;
{
int x = 2; // 블록 안에서 외부 지역 변수 섀도잉(가독성 저하)
Console.WriteLine(x);
}
Console.WriteLine(x); // 1

init과 속성 초기화

init 접근자는 객체 초기화 식에서 할당을 허용하고 그 후에는 읽기 전용입니다. 속성 초기화 식은 기본값을 제공하며, set 접근자는 검증을 추가할 수 있습니다.

1
2
3
4
5
6
public class Person {
public string Name { get; init; } = ""; // 초기화 시에만 설정 가능
public int Age { get; set; }
}
var p = new Person { Name = "Nick", Age = 30 }; // OK
// p.Name = "X"; // 컴파일 오류:init 이후 읽기 전용

구조 분해

튜플과 record는 구조 분해를 지원합니다. 괄호로 요소를 여러 변수로 분해합니다. Deconstruct 메서드로 구조 분해 로직을 사용자 정의할 수 있습니다.

1
2
3
4
5
var (sum, count) = (42, 7); // 튜플 분해
var (name, age) = GetPerson(); // 메서드가 튜플 반환
record Person(string Name, int Age);
var p = new Person("Nick", 30);
var (n, a) = p; // record 기본 분해 지원

3.데이터 타입

내장 타입, nullable, 열거형, 구조체, 제네릭, record, 튜플, 타입 변환.

내장 타입

int/long/double/bool 등의 내장 타입은 System 타입으로 매핑됩니다. var는 단지 추론이며 런타임 타입은 변하지 않습니다.

1
2
3
4
5
6
7
8
int i = 42; // System.Int32
long l = 42L;
double d = 3.14;
float f = 3.14f;
decimal m = 19.99m; // 고정밀도 10진수
bool b = true;
char c = 'A';
string s = "hi";

Nullable 타입

int?는 nullable 값 타입(Nullable<int>)이며 HasValue/Value로 접근합니다. Nullable enable이 활성화되면 nullable 참조 타입이 컴파일러에 의해 정적으로 검사됩니다.

1
2
3
4
5
int? x = null;
if (x.HasValue) { int v = x.Value; }
int y = x ?? 0; // 널 병합
string? name = null; // null 허용 참조 타입
// Nullable 활성 시 name 직접 접근하면 경고

열거형

enum은 명명된 정수 상수를 정의하며 기본적으로 int입니다. Flags 특성을 사용하면 비트 조합을 HasFlag이나 비트 연산으로 판정할 수 있습니다.

1
2
3
4
5
6
enum Color { Red, Green, Blue } // 기본 0,1,2
[Flags]
enum Perm { Read = 1, Write = 2, Exec = 4 }
var p = Perm.Read | Perm.Write;
if ((p & Perm.Write) != 0) { } // 비트 판별
var c = (Color)1; // Green

구조체

struct는 값 타입입니다. 할당 시 전체가 복사되고, 스택에 할당되며, null일 수 없습니다(null 허용하면 박싱됨). 작고 불변인 데이터에 적합합니다.

1
2
3
4
5
6
struct Point {
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
}
Point a = new(1, 2);
Point b = a; // 값 복사, b를 바꿔도 a에 영향 없음

클래스(참조 타입)

class는 참조 타입입니다. 할당 시 같은 객체를 공유하고, GC가 메모리를 관리하며, null일 수 있습니다. 가변 상태에는 class를 사용합니다.

1
2
3
4
5
6
class Counter {
public int Value { get; set; }
}
Counter a = new() { Value = 1 };
Counter b = a; // 참조 복사, 같은 객체 공유
b.Value = 99; // a.Value도 99

제네릭

제네릭은 타입을 매개변수화하여 컴파일 타임에 강타입 코드를 생성하므로 런타임 박싱이 없습니다. T는 타입 매개변수이며, where 절로 제약 조건을 추가합니다.

1
2
3
4
5
public class Box<T> {
public T Value { get; set; }
}
var b = new Box<int> { Value = 42 };
// 제약:where T : class, new()

record 타입

record(C# 9)는 값 의미론을 가진 참조 타입입니다. 값 동등성, ToString, 구조 분해, with 식이 내장되어 있습니다. DTO와 불변 데이터에 적합합니다.

1
2
3
4
5
record Person(string Name, int Age);
var p1 = new Person("Nick", 30);
var p2 = p1 with { Age = 31 }; // 비파괴적 수정
bool eq = p1 == p2; // false(값 비교)
var (name, age) = p1; // 분해

튜플

튜플은 여러 값을 묶고 명명된 요소를 지원합니다. 여러 값을 반환하거나 빠르게 데이터를 집계할 때 가장 편리하며, 임시에는 튜플, 장기에는 record를 사용합니다.

1
2
3
4
var t = (sum: 42, count: 7); // 명명된 튜플
Console.WriteLine(t.sum);
(int, int) swap((int a, int b) t) => (t.b, t.a);
var (a, b) = (1, 2);

타입 변환

is는 안전한 타입 검사, as는 안전한 변환(실패 시 null), (T)는 강제 변환(실패 시 예외), Convert/Parse는 명시적 파싱입니다.

1
2
3
4
5
6
object o = "hello";
if (o is string s) { } // 패턴 매칭 + 변환
var str = o as string; // null 또는 string
// var bad = (int)o; // InvalidCastException 발생
int n = int.Parse("42"); // 명시적 파싱
bool ok = int.TryParse("42", out int v); // 안전한 파싱

패턴 매칭

is / switch는 패턴 매칭을 사용합니다. 타입 패턴, 속성 패턴, 위치 패턴. 많은 if + cast를 대체하며 코드가 의도를 더 명확히 표현합니다.

1
2
3
4
5
6
7
string Describe(object o) => o switch {
int n when n > 0 => "positive",
int n => "integer",
string s when s.Length > 3 => "long string",
null => "null",
_ => "other", // 폴백
};

4.참조와 배열

값 타입과 참조 타입, 배열, Span, 인덱스/범위, ref/in/out, unsafe 포인터.

값 타입 vs 참조 타입

값 타입(struct / 열거형 / 기본 타입)은 할당 시 복사되고, 참조 타입(class / 인터페이스)은 같은 객체를 공유합니다. 이것이 C#에서 가장 중요한 멘탈 모델입니다.

1
2
3
4
5
6
7
8
// struct는 값 복사:b는 독립된 복사본
Point a = new(1, 2);
Point b = a;
b.X = 99; // a.X는 여전히 1
// class는 참조 공유
var c1 = new Counter();
var c2 = c1;
c2.Value = 5; // c1.Value도 5

배열

배열은 고정 길이 참조 타입이며 []로 인덱스 접근합니다. 다차원과 가변 배열은 다릅니다: [,]는 직사각형, [][]는 지그재그. 배열은 Count가 아닌 Length를 사용합니다.

1
2
3
4
5
6
int[] arr = { 1, 2, 3 };
int first = arr[0];
arr[2] = 99;
int len = arr.Length; // 3
int[,] rect = new int[2, 3]; // 2차원 직사각형 배열
int[][] jag = new int[2][]; // 들쭉날쭉(jagged) 배열

인덱스와 범위

^는 끝에서부터 인덱싱(^1은 마지막), ..은 범위(1..^1은 처음과 끝 제외)를 나타냅니다. 배열/List의 부분 집합을 자르는 모던한 표기법입니다.

1
2
3
4
5
int[] arr = { 1, 2, 3, 4, 5 };
int last = arr[^1]; // 5
int secondLast = arr[^2]; // 4
int[] mid = arr[1..^1]; // { 2, 3, 4 }
int[] tail = arr[2..]; // { 3, 4, 5 }

Span과 메모리

Span<T>는 임의의 연속 메모리에 대한 읽기 전용 뷰이며 할당 없이 슬라이스할 수 있어 고성능 데이터 처리의 핵심입니다. 스택이나 힙 메모리를 가리킬 수 있습니다.

1
2
3
4
5
int[] arr = { 1, 2, 3, 4 };
Span<int> s = arr;
Span<int> part = s[1..3]; // 슬라이스, 복사 없음
part[0] = 99; // 원본 배열 수정
// 스택 메모리도 가능:Span<int> st = stackalloc int[4];

ref / in / out 매개변수

ref는 읽기/쓰기 가능한 참조 전달, in은 읽기 전용 참조 전달, out은 반환값용(호출 전 초기화 불필요)입니다. 참조 전달로 값 타입 복사를 피합니다.

1
2
3
4
5
6
7
void Increment(ref int x) => x++;
void Init(out int x) => x = 42;
int ReadOnly(in int x) => x;
int v = 1;
Increment(ref v); // v = 2
Init(out v); // v = 42
ReadOnly(in v); // 읽기 전용 참조

unsafe 포인터

unsafe 컨텍스트에서는 실제 포인터(int* 등)를 사용할 수 있습니다. csproj에서 AllowUnsafeBlocks를 활성화해야 합니다. 네이티브 상호 운용 시에만 사용하고 일반 코드에서는 피하세요.

1
2
3
4
5
6
unsafe {
int x = 42;
int* p = &x; // 주소 취득
Console.WriteLine(*p); // 역참조
}
// csproj: <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

Memory와 버퍼

Memory<T>는 Span의 힙 안전 버전으로 필드에 저장하고 async를 가로질러 사용할 수 있습니다. ReadOnlyMemory는 읽기 전용 뷰입니다. 비동기 버퍼 데이터 처리에 사용됩니다.

1
2
3
4
byte[] data = GetBytes();
Memory<byte> mem = data; // async를 넘어 보관 가능
ReadOnlyMemory<byte> rom = mem; // 읽기 전용 뷰
// Span은 필드/await 불가, Memory는 가능

복사 의미론

값 타입은 기본적으로 값 전달(큰 struct는 복사 비용이 발생), 객체(참조)는 참조 전달입니다. 객체를 제자리에서 수정해야 한다면 ref를 사용하지 않습니다.

1
2
3
4
5
struct Big { public long A, B, C, D; }
void PassByValue(Big b) { } // struct 전체 복사
void PassByRef(ref Big b) { } // 참조 전달, 제로카피
var x = new Big();
PassByRef(ref x); // 원본 객체 수정됨

5.제어 흐름

if/else, for/foreach/while, switch 식, break/continue, throw 식.

if / else

if / else는 조건에 따라 분기합니다. C#는 비교에 ==, 단락(short-circuit)에 &&와 ||를 사용합니다. 단일 문은 중괄호를 생략할 수 있지만 유지하는 것을 권장합니다.

1
2
3
4
5
6
7
if (score >= 90) {
grade = "A";
} else if (score >= 60) {
grade = "B";
} else {
grade = "F";
}

for 루프

클래식 for: 초기화, 조건, 증감. 인덱스가 필요할 때 사용하고, 대부분의 경우 foreach가 더 안전합니다.

1
2
3
4
for (int i = 0; i < 10; i++) {
Console.WriteLine(i); // 0..9
}
for (int i = arr.Length - 1; i >= 0; i--) { } // 역순

foreach 반복

foreach는 인덱스 없이 컬렉션을 반복하며 컴파일 타임에 안전합니다. var와 LINQ와 함께 체인으로 데이터를 처리하는 것이 가장 일반적입니다.

1
2
3
4
5
foreach (var item in items) {
Console.WriteLine(item);
}
// 인덱스가 필요하면 for 또는 Enumerable.Select
foreach (var (i, item) in items.Select((x, i) => (x, i))) { }

while / do-while

while은 먼저 판단 후 실행하고, do-while은 최소 한 번은 실행합니다. EOF까지 스트림 읽기 등 루프 횟수를 모르는 시나리오에 적합합니다.

1
2
3
4
5
6
int i = 0;
while (i < 5) { i++; } // 먼저 판정
string? line;
do {
line = Console.ReadLine();
} while (line != null); // 최소 1회 실행

switch 식

switch 식(C# 8)은 =>로 값을 반환하며 긴 if-else 체인을 대체합니다. 타입 패턴과 when 가드의 조합은 강력합니다.

1
2
3
4
5
6
7
8
9
10
11
string Describe(int n) => n switch {
0 => "zero",
1 => "one",
_ => "other", // 폴백
};
// 타입 패턴:switch 객체를 타입으로 분기
string Type(object o) => o switch {
int => "int",
string => "string",
_ => "other",
};

break와 continue

continue는 현재 반복을 건너뛰고 다음으로, break는 루프를 빠져나가고, return은 메서드를 빠져나갑니다. 다중 루프 탈출에는 goto를 사용합니다.

1
2
3
4
5
6
7
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue; // 짝수 건너뜀
if (i > 7) break; // 루프 탈출
}
// 이중 루프 한 번에 탈출
goto Exit;
Exit: ;

삼항 연산자

cond ? a : b로 if / else 할당을 한 줄로 표현합니다. 분기 표현식 타입은 호환되어야 합니다(공통 타입으로 변환 가능).

1
2
var grade = score >= 60 ? "pass" : "fail";
var label = score switch { >= 90 => "A", _ => "other" };

throw 식

throw는 식으로 ??와 ?:의 오른쪽, 그리고 메서드 본문을 한 줄로 쓰는 데 사용할 수 있습니다. ArgumentNullException을 던져 매개변수 검증을 간결하게 작성합니다.

1
2
3
string s = name ?? throw new ArgumentNullException(nameof(name));
int v = dict.TryGetValue(k, out var val) ? val :
throw new KeyNotFoundException(k);

6.함수와 메서드

메서드 시그니처, 매개변수 전달, 오버로드, 로컬 함수, 람다, 식 본문 멤버.

메서드 정의

메서드 = 반환 타입 + 이름 + 매개변수 목록 + 본문. void는 반환값이 없음을 나타냅니다. 접근 한정자가 가시성을 제어합니다.

1
2
3
4
5
6
7
public int Add(int a, int b) {
return a + b;
}
public void Say(string msg) {
Console.WriteLine(msg); // void는 반환값 없음
}
private int twice = 0; // 필드

매개변수 전달

기본적으로 값 전달입니다. 메서드 내 수정은 호출자 변수에 영향을 주지 않습니다. 객체는 참조 복사본이 전달되므로 멤버를 수정하면 원본 객체에 영향을 줍니다.

1
2
3
4
5
6
void Set(int x) { x = 99; } // 값 전달:호출측 불변
void Mutate(List<int> l) { l.Add(1); } // 참조:호출측에 영향
var n = 1;
Set(n); // n은 여전히 1
var list = new List<int>();
Mutate(list); // list는 [1]이 됨

선택적 매개변수와 명명된 매개변수

선택적 매개변수는 기본값을 가지며(매개변수 목록 끝에 위치해야 함), 명명된 매개변수로 이름을 기반으로 전달하여 중간 선택적 매개변수를 건너뛸 수 있습니다.

1
2
3
4
5
6
void Greet(string name, string prefix = "Hello", int times = 1) {
Console.WriteLine($"{prefix}, {name}");
}
Greet("Nick"); // 기본값 사용
Greet("Nick", "Hi");
Greet("Nick", times: 3); // 명명된 인수로 prefix 생략

ref / out / in

ref는 읽기/쓰기 참조 전달, out은 출력 전용(호출 전 초기화 불필요), in은 읽기 전용 참조 전달(큰 값 타입의 복사 회피).

1
2
3
4
5
6
7
void Ref(ref int x) => x++;
void Out(out int x) => x = 42;
void In(in int x) { /* 읽기 전용 */ }
int v = 1;
Ref(ref v); // v = 2
Out(out v); // v = 42
In(in v); // 제로카피 읽기 전용

메서드 오버로드

오버로드 = 같은 이름의 메서드이지만 매개변수 목록이 다름(개수/타입). 컴파일러가 인자에 따라 가장 일치하는 버전을 선택합니다. 반환 타입은 오버로드 결정에 참여하지 않습니다.

1
2
3
4
int Parse(string s) => int.Parse(s);
int Parse(int x) => x; // 오버로드:인수 타입이 다름
long Parse(string s, int radix) => // 오버로드:인수 개수가 다름
Convert.ToInt64(s, radix);

식 본문 멤버

메서드/속성/생성자가 단일 식이면 =>로 간결하게 작성할 수 있습니다. 람다와 비슷하지만, 식 본문 멤버는 진짜 멤버입니다.

1
2
3
4
5
public int Square(int x) => x * x;
public string Name => "Nick"; // 읽기 전용 속성
public void Reset() => Count = 0;
// 생성자에도 사용 가능:
class Box { public Box(string s) => Label = s; string Label { get; } }

로컬 함수

메서드 안에 정의된 함수는 외부 변수(클로저)에 접근할 수 있으며, 재귀 보조 함수나 반복기 내부 로직에 자주 사용됩니다.

1
2
3
4
5
int Factorial(int n) {
int Helper(int x) => x <= 1 ? 1 : x * Helper(x - 1);
return Helper(n);
}
// Helper는 이 메서드 안에서만 사용 가능

람다 식

익명 함수: 매개변수 => 식. 대리자/Func/Action과 함께 사용되며 LINQ의 핵심 구문입니다.

1
2
3
4
5
6
7
8
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Action<string> print = msg => Console.WriteLine(msg);
// 여러 줄 본문:
Func<int, int> twice = (x) => {
int r = x * 2;
return r;
};

대리자

대리자는 메서드 타입입니다. 시그니처를 선언하고, +=로 여러 메서드를 구독하며, 대리자를 호출하면 순서대로 발화합니다. 이벤트는 대리자를 기반으로 합니다.

1
2
3
4
5
delegate void Notify(string msg);
void Log(string m) => Console.WriteLine(m);
Notify n = Log; // 델리게이트가 메서드를 가리킴
n += m => Console.WriteLine("!" + m);
n("hi"); // 등록된 모든 메서드 순차 호출

7.문자열

문자열 리터럴, 보간, 일반 메서드, 가변 StringBuilder, 포맷팅.

문자열 리터럴

이중 따옴표 일반 문자열, @ 축자 문자열(이스케이프 처리 안 함), $ 보간, $$ 복합 리터럴(C# 11).

1
2
3
string s = "Line\n\tIndent"; // 이스케이프
string raw = @"C:\Program Files\"; // 축자(verbatim):\ 이스케이프 안 함
string path = "C:\\Program Files\\"; // 동등한 일반 표기

문자열 보간

$ 접두사는 {} 안의 식 결과를 문자열에 인라인합니다. 포맷과 정렬도 지정할 수 있어 + 연결보다 가독성이 좋습니다.

1
2
3
4
5
int age = 30;
string name = "Nick";
var msg = $"{name} is {age} years old";
var pad = $"{name,10}"; // 오른쪽 정렬 폭 10
var money = $"{age:C}"; // 통화 형식

연결과 비교

+ 연결, string.Concat 일괄 연결, string.Join 구분자 연결. 문자열 동등 비교는 ==(값 비교)로 합니다.

1
2
3
4
string a = "foo" + "bar"; // "foobar"
string all = string.Join(", ", names); // "a, b, c"
bool eq = a == "foobar"; // true(값 비교)
int cmp = string.Compare("a", "b"); // 사전순 비교

일반 메서드

Length, Substring, Contains, StartsWith, IndexOf, Replace, Trim, Split, ToUpper/ToLower는 대부분의 문자열 처리를 포괄합니다.

1
2
3
4
5
6
string s = " Hello, World ";
int len = s.Length; // 14
var sub = s.Substring(7, 5); // "World"
bool has = s.Contains("World"); // true
var rep = s.Replace("World", "C#");
var words = s.Trim().Split(","); // ["Hello", " World"]

StringBuilder

대량의 연결에는 StringBuilder를 사용하여 문자열을 반복 생성하지 않습니다(문자열은 불변이며 +는 매번 새 객체를 생성). 루프 내 연결에서 특히 두드러집니다.

1
2
3
4
5
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.Append(i).Append(",");
}
string result = sb.ToString(); // 한 번에 문자열로 변환

문자 char

char는 UTF-16 단일 문자입니다. char.IsDigit/IsLetter/IsWhiteSpace로 분류를 판정합니다. string은 불변이고 char는 반복 가능합니다.

1
2
3
4
5
6
char c = 'A';
bool digit = char.IsDigit(c); // false
bool upper = char.IsUpper(c); // true
foreach (char ch in "Hi") {
Console.WriteLine(ch); // 'H', 'i'
}

포맷팅

string.Format / $ 보간은 포맷 지정자를 사용합니다: D 정수, F 소수, C 통화, P 백분율, X 16진수.

1
2
3
4
5
double d = 1234.567;
Console.WriteLine($"{d:F2}"); // 1234.57
Console.WriteLine($"{d:C}"); // ¥1,234.57(로케일 의존)
Console.WriteLine($"{42:D5}"); // 00042
Console.WriteLine($"{255:X}"); // FF

문자열 파싱

Parse는 문자열을 숫자로 변환하며(실패 시 예외), TryParse는 안전합니다(bool과 out 결과를 반환). TryParse를 우선합니다.

1
2
3
4
int n = int.Parse("42"); // 예외를 던질 수 있음
bool ok = int.TryParse("42", out int v); // 안전
if (ok) Console.WriteLine(v);
// 그 외:int.Parse("ff", NumberStyles.HexNumber)

8.컬렉션과 LINQ

List/Dictionary/HashSet 등 일반 컬렉션, IEnumerable, LINQ 체인 쿼리.

List 동적 배열

List<T>는 가변 길이 배열로 Add/Insert/Remove/IndexOf가 있으며, 인덱스 접근은 O(1)입니다. foreach로 반복합니다.

1
2
3
4
5
6
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Insert(0, 0); // [0,1,2,3,4]
list.Remove(2);
int count = list.Count; // Length 대신 Count
bool any = list.Contains(4);

Dictionary 사전

Dictionary<K,V>는 키-값 매핑으로 키 기준 O(1) 조회. TryGetValue로 안전하게 가져오고, KeyValuePair를 반복합니다.

1
2
3
4
5
6
7
8
var dict = new Dictionary<string, int>();
dict["Nick"] = 30;
if (dict.TryGetValue("Nick", out int age)) {
Console.WriteLine(age);
}
foreach (var kv in dict) {
Console.WriteLine($"{kv.Key}: {kv.Value}");
}

HashSet 집합

HashSet<T>는 중복이 없고 O(1)의 포함 판정. UnionWith/IntersectWith/ExceptWith로 집합 연산을 수행하여 중복 제거 및 병합합니다.

1
2
3
4
5
6
var set = new HashSet<int> { 1, 2, 3 };
set.Add(3); // 이미 존재, 효과 없음
bool has = set.Contains(2);
set.UnionWith(new[] { 3, 4 }); // {1,2,3,4}
var common = new HashSet<int> { 2 };
common.IntersectWith(set); // {2}

Queue와 Stack

Queue<T>는 FIFO(Enqueue/Dequeue), Stack<T>는 LIFO(Push/Pop). 작업 큐나 실행 취소 스택 처리에 사용합니다.

1
2
3
4
5
6
var q = new Queue<int>();
q.Enqueue(1); q.Enqueue(2);
int next = q.Dequeue(); // 1
var s = new Stack<int>();
s.Push(1); s.Push(2);
int top = s.Pop(); // 2

IEnumerable과 지연

IEnumerable<T>는 읽기 전용 시퀀스 인터페이스이며, LINQ는 이를 기반으로 지연 평가를 수행합니다(반복 시 계산). 배열/List/Dictionary가 모두 구현합니다.

1
2
3
4
IEnumerable<int> nums = GetNums(); // 지연:아직 계산 안 됨
int sum = nums.Where(n => n > 0)
.Sum(); // 열거할 때 평가
// ToList()/ToArray()로 구체화, 즉시 전체 실행

LINQ 필터링과 투영

Where는 필터링, Select는 투영(매핑), OrderBy는 정렬, Distinct는 중복 제거. 메서드 체인은 가독성이 좋고 지연됩니다.

1
2
3
4
5
var adults = people
.Where(p => p.Age >= 18) // 필터
.OrderByDescending(p => p.Age) // 정렬
.Select(p => p.Name) // 프로젝션
.ToList(); // 구체화

LINQ 집계

Count/Sum/Average/Min/Max로 집계하고, Any/All로 판정하며, First/Single로 요소를 가져옵니다(일치 없으면 예외, FirstOrDefault는 기본값 반환).

1
2
3
4
5
6
7
int[] nums = { 1, 2, 3, 4 };
int sum = nums.Sum(); // 10
double avg = nums.Average(); // 2.5
bool anyEven = nums.Any(n => n % 2 == 0); // true
bool allPos = nums.All(n => n > 0); // true
int first = nums.First(n => n > 2); // 3
int? maybe = nums.FirstOrDefault(n => n > 9); // null

GroupBy 그룹화

GroupBy는 키로 그룹화하여 IGrouping 시퀀스를 생성합니다. 통계(범주별 카운트, 합계)에 자주 사용됩니다.

1
2
3
4
5
6
7
8
var stats = orders
.GroupBy(o => o.Region)
.Select(g => new {
Region = g.Key,
Total = g.Sum(o => o.Amount),
Count = g.Count(),
})
.ToList();

LINQ 쿼리 구문

from/where/orderby/select 쿼리 구문은 메서드 체인의 선언적 작성법이며 컴파일 후 동등합니다. 가독성 선호의 문제입니다.

1
2
3
4
5
6
7
8
var adults = from p in people
where p.Age >= 18
orderby p.Age descending
select p.Name;
// 동등한 메서드 체인:
var same = people.Where(p => p.Age >= 18)
.OrderByDescending(p => p.Age)
.Select(p => p.Name);

9.메모리와 리소스 관리

GC 자동 회수, IDisposable과 using으로 비관리 리소스 해제, 약한 참조, 객체 풀.

GC 가비지 컬렉션

C# 메모리는 GC가 자동으로 관리합니다. 힙의 객체가 더 이상 참조되지 않으면 회수됩니다. 세대별 회수(0/1/2)가 성능을 최적화합니다.

1
2
3
var obj = new object(); // 힙 할당
// 참조가 없어지면 GC가 자동 회수, 수동 free 불필요
GC.Collect(); // 강제 회수(보통 수동 호출 안 함)

IDisposable 인터페이스

비관리 리소스(파일/네트워크/DB 연결)를 보유한 클래스는 IDisposable을 구현하고 Dispose에서 리소스를 해제합니다.

1
2
3
4
5
6
7
class FileWriter : IDisposable {
private StreamWriter? _sw;
public void Dispose() {
_sw?.Dispose(); // 비관리 리소스 해제
}
}
using (var w = new FileWriter()) { } // 자동 Dispose

using 문과 선언

using은 스코프 종료 시 Dispose를 보장합니다(finally 의미론). using 선언(C# 8)은 블록 종료 시 리소스를 자동 해제합니다.

1
2
3
4
5
using var reader = new StreamReader("file.txt");
string line = reader.ReadLine();
// 스코프 종료 시 자동 Dispose, 명시 불필요
// 동등한 전통적 표기:
using (var r = new StreamReader("file.txt")) { }

종료자

소멸자 ~Class()는 종료자입니다. GC 회수 전에 호출되지만 시점을 보장할 수 없습니다. 비관리 리소스 해제에만 사용하고, 일반적으로는 Dispose 패턴을 따릅니다.

1
2
3
4
5
6
class Resource {
~Resource() {
// GC 전에 호출, 시점 불확정——의존하지 말 것
// IDisposable을 구현해 명시적 해제 필요
}
}

약한 참조

WeakReference는 GC의 대상 회수를 막지 않으며, 캐시(무거운 객체의 사전 캐시, 회수 허용)에 사용됩니다. Target은 언제든지 null이 될 수 있습니다.

1
2
3
4
5
var weak = new WeakReference(new byte[1024]);
if (weak.IsAlive) {
var data = (byte[])weak.Target!; // 이미 회수됐을 수 있음
}
// GC 후 IsAlive == false가 될 수 있음

객체 풀과 ArrayPool

큰 배열을 자주 할당할 때는 ArrayPool로 버퍼를 재사용하여 GC 압력을 줄입니다. ArrayPool<T>.Shared.Rent와 Return을 쌍으로 사용합니다.

1
2
3
4
5
6
7
byte[] buf = ArrayPool<byte>.Shared.Rent(1024);
try {
// buf 사용
} finally {
ArrayPool<byte>.Shared.Return(buf);
}
// Rent 배열은 요청보다 클 수 있음, 사용 후 반드시 Return

stackalloc 스택 할당

stackalloc은 스택에 메모리를 할당하며 빠르고 GC를 발생시키지 않습니다. 작은 임시 버퍼에 적합하지만 스택 공간은 제한적이므로 주의하세요.

1
2
3
4
5
6
int length = 4;
// stackalloc은 스택에 할당, 빠르고 GC 없음
Span<int> buffer = stackalloc int[length];
buffer[0] = 42;
foreach (var n in buffer) { Console.WriteLine(n); }
// 스택 공간은 한정, 작은 임시 버퍼에만 적합

GC 메모리 압력

대량의 네이티브 메모리를 할당할 때는 GC.AddMemoryPressure로 GC에 알려 회수기가 관리 객체를 즉시 회수하게 하여 메모리 사용량 통제를 막습니다.

1
2
3
4
5
6
7
8
9
10
var size = 64 * 1024 * 1024; // 64 MB
IntPtr buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal(size);
GC.AddMemoryPressure(size); // GC에 네이티브 메모리 압력 알림
try {
// 네이티브 버퍼 사용(예:1바이트 쓰기)
System.Runtime.InteropServices.Marshal.WriteByte(buffer, 0, 1);
} finally {
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
GC.RemoveMemoryPressure(size); // 해제 후 압력 제거
}

10.객체 지향

클래스, 속성, 생성자, 상속, 다형성, 인터페이스, 추상 클래스, 접근 한정자.

클래스와 객체

class는 데이터 타입(참조 타입)을 정의합니다. 필드는 상태를 저장하고, 속성은 접근을 제어하며, 메서드는 동작을 정의하고, 생성자는 초기화합니다.

1
2
3
4
5
6
7
8
9
public class Person {
public string Name { get; set; } = "";
public int Age { get; set; }
public Person(string name, int age) {
Name = name; Age = age;
}
public string Greet() => $"Hi, {Name}";
}
var p = new Person("Nick", 30);

속성

속성은 필드의 안전한 접근자입니다. get은 읽기, set은 쓰기이며 접근 한정자와 검증 로직을 추가할 수 있습니다. 컴파일러가 백킹 필드를 생성합니다.

1
2
3
4
5
6
7
8
9
10
private int _age;
public int Age {
get => _age;
set {
if (value < 0) throw new ArgumentOutOfRangeException();
_age = value;
}
}
// 자동 속성:
public string Name { get; set; } = ""

상속

C#는 단일 상속입니다. class는 :로 하나의 기본 클래스를 상속합니다. 파생 클래스는 기본 클래스의 is-a입니다. private 멤버는 상속되지 않고, protected는 접근 가능합니다.

1
2
3
4
5
6
7
8
9
public class Animal {
public string Name { get; set; } = "";
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal {
public override void Speak() => Console.WriteLine("Woof");
}
Animal a = new Dog();
a.Speak(); // "Woof"(다형성)

다형성

virtual과 override로 다형성을 구현합니다. 기본 클래스 참조로 호출된 메서드는 실제 타입에 따라 디스패치됩니다. 메서드는 virtual이어야만 재정의될 수 있습니다.

1
2
3
4
5
6
7
8
9
public class Shape {
public virtual double Area() => 0;
}
public class Circle : Shape {
public double R { get; set; }
public override double Area() => Math.PI * R * R;
}
Shape s = new Circle { R = 2 };
double area = s.Area(); // 12.57(실제 타입에 따라)

추상 클래스

abstract 클래스는 인스턴스화할 수 없으며 abstract 메서드(파생 클래스가 반드시 구현)를 포함할 수 있습니다. 추상 메서드는 본문이 없습니다. 템플릿 기본 클래스에 사용됩니다.

1
2
3
4
5
6
7
8
9
public abstract class Shape {
public abstract double Area(); // 구현 없음
public void Describe() =>
Console.WriteLine($"Area: {Area()}");
}
public class Square : Shape {
public double Side { get; set; }
public override double Area() => Side * Side;
}

인터페이스

interface는 계약을 정의합니다. 멤버는 구현이 없으며 구현 클래스가 모두 제공합니다. C#는 다중 인터페이스 구현을 지원합니다(다중 상속의 대안).

1
2
3
4
5
6
7
8
public interface ILogger {
void Log(string msg);
}
public class ConsoleLogger : ILogger {
public void Log(string msg) => Console.WriteLine(msg);
}
ILogger logger = new ConsoleLogger();
logger.Log("hi"); // 인터페이스 지향 프로그래밍

접근 한정자

public은 공개, private은 비공개, protected는 파생 클래스에 표시, internal은 동일 어셈블리 내 표시. 클래스 기본값은 private, 멤버 기본값은 private.

1
2
3
4
5
6
public class Account {
public decimal Balance { get; private set; } // 외부 읽기, 내부 쓰기
private int _transactions; // 이 클래스만
protected void Audit() { } // 파생 클래스만
}
// internal:어셈블리 내에서 가시(최상위 클래스 기본)

sealed와 object 메서드

sealed 클래스는 상속할 수 없으며, sealed override 메서드는 더 이상 재정의할 수 없습니다. 모든 클래스는 암묵적으로 object(ToString/Equals/GetHashCode)를 상속합니다.

1
2
3
4
5
6
7
8
public sealed class Config { } // 상속 금지
public class Base {
public virtual void Run() { }
}
public class Child : Base {
public override void Run() { } // 오버라이드 가능
}
// Child에서 Run을 sealed override하면 손자 클래스에서 재정의 불가

정적 클래스와 확장 메서드

static class는 정적 멤버만 포함할 수 있으며 인스턴스화할 수 없습니다. 확장 메서드는 정적 클래스 안의 정적 메서드로, this 매개변수로 인스턴스에서 호출할 수 있습니다.

1
2
3
4
5
6
public static class StringExt {
public static bool IsEmpty(this string s) =>
string.IsNullOrEmpty(s);
}
string s = "";
bool empty = s.IsEmpty(); // 인스턴스 문법으로 확장 메서드 호출

11.예외 처리

try/catch/finally, 예외 타입, 사용자 정의 예외, 예외의 비용, 모범 사례.

try / catch / finally

try는 오류가 발생할 수 있는 코드를 담고, catch는 잡아 처리하며, finally는 성공/실패와 관계없이 실행됩니다(리소스 정리). 예외는 상위로 전파됩니다.

1
2
3
4
5
6
7
try {
int.Parse("abc");
} catch (FormatException ex) {
Console.WriteLine(ex.Message); // 오류 처리
} finally {
Console.WriteLine("cleanup"); // 항상 실행됨
}

다중 catch와 예외 필터

여러 catch는 타입으로 매치되며 구체적인 예외가 먼저 옵니다. when으로 필터 조건을 지정합니다. 변수를 선언하지 않는 catch는 예외 객체를 무시합니다.

1
2
3
4
5
6
try {
Process();
} catch (FileNotFoundException ex) when (ex.FileName == "cfg")
{ /* 필터:파일명이 일치하는 경우만 */ }
catch (IOException ex) { /* 더 넓은 예외 */ }
catch { /* 모두 포착 */ }

예외 던지기

throw new는 예외를 던지고, throw;는 인자 없이 그대로 재던지며(스택 유지), catch에서 new 후 throw는 스택을 재설정합니다.

1
2
3
4
5
6
throw new ArgumentException("invalid value", nameof(value));
try { ... }
catch (Exception ex) {
// throw; // 스택 유지
// throw ex; // 스택 재설정(정보 유실, 피할 것)
}

사용자 정의 예외

사용자 정의 예외는 Exception을 상속하며(관례상 접미사 Exception 사용), 생성자를 제공하며, 내부 예외를 InnerException으로 보존합니다.

1
2
3
4
5
6
7
public class ConfigException : Exception {
public ConfigException() { }
public ConfigException(string message) : base(message) { }
public ConfigException(string message, Exception inner)
: base(message, inner) { }
}
throw new ConfigException("bad config");

finally 정리

finally는 try의 예외 발생 여부와 관계없이 리소스 해제/상태 복원을 보장합니다. return도 finally를 먼저 실행합니다. using은 그것의 구문 설탕입니다.

1
2
3
4
5
6
7
try {
lockObj.Enter();
DoWork();
} finally {
lockObj.Exit(); // 성패와 무관하게 잠금 해제
}
// return은 finally 이후에 반환, finally는 반드시 실행

예외의 비용

예외 포획은 비용이 크므로 제어 흐름으로 사용하면 안 됩니다. 예측 가능한 오류는 반환 코드/TryXxx/Result 패턴으로 처리합니다.

1
2
3
4
5
6
// 느림:예외로 흐름 제어
bool ok1;
try { int.Parse(s); ok1 = true; }
catch { ok1 = false; }
// 빠름:TryParse가 결과 직접 반환
bool ok2 = int.TryParse(s, out _);

전역 예외 처리

최상위에서 try/catch로 미처리 예외를 잡고 로그를 남깁니다. ASP.NET에서는 미들웨어(UseExceptionHandler)로 일괄 처리합니다.

1
2
3
4
5
6
7
try {
await RunAsync();
} catch (Exception ex) {
Log(ex); // 기록
Environment.ExitCode = 1; // 실패 표시
}
// ASP.NET Core:app.UseExceptionHandler(...)

InnerException 체인

예외를 잡은 후 새 예외를 던지고 원본 예외를 InnerException으로 전달하여 전체 오류 체인을 보존하면, 로그 분석에 더 유용합니다.

1
2
3
4
5
6
7
try {
LoadConfig();
} catch (FileNotFoundException ex) {
// 예외를 감싸고 원인 유지
throw new InvalidOperationException("配置缺失", ex);
}
// 조사:ex.InnerException으로 근본 원인까지 추적

12.파일과 I/O

File/Directory/Path 정적 유틸리티, Stream 읽기/쓰기, 비동기 I/O, 바이너리 처리.

텍스트 파일 읽기/쓰기

File.ReadAllText/WriteAllText는 작은 파일을 한 번에 읽기/쓰기하며, ReadAllLines는 줄 단위 배열을 반환합니다. 큰 파일에는 StreamReader를 사용합니다.

1
2
3
4
5
string content = File.ReadAllText("in.txt");
File.WriteAllText("out.txt", content);
string[] lines = File.ReadAllLines("in.txt");
File.WriteAllLines("out.txt", lines);
// 인코딩:File.ReadAllText(path, Encoding.UTF8)

StreamReader 줄 단위

큰 파일은 줄 단위로 스트리밍 읽기하여 메모리를 차지하지 않습니다. StreamReader.ReadLine은 null(파일 끝)까지 루프합니다.

1
2
3
4
5
6
using var reader = new StreamReader("big.log");
string? line;
while ((line = reader.ReadLine()) != null) {
Process(line);
}
// using으로 읽고 나면 자동 해제

디렉터리 작업

Directory는 디렉터리 생성/삭제/열거, Directory.GetFiles/EnumerateFiles는 파일 검색. 재귀 열거에는 SearchOption을 사용합니다.

1
2
3
4
5
6
Directory.CreateDirectory("data");
if (Directory.Exists("data")) { }
string[] files = Directory.GetFiles("data", "*.txt");
// 재귀:
var all = Directory.EnumerateFiles("data", "*",
SearchOption.AllDirectories);

Path 경로 처리

Path는 크로스 플랫폼 경로 결합/파싱을 합니다: Combine, GetExtension, GetFileName, ChangeExtension. 경로 구분 기호를 손으로 작성하지 마세요.

1
2
3
4
string dir = Path.Combine("data", "files");
string ext = Path.GetExtension("a.txt"); // ".txt"
string name = Path.GetFileName("/a/b.txt"); // "b.txt"
string changed = Path.ChangeExtension("a.txt", ".md");

비동기 I/O

async/await I/O는 스레드를 차단하지 않습니다. ReadAllTextAsync/WriteAllTextAsync/Stream 메서드. UI/서버 동시성 시나리오에서 필수입니다.

1
2
3
4
5
6
string text = await File.ReadAllTextAsync("in.txt");
await File.WriteAllTextAsync("out.txt", text);
// async Main / async 메서드 내에서 사용 가능
using var reader = new StreamReader("big.log");
string? line;
while ((line = await reader.ReadLineAsync()) != null) { }

바이너리 읽기/쓰기

BinaryWriter/BinaryReader는 타입별로 읽기/쓰고, MemoryStream은 메모리 내 스트림입니다. 네트워크와 파일의 바이너리 프로토콜에서 흔히 사용됩니다.

1
2
3
4
5
6
7
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
w.Write(42); w.Write("data");
ms.Position = 0;
using var r = new BinaryReader(ms);
int n = r.ReadInt32();
string s = r.ReadString();

콘솔 I/O

Console.ReadLine은 한 줄을, ReadKey는 키 입력을, WriteLine은 출력을 담당합니다. Console.In/Out 스트림을 리디렉션하여 테스트할 수 있습니다.

1
2
3
4
5
Console.Write("Name: ");
string? name = Console.ReadLine();
var key = Console.ReadKey(); // 단일 키 입력 읽기
Console.WriteLine($"Hello {name} ({key.KeyChar})");
// 입력 리다이렉트:dotnet run < input.txt

파일 정보

FileInfo/DirectoryInfo는 파일 메타데이터와 메서드를 제공하며, File.Exists는 존재를 확인하고, GetCreationTime은 타임스탬프를 반환합니다.

1
2
3
4
5
6
7
var fi = new FileInfo("a.txt");
if (fi.Exists) {
long bytes = fi.Length;
DateTime created = fi.CreationTime;
fi.CopyTo("b.txt");
}
bool ok = File.Exists("a.txt");

13.일반적인 함정(FAQ)

C# 개발자가 가장 자주 빠지는 함정: 값 타입 vs 참조, 문자열 동등성, LINQ 지연, async/await 데드락 등.

문자열 == vs Equal

C#의 string ==는 값으로 비교합니다(Java와 다름). 다만 ==는 서수 비교를 사용하고, Equals는 StringComparison을 지정하여 대소문자 무시 등을 수행할 수 있습니다.

1
2
3
4
5
// BAD:기본 정렬 비교에 의존, 대소문자에 따라 예상 밖 결과
bool bad = a == "nick";
// GOOD:비교 규칙 명시
bool good = a.Equals("nick", StringComparison.OrdinalIgnoreCase);

값 전달 vs 참조 전달

메서드 매개변수는 기본적으로 값으로 전달됩니다. 객체는 참조 복사본이 전달되어 멤버를 수정하면 원본에 영향을 주고, 값 타입은 복사됩니다. 이 점을 잊는 것은 흔한 버그입니다.

1
2
3
4
5
6
7
8
9
// BAD:List가 값 전달이라 수정되지 않을 거라 오해
void Bad(List<int> l) => l.Add(1);
var list = new List<int>();
Bad(list); // 실제로 list가 수정됨
// GOOD:참조 전달을 인지하거나 ref로 의도 표현
void Good(ref List<int> l) => l.Add(1);
var list2 = new List<int>();
Good(ref list2);

LINQ 지연 평가

LINQ 쿼리는 지연됩니다. 열거하기 전에는 계산되지 않습니다. IQueryable/IEnumerable을 외부로 전달한 뒤 열거하면 부작용이나 오래된 데이터가 섞일 수 있습니다.

1
2
3
4
5
6
// BAD:LINQ는 지연 평가, list를 바꾸면 쿼리 결과 변경
var q = list.Where(n => n > 0);
list.Add(5); // q 결과가 달라짐
// GOOD:구체화해 스냅샷
var snapshot = list.Where(n => n > 0).ToList();

async void와 데드락

이벤트 핸들러는 async void를 사용할 수 있지만 일반 메서드는 Task를 반환해야 합니다. sync 컨텍스트(UI/WinForms)에서 .Result/.Wait()는 데드락을 유발합니다.

1
2
3
4
5
6
// BAD:async void 메서드, 예외 포착 불가, 테스트 취약
async void Bad() { await Task.Delay(10); }
// GOOD:Task 반환, await로 예외 전파
async Task Good() { await Task.Delay(10); }
// UI에서는 .Result로 블로킹하지 말 것

예외 삼키기와 재던지기

catch 후 처리하지 않으면 예외를 삼키고, throw ex는 스택을 재설정하여 원래 예외 체인을 잃게 합니다. 로그에는 반드시 InnerException을 남겨야 합니다.

1
2
3
4
5
6
7
8
// BAD:예외 삼키면 디버깅 불가
catch (Exception ex) { }
// GOOD:기록하거나 그대로 재던지기
catch (Exception ex) {
_logger.LogError(ex, "failed");
throw; // 스택 유지
}

리소스 미해제

Stream/HttpClient/DB 연결을 해제하지 않으면 리소스가 누수됩니다. IDisposable은 항상 using을 사용하거나 사용 후 Dispose합니다.

1
2
3
4
5
6
7
// BAD:Stream 미해제
var sr = new StreamReader("a.txt");
string t = sr.ReadToEnd();
// GOOD:using으로 해제 보장
using var good = new StreamReader("a.txt");
string t2 = good.ReadToEnd();

부동소수점 비교

float/double은 이진 표현이 정확하지 않아 직접 == 비교하면 예상치 못한 결과가 나옵니다. 엡실론 허용차를 사용하거나 정확한 연산에는 decimal을 사용하세요.

1
2
3
4
5
6
// BAD:부동소수점 정확 동등은 신뢰 불가
bool bad = (0.1 + 0.2) == 0.3; // false
// GOOD:허용 오차 비교
bool good = Math.Abs((0.1 + 0.2) - 0.3) < 1e-9;
// 금액은 decimal:0.1m + 0.2m == 0.3m은 true

반복 중 컬렉션 수정

foreach에서 컬렉션을 Add/Remove하면 InvalidOperationException이 던져집니다. 삭제할 요소를 수집한 후 반복 후 삭제하세요.

1
2
3
4
5
6
7
8
// BAD:foreach 중 Remove는 예외 발생
foreach (var x in list) {
if (x < 0) list.Remove(x);
}
// GOOD:모아서 일괄 삭제
var toRemove = list.Where(x => x < 0).ToList();
foreach (var x in toRemove) list.Remove(x);

Nullable과 Null 참조

Nullable 활성화 후 컴파일러가 null 참조 발견을 도와줍니다. null을 직접 반환하면 모호하므로 ?? / ?.로 폴백을 제공하세요.

1
2
3
4
5
6
7
// BAD:Nullable 꺼짐 + 직접 역참조
string s = GetMaybe();
Console.WriteLine(s.Length); // NullReference 가능
// GOOD:null 허용 어노테이션 + 널 병합
string? maybe = GetMaybe();
Console.WriteLine((maybe ?? "").Length);

로컬 시간 vs UTC

저장/전송하는 시간은 UTC(DateTimeKind.Utc)를 사용하고 표시 시 로컬로 변환합니다. Kind를 혼용하면 조용히 시간이 잘못 계산됩니다.

1
2
3
4
5
6
7
// BAD:기본 Kind 미지정, 왕복 변환 시 오류
var now = DateTime.Now;
// GOOD:UTC로 명확히 저장
var utc = DateTime.UtcNow;
var local = utc.ToLocalTime();
// DateTimeOffset은 오프셋 내장, 시간대 넘나들 때 추천

14.동시성과 비동기

Task 기반 비동기 프로그래밍, Parallel 병렬, lock 동기화, 스레드 안전 컬렉션.

async / await

async 메서드는 Task를 반환하고, await는 스레드를 양보했다가 완료 후 재개합니다. 그 사이에 스레드는 다른 작업에 사용되며 차단되지 않습니다.

1
2
3
4
5
6
async Task<string> FetchAsync() {
// await는 스레드 양보, 블로킹 없음
var data = await httpClient.GetStringAsync(url);
return data;
}
string result = await FetchAsync(); // 호출 측도 await

Task와 반환값

Task는 비동기 작업을 나타내고, Task<T>는 반환값을 가집니다. Task.Run은 동기 작업을 스레드 풀에 넣고, ContinueWith로 체이닝합니다.

1
2
3
4
5
Task<int> t = Task.Run(() => Compute());
int result = await t;
var t2 = Task.Run(async () => await FetchAsync());
// Task.WhenAll:여러 개 병렬 대기
var all = await Task.WhenAll(t, t2);

Parallel 병렬

Parallel.For/ForEach는 스레드 풀을 사용해 독립적인 작업을 병렬 실행합니다. CPU 집약적 작업은 멀티 코어를 활용할 수 있지만 스레드 안전성과 과도한 병렬화에 주의하세요.

1
2
3
4
5
Parallel.For(0, 100, i => {
Process(i); // 병렬 실행
});
Parallel.ForEach(items, item => Process(item));
// 최대 병렬도 제어:new ParallelOptions { MaxDegreeOfParallelism = 4 }

lock 동기화

lock은 동시에 하나의 스레드만 임계 영역에 들어가도록 보장합니다. 잠금 객체는 보통 private readonly 필드이며, this 잠금은 피하세요.

1
2
3
4
5
6
7
8
private readonly object _lock = new();
int _counter = 0;
void Increment() {
lock (_lock) {
_counter++; // 원자:멀티스레드 안전
}
}
// lock은 Monitor.Enter/Exit의 문법 설탕

Thread 스레드

Thread는 스레드를 수동 관리합니다. Start로 시작, Join으로 대기. 대부분의 시나리오에서는 Task/Parallel이 더 적합하며, Thread는 장기 백그라운드 작업에 사용합니다.

1
2
3
4
5
6
var thread = new Thread(() => {
Console.WriteLine("worker");
});
thread.IsBackground = true; // 백그라운드 스레드:메인 스레드 종료 시 중단
thread.Start();
thread.Join(); // 완료 대기

CancellationToken

CancellationToken은 협력적 취소입니다. 토큰이 IsCancellationRequested를 트리거하고, async 메서드는 OperationCanceledException을 던집니다.

1
2
3
4
5
6
7
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));
var data = await httpClient.GetStringAsync(url,
cts.Token);
try { await SlowAsync(cts.Token); }
catch (OperationCanceledException) { /* 취소됨 */ }
// cts.Token.ThrowIfCancellationRequested()로 직접 확인

동시성 컬렉션

스레드 안전 컬렉션: ConcurrentDictionary, ConcurrentQueue, ConcurrentBag. 일반 컬렉션에 수동으로 잠금을 거는 것을 대체합니다.

1
2
3
4
5
6
var dict = new ConcurrentDictionary<string, int>();
dict.TryAdd("a", 1);
dict.TryUpdate("a", 2, 1); // 조건부 업데이트
var queue = new ConcurrentQueue<int>();
queue.Enqueue(1);
if (queue.TryDequeue(out int v)) { }

비동기 모범 사례

Async all the way: .Result로 차단하지 마세요. 라이브러리 코드에서는 ConfigureAwait(false)를 사용하여 동기 컨텍스트로 돌아가지 않도록 합니다.

1
2
3
4
5
6
7
// 전체를 await 체인으로
async Task<string> OuterAsync() {
var data = await InnerAsync().ConfigureAwait(false);
return data;
}
// 라이브러리/비 UI 계층은 ConfigureAwait(false)
// UI 이벤트 핸들러는 컨텍스트 유지(붙이지 않음)

15.네트워킹과 HTTP

HttpClient로 요청 보내기, JsonSerializer로 직렬화, WebSocket, Socket.

HttpClient 기본

HttpClient는 HTTP 요청을 보냅니다. GetStringAsync는 단순 텍스트, GetAsync는 완전한 응답. HttpClient는 장기(싱글톤) 재사용해야 합니다.

1
2
3
4
5
6
7
var http = new HttpClient();
string html = await http.GetStringAsync("https://example.com");
var resp = await http.GetAsync("https://example.com/api");
if (resp.IsSuccessStatusCode) {
string body = await resp.Content.ReadAsStringAsync();
}
// DI/IHttpClientFactory:인스턴스 장기 재사용 권장

POST와 JSON

PostAsJsonAsync는 JSON을 보내고, PostAsync는 사용자 정의 콘텐츠를 보냅니다. 응답은 ReadAsStringAsync로 읽습니다. 직렬화에는 JsonSerializer를 사용합니다.

1
2
3
4
5
6
7
var data = new { Name = "Nick", Age = 30 };
var resp = await http.PostAsJsonAsync(url, data);
// JSON 응답 읽기:
var person = await resp.Content
.ReadFromJsonAsync<Person>();
// 수동 직렬화:
string json = JsonSerializer.Serialize(data);

헤더와 쿼리 매개변수

HttpRequestMessage는 Headers(인증/User-Agent)를 설정하고, URI는 쿼리 문자열을 구성합니다. 기본 User-Agent를 거부하는 API가 있음에 주의하세요.

1
2
3
4
5
6
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.Add("Authorization", "Bearer token");
req.Headers.Add("User-Agent", "MyApp/1.0");
var uri = new UriBuilder(url);
uri.Query = "key=value&page=2"; // 쿼리 문자열 수동 조립
var resp = await http.SendAsync(req);

System.Text.Json

JsonSerializer.Serialize/Deserialize는 JSON을 직렬화합니다. JsonSerializerOptions는 대소문자/열거형/null 무시를 구성합니다.

1
2
3
4
5
6
7
var opts = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
string json = JsonSerializer.Serialize(obj, opts);
var back = JsonSerializer.Deserialize<T>(json, opts);

Web API 호출

조합: 요청 구성 → 상태 검증 → 역직렬화. HttpStatusCode 검사, ReadFromJsonAsync로 한 줄에 처리합니다.

1
2
3
4
5
6
7
using var resp = await http.GetAsync($"api/users/{id}");
if (resp.StatusCode == HttpStatusCode.NotFound) {
return null;
}
resp.EnsureSuccessStatusCode(); // 2xx 외에는 예외 발생
var user = await resp.Content.ReadFromJsonAsync<User>();
return user;

WebSocket

ClientWebSocket은 전이중 채널을 설정하고, SendAsync/ReceiveAsync로 송수신합니다. 실시간 푸시(채팅, 시세)에 적합합니다.

1
2
3
4
5
6
7
8
using var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, CancellationToken.None);
var buffer = new byte[1024];
var result = await ws.ReceiveAsync(buffer,
CancellationToken.None);
string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
await ws.SendAsync(payload, WebSocketMessageType.Text,
true, CancellationToken.None);

DNS와 Socket

Dns.GetHostAddresses는 도메인을 해석하고, Socket은 하위 전송입니다. 대부분의 응용 프로그램은 HttpClient로 충분하며, Socket은 사용자 정의 프로토콜용입니다.

1
2
3
4
5
var ips = await Dns.GetHostAddressesAsync("example.com");
// 저수준 Socket:
using var socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync("example.com", 80);

다운로드와 스트림 처리

큰 파일 다운로드에는 Stream으로 스트리밍 쓰기하여 메모리를 차지하지 않습니다. HttpCompletionOption.ResponseHeadersRead는 스트림을 즉시 반환합니다.

1
2
3
4
5
using var resp = await http.GetAsync(url,
HttpCompletionOption.ResponseHeadersRead);
await using var stream = await resp.Content.ReadAsStreamAsync();
await using var file = File.Create("download.bin");
await stream.CopyToAsync(file); // 스트리밍으로 기록, 큰 메모리 사용 안 함

16.날짜와 시간

DateTime과 TimeSpan, 시간대와 오프셋, 포맷팅과 파싱.

DateTime 기본

DateTime은 날짜/시각을 나타냅니다. Now는 로컬, UtcNow는 UTC, Today는 날짜. AddDays/AddHours로 날짜 연산을 수행합니다.

1
2
3
4
5
DateTime now = DateTime.Now;
DateTime utc = DateTime.UtcNow;
DateTime today = DateTime.Today; // 당일 00:00
DateTime nextWeek = now.AddDays(7);
var str = now.ToString("yyyy-MM-dd HH:mm");

TimeSpan 시간 간격

TimeSpan은 간격을 나타냅니다. 두 시각의 차이로 얻고, FromDays/FromHours로 구축하며, Days/Hours로 구성 요소를 분해합니다.

1
2
3
4
5
TimeSpan duration = end - start;
double hours = duration.TotalHours; // 총 시간수(일수 포함)
int h = duration.Hours; // 시간 부분만
var delay = TimeSpan.FromMinutes(5);
if (duration > TimeSpan.Zero) { }

DateTimeOffset

DateTimeOffset은 시간대 오프셋을 가지며 시간대를 넘나드는 시나리오에 권장됩니다. UTC와 안전하게 상호 변환되며 로컬 시간대 모호성을 피합니다.

1
2
3
4
5
DateTimeOffset now = DateTimeOffset.Now;
DateTimeOffset utc = DateTimeOffset.UtcNow;
var offset = new DateTimeOffset(2024, 1, 1, 12, 0, 0,
TimeSpan.FromHours(8)); // +08:00
DateTime utcTime = now.ToUniversalTime();

DateTimeKind

DateTime에는 Kind가 있습니다: Unspecified/Local/Utc. Kind 미지정 로컬 시간을 UTC로 변환하면 잘못 동작하므로 Utc로 저장하세요.

1
2
3
4
5
DateTime local = DateTime.Now; // Kind = Local
DateTime utc = DateTime.UtcNow; // Kind = Utc
DateTime utcFromLocal = local.ToUniversalTime();
// 주의:new DateTime(...)은 기본 Unspecified
// 저장/전송은 UTC나 DateTimeOffset으로 통일

파싱과 포맷팅

DateTime.Parse/ParseExact로 파싱, TryParse는 안전합니다. 포맷 지정자: yyyy/MM/dd, HH:mm:ss, ffff는 밀리초.

1
2
3
4
5
var d = DateTime.Parse("2024-01-01");
bool ok = DateTime.TryParse("2024-01-01", out var dt);
var exact = DateTime.ParseExact("01/02/2024",
"MM/dd/yyyy", CultureInfo.InvariantCulture);
string s = dt.ToString("O"); // ISO 8601 왕복 안전

시간대 변환

TimeZoneInfo는 시간대 변환을 수행합니다: FindSystemTimeZoneById, ConvertTimeFromUtc. 서버는 보통 UTC로 저장하고 표시 시 로컬로 변환합니다.

1
2
3
4
5
DateTime utc = DateTime.UtcNow;
TimeZoneInfo shanghai =
TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
DateTime local = TimeZoneInfo.ConvertTimeFromUtc(utc, shanghai);
// 고정 오프셋이면 DateTimeOffset이 더 간단

Stopwatch 측정

Stopwatch는 고정밀(밀리초 단위) 시간 측정을 합니다. Start/Stop/Elapsed/ElapsedMilliseconds는 성능 측정의 표준입니다.

1
2
3
4
5
6
var sw = Stopwatch.StartNew();
DoWork();
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
// 또는 sw.Elapsed.TotalMilliseconds
// Restart()로 타이머 재사용

DateOnly와 TimeOnly

DateOnly는 날짜만, TimeOnly는 시각만 나타내는 타입(C# 10)이며 시간대 부담이 없습니다. 생일, 달력, 스케줄에 적합합니다.

1
2
3
4
5
6
DateOnly today = DateOnly.FromDateTime(DateTime.Now);
DateOnly birthday = new(2000, 6, 15);
TimeOnly start = new(9, 30);
TimeOnly end = new(18, 0);
TimeSpan span = end - start; // 8.5시간
Console.WriteLine(today); // 오늘 날짜 출력

17.프로세스와 시스템

외부 프로세스 시작, 환경 변수, 경로, 시스템 정보.

프로세스 시작

Process.Start는 외부 프로그램(셸 명령, 다른 실행 파일)을 시작합니다. ArgumentList로 인자를 전달하면 문자열 연결로 인한 인젝션을 피할 수 있습니다.

1
2
3
4
5
6
var psi = new ProcessStartInfo("git") {
ArgumentList = { "log", "-1" }, // 인수 배열, 인젝션 없음
RedirectStandardOutput = true, // 출력 캡처
};
using var proc = Process.Start(psi)!;
string output = await proc.StandardOutput.ReadToEndAsync();

환경 변수

Environment.GetEnvironmentVariable로 읽고, SetEnvironmentVariable로 씁니다(프로세스 레벨). GetEnvironmentVariables로 모두 가져옵니다.

1
2
3
4
5
string? path = Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("MY_VAR", "value");
// 시스템 레벨:
Environment.SetEnvironmentVariable("MY_VAR", "v",
EnvironmentVariableTarget.Machine);

시스템 정보

Environment는 시스템 정보를 제공합니다: OSVersion, MachineName, CurrentDirectory, ProcessorCount, TickCount.

1
2
3
4
5
string os = Environment.OSVersion.ToString();
string machine = Environment.MachineName;
string dir = Environment.CurrentDirectory;
int cores = Environment.ProcessorCount;
string user = Environment.UserName;

명령줄 파싱

args는 명령줄 인자를 전달하고, Environment.GetCommandLineArgs는 완전한 세트(프로그램 이름 포함)를 가져옵니다. CLI 도구에서 인자를 파싱합니다.

1
2
3
4
5
6
7
// Program.cs 최상위
if (args.Length == 0) {
Console.Error.WriteLine("usage: app <file>");
return 1;
}
// 처음 두 문자가 --면 옵션 취급(직접 구현)
foreach (var a in args.Skip(1)) { /* 처리 */ }

종료와 시그널

Environment.Exit(1)은 즉시 종료, ExitCode는 종료 코드를 설정합니다. AppDomain.ProcessExit 이벤트로 정리를 수행합니다.

1
2
3
4
5
AppDomain.CurrentDomain.ProcessExit += (s, e) => {
SaveState(); // 종료 전 정리
};
Environment.ExitCode = 2; // 런타임이 자연 종료하도록
// 또는 직접 Environment.Exit(0)

프로세스 열거

Process.GetProcesses는 시스템 프로세스를 열거하고 Id/ProcessName/WorkingSet64 등의 정보를 읽으며, Kill로 프로세스를 종료합니다.

1
2
3
4
5
6
foreach (var p in Process.GetProcesses()) {
Console.WriteLine($"{p.Id}: {p.ProcessName}");
}
var current = Process.GetCurrentProcess();
long mem = current.WorkingSet64;
Process.GetProcessById(pid)?.Kill(); // 지정 프로세스 종료

Windows 레지스트리

Microsoft.Win32.Registry는 레지스트리(Windows 전용)를 읽기/쓰기합니다. GetValue/SetValue로 키 값에 접근하며 적절한 권한이 필요합니다.

1
2
3
4
5
6
7
8
using Microsoft.Win32;
// 읽기:
object? val = Registry.CurrentUser.OpenSubKey("Software\\App")
?.GetValue("Setting");
// 쓰기:
using var key = Registry.CurrentUser.CreateSubKey("Software\\App");
key.SetValue("Setting", 42);
// 주의:Windows 전용, 크로스플랫폼 앱은 의존 피할 것

응용 프로그램 경로

AppContext.BaseDirectory는 프로그램 실행 디렉터리, Environment.ProcessPath는 현재 프로세스 경로입니다. 리소스 파일 위치 지정에 사용합니다.

1
2
3
4
string baseDir = AppContext.BaseDirectory; // 프로그램 실행 디렉터리
string? path = Environment.ProcessPath; // 현재 프로세스 전체 경로
string file = Path.Combine(baseDir, "data.json");
// 상대 경로는 작업 디렉터리 의존, 호출자 영향 받기 쉬움, BaseDirectory 사용

18.정규식

Regex 매칭, 캡처 그룹, 치환, 일반 패턴.

Regex 기본

Regex.IsMatch로 판정, Matches로 모두 찾기, Match로 첫 번째 찾기. @ 축자 문자열을 사용하면 이중 이스케이프를 피할 수 있습니다.

1
2
3
4
5
6
var r = new Regex(@"^\d{3}-\d{4}$");
bool ok = r.IsMatch("123-4567");
foreach (Match m in Regex.Matches(text, @"\b\w+\b")) {
Console.WriteLine(m.Value);
}
// @ 축자:\d는 정규식의 \d, 문자열 이스케이프 아님

캡처 그룹

괄호로 하위 문자열을 캡처하고, Groups[1]로 인덱스 참조, 명명된 그룹 (?<name>...)은 Groups["name"]으로 참조합니다. 필드 추출에 사용됩니다.

1
2
3
4
5
6
7
var r = new Regex(@"(\d{4})-(\d{2})-(\d{2})");
var m = r.Match("date: 2024-01-01");
if (m.Success) {
string year = m.Groups[1].Value; // 2024
}
// 명명 그룹:Regex(@"(?<year>\d{4})-")
string y = m.Groups["year"].Value;

치환

Regex.Replace는 패턴으로 치환하며, $1/$2로 캡처 그룹을 참조합니다. 포맷팅, 마스킹, 텍스트 정리에 사용할 수 있습니다.

1
2
3
4
5
string masked = Regex.Replace(
phone, @"(\d{3})\d{4}(\d{4})", "$1****$2");
string cleaned = Regex.Replace(
text, @"[\s\t]+", " ");
// 치환 콜백:Regex.Replace(text, pat, m => Process(m.Value))

RegexOptions

RegexOptions.IgnoreCase는 대소문자 무시, Multiline은 ^/$가 줄 단위로 매치, Compiled는 컴파일하여 재사용을 가속합니다.

1
2
3
4
5
var r = new Regex(@"^[a-z]+$",
RegexOptions.IgnoreCase | RegexOptions.Multiline);
// Compiled:정규식을 여러 번 쓸 때만 가치
var fast = new Regex(@"pattern", RegexOptions.Compiled);
// IgnorePatternWhitespace는 패턴 내 주석 허용

일반 패턴

이메일, URL, IP, 휴대폰 번호의 일반 정규식. 주의: 복잡한 검증(실제 이메일 등)에는 정규식 대신 전용 라이브러리를 사용하세요.

1
2
3
4
var email = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
var ipv4 = @"^(?:\d{1,3}\.){3}\d{1,3}$";
var url = @"^https?://[^\s]+$";
// 간단 검증이면 충분;운영급은 전용 라이브러리

수량자와 앵커

*는 0회 이상, +는 1회 이상, ?는 0 또는 1회, {n,m}은 지정 횟수. ^는 줄 시작, $는 줄 끝, \b는 단어 경계.

1
2
3
4
5
6
@"\d+" // 숫자 1개 이상
@"colou?r" // color 또는 colour
@"\d{2,4}" // 2~4자리 숫자
@"^start" // start로 시작(줄 시작)
@"end$" // end로 끝남
@"\bword\b" // 단독 단어 word

어설션과 룩어라운드

제로 폭 어설션은 문자를 소비하지 않습니다. (?<=...)는 후행, (?=...)는 선행, (?!...)는 부정 선행. 위치 조건 매칭에 사용됩니다.

1
2
3
4
5
6
7
8
// 후방 탐색 (?<=@):@ 뒤의 이메일 도메인 부분 추출
string domain = Regex.Match("[email protected]",
@"(?<=@)[^@]+").Value; // example.com
// 전방 탐색 (?=\d):문자 바로 뒤에 숫자가 있는지 판정
bool hasDigit = Regex.IsMatch("a1", @"[a-z](?=\d)"); // true
// 부정 전방 탐색 (?!...):world로 시작하지 않는 단어 일치
var first = Regex.Match("hello world",
@"\b(?!world)\w+").Value; // hello

역추적과 ReDoS

중첩 수량자와 대안 분기는 역추적을 일으키며, 악의적인 입력으로 지수 매칭(ReDoS)이 발생할 수 있습니다. 타임아웃을 제한하거나 역추적을 비활성화해야 합니다.

1
2
3
4
5
6
7
8
// 위험:중첩 수량자는 긴 입력에서 지수적 백트래킹 가능
// var risky = new Regex(@"(a+)+$");
// 대책 1:항상 매치 타임아웃 설정
var withTimeout = new Regex(@"(a+)+$", RegexOptions.None,
TimeSpan.FromSeconds(2));
// 대책 2:NonBacktracking으로 백트래킹 비활성(.NET 7+)
var safe = new Regex(@"(a+)+$", RegexOptions.NonBacktracking);
// 사용자 입력 정규식이나 텍스트는 반드시 타임아웃과 길이 제한

19.빌드와 디버그

dotnet CLI, 프로젝트 구성, 디버그, 로깅, 조건부 컴파일.

dotnet CLI

dotnet build는 빌드, run은 실행, test는 테스트, publish는 게시. --configuration Release로 릴리스 구성을 게시합니다.

1
2
3
4
5
// $ dotnet build # Debug 컴파일
// $ dotnet run # 컴파일 후 실행
// $ dotnet test # 테스트 실행
// $ dotnet publish -c Release -o out
// $ dotnet clean # 산출물 정리

csproj 구성

csproj는 빌드를 제어합니다: TargetFramework, Nullable, ImplicitUsings, PackageReference 의존성, LangVersion.

1
2
3
4
5
6
7
8
9
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

디버그와 로깅

Debug.WriteLine은 Debug 빌드에서만 동작하고, Trace는 모든 빌드에서 동작합니다. ILogger는 로그 레벨을 기록하며, Console.WriteLine은 임시 디버그에 사용합니다.

1
2
3
4
5
Debug.WriteLine($"value={x}"); // Debug 빌드만
Trace.TraceInformation("start"); // 모든 빌드에서 실행
var logger = LoggerFactory.Create(b =>
b.AddConsole()).CreateLogger("App");
logger.LogInformation("Processing {Id}", id);

조건부 컴파일

#if DEBUG / #elif / #endif로 심볼에 따라 코드를 분기합니다. 프로젝트 레벨 DefineConstants로 사용자 정의 심볼을 정의합니다.

1
2
3
4
5
6
#if DEBUG
Console.WriteLine("debug build");
#elif RELEASE
Console.WriteLine("release build");
#endif
// csproj:<DefineConstants>TRACE;MY_FLAG</DefineConstants>

단위 테스트

xUnit/NUnit/MSTest로 동작을 어서트합니다. [Fact]가 테스트 메서드, Assert.Equal이 어서션. dotnet test로 실행합니다.

1
2
3
4
5
6
7
[Fact]
public void Add_ReturnsSum() {
var calc = new Calculator();
var result = calc.Add(2, 3);
Assert.Equal(5, result);
}
// xUnit 테스트 클래스:public class CalculatorTests

NuGet 패키지 관리

dotnet add package로 의존성을 추가하고, restore로 복원합니다. PackageReference가 csproj에 버전을 기록합니다.

1
2
3
4
// $ dotnet add package Newtonsoft.Json
// $ dotnet restore # 의존성 복원
// $ dotnet list package # 의존성 확인
// 의존성은 csproj의 PackageReference에 기록

게시와 단일 파일

dotnet publish는 프로덕션 버전을 게시하며, PublishSingleFile은 단일 파일로 패키징, SelfContained는 런타임 설치가 필요 없습니다.

1
2
3
4
5
6
7
// Release를 out 디렉터리로 발행
// $ dotnet publish -c Release -o out
// 단일 파일로 패키징(대상 머신에 .NET 설치 필요):
// $ dotnet publish -r win-x64 -p:PublishSingleFile=true -o out
// 자체 포함(런타임 포함, 대상 머신 설치 불필요, 용량 큼):
// $ dotnet publish -r win-x64 --self-contained true -o out
// 미사용 코드 트림으로 용량 감소:-p:PublishTrimmed=true

정적 분석기

Roslyn 분석기는 컴파일 타임에 코드 품질과 잠재적 결함을 검사하며, 경고를 오류로 승격시켜 문제가 있는 빌드를 차단할 수 있습니다.

1
2
3
4
5
6
// 분석기는 컴파일 시 코드 품질과 잠재 결함을 보고
class AnalyzerDemo {
public int Add(int a, int b) => a + b;
}
// 경고를 오류로 취급:dotnet build -warnaserror
// 내장 품질 분석기 활성화:csproj에서 AnalysisLevel을 latest로

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 C# 12(.NET 8)의 자체 완결형 치트시트로, 언어 핵심, 자주 사용되는 BCL 타입, 실제 프로젝트에서 약 80%의 일반적인 사용을 차지하는 비동기 프로그래밍을 다룹니다. 내용은 현대적 관용구에 중점을 둡니다: 속성과 자동 속성, LINQ, async/await, record 타입, 패턴 매칭, nullable 참조 타입. C#은 Anders Hejlsberg가 2000년에 .NET 플랫폼과 함께 출시한 언어로, Windows 생태계, Unity 게임 개발, 백엔드 서비스의 핵심 언어이며 타입 안전성과 생산성의 조화를 강조합니다. 19개 섹션은 각각 한 가지 주제에 집중합니다: 기본 문법, 변수, 타입과 참조/값 의미론, 제어 흐름, 함수, 문자열, 컬렉션, 메모리 관리(GC), 객체 지향, 오류 처리, I/O, 일반적인 함정, 동시성, 네트워킹, 시간, 프로세스, 정규식, 빌드 도구. 각 하위 섹션에는 「개념 소개 + 바로 복사할 수 있는 코드 스니펫」가 함께 제공됩니다. 모든 코드와 텍스트는 브라우저에서 로컬로 렌더링되며, 어떠한 데이터도 기기를 떠나지 않습니다. 권위 있는 참조는 Microsoft Learn 공식 문서를 참조하세요.

버전 2.1.0