Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

닷넷의 관리 포인터(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

그런데, 여기서 한 가지 재미있는 점이 있습니다. 로컬 변수, 매개 변수, 반환 값에도 가능한 관리 포인터가 클래스/구조체의 필드로는 정의할 수 없다는 것입니다.

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://msdn.microsoft.com/en-us/library/system.typedreference(v=vs.110).aspx

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 타입의 성격을 한 마디로 표현하면, "스택에서만 존재할 수 있는 타입"이라는 점입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 5/23/2018 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2018-05-25 02시00분
[spowner] TypedReference에 대해 전혀 모르다가 좋은 배움 얻고 갑니다. TypedReference로 여러가지 확인을 하던 중 local function에서는 반환이 되는것으로 보아 local function의 scope규칙이 local field랑 동일한 것으로 추측이 됩니다.
덕분에 local function이 왜 생겨났고 람다랑은 어떤차이이며 어떠한 이점이 있는지 그리고 재귀함수의 경우 아쉽게도 스택오버플로우는 발생하더군요. 이것까지도 최적화 해주면 참 좋겠다는 생각해봅니다;

[손님]
2018-05-25 02시57분
@spowner님 "아쉽게도 스택오버플로우는 발생"이 어떤 상황을 말하는 건가요? ^^ 그리고 Local Function의 경우 반환되는 것은 C# 컴파일러의 버그로 보입니다. 실제로 Local Function은 추출되어 일반 메서드와 (특별한 처리 없이) 동일하게 빌드됩니다. 일례로 다음과 같은 경우 (Debug 빌드에서) 잘 결과가 나오는 듯 합니다.

    private static void TestLocalFunction()
    {
        TypedReference inst = GetInstanceFromLocalFunction();
        int value = __refvalue(inst, int);
        Console.WriteLine(value); // 출력 결과: 100

        TypedReference GetInstanceFromLocalFunction()
        {
            int x = 100;
            TypedReference tr = __makeref(x);
            return tr;
        }
    }

하지만 운이 좋아 스택이 보관된 것인데요, Release 빌드인 경우이거나 해제된 스택을 덧쓰는 또 다른 호출이 그 사이에 끼어 있으면 잘못된 결과가 나옵니다.

    // release 빌드이거나,
    private static void TestBrokenLocalFunction()
    {
        TypedReference inst = GetInstanceFromLocalFunction();

        Console.WriteLine(); // 스택을 깨는 이런 코드가 끼어 있거나!
        int value = __refvalue(inst, int);
        Console.WriteLine("expected 100: " + value);

        TypedReference GetInstanceFromLocalFunction()
        {
            int x = 100;
            TypedReference tr = __makeref(x);
            return tr;
        }
    }


정성태
2018-06-07 04시42분
Local Function에서 TypedReference 반환에 대해 이슈를 올렸더니 버그로 태그가 달렸고 아마 조만간 수정될 것 같습니다. ^^

TypedReference shoud not be returned from C# 7.0 Local Function #27463
; https://github.com/dotnet/roslyn/issues/27463
정성태
2018-06-07 06시03분
[spowner] 엄지 척!
[손님]
2018-10-23 11시45분
@spowner Local Function에서 TypedReference를 반환하지 못하도록 Roslyn에 반영되었다고 합니다. 이제 merge가 되었으니 아마도 다음 번(정식으로는 8.0) C# 버전에는 반영이 될 것입니다.

Disallow returning TypedReference in local functions
; https://github.com/dotnet/roslyn/pull/30654
정성태

... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
11613정성태7/17/20183811Graphics: 7. Unity로 실습하는 Shader (5) - Flat Shading
11612정성태7/16/20183170Windows: 148. Windows - Raw Input의 Top level collection 의미
11611정성태8/3/20183991Graphics: 6. Unity로 실습하는 Shader (4) - 퐁 셰이딩(phong shading)
11610정성태8/3/20182599Graphics: 5. Unity로 실습하는 Shader (3) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model) + Texture
11609정성태8/3/20183760Graphics: 4. Unity로 실습하는 Shader (2) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model)
11608정성태7/17/20186064Graphics: 3. Unity로 실습하는 Shader (1) - 컬러 반전 및 상하/좌우 뒤집기
11607정성태8/30/20186018Graphics: 2. Unity로 실습하는 Shader
11606정성태8/14/20186612사물인터넷: 19. PC에 연결해 동작하는 자신만의 USB 장치 만들어 보기파일 다운로드1
11605정성태8/9/20183418사물인터넷: 18. New NodeMcu v3 아두이노 호환 보드의 내장 LED 및 입력 핀 사용법파일 다운로드1
11604정성태7/12/20182871Math: 47. GeoGebra 기하 (24) - 정다각형파일 다운로드1
11603정성태7/12/20182413Math: 46. GeoGebra 기하 (23) - sqrt(n) 제곱근파일 다운로드1
11602정성태7/11/20182670Math: 45. GeoGebra 기하 (22) - 반전기하학의 원에 관한 반사변환파일 다운로드1
11601정성태7/11/20183091Math: 44. GeoGebra 기하 (21) - 반전기하학의 직선 및 원에 관한 반사변환파일 다운로드1
11600정성태7/10/20182933Math: 43. GeoGebra 기하 (20) - 세 점을 지나는 원파일 다운로드1
11599정성태7/10/20182417Math: 42. GeoGebra 기하 (19) - 두 원의 안과 밖으로 접하는 직선파일 다운로드1
11598정성태7/10/20182402Windows: 147. 시스템 복구 디스크를 USB 디스크에 만드는 방법
11597정성태8/9/20183074사물인터넷: 17. Thinary Electronic - ATmega328PB 아두이노 호환 보드의 개발 환경 구성
11596정성태7/10/20182691기타: 72. 과거의 용어 설명 - OWIN
11595정성태11/20/20186217사물인터넷: 16. New NodeMcu v3 아두이노 호환 보드의 기본 개발 환경 구성
11594정성태7/8/20183228Math: 41. GeoGebra 기하 (18) - 원의 중심 및 접선파일 다운로드1
11593정성태7/8/20182923Math: 40. GeoGebra 기하 (17) - 각의 복사파일 다운로드1
11591정성태7/7/20182745Math: 39. GeoGebra 기하 (16) - 삼각형의 방심과 방접원파일 다운로드1
11590정성태7/7/20182240Math: 38. GeoGebra 기하 (15) - 삼각형의 수심파일 다운로드1
11589정성태7/7/20183038.NET Framework: 787. object로 형변환된 인스턴스를 원래의 타입 인자로 제네릭 메서드를 호출하는 방법 [2]파일 다운로드1
11588정성태12/19/20192890디버깅 기술: 116. windbg 분석 사례 - ASP.NET 웹 응용 프로그램의 CPU 100% 현상
11587정성태7/5/20182567.NET Framework: 786. ASP.NET - HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상
... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...