Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법 [링크 복사], [링크+제목 복사],
조회: 7965
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

C# 11에 추가된 ref 필드의 (우회) 구현 방법

지난 글에서,

C# 11  - (18) ref struct에 ref 필드를 허용
; https://www.sysnet.pe.kr/2/0/13015

C# 11에 ref 필드를 허용한다고 했는데요, 이와 관련해 재미있는 트윗이 있었습니다.

// https://twitter.com/SergioPedri/status/1507006531539144717?s=20&t=EFOovlJnTChnuaM4Ju2Txg

"Mom can we have ref fields?"
"No, we have ref fields at home"

Ref fields at home:

    public unsafe ref struct MyWrapperRefStruct
    {
        private readonly void* _target;

        public MyWrapperRefStruct(ref MyWrapperRefStruct target)
        {
            var f1 = (delegate*<ref byte, void*>)&Unsafe.AsPointer<byte>;
            var f2 = (delegate*<ref MyWrapperRefStruct, void*>)f1;
            var p = f2(ref target);

            _target = p;
        }

        public ref MyWrapperRefStruct Target
        {
            get
            {
                var f1 = (delegate*<void*, ref byte>)&Unsafe.AsRef<byte>;
                var f2 = (delegate*<void*, ref MyWrapperRefStruct>)f1;
                ref var target = ref f2(_target);

                return ref target;
            }
        }
    }

덧글에서 추가로 밝히고 있지만,

C# 11 will let you declare ref fields in a ref struct. This is a hack to achieve functionally the same without runtime/language support, by using a void* field, Unsafe.AsPointer to convert a ref T to a void*, and a delegate cast to bypass the ref struct type argument restriction.


C# 10 이하에서도 저 방법을 사용해 ref 필드와 유사한 역할을 구현할 수 있습니다. 위의 소스 코드를 하나씩 좀 뜯어볼까요? ^^

우선, void* 포인터 변수를 하나 선언해 두고, 특정 변수의 메모리 위치를 얻기 위해 Unsafe.AsPointer 메서드를 호출한 후 그 값을 void* 포인터 변수에 넣어두고 있습니다.

int age = 50;

delegate*<ref byte, void*> f1 = (delegate*<ref byte, void*>)&Unsafe.AsPointer<byte>;
delegate*<ref int, void*> f2 = (delegate*<ref int, void*>)f1;
void *p = f2(ref age);

*((int*)p) = 60;
Console.WriteLine(age); // 출력 결과: 60

결국 핵심은 Unsafe.AsPointer 메서드인데요, 저 코드를 굳이 2단계로 분리할 필요는 없으므로 다음과 같이 간단하게 정리할 수 있습니다.

int age = 50;
delegate*<ref int, void*> f1 = (delegate*<ref int, void*>)&Unsafe.AsPointer<int>;
void *p = f1(ref age);

*((int*)p) = 60;
Console.WriteLine(age); // 출력 결과: 60

그리고 그 포인터 변수로부터 원래의 ref 타입 변수로 복원하는 것을 Unsafe.AsRef 메서드가 하는데요, 이것 역시 2단계로 분리할 필요 없이 다음과 같이 곧바로 형변환해 사용할 수 있습니다.

var f2 = (delegate*<void*, ref int>)&Unsafe.AsRef<int>;
ref int target = ref f2(p);

target = 99;
Console.WriteLine(age); // 출력 결과: 99

따라서, 이걸 일반화시켜 원래의 트윗에 해당하던 코드를 다음과 같은 식으로 바꿔 코딩할 수 있습니다.

using System;
using System.Runtime.CompilerServices;

public unsafe class MyType<T>
{
    private readonly void* _target;

    public MyType(ref T target)
    {
        var f = (delegate*<ref T, void*>)&Unsafe.AsPointer<T>;
        var p = f(ref target);

        _target = p;
    }

    public ref T? Target
    {
        get
        {
            var f = (delegate*<void*, ref T>)&Unsafe.AsRef<T>;
            ref var target = ref f(_target);

            return ref target;
        }
    }
}

internal class Program
{
    static void Main(string[] args)
    {
        int age = 50;
        MyType<int> t = new MyType<int>(ref age);

        t.Target = 20;

        Console.WriteLine(age); // 출력 결과: 20
    }
}

정리하면, C# 11의 ref 필드를 void* 필드로 바꿔 동일하게 구현하고 있는 것입니다.




물론, 해당 원글의 주인장이 말한 대로,

C# 11 will let you declare ref fields in a ref struct.

A few notes. Yes, this does work. No, you should not actually use this. Yes, I might not always follow my own advice. Yes, this will lead to GC holes if you don't manually handle the lifetimes of your targets, as the language won't help you here. Also, @jaredpar I'm so sorry


이 글에서 정의한 MyType 클래스는 C# 11의 ref 필드와 같은 "문법"을 통해 구현한 것은 아니므로, 기본적으로 컴파일러가 제공하는 기타 제약 사항에 대한 도움을 받지 못합니다. 즉, MyType은 ref struct일 필요도 없기 때문에, ref 필드의 대상에 대한 관리를 꼼꼼하게 하지 않는다면 프로그램은 오동작할 수밖에 없습니다. 실제로 MyType은 이렇게 사용하는 것도 가능한데,

MyType<int> t = GetMyType();
Console.WriteLine(t.Target); // 예측할 수 없는 값

private static MyType<int> GetMyType()
{
    int age = 50;
    MyType<int> t = new MyType<int>(ref age);
    return t;
}


당연히, int age가 속한 스택이 GetMyType 메서드의 실행 중에만 유효한 탓에 위의 코드는 Console.WriteLine의 출력 결과가 예측할 수 없는 값이 나오게 됩니다.




참고로, 이것은 결국 TypedReference의 제약을 없앤 것과 같습니다.

닷넷의 관리 포인터(Managed Pointer)와 System.TypedReference
; https://www.sysnet.pe.kr/2/0/11529

현재 BCL에서 TypedReference는 ref struct 유형이므로, 원래는 다른 ref struct 타입 내의 필드로 정의될 수 있어야 합니다. 하지만, 실제로는 이런 경우 빌드 오류가 발생합니다.

public ref struct MyStruct1
{
    System.TypedReference xRef; // Error CS0610 Field or property cannot be of type 'TypedReference'
}

아마도, C# 11에서는 System.TypedReference 역시 ref struct 타입 내의 필드로 정의하는 것이 가능하지 않을까요? ^^

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/30/2022]

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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12664정성태6/9/20217710오류 유형: 723. COM+ PIA 참조 시 "This operation failed because the QueryInterface call on the COM component" 오류
12663정성태6/9/20219249.NET Framework: 1065. Windows Forms - 속성 창의 디자인 설정 지원: 문자열 목록 내에서 항목을 선택하는 TypeConverter 제작파일 다운로드1
12662정성태6/8/20218358.NET Framework: 1064. C# COM 개체를 PIA(Primary Interop Assembly)로써 "Embed Interop Types" 참조하는 방법파일 다운로드1
12661정성태6/4/202119053.NET Framework: 1063. C# - MQTT를 이용한 클라이언트/서버(Broker) 통신 예제 [4]파일 다운로드1
12660정성태6/3/202110153.NET Framework: 1062. Windows Forms - 폼 내에서 발생하는 마우스 이벤트를 자식 컨트롤 영역에 상관없이 수신하는 방법 [1]파일 다운로드1
12659정성태6/2/202111383Linux: 40. 우분투 설치 후 MBR 디스크 드라이브 여유 공간이 인식되지 않은 경우 - Logical Volume Management
12658정성태6/2/20218837Windows: 194. Microsoft Store에 있는 구글의 공식 Youtube App
12657정성태6/2/202110098Windows: 193. 윈도우 패키지 관리자 - winget 설치
12656정성태6/1/20218342.NET Framework: 1061. 서버 유형의 COM+에 적용할 수 없는 Server GC
12655정성태6/1/20217798오류 유형: 722. windbg/sos - savemodule - Fail to read memory
12654정성태5/31/20217824오류 유형: 721. Hyper-V - Saved 상태의 VM을 시작 시 오류 발생
12653정성태5/31/202110520.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
12652정성태5/31/20218642VS.NET IDE: 164. Visual Studio - Web Deploy로 Publish 시 암호창이 매번 뜨는 문제
12651정성태5/31/20218898오류 유형: 720. PostgreSQL - ERROR: 22P02: malformed array literal: "..."
12650정성태5/17/20218223기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
12649정성태5/16/20219535.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미 [1]
12648정성태5/16/20218183사물인터넷: 66. PC -> FTDI -> NodeMCU v1 ESP8266 기기를 UART 핀을 연결해 직렬 통신하는 방법파일 다운로드1
12647정성태5/15/20219425.NET Framework: 1058. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용파일 다운로드1
12646정성태5/15/20218565사물인터넷: 65. C# - Arduino IDE의 Serial Monitor 기능 구현파일 다운로드1
12645정성태5/14/20218245사물인터넷: 64. NodeMCU v1 ESP8266 - LittleFS를 이용한 와이파이 접속 정보 업데이트파일 다운로드1
12644정성태5/14/20219413오류 유형: 719. 윈도우 - 제어판의 "프로그램 및 기능" / "Windows 기능 켜기/끄기" 오류 0x800736B3
12643정성태5/14/20218604오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/20219531오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/20219234디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/202112180오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
12639정성태5/12/20219081오류 유형: 715. Arduino: Open Serial Monitor - The module '...\detection.node' was compiled against a different Node.js version using NODE_MODULE_VERSION
... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...