C# 11: ref struct안의 ref 필드
ref struct는 C# 7.2에서 도입된 기능으로 구조체(struct)가 항상 스택(stack) 상에 존재하도록 여러가지 제약점을 가한 구조체이다. 구조체(non-ref struct)는 일반적으로 스택에 존재하지만, Boxing을 하거나 다른 클래스의 멤버로 사용되는 경우 Heap 상에 존재할 수 있다. ref struct는 이렇게 Heap 상에 존재할 수 있는 것음 금지시키고 항상 스택 상에 존재하도록 한 것으로 이렇게 하기 위해서 ref struct는 다른 클래스의 멤버가 될 수 없고, object boxing 등이 금지되는 등의 여러가지 제약점을 갖게 된다. 컴파일러는 ref struct 타입이 Heap 상에 존재할 수 있는 상황들을 자동 체크해서 만약 그런 경우 컴파일 에러를 발생한다. 일반적으로 ref struct는 스택 상에서 사용함으로써 성능향상을 하거나 또는 다른 ref struct 타입을 그 구조체의 멤버로 포함할 경우에 사용한다. (자세한 내용은 C# 7: ref struct 아티클 참고)
C# 11부터 ref struct 안에 ref 필드를 선언할 수 있게 되었다. ref struct안의 필드를 ref 필드로 선언할 수 있게 한 것은 특히 Span<T> 구조체의 기능을 향상시키고자 함이 있었다. 아래는 Span<T> 구조체 코드의 일부로서 _reference 필드가 ref 필드로 정의되어 있음을 볼 수 있다.
C# 11부터 ref struct 안에 ref 필드를 선언할 수 있게 되었다. ref struct안의 필드를 ref 필드로 선언할 수 있게 한 것은 특히 Span<T> 구조체의 기능을 향상시키고자 함이 있었다. 아래는 Span<T> 구조체 코드의 일부로서 _reference 필드가 ref 필드로 정의되어 있음을 볼 수 있다.
예제
public readonly ref struct Span<T>
{
/// ref 필드
internal readonly ref T _reference;
private readonly int _length;
public Span(T[]? array)
{
//... 생략 ...
_reference = ref MemoryMarshal.GetArrayDataReference(array);
_length = array.Length;
}
//... 생략 ...
}
C# 11: ref 필드 선언 및 사용
ref struct 안에 ref 필드를 선언하는 것은 일반 필드 앞에 ref 를 붙이면 된다.
ref 필드에 'ref 값'을 할당하기 위해서는 (일반 값을 할당하는 '=' operator와 다른) '= ref' operator (ref reassign)를 사용한다. 만약 생성자나 init 엑세서에서만 ref reassign을 할 수 있도록 제한하고자 한다면, 필드 앞에 'readonly ref'을 사용한다. 'readonly ref' 필드의 경우 이렇한 ref 값이 할당된 후에 일단 메모리 위치는 고정지만 그 안의 실제 값(value)을 언제든 변경할 수 있게 된다.
만약 ref 필드 안의 값을 읽기 전용으로 만들기 위해서는 'ref readonly'를 필드 앞에 붙이면 된다. 만약 ref reassign(= ref)과 값 할당(=)을 모두 읽기 전용으로 하기 위해서는 'readonly ref readonly'를 필드 앞에 붙인다.
아래는 MyStruct라는 ref struct 안에 number라는 정수형 ref 필드를 정의하고, 이를 외부에서 사용하는 것을 예시한 것이다.
ref 필드에 'ref 값'을 할당하기 위해서는 (일반 값을 할당하는 '=' operator와 다른) '= ref' operator (ref reassign)를 사용한다. 만약 생성자나 init 엑세서에서만 ref reassign을 할 수 있도록 제한하고자 한다면, 필드 앞에 'readonly ref'을 사용한다. 'readonly ref' 필드의 경우 이렇한 ref 값이 할당된 후에 일단 메모리 위치는 고정지만 그 안의 실제 값(value)을 언제든 변경할 수 있게 된다.
만약 ref 필드 안의 값을 읽기 전용으로 만들기 위해서는 'ref readonly'를 필드 앞에 붙이면 된다. 만약 ref reassign(= ref)과 값 할당(=)을 모두 읽기 전용으로 하기 위해서는 'readonly ref readonly'를 필드 앞에 붙인다.
아래는 MyStruct라는 ref struct 안에 number라는 정수형 ref 필드를 정의하고, 이를 외부에서 사용하는 것을 예시한 것이다.
예제
// calling 하는 코드
int i = 1;
MyStruct r = new MyStruct(ref i);
int n = r.GetNumber();
Console.WriteLine(n);
// ref struct 구조체
public ref struct MyStruct
{
private ref int number;
public MyStruct(ref int num) { number = ref num; }
public int GetNumber()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref number))
{
throw new InvalidOperationException("The number ref field is not initialized.");
}
return number;
}
}
int i = 1;
MyStruct r = new MyStruct(ref i);
int n = r.GetNumber();
Console.WriteLine(n);
// ref struct 구조체
public ref struct MyStruct
{
private ref int number;
public MyStruct(ref int num) { number = ref num; }
public int GetNumber()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref number))
{
throw new InvalidOperationException("The number ref field is not initialized.");
}
return number;
}
}
C# 11: scoped modifier
C# 11의 새로운 키워드 'scoped' modifier는 파라미터나 로컬 변수의 lifetime을 해당 메서드 내로 제한하기 위해 사용된다. 즉, scoped 가 선언되면 그 파라미터나 변수는 그 메서드 외부에서 엑세스될 수 없다. scoped는 value 타입, ref value 타입, 또는 변수 앞에 적용될 수 있다.
아래 [예제1]을 살펴보면, Run() 메서드에서 스택에 4개의 문자를 갖는 배열을 생성(stackalloc)하고, 이를 MyStruct.MyMethod() 메서드의 파라미터로 전달하고 있다. 결과적으로 Run() 메서드에서 생성된 스택 문자배열(span)이 MyMethod() 메서드에서 사용되게 되는데, 만약 span이 어떤 조작으로 통해 MyStruct에서 계속 사용되면서 Run() 메서드가 빠져 나간다면, MyStruct에 남아 있는 레퍼런스는 이미 소실된 스택 위치를 가리킬 가능성이 있다. 컴파일러는 이러한 상황을 방지하고자 CS8352와 같은 에러를 발생시킨다.
[예제A]는 예제1의 에러를 수정한 것으로 MyMethod의 파라미터 앞에 scoped modifier를 넣어 해당 파라미터(chars)가 MyMethod 안에서만 사용됨을 명시한 것이다. scoped modifier는 [예제B]에서 처럼 ref 값 타입에 적용될 수도 있으며, [예제C]에서 처럼 로컬 변수 선언시 앞에 적용될 수도 있다.
아래 [예제1]을 살펴보면, Run() 메서드에서 스택에 4개의 문자를 갖는 배열을 생성(stackalloc)하고, 이를 MyStruct.MyMethod() 메서드의 파라미터로 전달하고 있다. 결과적으로 Run() 메서드에서 생성된 스택 문자배열(span)이 MyMethod() 메서드에서 사용되게 되는데, 만약 span이 어떤 조작으로 통해 MyStruct에서 계속 사용되면서 Run() 메서드가 빠져 나간다면, MyStruct에 남아 있는 레퍼런스는 이미 소실된 스택 위치를 가리킬 가능성이 있다. 컴파일러는 이러한 상황을 방지하고자 CS8352와 같은 에러를 발생시킨다.
[예제A]는 예제1의 에러를 수정한 것으로 MyMethod의 파라미터 앞에 scoped modifier를 넣어 해당 파라미터(chars)가 MyMethod 안에서만 사용됨을 명시한 것이다. scoped modifier는 [예제B]에서 처럼 ref 값 타입에 적용될 수도 있으며, [예제C]에서 처럼 로컬 변수 선언시 앞에 적용될 수도 있다.
예제
// (예제1) scoped를 사용하지 않고 스택배열을
// 파라미터로 전달한 경우 컴파일 에러 발생
Run();
static void Run()
{
// Run() 메서드의 스택에 문자배열 생성
Span<char> span = stackalloc char[] { 'A', 'l', 'e', 'x' };
// 컴파일 에러
// (1) CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope
// (2) CS8350: This combination of arguments to 'MyStruct.MyMethod(ReadOnlySpan<char>)' is disallowed
// because it may expose variables referenced by parameter 'chars' outside of their declaration scope
new MyStruct().MyMethod(span);
Console.WriteLine(span.Length);
}
ref struct MyStruct
{
public void MyMethod(ReadOnlySpan<char> chars)
{
// ... 생략 ...
}
}
// (예제A) "scoped" notation
// scoped를 사용하여, 컴파일러에게 파라미터가 MyMethod 안에서만 사용되고
// 리턴되지도 않는 것을 명시하여 컴파일 에러가 발생하지 않음
ref struct MyStruct
{
public void MyMethod(scoped ReadOnlySpan<char> chars)
{
// ... 생략 ...
}
}
// (예제B) "scoped ref" notation
Span<int> CreateSpan(scoped ref int count)
{
// ... 생략 ...
}
// (예제C) "scoped variable" notation
scoped Span<int> span = stackalloc int[5];
// 파라미터로 전달한 경우 컴파일 에러 발생
Run();
static void Run()
{
// Run() 메서드의 스택에 문자배열 생성
Span<char> span = stackalloc char[] { 'A', 'l', 'e', 'x' };
// 컴파일 에러
// (1) CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope
// (2) CS8350: This combination of arguments to 'MyStruct.MyMethod(ReadOnlySpan<char>)' is disallowed
// because it may expose variables referenced by parameter 'chars' outside of their declaration scope
new MyStruct().MyMethod(span);
Console.WriteLine(span.Length);
}
ref struct MyStruct
{
public void MyMethod(ReadOnlySpan<char> chars)
{
// ... 생략 ...
}
}
// (예제A) "scoped" notation
// scoped를 사용하여, 컴파일러에게 파라미터가 MyMethod 안에서만 사용되고
// 리턴되지도 않는 것을 명시하여 컴파일 에러가 발생하지 않음
ref struct MyStruct
{
public void MyMethod(scoped ReadOnlySpan<char> chars)
{
// ... 생략 ...
}
}
// (예제B) "scoped ref" notation
Span<int> CreateSpan(scoped ref int count)
{
// ... 생략 ...
}
// (예제C) "scoped variable" notation
scoped Span<int> span = stackalloc int[5];