Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 7개 있습니다.)

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

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/1/2023]

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

비밀번호

댓글 작성자
 



2018-05-25 02시00분
[spowner] TypedReference에 대해 전혀 모르다가 좋은 배움 얻고 갑니다. TypedReference로 여러가지 확인을 하던 중 local function에서는 반환이 되는것으로 보아 local function의 scope규칙이 local field랑 동일한 것으로 추측이 됩니다.
덕분에 local function이 왜 생겨났고 람다랑은 어떤차이이며 어떠한 이점이 있는지 그리고 재귀함수의 경우 아쉽게도 스택오버플로우는 발생하더군요. 이것까지도 최적화 해주면 참 좋겠다는 생각해봅니다;
[guest]
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] 엄지 척!
[guest]
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
정성태
2022-10-27 09시13분
Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword
; https://blog.ndepend.com/managed-pointers-span-ref-struct-c11-ref-fields-and-the-scoped-keyword/

C# 11 Language design: Low Level Struct Improvements (August 2022)
; https://github.com/dotnet/csharplang/blob/d45a4817470319e08707d2426b9ad4edebe99676/proposals/low-level-struct-improvements.md

Runtime implementation
; https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Span.cs

Managed pointers in .NET by Konrad Konkosa (January 2019)
; https://tooslowexception.com/managed-pointers-in-net/

Span by Adam Sitnik (July 2017)
; https://adamsitnik.com/Span/

Unsafe array access and pointer arithmetics in C# by Nicolas Portmann (January 2019)
; https://ndportmann.com/system-runtime-compilerservices-unsafe/

Improve C# code performance with Span<T> by Patrick Smacchia (February 2022)
; https://blog.ndepend.com/improve-c-code-performance-with-spant/

ref structs in C# 7.2 – .NET Concept of the Week by Gergely Kalapos (August 2018)
; https://kalapos.net/Blog/ShowPost/DotNetConceptOfTheWeek16-RefStruct

Stackoverflow question: What is ref struct in definition site (January 2018)
; https://stackoverflow.com/questions/48234842/what-is-ref-struct-in-definition-site
정성태

... 31  32  33  34  35  36  37  [38]  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12677정성태6/17/20219548VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
12676정성태6/16/20219619VC++: 143. ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법파일 다운로드1
12675정성태6/16/20217634Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/20218396VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/20219577Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/202110698오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/202117405오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
12670정성태6/13/20218949.NET Framework: 1071. DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제파일 다운로드1
12669정성태6/11/20218914.NET Framework: 1070. 사용자 정의 GetHashCode 메서드 구현은 C# 9.0의 record 또는 리팩터링에 맡기세요.
12668정성태6/11/202110667.NET Framework: 1069. C# - DLL Surrogate를 이용한 Out-of-process COM 개체 제작파일 다운로드2
12667정성태6/10/20219279.NET Framework: 1068. COM+ 서버 응용 프로그램을 이용해 CoInitializeSecurity 제약 해결파일 다운로드1
12666정성태6/10/20217917.NET Framework: 1067. 별도 DLL에 포함된 타입을 STAThread Main 메서드에서 사용하는 경우 CoInitializeSecurity 자동 호출파일 다운로드1
12665정성태6/9/20219244.NET Framework: 1066. Wslhub.Sdk 사용으로 알아보는 CoInitializeSecurity 사용 제약파일 다운로드1
12664정성태6/9/20217539오류 유형: 723. COM+ PIA 참조 시 "This operation failed because the QueryInterface call on the COM component" 오류
12663정성태6/9/20219022.NET Framework: 1065. Windows Forms - 속성 창의 디자인 설정 지원: 문자열 목록 내에서 항목을 선택하는 TypeConverter 제작파일 다운로드1
12662정성태6/8/20218199.NET Framework: 1064. C# COM 개체를 PIA(Primary Interop Assembly)로써 "Embed Interop Types" 참조하는 방법파일 다운로드1
12661정성태6/4/202118805.NET Framework: 1063. C# - MQTT를 이용한 클라이언트/서버(Broker) 통신 예제 [4]파일 다운로드1
12660정성태6/3/20219902.NET Framework: 1062. Windows Forms - 폼 내에서 발생하는 마우스 이벤트를 자식 컨트롤 영역에 상관없이 수신하는 방법 [1]파일 다운로드1
12659정성태6/2/202111181Linux: 40. 우분투 설치 후 MBR 디스크 드라이브 여유 공간이 인식되지 않은 경우 - Logical Volume Management
12658정성태6/2/20218628Windows: 194. Microsoft Store에 있는 구글의 공식 Youtube App
12657정성태6/2/20219909Windows: 193. 윈도우 패키지 관리자 - winget 설치
12656정성태6/1/20218132.NET Framework: 1061. 서버 유형의 COM+에 적용할 수 없는 Server GC
12655정성태6/1/20217682오류 유형: 722. windbg/sos - savemodule - Fail to read memory
12654정성태5/31/20217709오류 유형: 721. Hyper-V - Saved 상태의 VM을 시작 시 오류 발생
12653정성태5/31/202110334.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
12652정성태5/31/20218462VS.NET IDE: 164. Visual Studio - Web Deploy로 Publish 시 암호창이 매번 뜨는 문제
... 31  32  33  34  35  36  37  [38]  39  40  41  42  43  44  45  ...