C# · 심층 가이드
C# 기초 문법 완전 정리
변수 선언과 문자열 다루기부터 분기, 반복, null 기본값 처리, 타입 변환, 날짜 포맷까지 C# 기초 문법 스무 가지를 실행 가능한 조각으로 정리했습니다.
정적 타입 언어이면서도 C#은 var로 타입을 생략할 수 있어, 처음 보면 동적 타입처럼 느껴집니다. 하지만 var는 컴파일 시점에 타입이 확정되는 추론일 뿐이고 나중에 다른 타입을 넣을 수 없습니다. 여기에 nullable 참조 타입이 더해지면서 string과 string?가 다른 의미를 갖게 됐습니다. 이 두 축, 즉 추론과 null 허용 여부를 구분해 두면 기초 문법에서 헤맬 일이 크게 줄어듭니다.
앞쪽 예제는 숫자 변수 선언과 문자열 상수 선언처럼 값을 담는 방법을 훑고, 문자열 정리하기와 문자열 길이 구하기에서 Trim 계열이 새 문자열을 돌려준다는 점을 확인합니다. 중반의 if else 분기와 switch 또는 match 사용은 같은 판단을 문장과 식 두 형태로 쓰는 비교이고, null 기본값 처리의 ??는 그 분기를 한 줄로 압축한 형태로 읽으면 자연스럽게 이어집니다. 뒤쪽의 배열 생성과 순회와 맵 또는 객체 생성은 값 여러 개를 담는 두 가지 기본 그릇이라, 정렬 기초로 넘어가기 전에 짚어 두면 좋습니다.
나머지 연산 확인은 함정이 숨어 있는 자리입니다. C#의 %는 결과 부호가 왼쪽 피연산자를 따라가서 -7 % 3이 -1입니다. 인덱스를 순환시키려고 %를 썼다가 음수 인덱스를 만드는 사고가 여기서 나옵니다. 타입 변환과 날짜 문자열 만들기도 마찬가지로, Parse와 ToString은 별도 지정이 없으면 현재 문화권을 따릅니다. 개발 장비에서 잘 돌던 코드가 다른 지역 설정의 서버에서 소수점 기호나 날짜 순서 때문에 어긋나는 일이 여기서 생기므로, 저장하거나 전송할 문자열에는 불변 문화권을 명시하는 편이 안전합니다.
01숫자 변수 선언
var count = 1;로 정수 변수를 만들고, 삼항 연산자로 5보다 큰지 비교해 문자열을 출력합니다. 값이 1이므로 결과는 small입니다. var는 타입을 생략하는 것일 뿐 동적 타입이 아니어서, 컴파일 시점에 int로 고정되고 나중에 문자열을 넣을 수 없습니다.
C# code
// 숫자 변수 선언
using System;
var count = 1;
Console.WriteLine(count > 5 ? "big" : "small");var는 초기화식이 있어야만 쓸 수 있어 선언과 대입을 나누는 코드에는 사용할 수 없고, 필드나 매개변수 선언에도 허용되지 않습니다.
리터럴 1은 int로 추론되므로 나중에 소수를 넣으려 하면 컴파일 오류가 납니다. 실수를 담을 생각이라면 1.0으로 적거나 타입을 직접 명시해야 합니다.
02문자열 상수 선언
정수 배열 { 3, 1, 4, 2 }를 OrderBy로 오름차순 정렬해 쉼표로 이어 출력합니다. 원본 배열은 손대지 않고 새 시퀀스를 만들어 내는 방식입니다. OrderBy는 호출 즉시 정렬하지 않고 string.Join이 열거를 시작할 때 계산을 수행합니다.
C# code
// 문자열 상수 선언
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 2 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));OrderBy는 결과를 만들 때 원본을 버퍼에 복사한 뒤 키를 계산하므로, 키 선택자에 무거운 연산을 넣어도 원소마다 한 번씩만 호출됩니다.
정렬 결과를 변수에 담아 두고 여러 번 열거하면 그때마다 정렬이 다시 수행됩니다. 반복해서 쓸 것이라면 ToArray로 한 번 굳혀야 합니다.
03불리언 플래그 만들기
값 3을 담은 count를 count > 5로 판정해 두 문자열 중 하나를 고릅니다. 조건식 자체가 bool이므로 별도의 플래그 변수 없이도 분기가 성립합니다. C#의 삼항 연산자는 양쪽 가지의 타입이 서로 변환 가능해야 하므로, 한쪽만 문자열이고 다른 쪽이 숫자면 컴파일 오류가 납니다.
C# code
// 불리언 플래그 만들기
using System;
var count = 3;
Console.WriteLine(count > 5 ? "big" : "small");bool?는 조건 자리에 그대로 넣을 수 없어 == true처럼 명시적으로 비교해야 하지만, 일반 bool에는 그런 비교가 필요 없습니다.
C#의 if는 정수나 참조를 조건으로 받지 않아 C 스타일로 if (count)라고 적으면 컴파일 오류가 납니다. 반드시 bool 식을 적어야 합니다.
04문자열 길이 구하기
같은 값 4가 두 번 들어 있는 배열을 정렬해 출력합니다. 문자열 길이를 재는 코드는 없고 숫자 나열만 다룹니다. 눈여겨볼 부분은 동점 처리인데, LINQ의 OrderBy는 안정 정렬이라 값이 같은 원소의 상대 순서를 유지하는 반면 Array.Sort는 그 보장이 없습니다.
C# code
// 문자열 길이 구하기
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 4 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));동점의 순서를 직접 정하려면 OrderBy 뒤에 ThenBy를 붙입니다. 두 번째 OrderBy를 이어 쓰면 앞의 정렬이 무시되고 나중 키만 적용됩니다.
문자열 길이로 정렬할 때 Length는 글자 수가 아니라 UTF-16 코드 단위 수라, 이모지처럼 두 단위를 쓰는 문자가 섞이면 보이는 길이와 어긋납니다.
05문자열 정리하기
정수 5를 비교해 big과 small 중 하나를 출력하며, 경계값이라 조건이 거짓이 되어 small이 나옵니다. 문자열 다듬기 대신 순수 수치 비교만 수행합니다. 실제로 공백을 걷어낼 때 쓰는 Trim은 원본을 바꾸지 않고 새 문자열을 돌려주는데, C# 문자열이 불변이기 때문입니다.
C# code
// 문자열 정리하기
using System;
var count = 5;
Console.WriteLine(count > 5 ? "big" : "small");Trim은 공백뿐 아니라 탭과 개행까지 걷어내며, 특정 문자만 지우려면 Trim('/')처럼 지울 문자 목록을 넘기면 됩니다.
count > 5는 5에서 거짓이 되므로 경계값을 포함하려면 >=로 바꿔야 합니다. 이 한 글자 차이가 등급 구간 판정에서 가장 흔한 오차입니다.
06숫자 합계 계산
배열을 정렬한 결과를 이어 붙일 뿐, 합계를 구하지는 않습니다. 총합이 필요하면 같은 LINQ 계열의 Sum을 이어 붙이면 됩니다. 다만 Enumerable.Sum은 int 범위를 넘어서면 조용히 넘어가지 않고 OverflowException을 던집니다.
C# code
// 숫자 합계 계산
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 6 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));Sum에는 선택자를 받는 오버로드가 있어 values.Sum(v => v * 2)처럼 변환과 합산을 한 번의 순회로 끝낼 수 있습니다.
int 배열에 Sum을 쓰면 누적도 int로 이뤄져 중간에 넘칠 수 있습니다. 값이 커질 가능성이 있으면 Sum(v => (long)v)로 폭을 넓혀야 합니다.
07나머지 연산 확인
count에 7을 넣고 5와 비교해 big을 출력하는 코드로, % 연산자는 등장하지 않습니다. 짝수 판정 같은 곳에 나머지 연산을 쓸 때는 부호를 조심해야 합니다. C#에서 -7 % 2는 1이 아니라 -1이므로, == 1로 홀수를 판정하면 음수에서 어긋납니다.
C# code
// 나머지 연산 확인
using System;
var count = 7;
Console.WriteLine(count > 5 ? "big" : "small");항상 음이 아닌 나머지가 필요하면 ((n % m) + m) % m 형태를 쓰고, 홀수 판정은 n % 2 != 0으로 적으면 부호에 관계없이 정확합니다.
%의 오른쪽이 0이면 정수 연산에서 DivideByZeroException이 납니다. 나눌 값이 변수로 들어온다면 연산 전에 0인지 확인해야 합니다.
08if else 분기
if와 else 블록 없이, 배열을 정렬해 문자열로 만드는 표현식 하나로 끝납니다. 값 판정과 데이터 변환을 분리해 보여 주는 형태입니다. 조건 분기를 값으로 다루고 싶다면 if문 대신 C# 8에서 들어온 switch 식이 대입문 안에 그대로 들어갑니다.
C# code
// if else 분기
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 8 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));switch 식의 각 가지는 값을 만들어 내야 하므로, 값을 돌려주지 않는 동작을 넣으려면 식이 아니라 switch 문으로 적어야 합니다.
if와 else 뒤의 중괄호를 생략한 채 나중에 줄을 하나 더 붙이면 그 줄은 조건과 무관하게 실행됩니다. 들여쓰기만으로는 블록이 되지 않습니다.
09switch 또는 match 사용
패턴 매칭 없이 count > 5 한 가지 조건만 평가해 결과를 찍습니다. 조건이 셋 이상으로 늘어나면 switch 식이 읽기 편합니다. 대신 switch 식은 어떤 가지에도 걸리지 않는 값이 들어오면 SwitchExpressionException이 실행 중에 터지므로, 기본 가지 _를 남겨 두는 편이 안전합니다.
C# code
// switch 또는 match 사용
using System;
var count = 9;
Console.WriteLine(count > 5 ? "big" : "small");숫자 구간은 > 5처럼 관계 패턴으로 적을 수 있고, and와 or를 써서 여러 범위를 한 가지로 묶을 수도 있습니다.
가지 순서가 중요합니다. 넓은 조건을 위에 두면 아래의 좁은 조건은 도달할 수 없어 컴파일러가 오류로 막으므로, 구체적인 조건부터 적어야 합니다.
10null 기본값 처리
정수 배열이라 null이 끼어들 여지가 없고, 정렬 결과만 그대로 출력됩니다. int?처럼 nullable을 정렬하면 null이 가장 앞에 몰립니다. 기본값 대체가 필요할 때는 ?? 연산자를, 시퀀스가 통째로 비었을 때는 DefaultIfEmpty를 씁니다.
C# code
// null 기본값 처리
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 10 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));??=를 쓰면 값이 null일 때만 대입하는 코드를 한 줄로 줄일 수 있고, 참조가 null일 수 있는 경로에서는 ?.로 호출 자체를 건너뛸 수 있습니다.
DefaultIfEmpty는 비어 있을 때 원소 하나짜리 시퀀스를 돌려주므로, 그 뒤에 Count()를 세면 0이 아니라 1이 나옵니다.
11삼항 연산자 사용
?: 한 번으로 11과 5를 비교해 big을 만들어 냅니다. 조건에 따라 값을 고를 때 if문보다 짧게 쓰기 좋습니다. 삼항 연산자는 값을 만들어 내는 식이라서 그 자체를 문장처럼 세워 둘 수 없고, 반드시 대입하거나 인자로 넘겨야 합니다.
C# code
// 삼항 연산자 사용
using System;
var count = 11;
Console.WriteLine(count > 5 ? "big" : "small");삼항 연산자는 오른쪽 결합이라 a ? x : b ? y : z가 의도대로 동작하지만, 단계가 셋을 넘어가면 switch 식으로 옮기는 편이 읽기 쉽습니다.
?:는 선택된 쪽만 평가하므로 반대편에 부작용이 있는 호출을 넣어 두면 실행되지 않습니다. 두 동작을 모두 수행할 의도로 쓰면 하나가 조용히 빠집니다.
12for 반복문
for 키워드는 없지만, string.Join이 내부에서 정렬된 시퀀스를 끝까지 열거합니다. 인덱스가 필요 없는 순회라면 이렇게 LINQ만으로 충분합니다. 다만 OrderBy의 결과는 인덱서를 제공하지 않으므로 [i]로 접근하려면 ToArray나 ToList로 한 번 굳혀야 합니다.
C# code
// for 반복문
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 12 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));Select((v, i) => ...) 오버로드를 쓰면 for문 없이도 순번을 함께 다룰 수 있어, 인덱스가 필요한 변환도 LINQ 안에서 끝납니다.
foreach로 도는 중에 원본 컬렉션에 원소를 넣거나 지우면 InvalidOperationException이 납니다. 지울 항목은 모아 두었다가 순회 후에 처리해야 합니다.
13while 누적 계산
누적 변수를 굴리는 while 루프 대신, 13을 5와 한 번 비교해 출력하고 끝납니다. 반복 누적을 실제로 짤 때 조심할 부분은 자릿수입니다. C#은 기본이 unchecked라 int 누적이 범위를 넘으면 예외 없이 음수로 돌아가므로, 큰 값은 long이나 checked 블록으로 감싸야 합니다.
C# code
// while 누적 계산
using System;
var count = 13;
Console.WriteLine(count > 5 ? "big" : "small");checked 블록으로 감싸면 오버플로 시 값이 돌아가는 대신 OverflowException이 나므로, 조용한 오류보다 즉시 실패가 나은 계산에 적합합니다.
종료 조건에 쓰이는 변수를 루프 안에서 갱신하지 않으면 무한 루프가 됩니다. 특히 continue로 건너뛰는 경로에서 증가 문장을 빠뜨리는 실수가 잦습니다.
14break 와 continue
루프를 중간에 끊는 break나 건너뛰는 continue 없이, 배열을 정렬해 한 줄로 출력합니다. LINQ 체인 안에서는 이 두 키워드를 쓸 수 없습니다. 조건에 걸리면 멈추고 싶다면 TakeWhile, 특정 원소만 건너뛰려면 Where가 대응되는 도구입니다.
C# code
// break 와 continue
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 14 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));중첩 루프에서 바깥까지 한 번에 빠져나오려면 break만으로는 부족해, 플래그를 두거나 반복 부분을 메서드로 빼고 return하는 편이 깔끔합니다.
switch 문 안의 break는 그 switch를 빠져나갈 뿐 감싸고 있는 반복문을 끝내지 않습니다. 루프를 끊으려면 플래그나 goto가 필요합니다.
15배열 생성과 순회
배열 순회 대신 정수 하나를 비교해 결과 문자열을 고릅니다. 배열을 다룰 때 알아 둘 점은 크기가 생성 시점에 고정된다는 것으로, 원소를 덧붙이려면 List<T>가 필요합니다. foreach의 반복 변수는 읽기 전용이라 그 자리에서 값을 대입할 수도 없습니다.
C# code
// 배열 생성과 순회
using System;
var count = 15;
Console.WriteLine(count > 5 ? "big" : "small");Array.Resize가 있긴 하지만 실제로는 새 배열을 만들어 복사하는 동작이라, 크기가 자주 바뀐다면 처음부터 List<T>를 쓰는 편이 낫습니다.
new int[3]은 0으로 채워지지만 참조 타입 배열의 원소는 전부 null이라, 채우기 전에 접근하면 NullReferenceException이 납니다.
16맵 또는 객체 생성
키와 값을 담는 컬렉션이 아니라 정수 배열을 정렬해 출력하는 코드입니다. 사전 형태가 필요하면 Dictionary<TKey, TValue>나 ToDictionary로 바꾸게 됩니다. 이때 같은 키가 두 번 들어오면 덮어쓰지 않고 예외를 던지므로, 중복 가능성이 있으면 TryAdd 쪽이 안전합니다.
C# code
// 맵 또는 객체 생성
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 16 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));ToDictionary는 키 선택자만 넘기면 원소 자체가 값이 되고, 두 번째 선택자를 함께 주면 값까지 변환해 담을 수 있습니다.
Dictionary의 인덱서는 없는 키를 읽으면 예외를 내지만 대입은 조용히 추가합니다. 읽기와 쓰기 문법이 같아 오타 난 키가 새 항목으로 들어가는 사고가 잦습니다.
17정렬 기초
count > 5 비교 하나가 전부이고, 정렬 알고리즘은 등장하지 않습니다. 정렬은 결국 이런 두 값 비교를 반복해 순서를 정하는 일입니다. 실수를 다룰 때는 비교 자체가 흔들리는데, double.NaN은 >든 <든 모두 거짓이라 비교자 구현에서 예상 밖의 순서를 만듭니다.
C# code
// 정렬 기초
using System;
var count = 17;
Console.WriteLine(count > 5 ? "big" : "small");비교자를 직접 구현할 때는 두 인자를 바꿔 부르면 부호도 뒤집혀야 한다는 일관성을 지켜야 하며, 이를 어기면 정렬 도중 예외가 날 수 있습니다.
double 목록에 NaN이 섞이면 비교 결과가 모순되어 정렬 순서를 예측할 수 없습니다. 정렬 전에 double.IsNaN으로 걸러 내야 합니다.
18튜플 또는 페어 사용
두 값을 한 묶음으로 다루는 튜플 대신, 단일 정수 배열을 정렬해 출력합니다. 값 두 개를 함께 반환해야 할 때는 (int Rank, string Name) 같은 이름 있는 튜플이 별도 클래스보다 간결합니다. 튜플을 정렬 키로 넘기면 첫 항목부터 차례로 비교하는 사전식 순서가 적용됩니다.
C# code
// 튜플 또는 페어 사용
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 18 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));값 튜플은 구조체라 힙 할당이 없고, var (rank, name) = pair;처럼 분해해 받으면 임시 변수를 따로 만들지 않아도 됩니다.
튜플 요소의 이름은 컴파일 시점의 편의일 뿐이라, object로 담았다가 꺼내거나 리플렉션으로 들여다볼 때는 Item1, Item2만 남습니다.
19타입 변환
var count = 19;에서 타입은 리터럴로부터 int로 추론되며, 명시적 변환은 일어나지 않습니다. 문자열을 숫자로 바꿔야 한다면 형식이 어긋날 때 예외를 던지는 int.Parse보다 int.TryParse가 낫습니다. double을 (int)로 캐스팅하면 반올림이 아니라 소수점 아래를 잘라 버립니다.
C# code
// 타입 변환
using System;
var count = 19;
Console.WriteLine(count > 5 ? "big" : "small");int.TryParse에는 NumberStyles와 문화권을 받는 오버로드가 있어, 천 단위 쉼표가 섞인 문자열이나 특정 로캘 형식도 다룰 수 있습니다.
(int) 캐스팅은 unchecked 문맥에서 범위를 넘는 값을 잘라 엉뚱한 수를 만들고, double.NaN을 int로 캐스팅하면 예외 없이 의미 없는 값이 나옵니다.
20날짜 문자열 만들기
날짜를 다루지 않고 정수 배열을 정렬해 문자열로 이어 붙입니다. 날짜를 문자열로 만들 때는 실행 환경의 로캘에 따라 결과가 달라지므로 CultureInfo.InvariantCulture를 함께 넘기는 편이 안전합니다. yyyy-MM-dd 형식으로 적어 두면 문자열 상태 그대로 정렬해도 날짜 순서와 일치합니다.
C# code
// 날짜 문자열 만들기
using System;
using System.Linq;
var values = new[] { 3, 1, 4, 20 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));왕복 가능한 형식 지정자 o를 쓰면 문자열로 저장했다가 다시 파싱해도 값이 정확히 복원되므로, 전송하거나 보관하는 날짜에 적합합니다.
형식 문자열에서 MM은 월이고 mm은 분입니다. 대소문자를 바꿔 적어도 예외 없이 엉뚱한 숫자가 찍혀 한참 뒤에야 발견됩니다.
정리하며
var는 타입 생략이 아니라 컴파일 시점 추론이며 타입은 그 자리에서 고정됩니다%의 부호는 왼쪽 피연산자를 따르므로 음수 입력에서 인덱스 계산이 깨질 수 있습니다- 문자열 메서드는 원본을 바꾸지 않고 새 문자열을 반환하니 결과를 반드시 받아 씁니다
- 저장하거나 주고받을 숫자와 날짜 문자열에는 문화권을 명시해 환경 차이를 없앱니다
더 깊이 들어가고 싶다면 C# 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.