`ref` 구조체 타입
ref 구조체 타입 (C# 참조)
ref struct는 스택에만 할당되고 관리되는 힙(managed heap)으로는 절대 벗어날 수 없다는 점이 핵심이에요. 그래서 이런 특성을 보장하기 위해 컴파일러가 사용 범위를 엄격하게 제한하는데, 그 제한 목록부터 하나씩 살펴볼게요.
출처: Microsoft Learn
본문
구조체 타입을 선언할 때 ref 한정자를 붙이면 ref struct 타입이 돼요. ref struct 타입의 인스턴스는 항상 스택에 할당되며, 관리되는 힙으로 벗어날 수 없어요. 이 특성을 보장하기 위해 컴파일러는 ref struct 타입의 사용을 다음과 같이 제한합니다:
ref struct를 배열의 요소 타입으로 사용할 수 없어요.- 클래스나
ref struct가 아닌 타입의 필드 타입으로ref struct를 선언할 수 없어요. ref struct를 xref:System.ValueType?displayProperty=nameWithType 또는 xref:System.Object?displayProperty=nameWithType으로 박싱(boxing)할 수 없어요.- 람다 식이나 로컬 함수에서
ref struct변수를 캡처할 수 없어요. - C# 13 이전에는
async메서드에서ref struct변수를 쓸 수 없었어요. C# 13부터는async메서드에서await식과 같은 블록 안에는ref struct변수를 사용할 수 없어요. 다만 동기 메서드 — 예를 들어 xref:System.Threading.Tasks.Task 또는 xref:System.Threading.Tasks.Task`1을 반환하는 메서드 — 에서는ref struct변수를 사용할 수 있어요. - C# 13 이전에는 반복기(iterator)에서
ref struct변수를 사용할 수 없었어요. C# 13부터는yield return문이 있는 코드 구간만 아니라면, 반복기에서ref struct타입과ref로컬을 사용할 수 있어요. - C# 13 이전에는
ref struct가 인터페이스를 구현할 수 없었어요. C# 13부터는ref구조체가 인터페이스를 구현할 수 있지만, ref 안전성 규칙을 반드시 지켜야 해요. 예를 들어ref struct타입은 박싱 변환을 필요로 하기 때문에 인터페이스 타입으로 변환할 수 없어요. - C# 13 이전에는
ref struct가 타입 인수로 사용될 수 없었어요. C# 13부터는 타입 매개변수의where절에allows ref struct가 지정된 경우에만ref struct를 타입 인수로 사용할 수 있어요.
[!INCLUDEcsharp-version-note]
보통은 데이터 멤버로 ref struct 타입을 포함해야 하는 타입이 필요할 때 ref struct 타입을 정의해요:
:::code language="csharp" source="snippets/shared/StructType.cs" id="SnippetRefStruct":::
ref struct를 readonly로 선언하려면 타입 선언에서 readonly와 ref 한정자를 함께 쓰면 돼요. 이때 readonly 한정자가 ref 한정자보다 앞에 와야 해요:
:::code language="csharp" source="snippets/shared/StructType.cs" id="SnippetReadonlyRef":::
.NET에서 ref struct의 대표적인 예는 xref:System.Span`1?displayProperty=nameWithType와 xref:System.ReadOnlySpan`1?displayProperty=nameWithType이에요.
ref 필드
ref struct 안에는 ref 필드를 선언할 수 있어요. 다음 예시를 볼게요:
:::code language="csharp" source="snippets/shared/StructType.cs" id="SnippetRefField":::
ref 필드는 null 값을 가질 수 있어요. xref:System.Runtime.CompilerServices.Unsafe.IsNullRef``1(``0@)?displayProperty=nameWithType 메서드를 사용하면 ref 필드가 null인지 판별할 수 있어요.
ref 필드에는 readonly 한정자를 다음과 같은 방식으로 적용할 수 있어요:
readonly ref:= ref연산자를 이용한 ref 재할당은 생성자나init접근자 안에서만 가능해요.=연산자로 값을 할당하는 것은 필드의 접근 한정자가 허용하는 범위 안에서는 언제든 가능해요.ref readonly: 이 필드에는 어떤 시점에서도=연산자로 값을 할당할 수 없어요. 하지만= ref연산자로 ref 재할당은 할 수 있어요.readonly ref readonly: 이 필드는 생성자나init접근자에서만 ref 재할당할 수 있어요. 어떤 시점에서도 필드에 값을 할당할 수는 없어요.
컴파일러는 ref 필드에 저장된 참조가 가리키는 대상(referent)보다 오래 살지 않음을 보장해요.
ref 필드 기능 덕분에 xref:System.Span`1?displayProperty=fullName 같은 타입을 안전하게 구현할 수 있어요:
public readonly ref struct Span<T>
{
internal readonly ref T _reference;
private readonly int _length;
// Omitted for brevity...
}
Span<T> 타입은 참조를 저장해 두고 이를 통해 메모리의 연속된 요소들에 접근해요. 참조를 사용하기 때문에 Span<T> 인스턴스는 자신이 가리키는 저장 공간을 복사하지 않아도 돼요.
삭제 패턴 (disposable pattern)
삭제 가능한 ref struct를 정의할 수 있어요. 그러려면 그 ref struct가 삭제 패턴에 맞아야 해요. 즉, 접근 가능하고 매개변수가 없으며 void 반환 타입인 인스턴스 Dispose 메서드를 가져야 한다는 뜻이에요. 삭제 가능한 ref struct의 인스턴스에는 using 문 또는 using 선언을 사용할 수 있어요.
C# 13부터는 ref struct 타입에 xref:System.IDisposable?displayName=nameWithType을 구현할 수도 있어요. 다만 오버로드 해석은 인터페이스 메서드보다 삭제 패턴을 우선해요. 컴파일러는 적절한 Dispose 메서드를 찾지 못했을 때에만 IDisposable.Dispose 메서드로 해석해요.
인터페이스를 구현하는 ref struct 타입의 제한 사항
이 제한들은 인터페이스를 구현하는 ref struct 타입이 꼭 필요한 ref 안전성 규칙을 따르도록 보장해 줘요.
ref struct를 자신이 구현한 인터페이스의 인스턴스로 변환할 수 없어요. 이 제한에는ref struct타입을 인수로 사용하면서 매개변수가 인터페이스 타입일 때의 암시적 변환도 포함돼요. 그런 변환은 박싱 변환이 되어 ref 안전성을 위반하거든요.ref struct는 메서드를 명시적 인터페이스 선언으로 선언할 수 있어요. 다만 그 메서드들은 타입 매개변수가allows ref struct타입인 제네릭 메서드에서만 접근할 수 있어요.- 인터페이스를 구현하는
ref struct는 모든 인스턴스 인터페이스 멤버를 반드시 구현해야 해요. 인터페이스에 기본 구현(default implementation)이 있더라도ref struct는 인스턴스 멤버를 구현해야 해요.
컴파일러가 이 제한들을 강제해요. 인터페이스를 구현하는 ref struct 타입을 작성한다면, 업데이트마다 새로운 기본 인터페이스 멤버가 추가될 수 있어요. 어떤 새 인스턴스 메서드에 대해 구현을 제공하기 전까지는 애플리케이션이 컴파일되지 않아요. 기본 구현이 있는 static 인터페이스 메서드에는 특정 구현을 제공할 수 없어요.
[!IMPORTANT]
ref struct타입으로 인터페이스를 구현하면 나중에 소스 호환성(Source-breaking)과 이진 호환성(Binary-breaking)이 깨질 가능성이 생겨요.ref struct가 다른 어셈블리에 정의된 인터페이스를 구현하고, 그 어셈블리가 해당 인터페이스에 기본 멤버를 추가하는 업데이트를 제공할 때 이런 깨짐이 발생해요.소스 호환성이 깨지는 경우는
ref struct를 다시 컴파일할 때예요. 기본 구현이 있더라도 새 멤버를 반드시 구현해야 하거든요.이진 호환성이 깨지는 경우는
ref struct타입을 다시 컴파일하지 않고 외부 어셈블리를 업그레이드했을 때, 그리고 업데이트된 코드가 새 메서드의 기본 구현을 호출할 때예요. 런타임은 기본 멤버에 접근할 때 예외를 던져요.
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 섹션을 참고하세요:
ref 필드에 대한 자세한 내용은 C# 언어 사양의 Ref fields를 참고하세요.