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

C# 7.2 - 메서드의 반환값 및 로컬 변수에 ref readonly 기능 추가

C# 7.2 (1) - readonly 구조체
; https://www.sysnet.pe.kr/2/0/11524

C# 7.2 (2) - 메서드의 매개 변수에 in 변경자 추가
; https://www.sysnet.pe.kr/2/0/11525

C# 7.2 (3) - 메서드의 반환값 및 로컬 변수에 ref readonly 기능 추가
; https://www.sysnet.pe.kr/2/0/11526

C# 7.2 (4) - 3항 연산자에 ref 지원(conditional ref operator)
; https://www.sysnet.pe.kr/2/0/11528

C# 7.2 (5) - 스택에만 생성할 수 있는 값 타입 지원 - "ref struct"
; https://www.sysnet.pe.kr/2/0/11530

C# 7.2 (6) - Span<T>
; https://www.sysnet.pe.kr/2/0/11534

C# 7.2 (7) - private protected 접근자 추가
; https://www.sysnet.pe.kr/2/0/11543

C# 7.2 (8) - 숫자 리터럴의 선행 밑줄과 뒤에 오지 않는 명명된 인수
; https://www.sysnet.pe.kr/2/0/11544

기타 - Microsoft Build 2018 - The future of C# 동영상 내용 정리
; https://www.sysnet.pe.kr/2/0/11536




지난 글에서, C# 7.2의 매개 변수에 대한 ref readonly 기능, 즉 in 예약어에 대한 설명을 했습니다.

C# 7.2 - 메서드의 매개 변수에 in 변경자 추가
; https://www.sysnet.pe.kr/2/0/11525

마찬가지로 메서드의 반환값 및 로컬 변수에 대해서 in 예약어의 기능, 즉 ref readonly 기능을 C# 7.2부터 제공하는데 단지 그 이름이 in이 아닌 ref readonly 그대로 적용된다는 차이만 있습니다.




매개 변수에 대한 값 복사의 부하를 없애기 위해 in을 추가한 것처럼, 반환값에 대한 값 복사의 부하를 없애는 용도로 ref readonly를 사용할 수 있습니다. 이해를 돕기 위해 예제를 보겠습니다.

using System;

class Program
{
    readonly StructPerson sarah = new StructPerson() { Name = "Kerrigan", Age = 27 };

    static void Main(string[] args)
    {
        Program pg = new Program();
        pg.StructParam(pg.GetSarah());
    }

    private StructPerson GetSarah()
    {
        return sarah;
    }

    void StructParam(in /* ref readonly */ StructPerson p)
    {
        p.IncAge();
        Console.WriteLine("StructParam(in StructPerson p): " + p.Age);
    }
}

struct StructPerson
{
    public int Age;
    public string Name;

    public void IncAge()
    {
        Age++;
    }
}

위의 코드에서 GetSarah 메서드는 값 형식의 인스턴스를 반환합니다. 그리고 그렇게 반환된 인스턴스가 in 매개 변수를 갖는 StructParam 메서드에 전달되지만, 이 짧은 순간에도 구조체의 값 복사가 발생합니다. 실제로 위의 코드를 IL 수준에서 살펴보면,

.locals init (
    [0] valuetype StructPerson person,
    [1] valuetype StructPerson person2)

L_0007: callvirt instance valuetype StructPerson Program::GetSarah()
L_000c: stloc.1 // 값 복사 발생
L_000d: ldloca.s person2
L_000f: callvirt instance void Program::StructParam(valuetype StructPerson&)

GetSarah 메서드의 반환 시점에 스택에 있는 값(sarah 인스턴스)을 1번 변수(person2)에 대입(stloc.1)하면서 "값 복사"가 발생합니다. 그다음, 복사된 인스턴스인 person2 변수의 주소를 스택에 올리면서(ldloca.s person2) StructParam 메서드의 인자로 전달하고 있습니다.

이러한 값 복사를 없애려면 GetSarah 메서드가 애당초 값 형식에 대한 참조 값, 즉 reference를 반환해야 합니다. 이를 위해 C# 7.0부터 추가된 참조 반환 구문을 시도해 볼 수 있습니다.

ref StructPerson GetRefSarah()
{
    return ref sarah; // 컴파일 에러: CS8160 A readonly field cannot be returned by writable reference
}

하지만 보다시피, CS8160 오류가 발생하는데 sarah 인스턴스가 readonly로 적용된 인스턴스이기 때문입니다. 따라서 이런 경우에 대한 오류를 없애려면 ref + readonly의 반환 기능이 있어야 하므로 C# 7.2부터 이를 추가한 것입니다.

ref readonly StructPerson GetRefReadOnlySarah()
{
    return ref sarah;
}

자, 그럼 새롭게 추가된 GetRefReadOnlySarah 메서드와 StructParam의 호출 코드를,

Program pg = new Program();
pg.StructParam(pg.GetRefReadOnlySarah());

// 또는 명시적으로 in 예약어를 함께 지정해도 무방
pg.StructParam(in pg.GetRefReadOnlySarah());


IL 수준에서 살펴보면,

L_0016: callvirt instance valuetype StructPerson& modreq([mscorlib]System.Runtime.InteropServices.InAttribute) Program::GetRefReadOnlySarah()
L_001b: callvirt instance void Program::StructParam(valuetype StructPerson&)

GetRefReadOnlySarah 메서드의 반환값이 스택에 놓여 있는 상태 그대로 StructParam의 인자로 전달되는 것을 볼 수 있습니다. 즉, 값 복사에 대한 부하가 없어진 것입니다.




반환값에 ref readonly가 가능한 것처럼 로컬 변수에도 적용할 수 있습니다.

static void Main(string[] args)
{
    Program pg = new Program();

    StructPerson p1 = pg.GetSarah();
    p1.IncAge();

    ref readonly StructPerson p2 = ref pg.GetRefReadOnlySarah();
    p2.IncAge();
}

그런데, 여기서도 마찬가지로 "C# 7.2 - 메서드의 매개 변수에 in 변경자 추가" 글에서 소개한 문제점이 발생합니다. in 매개 변수의 경우에도 여전히 "defensive copy"로부터 자유로울 수 없다고 했는데, ref readonly 로컬 변수 역시 값 형식의 메서드/속성을 접근할 때 "defensive copy" 문제가 발생합니다.

실제로 위의 p2.IncAge() 호출을 IL 코드로 보면,

.locals init (
    [0] valuetype StructPerson person,
    [1] valuetype StructPerson person2)

L_002e: callvirt instance valuetype StructPerson& modreq([mscorlib]System.Runtime.InteropServices.InAttribute) Program::GetRefReadOnlySarah()
L_0033: ldobj StructPerson
L_0038: stloc.1 // 1번 변수에 값 복사 ("defensive copy")
L_0039: ldloca.s person2
L_003b: call instance void StructPerson::IncAge()

값 복사가 발생하는 것을 확인할 수 있습니다. 역시 이 문제를 없애려면 "C# 7.2 - 메서드의 매개 변수에 in 변경자 추가" 글에서와 마찬가지로 readonly 구조체를 사용하도록 바꿔야 합니다. 다음은 "C# 7.2 - readonly 구조체" 글의 설명대로 readonly 구조체를 적용한 것입니다.
using System;

class Program
{
    readonly StructPerson sarah = new StructPerson("Kerrigan", 27);

    static void Main(string[] args)
    {
        Program pg = new Program();
        pg.StructParam(pg.GetSarah());
        pg.StructParam(pg.GetRefReadOnlySarah());

        StructPerson p1 = pg.GetSarah();
        p1.IncAge();

        ref readonly StructPerson p2 = ref pg.GetRefReadOnlySarah();
        p2.IncAge();
    }

    StructPerson GetSarah()
    {
        return sarah;
    }

    ref readonly StructPerson GetRefReadOnlySarah()
    {
        return ref sarah;
    }

    void StructParam(in /* ref readonly */ StructPerson p)
    {
        p.IncAge();
        Console.WriteLine("StructParam(in StructPerson p): " + p.Age);
    }
}

readonly struct StructPerson
{
    public readonly int Age;
    public readonly string Name;

    public StructPerson(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public StructPerson IncAge()
    {
        return new StructPerson(this.Name, this.Age + 1);
    }
}
따라서 이번에도 역시 readonly 구조체의 불변성이 보장되는 덕분에 C# 컴파일러는 ref readonly 값 형식에 대한 로컬 변수의 메서드/속성 접근 시 "defensive copy"를 제거해 다음과 같이 부하 없는 코드가 산출됩니다.
// 값 복사가 발생하지 않음.
L_002f: callvirt instance valuetype StructPerson& modreq([mscorlib]System.Runtime.InteropServices.InAttribute) Program::GetRefReadOnlySarah()
L_0034: call instance valuetype StructPerson StructPerson::IncAge()

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




이쯤에서 "ref readonly"를 정리해 볼까요? 결국 ref + readonly가 C# 7.2부터 메서드의 반환값과 로컬 변수에 사용할 수 있게 되었고, 특별히 매개 변수에 쓰이는 경우를 위해 "in" 예약어가 나온 것입니다.

"ref readonly"의 주요 목적은 값 형식의 "복사로 인한 오버헤드" 문제를 해결하는 것입니다. 부분적으로 오버헤드를 제거하긴 하지만, 완전히 제거하고 싶다면 해당 값 형식을 "readonly struct"로 만들어야 합니다.

즉, C# 7.2의 "ref readonly"는 결국 "readonly struct"를 사용할 것을 장려하게 만들고 이는 곧 기존의 불변 타입 사용 시 발생했던 모든 부하를 제거하게 됩니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/11/2018]

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13449정성태11/21/20232351개발 환경 구성: 688. Azure OpenAI 서비스 신청 방법
13448정성태11/20/20232626닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20232487닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20232419닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232699Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232455닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232492개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232321개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232652닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232352Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232844닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232462닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232656닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232893닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232828닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232618스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232341스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232378오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232707스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232597닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232856닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20232919닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233100닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233283스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233099닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233078스크립트: 58. 파이썬 - async/await 기본 사용법
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...