닷넷의 관리 포인터(Managed Pointer)와 System.TypedReference
관리 포인터(Managed Pointer)가 그동안 C# 문법에서 "ref" 예약어를 이용해 사용할 수 있었지만, 사실 "참조"라는 말로 대신 이해해도 되었기 때문에 굳이 그것의 특성에 관해 몰라도 상관없었습니다. 그런데, C# 7.2부터 추가된 기능들을 (사용이 아닌) 이해하기 위해서는 관리 포인터에 대한 사전 지식을 갖추는 것이 좋습니다.
이에 대해 다음의 2가지 글이 아주 잘 설명해 주고 있습니다.
C# Futures: Managed Pointers
; https://www.infoq.com/news/2015/04/CSharp-7-Pointers
Managed pointers
; http://mustoverride.com/managed-refs-CLR/
C# 6.0 이전까지 관리 포인터는 매개 변수의 ref 구문으로 사용할 수 있었습니다.
using System;
class Program
{
static void Main(string[] args)
{
Program pg = new Program();
StructPerson sarah = new StructPerson() { Name = "Kerrigan", Age = 27 };
pg.PassByManagedPointer(ref sarah);
}
private void PassByManagedPointer(ref StructPerson sarah)
{
}
}
struct StructPerson
{
public int Age;
public string Name;
}
그러다가 C# 7.0에서 로컬 변수와 메서드의 반환값에 대해 관리 포인터를 사용하는 것이 가능해졌습니다.
class Program
{
static void Main(string[] args)
{
Program pg = new Program();
{
int a = 5;
ref int refA = ref a;
ref int refB = ref pg.GetRef(ref a);
refB = 10;
Console.WriteLine(a); // 출력 결과 10
}
}
private ref int GetRef(ref int a)
{
return ref a;
}
}
이후 C# 7.2부터는 3항 연산자에도 ref를 사용할 수 있게 했습니다.
C# 7.2 - 3항 연산자에 ref 지원
; https://www.sysnet.pe.kr/2/0/11528
그런데, 여기서 한 가지 재미있는 점이 있습니다. 로컬 변수, 매개 변수, 반환 값에도 가능한 관리 포인터가 클래스/구조체의 필드로는 정의할 수 없다는 것입니다. (업데이트: C# 11부터 ref struct에 한해 필드로 정의할 수 있습니다.)
struct StructPerson
{
public int Age;
public string Name;
// 컴파일 오류: CS0501 'StructPerson.Height()' must declare a body because it is not marked abstract, extern, or partial
ref int Height;
}
그 이유는 "
Managed pointers" 글에서 설명하고 있습니다.
Fields and array elements are not permitted to have & types. & cannot be boxed either. These restrictions are a bit artificial. It just makes the job of GC easier if & themselves are never on the heap.
그러니까, 관리 포인터를 필드로 갖는 타입을 정의하게 되면 GC 구현이 더 난해해지기 때문에 의도적으로 제약을 둔 것입니다.
관리 포인터를 다뤘으니 이제 System.TypedReference를 설명할 수 있습니다.
TypedReference Structure
; https://learn.microsoft.com/en-us/dotnet/api/system.typedreference
TypedReference 타입은 .NET 1.1부터 있어왔지만 사실 개발자들이 거의 사용하지 않는 타입입니다. 저도 이에 관해 이야기한 적이 딱 2번 있었군요. ^^
C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법
; https://www.sysnet.pe.kr/2/0/11312
C#에서 enum을 boxing 없이 int로 변환하기 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/11506
문서에 있는 바대로,
Describes objects that contain both a managed pointer to a location and a runtime representation of the type that may be stored at that location.
TypedReference는 내부적으로 관리 포인터와 대상 타입의 정보를 보관하고 있습니다. "관리 포인터"를 가지고 있다는 것은 곧, 위에서 이야기했던 ref의 제약을 그대로 가지고 있음을 의미합니다. 즉, "관리 포인터"가 힙 안에 보관될 수 없기 때문에 "관리 포인터"를 소유한 TypedReference 역시 힙 안에 보관될 수 없으므로 타입의 필드로 정의될 수 없습니다.
struct StructPerson
{
public int Age;
public string Name;
// 컴파일 오류: CS0610 Field or property cannot be of type 'TypedReference'
public TypedReference tr;
}
대신 ref와 유사하게 (반환을 제외한) 로컬 변수와 매개 변수로 사용하는 것은 가능합니다.
using System;
class Program
{
static int _number;
static void Main(string[] args)
{
int x = 3;
System.TypedReference xRef = __makeref(x);
{
PassTR(xRef);
}
{
System.TypedReference tr = GetTR();
}
}
// 컴파일 오류: CS1599 Method or delegate cannot return type 'TypedReference'
private static TypedReference GetTR()
{
TypedReference tr = __makeref(_number);
return tr;
}
private static void PassTR(TypedReference tr)
{
}
}
그런데 왜? ref는 메서드의 반환으로 사용할 수 있으면서 TypedReference는 안되는 걸까요? 그 이유는, ref의 경우에도 안전하지 않은 반환 유형이 있는데 그것을 TypedReference에 적용할 수 없기 때문입니다. (아마도 ref가 그렇듯이 추적할 수는 있겠지만 굳이 그런 체크를 추가하진 않은 듯합니다.) 예를 들어, 로컬 변수에 대한 참조는 ref 예약어로도 반환할 수 없습니다.
private static ref int GetRefInt1()
{
int number = 10;
return ref number; // CS8168 Cannot return local 'number' by reference because it is not a ref local
}
private static ref int GetRefInt2()
{
int number = 10;
ref int refA = ref number;
ref int refB = ref refA;
// 결국 로컬 변수의 참조이므로!
return ref refB; // 컴파일 오류: CS8157 Cannot return 'refB' by reference because it was initialized to a value that cannot be returned by reference
}
위와 같이 C# 컴파일러는 ref가 적용된 참조 변수에 대해서는 그 대상이 로컬 변수인지를 관리할 수 있는 반면, TypedReference에 대해서는 관리를 못해 아예 반환값으로는 사용할 수 없도록 제약을 둔 것입니다.
이와 함께 TypedReference의 경우 자신이 정의하지 않은 메서드에 대한 호출은 모두 (내부적으로 관리 힙에 놓일 가능성이 있으므로) 허용되지 않습니다.
int number = 50;
TypedReference tr = __makeref(number);
// 컴파일 오류: CS0029 Cannot implicitly convert type 'System.TypedReference' to 'object'
TypedReference.ReferenceEquals(obj1, obj2);
// 컴파일 오류: CS0029 Cannot implicitly convert type 'System.TypedReference' to 'System.ValueType'
tr.ToString();
// 컴파일 오류: CS0029 Cannot implicitly convert type 'System.TypedReference' to 'object'
tr.GetType();
반면 Equals와 GetHashCode에 대해서는 TypedReference가 override하고 있기 때문에 호출은 가능하지만 그나마도 Equals의 경우 지원하지 않는다는 예외를 발생시키도록 재정의되었고 오직 동작하는 것은 GetHashCode 하나입니다.
// 런타임 에러 - An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll
// This feature is not currently implemented.
tr.Equals(null);
int result = tr.GetHashCode();
아울러 (절대로 "힙"위에는 올라가서는 안되므로) 배열로도 생성할 수 없습니다.
// 런타임 오류: System.TypeLoadException: 'Could not create array type 'System.TypedReference[]' from assembly 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.'
Assembly.Load("mscorlib.dll").GetType("System.TypedReference[]");
결국 이러한 TypedReference 타입의 성격을 한마디로 표현하면, "스택에서만 존재할 수 있는 타입"이라는 점입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]