클래스는 직접 정의하는 타입으로, 데이터(필드)와 그 데이터로 일하는 코드(메서드)를 묶습니다. 객체는 실행 시점에 new로 만드는 클래스의 인스턴스 하나입니다. 클래스 하나에 객체 여러 개가 있고, 각 객체는 자기 값을 가집니다.
클래스 선언과 객체 생성
클래스 선언은 중괄호 안에 멤버를 나열합니다. 필드는 데이터를, 메서드는 동작을 담습니다. new ClassName()은 객체를 할당하고 그에 대한 참조를 줍니다.
출력:
Maya: 120.50
Omar: 40
Withdraw 200 succeeded: False
각 객체는 자기 Owner와 Balance를 가집니다. 메서드 안에서 Balance는 "이 메서드가 호출된 객체의 잔액"을 뜻하며, 그래서 maya.Deposit과 omar.Deposit이 서로 다른 숫자를 바꿉니다. 이 암묵적인 객체는 이름을 붙여야 할 때 this로 쓸 수 있습니다.
대입하지 않은 필드는 기본값으로 시작합니다. 숫자는 0, bool은 false, 문자열과 그 밖의 클래스 타입은 null입니다.
여기서 멤버는 Main이 접근할 수 있도록 public입니다. 실제 클래스는 보통 필드를 private으로 두고 대신 속성을 노출하며, 규칙은 접근 한정자 페이지에 있습니다. 클래스는 객체가 만들어질 때 필드를 설정하는 생성자도 선언할 수 있으며, 생성자 페이지에서 다룹니다.
클래스는 참조 형식입니다
클래스 타입의 변수는 객체를 담지 않습니다. 힙에 있는 객체에 대한 참조를 담습니다. 변수를 대입하면 참조가 복사되므로 두 변수가 같은 객체를 가리킬 수 있습니다.
출력:
5
6
True
False
여기서 세 가지 결과가 드러납니다:
- 객체가 하나뿐이므로
second를 통해 객체를 바꾸면first를 통해서도 보입니다. - 객체를 메서드에 넘기면 참조가 넘어갑니다. 메서드는 객체의 필드를 바꿀 수 있고 호출하는 쪽도 그것을 봅니다. (메서드 안에서 매개변수에 새 객체를 대입해도 호출하는 쪽의 변수에는 영향이 없습니다. 그러려면
ref가 필요합니다.) - 두 클래스 인스턴스에 대한
==는 기본적으로 참조를 비교합니다. "값이 같은가?"가 아니라 "같은 객체인가?"입니다.lookalike는 데이터가 똑같지만 여전히 다른 객체입니다.
대입할 때 복사되고 값으로 비교되기를 원한다면 값 형식인 구조체나, 값으로 비교하는 레코드를 보세요.
null과 NullReferenceException
참조 변수는 "객체 없음"을 뜻하는 null을 담을 수 있습니다. null을 통해 멤버를 쓰면 C# 코드에서 가장 흔한 런타임 예외인 NullReferenceException이 발생합니다.
출력:
True
no city on file
NullReferenceException: Address was never set
Lisbon
해결책이 예외를 잡는 것인 경우는 거의 없습니다. 참조가 왜 null인지 찾아서, 객체를 더 일찍 만들거나, 쓰기 전에 확인하거나, 예외를 던지는 대신 멈추고 null을 내놓는 null 조건부 연산자 ?.를 쓰세요. 그러면 ??가 대체값을 줍니다. 두 연산자는 nullable 형식에서 다룹니다.
ToString 재정의하기
모든 클래스는 ToString()을 제공하는 object를 상속합니다. 상속된 버전은 타입의 전체 이름(네임스페이스가 있으면 포함)을 반환하며, 출력하고 싶은 것인 경우는 드뭅니다. 재정의하세요:
출력:
Plain
Mug (8.50)
In cart: Mug (8.50)
Total line: Mug (8.50)
Console.WriteLine, 문자열 보간, 문자열과의 +는 모두 대신 ToString()을 호출합니다. 디버거도 이를 보여 주므로, 객체를 직접 출력하지 않더라도 재정의해 두면 유용합니다.
개체 이니셜라이저
개체 이니셜라이저는 객체가 생성된 직후, 같은 식 안에서 public 필드와 속성을 설정합니다. 대입문을 나열하는 것보다 읽기 좋고 컬렉션 리터럴 안에서도 잘 동작합니다.
출력:
A12: 25 (0 extras)
A13: 40 (2 extras)
두 번째 티켓에 세부 사항이 두 가지 있습니다. 개체 이니셜라이저는 객체가 만들어진 뒤 실행되므로 Price = 40m이 필드 이니셜라이저의 25m을 덮어씁니다. Extras = { "Parking", "Poster" }에는 new가 없습니다. 필드가 이미 담고 있는 리스트를 교체하는 대신 그 리스트에 Add를 호출합니다.
개체 이니셜라이저는 생성자가 아닙니다. 호출하는 코드에서 접근할 수 있는 멤버만 설정할 수 있고, 멤버가 반드시 설정되도록 강제할 수도 없습니다. 특정 값이 없으면 객체가 올바르지 않다면 생성자에서 그 값을 요구하세요.
partial 클래스
partial 키워드는 한 클래스를 여러 파일에 나눕니다. 컴파일러가 부분들을 하나의 클래스로 합칩니다.
// Invoice.cs
public partial class Invoice
{
public decimal Subtotal;
public decimal Total() => Subtotal + Tax();
}
// Invoice.Tax.cs
public partial class Invoice
{
private decimal Tax() => Subtotal * 0.2m;
}
모든 부분은 partial이라고 써야 하고, 같은 네임스페이스와 같은 프로젝트에 있어야 합니다. 흔한 용도는 생성된 코드입니다. WinForms 디자이너, 소스 생성기, 일부 ORM이 한 부분을 작성하고 다른 부분은 여러분에게 남겨 두므로, 다시 생성해도 여러분의 코드를 덮어쓰지 않습니다. 직접 작성한 클래스를 짧게 보이려고 여러 파일로 나누는 것은 보통 두 개의 클래스여야 할 클래스를 숨기는 것입니다.
흔한 실수
- 만들기 전에 객체 쓰기. 클래스 타입의 필드는
new ...를 대입하기 전까지null입니다. 첫 멤버 접근이NullReferenceException을 던집니다. - 대입이 복사하리라 기대하기. 클래스에서
var b = a;는 같은 객체에 두 번째 이름을 줍니다. 독립적인 객체가 필요하면 새 객체를 만들고 값을 복사하세요. ==로 객체를 비교하고 값 비교를 기대하기. 클래스가==를 오버로딩하지 않는 한(string처럼) 참조를 비교합니다. 관련 필드를 비교하거나Equals를 재정의하세요.- 객체를 출력했는데 타입 이름이 나오기.
ToString()을 재정의하세요. - 모든 것을
public으로 선언하기. 작은 예제에서는 동작하지만, 어떤 코드든 객체를 음수 잔액 같은 잘못된 상태로 만들 수 있게 합니다. 필드는 private으로 두고 규칙을 검사하는 메서드로 바꾸세요.
자주 묻는 질문
C#에서 클래스란 무엇인가요?
클래스는 직접 정의하는 타입으로, 데이터(필드와 속성)와 동작(메서드)을 묶은 이름 붙은 단위입니다. class Account { public decimal Balance; public void Deposit(decimal amount) { Balance += amount; } }가 클래스를 선언합니다. 클래스는 설계도이며, new로 객체를 만들기 전까지는 아무 일도 하지 않습니다.
C#에서 클래스와 객체의 차이는 무엇인가요?
클래스는 소스 코드에 한 번 작성하는 정의입니다. 객체는 실행 시점에 new로 만드는 그 클래스의 인스턴스 하나이며, 모든 인스턴스 필드의 자기 복사본을 갖습니다. new Account()를 두 번 하면 같은 클래스의 객체 두 개가 생기고 잔액은 서로 독립적입니다.
C#에서 객체는 어떻게 만드나요?
new 뒤에 클래스 이름과 생성자 인수를 씁니다: var account = new Account();. 개체 이니셜라이저로 같은 식 안에서 public 필드와 속성을 설정할 수 있습니다: new Account { Owner = "Maya" }. 변수는 객체 자체가 아니라 새 객체에 대한 참조를 담습니다.
C#에서 NullReferenceException이 나는 이유는 무엇인가요?
null을 담고 있는 변수를 통해 멤버(.Name, .Length, 메서드 호출)를 썼기 때문에 작업할 객체가 없는 것입니다. 흔한 원인은 대입된 적 없는 필드(클래스 필드는 null로 시작합니다), null을 반환한 조회, 실패하면 null을 반환하는 메서드입니다. if (x != null)로 확인하거나, x?.Name을 쓰거나, 쓰기 전에 객체가 만들어졌는지 확인하세요.
C#에서 ToString은 어떻게 재정의하나요?
클래스에 public override string ToString()을 선언하고 원하는 텍스트를 반환하세요. 예를 들어 public override string ToString() => $"{Name} ({Price:F2})";입니다. 재정의하지 않으면 Console.WriteLine(obj)와 문자열 보간이 클래스 이름을 출력합니다. object.ToString()이 그렇게 반환하기 때문입니다.