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
정성태

... 16  17  18  19  20  21  22  23  24  25  [26]  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
12968정성태2/14/20226590오류 유형: 794. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for '...'.
12967정성태2/14/20226978VC++: 153. Visual C++ - C99 표준의 Compund Literals 빌드 방법 [4]
12966정성태2/13/20226835.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
12965정성태2/13/20226705.NET Framework: 1154. "Hanja Hangul Project v1.01 (파이썬)"의 C# 버전
12964정성태2/11/20227019.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/20227760.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/20227603오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20227737.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/20227160개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20227874.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20227945.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20227570개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/20226779.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/20226175개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/20226029VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20226278Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/20226749.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/20226716.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20226376.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226215.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226338.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227496개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/20225996오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226249오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20228616.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227530.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
... 16  17  18  19  20  21  22  23  24  25  [26]  27  28  29  30  ...