출시·고도화 중
C# 안내서 · 3/6
C#의 모든 값은 타입을 가지며, 타입은 크게 값 형식과 참조 형식으로 나뉩니다. 이 차이를 이해하면 대입, 메서드 호출, 비교가 어떻게 동작하는지 예측할 수 있습니다. 이 장에서는 두 형식의 차이, 클래스 · 구조체 · 레코드, 속성, 자주 쓰는 컬렉션, 그리고 null을 안전하게 다루는 방법을 살펴봅니다.
int, double, bool, decimal, DateTime 같은 기본 타입과 struct, enum은 값 형식입니다. 변수에 값 자체가 들어 있어서 대입하면 값이 복사됩니다. 반면 class, string, 배열, 레코드 클래스는 참조 형식입니다. 변수에는 객체를 가리키는 참조가 들어 있어서 대입하면 같은 객체를 함께 가리키게 됩니다.
var p1 = new PointStruct { X = 1 };
var p2 = p1; // 값이 복사됩니다
p2.X = 99;
Console.WriteLine(p1.X); // 1
var c1 = new PointClass { X = 1 };
var c2 = c1; // 참조가 복사됩니다
c2.X = 99;
Console.WriteLine(c1.X); // 99
struct PointStruct { public int X; }
class PointClass { public int X; }string은 참조 형식이지만 한 번 만든 문자열은 바뀌지 않는(불변) 객체이고, ==가 내용을 비교하므로 값처럼 다룰 수 있습니다. 구조체는 작고 불변인 데이터(좌표, 금액 등)에 알맞고, 대부분의 경우에는 클래스를 씁니다.
클래스는 데이터(필드 · 속성)와 동작(메서드)을 함께 묶습니다. 바깥에 공개할 데이터는 필드 대신 속성으로 노출하는 것이 관례입니다. 속성은 get · set 접근자를 가진 멤버로, 나중에 검증 로직을 넣어도 사용하는 쪽 코드는 바뀌지 않습니다.
var account = new Account("김하늘") { Number = "110-123-456" };
account.Deposit(50_000m);
Console.WriteLine($"{account.Owner}: {account.Balance:N0}원");
public class Account
{
private decimal _balance;
public Account(string owner) => Owner = owner;
public string Owner { get; } // 읽기 전용 속성
public string? Memo { get; set; } // 자동 구현 속성
public required string Number { get; init; } // 생성할 때만 정할 수 있는 필수 속성
public decimal Balance => _balance; // 계산 속성
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "입금액은 0보다 커야 합니다.");
_balance += amount;
}
}init 접근자는 객체 초기화 식({ Number = ... })에서만 값을 넣을 수 있게 하고, required는 그 값을 빠뜨리면 컴파일 오류를 냅니다. 숫자 리터럴의 _는 자릿수 구분용이며 값에는 영향이 없습니다.
레코드는 데이터를 담는 것이 주목적인 타입을 짧게 선언하는 방법입니다. 위치 매개변수로 선언하면 생성자, 읽기 전용 속성, 값 기반 같음 비교, 보기 좋은 ToString이 자동으로 만들어집니다.
var a = new Product("마우스", 25_000m);
var b = new Product("마우스", 25_000m);
Console.WriteLine(a == b); // True: 내용이 같으면 같습니다
Console.WriteLine(a); // Product { Name = 마우스, Price = 25000 }
var discounted = a with { Price = 20_000m }; // 일부만 바꾼 복사본
public record Product(string Name, decimal Price);with 식은 원본을 바꾸지 않고 일부 속성만 다른 새 레코드를 만듭니다. record는 기본적으로 참조 형식이며, 값 형식이 필요하면 record struct로 선언합니다.
가장 많이 쓰는 컬렉션은 System.Collections.Generic 네임스페이스의 List<T>와 Dictionary<TKey, TValue>입니다. 꺾쇠 안의 타입 매개변수 덕분에 어떤 타입의 값을 담는지 컴파일러가 확인합니다.
List<string> langs = ["C#", "F#", "Rust"]; // 컬렉션 식
langs.Add("Go");
langs.Remove("F#");
Console.WriteLine($"{langs.Count}개, 첫 번째: {langs[0]}");
var stock = new Dictionary<string, int>
{
["사과"] = 10,
["배"] = 4,
};
stock["귤"] = 7; // 추가하거나 덮어씁니다
if (stock.TryGetValue("배", out var pears))
Console.WriteLine($"배 재고: {pears}");
foreach (var (fruit, qty) in stock)
Console.WriteLine($"{fruit} = {qty}");없는 키를 인덱서(stock["감"])로 읽으면 KeyNotFoundException이 발생하므로, 있는지 모를 때는 TryGetValue나 ContainsKey를 씁니다. 이 밖에 중복 없는 집합 HashSet<T>, 선입선출 Queue<T>, 후입선출 Stack<T>도 자주 쓰입니다. 크기가 정해진 데이터는 배열(int[])로 다룹니다.
프로젝트 파일에 <Nullable>enable</Nullable>이 켜져 있으면, 컴파일러는 참조 형식 변수에 null이 들어갈 수 있는지 추적합니다. string은 null이 아니라는 뜻이고, null을 허용하려면 string?처럼 물음표를 붙입니다. null일 수 있는 값을 확인 없이 쓰면 경고가 나옵니다.
string? FindNickname(int userId) => userId == 1 ? "하늘" : null;
string? nick = FindNickname(2);
Console.WriteLine(nick?.Length); // null 이면 null (예외 없음)
Console.WriteLine(nick ?? "(별명 없음)"); // null 이면 오른쪽 값
nick ??= "손님"; // null 일 때만 대입
if (nick is not null)
Console.WriteLine(nick.ToUpper()); // 여기서는 null 이 아님을 압니다
int? age = null; // null 허용 값 형식
Console.WriteLine(age.HasValue ? age.Value : -1);?.는 null이면 멤버 접근을 건너뛰고, ??는 기본값을 주고, ??=는 비어 있을 때만 채웁니다. 값이 null이 아님을 확신할 때 !(null 용서 연산자)로 경고를 끌 수 있지만, 실제 검사를 하지는 않으므로 아껴 써야 합니다.
init · required로 생성 규칙을 표현합니다.with 식을 제공하는 데이터 중심 타입입니다.List<T>와 Dictionary<TKey, TValue>가 기본 컬렉션이며, 키 조회에는 TryGetValue를 씁니다.string?, ?., ??로 null 오류를 컴파일 단계에서 줄일 수 있습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.