C# 클래스
C# 클래스
클래스는 객체의 설계도(blueprint) 역할을 하는 참조 형식이에요. 클래스 형식의 변수를 만들면 그 변수는 관리되는 힙에 있는 객체에 대한 참조(reference) 를 담지, 객체 데이터 자체를 담는 게 아니에요. 클래스 변수를 다른 변수에 대입하면 참조가 복사되니까 두 변수가 같은 객체를 가리키게 되죠. C#에서 사용자 정의 형식을 만들 때 가장 흔히 쓰는 방법이 클래스예요.
복잡한 동작이나 상속, 그리고 참조 사이의 공유된 정체성이 필요할 때 클래스를 써요. 다만 C#의 클래스는 Java나 C++의 클래스와 비슷해서 다른 언어에 익숙하다면 금방 적응할 수 있어요. 데이터 중심이라면 클래스 대신 레코드(records)를 살펴보는 것도 좋아요.
언제 클래스를 쓸까
다음 상황에서는 클래스를 사용해요.
- 형식이 복잡한 동작을 가지거나 가변 상태(mutable state)를 관리해요.
- 상속이 필요해서 기본 클래스를 만들어 파생 특화를 하거나, 기존 클래스를 확장하는 파생 형식을 만들 때.
- 인스턴스가 우연히 같은 값을 가진 객체가 아니라 공유된 정체성을 나타낼 때. 같은 객체를 가리키는 두 참조는 계속 동기화되어야 해요.
- 형식이 크거나 오래 살아남아서 힙 할당과 참조 의미론의 이점을 얻을 때.
클래스 선언하기
class 키워드 다음에 형식 이름을 붙여 클래스를 정의해요. 선택으로 붙이는 접근 한정자(access modifier) 가 가시성을 제어하는데, 기본값은 internal이에요. 다른 어셈블리의 호출자가 쓰게 하려면 public을 지정해요.
public class Customer
{
public string Name { get; set; }
public Customer(string name) => Name = name;
}
클래스 본문에는 필드, 속성, 메서드, 이벤트가 들어가는데, 이들을 통틀어 클래스 멤버(class members) 라고 불러요. 이름은 C#의 유효한 식별자 이름이어야 합니다.
객체 만들기
클래스는 형식을 정의할 뿐 그 자체가 객체는 아니에요. new 키워드로 클래스의 객체(인스턴스)를 만들어요.
var customer = new Customer("Allison");
Console.WriteLine(customer.Name); // Allison
customer 변수는 객체가 아니라 객체에 대한 참조를 담아요. 여러 변수를 같은 객체에 대입할 수 있고, 한 참조로 바꾼 내용이 다른 참조에서도 보이죠.
var c1 = new Customer("Grace");
var c2 = c1; // both variables reference the same object
c2.Name = "Hopper";
Console.WriteLine(c1.Name); // Hopper — c1 sees the change made through c2
이 참조 공유 방식이 클래스와 구조체의 차이 중 하나예요. 구조체는 대입하면 데이터가 복사되죠. 더 중요하게, 클래스는 상속을 지원해서 파생 형식이 기본 클래스의 동작을 재사용하고 특화할 수 있어요. 구조체는 상속 계층에 참여하지 못해요.
생성자와 초기화
인스턴스를 만들 때는 필드와 속성이 유용한 값으로 초기화되길 원해요. C#은 필드 이니셜라이저, 생성자 매개변수, 기본 생성자(primary constructor), 필수 속성(required property) 같은 여러 방법을 제공해요.
필드 이니셜라이저는 필드 선언 자체에 기본값을 지정해요.
public class Container
{
private int _capacity = 10;
}
생성자 매개변수는 호출하는 쪽이 값을 제공하도록 요구해요.
public class Container
{
private int _capacity;
public Container(int capacity) => _capacity = capacity;
}
기본 생성자(C# 12 이상)는 클래스 선언에 직접 매개변수를 추가해요. 그 매개변수는 클래스 본문 어디에서든 쓸 수 있죠.
public class Container(int capacity)
{
private int _capacity = capacity;
}
기본 생성자와 필드 이니셜라이저는 함께 동작할 수 있어요. 위 예의 필드 이니셜라이저 _capacity = capacity가 기본 생성자의 매개변수를 값으로 쓰는 게 그 예시예요. 이 패턴은 생성자 인자를 간결한 선언 하나로 필드에 담게 해 줍니다.
**필수 속성**은 호출하는 쪽이 객체 이니셜라이저로 특정 속성을 반드시 설정하도록 강제해요.
public class Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
}
// var missing = new Person(); // Error: required properties not set
var person = new Person { FirstName = "Grace", LastName = "Hopper" };
Console.WriteLine($"{person.FirstName} {person.LastName}"); // Grace Hopper
정적 클래스
static 클래스는 인스턴스를 만들 수 없고 정적 멤버만 담아요. 인스턴스 데이터를 다루지 않는 유틸리티 메서드를 정리할 때 사용해요.
static class MathHelpers
{
public static double CircleCircumference(double radius) =>
2 * Math.PI * radius;
}
double circumference = MathHelpers.CircleCircumference(5.0);
Console.WriteLine($"Circumference: {circumference:F2}"); // Circumference: 31.42
.NET 클래스 라이브러리에는 System.Math, System.Console처럼 정적 클래스가 많아요. 정적 클래스는 암시적으로 sealed라서 상속도 인스턴스화도 할 수 없어요.
객체 이니셜라이저
**객체 이니셜라이저**는 값의 모든 조합마다 생성자를 쓰지 않고, 객체를 만들 때 속성을 설정하게 해 줘요.
class ConnectionOptions
{
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 80;
public bool UseSsl { get; init; }
}
var options = new ConnectionOptions
{
Host = "db.example.com",
Port = 5432,
UseSsl = true
};
Console.WriteLine($"{options.Host}:{options.Port} (SSL: {options.UseSsl})");
// db.example.com:5432 (SSL: True)
객체 이니셜라이저는 set 또는 init 접근자가 있는 접근 가능한 속성과 함께 동작해요. required 속성이나 일부 매개변수를 받고 나머지를 호출자가 설정하게 하는 생성자와도 자연스럽게 결합돼요.
상속
클래스는 상속을 지원해요. 기존 클래스의 동작을 재사용하거나, 확장하거나, 수정하는 새 클래스를 정의할 수 있죠. 상속받는 클래스가 기본 클래스(base class), 새로 만드는 클래스가 파생 클래스(derived class) 예요.
class Employee
{
public string Name { get; set; }
public Employee(string name) => Name = name;
}
class Manager : Employee
{
public string Department { get; set; }
public Manager(string name, string department) : base(name) =>
Department = department;
}
var manager = new Manager("Satya", "Engineering");
Console.WriteLine($"{manager.Name} manages {manager.Department}");
// Satya manages Engineering
클래스는 하나의 기본 클래스에서 상속받으면서 동시에 여러 인터페이스를 구현할 수 있어요. 파생 클래스는 생성자를 제외한 기본 클래스의 모든 멤버를 상속받아요.