반응형
1) readonly가 뭔가요?
- 필드(멤버 변수)에 붙이는 한정자로,
선언 시 또는 생성자(ctor) 안에서만 한 번 값을 할당할 수 있고, 그 이후에는 수정이 불가능합니다. - 즉, 객체가 만들어진 뒤에는 바뀌지 않아야 할 값에 사용합니다.
(정확히 말하면 참조형의 경우 **참조(주소)**가 고정될 뿐, 참조 대상 내부 상태까지 자동으로 불변이 되는 것은 아닙니다.)
2) 왜/언제 쓰나요? (써야 할 이유)
- 불변성(immutability) 보장: 실수로 값이 바뀌는 버그를 차단합니다. 클래스 불변식을 지키기 쉬워요.
- 코드 의도 표현: “이 값은 생성 시 결정되고 이후 변하지 않는다”를 컴파일러 수준에서 명시합니다.
- 스레드 안정성에 기여: 공유되는 상태가 줄어들어 동시성 버그 위험을 줄입니다.
- 설계 간결화: 의존성, 구성(config), ID, 상수에 준하는 값 등 “생성 시점에 확정되는 속성”을 표현하기 좋습니다.
- const보다 유연: const는 컴파일 타임 상수만 가능하지만, readonly는 런타임에 계산한 값을 생성자에서 넣을 수 있습니다. 또한 readonly는 참조형/값형 모두 가능.
3) 자주 쓰는 상황
- 설정 값, 경로, 연결 문자열 등 실행 시 한 번 결정되면 끝까지 안 바뀌는 값
- DI로 주입한 서비스, 리포지토리 등 참조는 고정하고 싶을 때
- 엔터티의 ID/생성시각 같이 객체 정체성을 규정하는 필드
- 캐시된 계산 결과: 생성 시 계산하거나, 지연 초기화 후 더는 바꾸지 않을 때(초기화 패턴과 함께)
4) const vs readonly (헷갈리는 포인트)
항목 const readonly
값 결정 시점 | 컴파일 타임 | 런타임(생성자 또는 선언 시) |
대상 타입 | 숫자/문자열/열거형 등 컴파일 상수 | 어떤 타입이든 가능(참조형 포함) |
변경 가능 여부 | 절대 불가 | 생성자 이후 불가 |
바인딩 | 호출하는 어셈블리에 값이 박힘(binary baked-in) | 필드 참조로 남음(버전 변경 시 안전) |
static 여부 | 자동으로 static | 필요하면 static readonly 명시 |
라이브러리 공개 상수는 const 남발보다 **public static readonly**가 버전 호환성 측면에서 안전한 경우가 많습니다.
5) 샘플 코드
(A) 기본 사용: 인스턴스 readonly 필드
public class User
{
// 선언과 동시에 할당 가능
private readonly DateTime _createdAt = DateTime.UtcNow;
// 생성자에서 한 번만 할당 가능
private readonly Guid _id;
private readonly string _name;
public User(string name)
{
_id = Guid.NewGuid();
_name = name ?? throw new ArgumentNullException(nameof(name));
}
public Guid Id => _id;
public string Name => _name;
public DateTime CreatedAt => _createdAt;
public void Rename(string newName)
{
// _name = newName; // 컴파일 오류: readonly 필드 수정 불가
}
}
(B) const vs static readonly
public static class Paths
{
// 컴파일 상수 (빌드 시 값이 박힘)
public const string AppName = "MyApp";
// 런타임에 조합 가능, 외부 환경 따라 달라질 수 있음
public static readonly string DataDir =
System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
AppName);
}
(C) 참조형 readonly 주의점(참조는 고정, 내부는 가변)
public class Basket
{
private readonly List<string> _items;
public Basket(IEnumerable<string> initial)
{
_items = new List<string>(initial); // 생성자에서 한 번 할당 OK
}
public void Add(string item)
{
// _items 변수 자체는 다른 List로 바꿀 수 없음(참조 고정)
// 하지만 List 내부에는 항목 추가/삭제가 가능 → 완전 불변 아님
_items.Add(item);
}
}
“정말 완전 불변”을 원하면 IReadOnlyList<T>를 노출하거나, 불변 컬렉션(예: ImmutableList<T>)을 사용하세요.
(D) readonly struct & readonly 멤버 (성능/의도)
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
// 이 메서드는 상태를 절대 바꾸지 않음을 보장
public readonly int ManhattanDistance() => Math.Abs(X) + Math.Abs(Y);
}
- readonly struct는 모든 인스턴스 멤버가 상태를 변경하지 않음을 약속합니다.
- 큰 값 형식을 다룰 때는 in 매개변수(읽기 전용 by-ref)로 복사 비용을 줄일 수 있습니다.
(E) in 매개변수 & ref readonly 반환(고급)
public static class Geometry
{
// 큰 구조체를 복사 없이 읽기 전용으로 전달
public static int Distance(in Point p) => p.ManhattanDistance();
}
public static class MinFinder
{
// 배열에서 최소값 요소를 '읽기 전용 참조'로 돌려주기
public static ref readonly int MinRef(ReadOnlySpan<int> span)
{
if (span.IsEmpty) throw new ArgumentException("empty");
int minIdx = 0;
for (int i = 1; i < span.Length; i++)
if (span[i] < span[minIdx]) minIdx = i;
return ref span[minIdx]; // 호출 측에서 복사 없이 읽기 전용 참조 사용
}
}
- 이런 패턴은 복사를 줄여 성능을 얻는 상황에서 쓰입니다. (특히 큰 struct)
6) 사용 시 주의사항 & 팁
- 할당 시점: readonly 필드는 선언 시 또는 해당 타입의 생성자 안에서만 할당하세요.
- 인스턴스 필드 → 인스턴스 생성자
- static readonly → 정적 필드이므로 정적 생성자에서 할당 가능
- 프로퍼티에는 readonly 키워드를 직접 붙이지 않습니다. 대신 getter만 제공하거나, C# 9의 init 접근자를 사용해 “초기화만 허용”하는 패턴을 씁니다.
- public class Person { public string Name { get; init; } // 생성 시점에만 설정 가능 public int Age { get; } = 20; // 사실상 읽기 전용 }
- 배열/컬렉션은 불변 아님: readonly는 참조 보호일 뿐, 요소 수정은 막지 않습니다. 외부로는 IReadOnlyList<T> 인터페이스로 노출하거나, 불변 컬렉션을 고려하세요.
- 라이브러리 상수 공개: 공개 API에서 값이 변할 여지가 있거나 버전 영향이 우려되면 public static readonly가 const보다 안전합니다.
반응형
'C#' 카테고리의 다른 글
volatile는 멀티스레드에서 “가벼운 동기화”를 위한 읽기/쓰기 가시성 보장 장치 (0) | 2025.09.19 |
---|---|
왜 IDisposable을 구현하나요? (0) | 2025.09.19 |
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
sealed를 왜 써야 할까? (0) | 2025.09.19 |
null-coalescing assignment 연산자(??=) 가 C# 8.0에서 추가된 문법이라, C# 7.3 컴파일러로는 사용할 수 없다는 뜻입니다. (0) | 2025.09.11 |
VisionThread/EmguVision 라이브·검사 공통 파이프라인 리팩토링 (풀코드) (0) | 2025.09.10 |
EmguImage 안에 IInputArray를 반환하는 getter (0) | 2025.09.09 |
private static bool IsFiniteFloat(float v) => !(float.IsNaN(v) || float.IsInfinity(v)); (0) | 2025.09.09 |