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

비밀번호

댓글 작성자
 




... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12853정성태11/10/20216138오류 유형: 766. Azure App Service - JBoss 호스팅 생성 시 "This region has quota of 0 PremiumV3 instances for your subscription. Try selecting different region or SKU."
12851정성태11/1/20217518스크립트: 34. 파이썬 - MySQLdb 기본 예제 코드
12850정성태10/27/20218674오류 유형: 765. 우분투에서 pip install mysqlclient 실행 시 "OSError: mysql_config not found" 오류
12849정성태10/17/20217781스크립트: 33. JavaScript와 C#의 시간 변환 [1]
12848정성태10/17/20218769스크립트: 32. 파이썬 - sqlite3 기본 예제 코드 [1]
12847정성태10/14/20218630스크립트: 31. 파이썬 gunicorn - WORKER TIMEOUT 오류 발생
12846정성태10/7/20218383스크립트: 30. 파이썬 __debug__ 플래그 변수에 따른 코드 실행 제어
12845정성태10/6/20218215.NET Framework: 1120. C# - BufferBlock<T> 사용 예제 [5]파일 다운로드1
12844정성태10/3/20216228오류 유형: 764. MSI 설치 시 "... is accessible and not read-only." 오류 메시지
12843정성태10/3/20216674스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/202125008오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/20218497스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/20218595.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/20219646.NET Framework: 1118. C# 11 - 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/20219289.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/20218203VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/20217808Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/20219057.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가 [2]파일 다운로드1
12834정성태9/7/20217435오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/20216895VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/20217752VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/20216286VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/20218548오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/202110187.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/20218314.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/20218286스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...