Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

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

... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
11573정성태7/2/20182086Math: 26. GeoGebra 기하 (3) - 평행선
11572정성태7/1/20181891.NET Framework: 783. C# 컴파일러가 허용하지 않는 (유효한) 코드를 컴파일해 테스트하는 방법
11571정성태7/1/20181835.NET Framework: 782. C# - JIRA에 등록된 Project의 Version 항목 추가하는 방법파일 다운로드1
11570정성태7/2/20182888Math: 25. GeoGebra 기하 (2) - 임의의 선분과 특정 점을 지나는 수직선파일 다운로드1
11569정성태7/1/20182520Math: 24. GeoGebra 기하 (1) - 수직 이등분선파일 다운로드1
11568정성태7/12/20183756Math: 23. GeoGebra 기하 - 컴퍼스와 자를 이용한 작도 프로그램 [1]
11567정성태6/28/20182265.NET Framework: 781. C# - OpenCvSharp 사용 시 포인터를 이용한 속도 향상파일 다운로드1
11566정성태6/28/20184085.NET Framework: 780. C# - JIRA REST API 사용 정리파일 다운로드1
11565정성태6/28/20182366.NET Framework: 779. C# 7.3에서 enum을 boxing 없이 int 변환하기 - 세 번째 이야기파일 다운로드1
11564정성태6/27/20182309.NET Framework: 778. (Unity가 사용하는) 모노 런타임의 __makeref 오류
11563정성태6/27/20182049개발 환경 구성: 386. .NET Framework Native compiler 프리뷰 버전 사용법
11562정성태6/26/20181904개발 환경 구성: 385. 레지스트리에 등록된 원격지 스크립트 COM 객체 실행 방법
11561정성태6/26/20183849.NET Framework: 777. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! [3]파일 다운로드1
11560정성태3/2/20191811.NET Framework: 776. C# 7.3 - 초기화 식에서 변수 사용 가능(expression variables in initializers)파일 다운로드1
11559정성태1/27/20203923개발 환경 구성: 384. 영문 설정의 Windows 10 명령행 창(cmd.exe)의 한글 지원 [3]
11558정성태6/25/20182282.NET Framework: 775. C# 7.3 - unmanaged(blittable) 제네릭 제약파일 다운로드1
11557정성태6/22/20182589.NET Framework: 774. C# - blittable 타입이란?파일 다운로드1
11556정성태6/25/20184696.NET Framework: 773. C# 7.3 - 구조체의 고정 크기를 갖는 fixed 배열 필드에 대한 직접 접근 가능 [1]파일 다운로드1
11555정성태6/25/20182174.NET Framework: 772. C# 7.3 - 사용자 정의 타입에 fixed 적용 가능(Custom fixed)파일 다운로드1
11554정성태6/25/20182310.NET Framework: 771. C# 7.3 - 자동 구현 속성에 특성 적용 가능(Attribute on backing field)
11553정성태6/25/20182559.NET Framework: 770. C# 7.3 - 개선된 메서드 선택 규칙 3가지(Improved overload candidates)파일 다운로드1
11552정성태6/25/20182487.NET Framework: 769. C# 7.3에서 개선된 문법 4개(Support == and != for tuples, Ref Reassignment, Constraints, Stackalloc initializers)파일 다운로드1
11551정성태6/14/20181683개발 환경 구성: 383. BenchmarkDotNet 사용 시 주의 사항
11550정성태6/13/20182083.NET Framework: 768. BenchmarkDotNet으로 Span<T> 성능 측정
11549정성태6/13/20181809개발 환경 구성: 382. BenchmarkDotNet에서 생성한 BuildPlots.R 파일을 실행하는 방법
11548정성태6/13/20182072오류 유형: 470. .NET Core + BenchmarkDotNet 실행 시 프레임워크를 찾지 못하는 문제
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...