Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2101. C# 11의 ref 필드 설명 [링크 복사], [링크+제목 복사],
조회: 13101
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 6개 있습니다.)
.NET Framework: 754. 닷넷의 관리 포인터(Managed Pointer)와 System.TypedReference
; https://www.sysnet.pe.kr/2/0/11529

.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법
; https://www.sysnet.pe.kr/2/0/13016

.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
; https://www.sysnet.pe.kr/2/0/13273

.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
; https://www.sysnet.pe.kr/2/0/13274

.NET Framework: 2101. C# 11의 ref 필드 설명
; https://www.sysnet.pe.kr/2/0/13275

닷넷: 2327. C# - 초기화되지 않은 메모리에 접근하는 버그?
; https://www.sysnet.pe.kr/2/0/13906




C# 11의 ref 필드 설명

사실 ref struct도 잘 쓰지 않는 상황에서 ref 필드는 더더욱 쓸 사례가 없습니다. 제 생각으로는, 아마 이번에도 외부 개발사보다는 마이크로소프트 스스로가 제일 잘 쓸 만한 문법으로 보이는데요, 그나저나 도대체 이걸 누가 제안한 것일까요? 아마도 ref 필드에 대한 처음 요구 사항은 아래의 이슈였을 것입니다.

Allow ref struct to contain ref fields
; https://github.com/dotnet/runtime/issues/32060

이슈 생성자는 다름 아닌 마이크로소프트의 직원이군요. ^^ 어쨌든 원래의 요구 사항은 "System.ByReference<T>" 타입을 internal이 아닌 public으로 만들어 달라는 것에 불과했습니다.

이해를 돕기 위해 좀 더 설명을 추가해 볼까요? ^^

이미 Span<T> 등의 타입에서는 마이크로소프트가 "(C# 11의) ref field"와 동일한 기능을 System.ByReference<T>라는 타입을 이용해 내부적으로 사용하고 있었습니다.

corert/src/System.Private.CoreLib/shared/System/Span.cs 
; https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Span.cs

public readonly ref struct Span<T>
{
    internal readonly ByReference<T> _pointer;
    private readonly int _length;

    // ...[생략]...
}

여기서 재미있는 것은, 위의 코드에서 사용한 ByReference<T>의 소스 코드입니다.

corert/src/System.Private.CoreLib/shared/System/ByReference.cs 
; https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/ByReference.cs

// ByReference<T> is meant to be used to represent "ref T" fields. It is working
// around lack of first class support for byref fields in C# and IL. The JIT and
// type loader has special handling for it that turns it into a thin wrapper around ref T.
[NonVersionable]
internal readonly ref struct ByReference<T>
{
#pragma warning disable CA1823, 169 // private field '{blah}' is never used
    private readonly IntPtr _value;
#pragma warning restore CA1823, 169

    [Intrinsic]
    public ByReference(ref T value)
    {
        // Implemented as a JIT intrinsic - This default implementation is for
        // completeness and to provide a concrete error if called via reflection
        // or if intrinsic is missed.
        throw new PlatformNotSupportedException();
    }

    public ref T Value
    {
        // Implemented as a JIT intrinsic - This default implementation is for
        // completeness and to provide a concrete error if called via reflection
        // or if the intrinsic is missed.
        [Intrinsic]
        get => throw new PlatformNotSupportedException();
    }
}

보는 바와 같이 구현 코드가 없음에도 이것이 잘 동작하는 이유는, JIT 컴파일러에 의해 "ref T"와 같은 역할로 번역되기 때문입니다. 아마도 마이크로소프트는 저렇게 구현한 ByReference를 단순히 public으로 접근자만 바꿔서 제공하기에는 많이 찜찜했을 것입니다. ^^ 그리하여 ref 필드라는 유형으로 공식 지원하게 되었고, 실제로 그 문법을 활용한 첫 번째 사례가 바로 .NET 7의 Span<T> 타입입니다.

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

public readonly ref struct Span<T>
{
    internal readonly ref T _reference;
    private readonly int _length;

    // ...[생략]...
}

그러고 보니, C# 7.2부터 마이크로소프트의 아주 특별한 혜택을 받아 탄생한 Span<T> 타입이 이제서야 비로소 순수 C# 문법으로 만드는 것이 가능해졌습니다.




재미있는 건, ref 필드가 나오기 전에도 이미 기존 문법과 BCL의 조합으로 동일한 기능을 구현할 수 있었다는 점입니다. 가령 "Allow ref struct to contain ref fields" 이슈에도 나오지만,

using System.Runtime.InteropServices;

internal class Program
{
    static void Main(string[] args)
    {
        MyData data = new MyData();

        Console.WriteLine(data.n.Value); // System.NullReferenceException: 'Object reference not set to an instance of an object.'
    }
}

public ref struct MyData
{
    public FakeByReference<int> n; // == ref int n; 코드와 동일

    public MyData(ref int n1)
    {
        n = new FakeByReference<int>(ref n1);
    }
}

public readonly ref struct FakeByReference<T>
{
    private readonly Span<T> span;

    public FakeByReference(ref T value)
    {
        span = MemoryMarshal.CreateSpan(ref value, 1);
    }

    public ref T Value => ref MemoryMarshal.GetReference(span);
}

ref struct + MemoryMarshal.CreateSpan/MemoryMarshal.GetReference로 구현을 할 수 있었고, 예전에 소개한 글에서도,

C# 11에 추가된 ref 필드의 (우회) 구현 방법
; https://www.sysnet.pe.kr/2/0/13016

unsafe ref struct + Unsafe.AsPointer/Unsafe.AsRef로 구현했었습니다. 최초 이슈를 제기했던 "Allow ref struct to contain ref fields" 글에서는 이러한 경우를 바탕으로 다음과 같이 요구합니다.


Given that this is already possible with existing APIs, as shown above, but it's less performant than the original ByReference<T>, more verbose and more error prone, why not just make the original ByReference<T> type public so everyone can use it with no need to reinvent the wheel?


"C# - 관리 포인터로서의 ref 예약어 의미" 글에서 이미 설명했듯이, ref를 이용한 관리 포인터 사용은 스택에 있는 인스턴스를 가리키는 상황에서 C# 컴파일러에 의해 한 번 더 안정성 체크를 할 수 있는 기회를 부여합니다.

유사한 상황이 ref 필드에도 발생한 것인데요, 위에서 든 ref field에 대한 우회 방법들은 성능도 떨어지고 오류 발생 가능성도 내포하고 있습니다. 그런데, 만약 이것을 C# 컴파일러에서 "ref field" 문법으로 정식 지원을 한다면 기존의 ref 관리 포인터와 마찬가지로 C# 컴파일러의 안정성 체크에서 유효하지 않은 코드들은 자연스럽게 걸러질 것이므로 나쁘지 않은 요구 사항입니다. ^^




이렇게 우여곡절 끝에 탄생한 ref 필드 덕분에 이제 C# 컴파일러는 아래와 같은 전형적인 사례에서 스택 변수의 scope를 계산해 컴파일 에러를 낼 수 있게 되었습니다.

internal class Program
{
    static void Main(string[] args) { }

    static public MyType GetData()
    {
        int n = 5;

        var t = new MyType(ref n);
        return t; // 컴파일 오류: error CS8352: Cannot use variable 't' in this context because it may expose referenced variables outside of their declaration scope
    }
}

public ref struct MyType
{
    ref int _n;
    public MyType(ref int target)
    {
        _n = ref target;
    }
}

GetData 메서드가 실행 중일 때만 유효한 "int n" 변수가 메서드의 반환값으로 사용되는 MyType에 참조가 전해진 것을 막을 수 있게 되었습니다. 일면 납득이 가시죠? ^^

그런데, 위의 오류 메시지를 보면 "it may expose ..."라는 문구가 나옵니다. 왜냐하면, GetData 메서드를 컴파일하는 입장에서 MyType이 외부 어셈블리에 정의된 것일 수도 있으므로 그렇게 판정할 수밖에 없습니다. 왜냐하면, MyType이 다음과 같이 구성된 경우라면,

public ref struct MyType 
{
    public MyType(ref int target) { }
}

이번에는 문제가 없습니다. 즉, ref struct의 생성자에 ref 인자를 전달하는 것만으로도 C# 컴파일러는 보수적으로 컴파일 에러를 발생시키는 겁니다.

이와는 달리 "C# - ref 필드로 ref struct 타입을 허용하지 않는 이유" 글에서도 들었던 사례처럼 ref 필드에 대한 C# 컴파일러의 유효성 체크 구현이 그리 완벽하진 않은 경우도 있습니다. 가령, 위의 소스 코드에 살짝 변화만 가해주면,

private static void Case1()
{
    int n = 5;
    MyType data;
    data = new MyType(ref n); // error CS8168: Cannot return local 'n' by reference because it is not a ref local
                              // error CS8347: Cannot use a result of 'MyData.MyData(ref int)' in this context because it may expose variables referenced by parameter 'n1' outside of their declaration scope

    Console.WriteLine(data.value);
}

private static void Case2()
{
    int n = 5;

    MyType data = new MyType(ref n); // 이 코드는 잘 통과했으면서!
    Console.WriteLine(data.value);
}

저렇게 알 수 없는 컴파일 오류가 발생해 버립니다. 음... 아마도 차차 나아지겠죠?!!! ^^ (아니면 제가 뭘 몰라서 그런 걸까요? ^^ 혹시 저 차이점을 아시는 분은 덧글 부탁드립니다.)




휴~~~ 예전에 그냥 단순히 소개만 하고 지나갔던 것을,

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

이제서야 여러 가지 이야기와 함께 풀어냈습니다. ^^

C# - 관리 포인터로서의 ref 예약어 의미
; https://www.sysnet.pe.kr/2/0/13273

C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
; https://www.sysnet.pe.kr/2/0/13274

그래도 아직 scoped 신규 예약어에 대한 이야기가 남았는데요, ^^ 그건 다른 글에서 다뤄보겠습니다.




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

[연관 글]






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

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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  [102]  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11383정성태12/4/201723405디버깅 기술: 110. 비동기 코드 실행 중 예외로 인한 ASP.NET 프로세스 비정상 종료 현상 [1]
11382정성태12/4/201721945오류 유형: 436. System.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired 예외 발생 시 "[Pre-Login] initialization=48; handshake=1944;" 값의 의미
11381정성태11/30/201718441.NET Framework: 702. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법(두 번째 이야기)파일 다운로드1
11380정성태11/30/201718477디버깅 기술: 109. windbg - (x64에서의 인자 값 추적을 이용한) Thread.Abort 시 대상이 되는 스레드를 식별하는 방법
11379정성태11/30/201719141오류 유형: 435. System.Web.HttpException - Session state has created a session id, but cannot save it because the response was already flushed by the application.
11378정성태11/29/201720640.NET Framework: 701. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법 [1]파일 다운로드1
11377정성태11/29/201719890.NET Framework: 700. CommonOpenFileDialog 사용 시 사용자가 선택한 파일 목록을 구하는 방법 [3]파일 다운로드1
11376정성태11/28/201724285VS.NET IDE: 123. Visual Studio 편집기의 \r\n (crlf) 개행을 \n으로 폴더 단위로 설정하는 방법
11375정성태11/28/201719085오류 유형: 434. Visual Studio로 ASP.NET 디버깅 중 System.Web.HttpException - Could not load type 오류
11374정성태11/27/201724177사물인터넷: 14. 라즈베리 파이 - (윈도우의 NT 서비스처럼) 부팅 시 시작하는 프로그램 설정 [1]
11373정성태11/27/201723173오류 유형: 433. Raspberry Pi/Windows 다중 플랫폼 지원 컴파일 관련 오류 기록
11372정성태11/25/201726136사물인터넷: 13. 윈도우즈 사용자를 위한 라즈베리 파이 제로 W 모델을 설정하는 방법 [4]
11371정성태11/25/201719838오류 유형: 432. Hyper-V 가상 스위치 생성 시 Failed to connect Ethernet switch port 0x80070002 오류 발생
11370정성태11/25/201719844오류 유형: 431. Hyper-V의 Virtual Switch 생성 시 "External network" 목록에 특정 네트워크 어댑터 항목이 없는 경우
11369정성태11/25/201721828사물인터넷: 12. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드 및 마우스로 쓰는 방법 (절대 좌표, 상대 좌표, 휠) [1]
11368정성태11/25/201727449.NET Framework: 699. UDP 브로드캐스트 주소 255.255.255.255와 192.168.0.255의 차이점과 이를 고려한 C# UDP 서버/클라이언트 예제 [2]파일 다운로드1
11367정성태11/25/201727496개발 환경 구성: 337. 윈도우 운영체제의 route 명령어 사용법
11366정성태11/25/201719136오류 유형: 430. 이벤트 로그 - Cryptographic Services failed while processing the OnIdentity() call in the System Writer Object.
11365정성태11/25/201721381오류 유형: 429. 이벤트 로그 - User Policy could not be updated successfully
11364정성태11/24/201723340사물인터넷: 11. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 (절대 좌표) [2]
11363정성태11/23/201723391사물인터넷: 10. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 (두 번째 이야기)
11362정성태11/22/201719754오류 유형: 428. 윈도우 업데이트 KB4048953 - 0x800705b4 [2]
11361정성태11/22/201722572오류 유형: 427. 이벤트 로그 - Filter Manager failed to attach to volume '\Device\HarddiskVolume??' 0xC03A001C
11360정성태11/22/201722438오류 유형: 426. 이벤트 로그 - The kernel power manager has initiated a shutdown transition.
11359정성태11/16/201721931오류 유형: 425. 윈도우 10 Version 1709 (OS Build 16299.64) 업그레이드 시 발생한 문제 2가지
11358정성태11/15/201726710사물인터넷: 9. Visual Studio 2017에서 Raspberry Pi C++ 응용 프로그램 제작 [1]
... 91  92  93  94  95  96  97  98  99  100  101  [102]  103  104  105  ...