Microsoft MVP성태의 닷넷 이야기
닷넷: 2151. C# 12 - ref readonly 매개변수 [링크 복사], [링크+제목 복사],
조회: 11391
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 9개 있습니다.)
닷넷: 2112. C# 12 - 기본 람다 매개 변수
; https://www.sysnet.pe.kr/2/0/13338

닷넷: 2113. C# 12 - 기본 생성자(Primary Constructors)
; https://www.sysnet.pe.kr/2/0/13339

닷넷: 2114. C# 12 - 모든 형식의 별칭(Using aliases for any type)
; https://www.sysnet.pe.kr/2/0/13341

닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성)
; https://www.sysnet.pe.kr/2/0/13410

닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays)
; https://www.sysnet.pe.kr/2/0/13412

닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
; https://www.sysnet.pe.kr/2/0/13415

닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
; https://www.sysnet.pe.kr/2/0/13427

닷넷: 2151. C# 12 - ref readonly 매개변수
; https://www.sysnet.pe.kr/2/0/13428

닷넷: 2160. C# 12 - Experimental 특성 지원
; https://www.sysnet.pe.kr/2/0/13444




C# 12 - ref readonly 매개변수

자, 이제 드디어 C# 12의 마지막 8번째 개선 사항입니다. ^^

ref readonly parameters
; https://github.com/dotnet/csharplang/issues/6010
; https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/ref-readonly-parameters.md

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md#c-120

다른 7개와는 달리, 현재(2023-10-25) Visual Studio 2022의 Preview 버전으로만 테스트할 수 있습니다.




ref readonly는 기존에도 타입의 멤버 변수, 로컬 변수, 메서드의 반환 타입으로 사용하는 것이 가능했는데요,

public ref struct MyStruct
{
    public ref readonly int Value;

    public MyStruct(int value)
    {
        ref readonly int localVar = ref GetValue();
    }

    private ref readonly int GetValue()
    {
        return ref Value;
    }
}

C# 12부터는 이것을 메서드의 매개변수 유형에도 사용할 수 있게 되었습니다.

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        int i = 5;
        StructType st = new StructType { Value = 5 };
        ProcessRefReadOnly(ref st);
    }

    public static void ProcessRefReadOnly(ref readonly StructType instance)
    {
    }
}

public struct StructType
{
    public int Value;

    public void Increment() { Value++; }
}


기존의 ref 유형과 동일하지만 readonly이기 때문에 읽기 전용이라는 차이점만 있습니다. 이로 인해 다음과 같이,

public static void ProcessRefReadOnly(ref readonly StructType instance)
{
    instance = new StructType { Value = 10 }; // readonly이므로 ref 참조에 대해 새로운 값 대입 불가능

    instance.Value = 10; // readonly이므로 멤버 변경 불가능

    instance.Increment(); // 호출은 가능하지만 defensive copy 발생
}

직접적인 매개변수의 대입과 멤버 값 변경은 컴파일 단계에서 허용하지 않습니다. 반면, 메서드 호출의 경우에는 컴파일러가 (내부 코드에서 값을 변경하는지 알 수 없으므로) 허용은 하지만 단지 defensive copy가 발생하므로 잘 알고 사용해야 합니다. (눈치채셨겠지만, 이러한 규칙은 in 변경자와 정확히 일치합니다.)




이상적으로는 ^^; ref readonly에 대한 설명은 저것으로 끝일 수 있었습니다. 문제는, C# 개발자들이 ref readonly 이전에 "in 변경자"를 이미 만들었다는 점입니다.

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

실제로 저는 저 글을 쓰면서 in의 의미가 사실상 "ref + readonly"라고 설명했습니다. 그런데 ^^; C# 12에서 정확히 그 문구에 해당하는 "ref readonly" 변경자를 추가한 것입니다.

게다가 in 변경자는 여전히 "ref + readonly"의 의미를 갖고 있는 것이 맞습니다. 그럼 도대체 왜? in이 있는데도 불구하고 굳이 "ref readonly"를 새롭게 추가했을까요?

왜냐하면, in 변경자가 "ref + readonly"와 동일한데도 불구하고 "ref"의 원칙에 위배되는 기능을 하나 갖고 있기 때문입니다.

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        int i = 5;

        ProcessIn(i); // "in" 없이도 전달 가능
        ProcessIn(in i); // in을 명시하는 것도 가능
        ProcessIn(5); // 값을 전달하는 것도 가능

        ProcessRef(ref 5); // 값 전달은 컴파일 오류 - error CS1510: A ref or out value must be an assignable variable
    }

    public static void ProcessIn(in int instance)
    {
    }

    public static void ProcessRef(ref int instance)
    {
    }
}

ref가 갖는 참조의 의미에 따라 원래는 값을 직접 전달하는 것은 가능하지 않습니다. 하지만, in 변경자의 경우에는 어차피 readonly라는 특성을 감안해 내부 코드에서 대입을 허용하지 않을 것이므로, 어쩌면 C# 개발자들은 컴파일을 허용해도 무방할 것이라 생각했을 것입니다.

그래서 다시 "ref"의 원칙에 맞게 값 전달을 (막을 수는 없고) 경고하는 "ref readonly"가 추가된 것입니다.

ProcessRefReadOnly(5); // ref readonly에 값을 전달하는 경우 CS9193 경고 발생
                       // warning CS9193: Argument 1 should be a variable because it is passed to a 'ref readonly' parameter

일단, 문서상으로는 저것 때문에 추가했다고는 하지만, 아마 현실적으로는 추가할 수밖에 없었을 것입니다. ^^ 왜냐하면, 이미 "in"은 "ref + readonly"의 의미를 갖고 있었으므로 기왕에 구현했으니 일관성을 가져가는 것이 좋았을 것입니다. 그런 의미에서, 기존에 가능했던 "ref readonly" 코드를 아래와 같이,

public ref struct MyStruct
{
    public in int Value;

    public MyStruct(int value)
    {
        in int localVar = ref GetValue();
    }

    private in int GetValue()
    {
        return ref Value;
    }
}

in으로 통일하는 것도 우스웠을지도 모릅니다. 암튼, 이런 것까지 감안하면 그동안의 문법을 한번 리부팅하는 (C#이 아닌) CSharp과 같은 식의 언어로 내놓는 것이 어떨까... 하는 생각을 갖게 됩니다. CSharp 언어에서는 "in" 예약어는 없어지고 "ref readonly"만 있는 식으로. ^^




개인적인 판단으로, 이것은 C# 개발팀의 성급함에서 온 실수라고 봅니다. 처음부터 ref readonly를 추가했어야 하고, 값 전달은 "ref"의 원칙에 맞게 (경고가 아닌) 오류로 처리했어야 합니다.

물론, C# 7.2 시절에는 "ref readonly"까진 필요하지 않았었고, 그 와중에 "값 형식(struct)"에 대한 성능 보완을 추진하다 보니 "in 변경자"의 사양은 적절했습니다. 단지 결과적으로 그것은 섣부른 결정이 되었고, 그로 인해 C# 12에서 "ref readonly"가 추가된 것입니다.

그런 의미에서, 아마도 처음부터 "ref readonly"를 구현했다면 "in"은 없었을 것입니다.

아무튼, in과 ref readonly는 IL 코드상으로 봐도 별다른 차이점이 없습니다.

public static void ProcessIn([IsReadOnly] [In] ref StructType instance)
{
}

public static void ProcessRefReadOnly([RequiresLocation] [In] ref StructType instance)
{
}

보는 바와 같이 "in 변경자"의 메서드 정의에 IsReadOnly 대신 (의미상으로는 IsReadOnly도 포함한) RequiresLocation 특성을 쓴 것이 "ref readonly" 변경자입니다. 또한, "[RequiresLocation]" 특성은 "modopt"로 선택적이어서 이것을 이해하지 못하는 낮은 버전의 컴파일러에게는 "ref readonly" 매개변수는 "in" 매개변수와 동일하게 해석됩니다.

따라서, "ref readonly"와 "in" 매개변수를 갖는 동일한 이름의 메서드는 재정의할 수 없습니다.

public static void ProcessRef(ref int instance)
{
}

public static void ProcessRef(in int instance) // ref와 함께 정의할 수 있지만,
{
}

public static void ProcessRef(ref readonly int instance) // in과 ref readonly는 구분하지 못하므로,
                      // 컴파일 에러 - CS0663 '...' cannot define an overloaded method that differs only on parameter modifiers 'in' and 'ref readonly'
{
}




자, 이제 하나의 ref는 총 4개(ref, ref readonly, in, out)의 변경자로 나뉘게 되었습니다. 그리고, 해당 매개변수를 갖는 메서드를 호출하는 측에서도 다음과 같이 다양한 규칙을 적용해 변경자를 지정할 수 있습니다.

 

메서드 측 매개변수

ref

ref readonly (C# 12)

in

out

호출 측 변경자 표시

ref

허용

허용

경고(C# 11 이하 오류)

에러

in


에러

허용

허용

에러

out

에러

에러

에러

허용

변경자 없이 전달

에러

경고

허용

에러


새롭게 추가된 "ref readonly"의 경우 간단하게 다음과 같이 코드로 재현할 수 있습니다.

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        int value = 5;

        ProcessRefReadOnly(ref value); // 허용
        ProcessRefReadOnly(in value); // 허용
        ProcessRefReadOnly(out value); // 에러: error CS1615: Argument 1 may not be passed with the 'out' keyword
        ProcessRefReadOnly(value); // 경고: warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword
    }

    public static void ProcessRefReadOnly(ref readonly int instance)
    {
    }
}

또한, "in" 변경자의 경우 기존에는 호출 측에서 "ref"를 지정하면 에러가 발생했지만, C# 12부터는 경고 처리로 바뀌었다는 정도만 알고 지나가면 되겠습니다.

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        int value = 5;
        ProcessIn(ref value); // C# 11 미만의 경우 컴파일 에러
                              // C# 12 이후부터는 컴파일 경고: warning CS9191: The 'ref' modifier for argument 1 corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead.
    }

    public static void ProcessIn(in int instance)
    {
    }
}

마지막으로, 이전에 설명한 대로 "값"을 직접 전달하면 경고가 발생한다는 점에서 rvalue/lvalue 용어를 사용해 다음과 같은 표로 정리할 수 있습니다.


 

메서드 측 매개변수

ref

ref readonly (C# 12)

in

out

호출 측 값 유형

*rvalue

에러

경고

허용

에러

*lvalue


에러

허용

허용

에러


* Where lvalue means a variable (i.e., a value with a location; does not have to be writable/assignable) and rvalue means any kind of value.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/7/2024]

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

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019239개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020686.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017403오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202015963Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015799스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018422오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015092스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015089스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202017935오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021195개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018434오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018523VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016167오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019527오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018809VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202015942오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018262.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202020992오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202017903개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202019921개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021040개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015620오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021203개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026020Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015574오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017398오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...