`ref` 구조체 타입

ref 구조체 타입 (C# 참조)

ref struct는 스택에만 할당되고 관리되는 힙(managed heap)으로는 절대 벗어날 수 없다는 점이 핵심이에요. 그래서 이런 특성을 보장하기 위해 컴파일러가 사용 범위를 엄격하게 제한하는데, 그 제한 목록부터 하나씩 살펴볼게요.

출처: Microsoft Learn

본문

구조체 타입을 선언할 때 ref 한정자를 붙이면 ref struct 타입이 돼요. ref struct 타입의 인스턴스는 항상 스택에 할당되며, 관리되는 힙으로 벗어날 수 없어요. 이 특성을 보장하기 위해 컴파일러는 ref struct 타입의 사용을 다음과 같이 제한합니다:

  • ref struct를 배열의 요소 타입으로 사용할 수 없어요.
  • 클래스나 ref struct가 아닌 타입의 필드 타입으로 ref struct를 선언할 수 없어요.
  • ref structxref: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 structreadonly로 선언하려면 타입 선언에서 readonlyref 한정자를 함께 쓰면 돼요. 이때 readonly 한정자가 ref 한정자보다 앞에 와야 해요:

:::code language="csharp" source="snippets/shared/StructType.cs" id="SnippetReadonlyRef":::

.NET에서 ref struct의 대표적인 예는 xref:System.Span`1?displayProperty=nameWithTypexref: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를 참고하세요.

더 알아보기