Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 9개 있습니다.)
닷넷: 2275. C# 13 - (1) 신규 이스케이프 시퀀스 '\e'
; https://www.sysnet.pe.kr/2/0/13673

닷넷: 2277. C# 13 - (2) 메서드 그룹의 자연 타입 개선 (메서드 추론 개선)
; https://www.sysnet.pe.kr/2/0/13681

닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입
; https://www.sysnet.pe.kr/2/0/13699

닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용
; https://www.sysnet.pe.kr/2/0/13701

닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용
; https://www.sysnet.pe.kr/2/0/13705

닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용
; https://www.sysnet.pe.kr/2/0/13710

닷넷: 2303. C# 13 - (7) ref struct의 interface 상속 및 제네릭 제약으로 사용 가능
; https://www.sysnet.pe.kr/2/0/13752

닷넷: 2304. C# 13 - (8) 부분 메서드 정의를 속성 및 인덱서에도 확대
; https://www.sysnet.pe.kr/2/0/13754

닷넷: 2305. C# 13 - (9) 메서드 바인딩의 우선순위를 지정하는 OverloadResolutionPriority 특성 도입 (Overload resolution priority)
; https://www.sysnet.pe.kr/2/0/13755




C# 13 - (7) ref struct의 interface 상속 및 제네릭 제약으로 사용 가능

C# 7.2에 추가됐었던,

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

ref struct에 interface를 상속할 수 있게 됐습니다.

[Proposal]: Let ref structs implement interfaces and substitute into type parameters (VS 17.11, .NET 9)
; https://github.com/dotnet/csharplang/issues/7608

따라서 C# 13부터 다음과 같은 코드가 가능합니다.

internal class Program
{
    static void Main(string[] args)
    {
        MyStruct myStruct = new MyStruct(42);
        myStruct.Log(Console.Out);
    }
}

interface ILog
{
    void Log(TextWriter tw);
}

ref struct MyStruct : ILog
{
    int _x;

    public MyStruct(int x) => _x = x;

    public void Log(TextWriter tw)
    {
        tw.WriteLine($"x == {_x}");
    }
}

하지만, 그렇다고 해서 ref struct 본연의 특징인 "스택에만 생성할 수 있다"라는 제약을 벗어날 수는 없습니다. 이로 인해 인스턴스를 인터페이스로 형변환하는 것은 여전히 불가능합니다.

MyStruct myStruct = new MyStruct(42);

IMy my = myStruct as IMy; // interface로의 변환은 Boxing을 유발하므로 불가능 (컴파일 오류)
Console.WriteLine(my.X);

error CS0039: Cannot convert type 'MyStruct' to 'ILog' via a reference conversion, boxing conversion, unboxing conversion, wrapping conversion, or null type conversion


이런 특징은 자연스럽게 (C# 8.0에 추가된) 메서드 구현을 포함하는 인터페이스까지 영향을 미칩니다.

interface IA
{
    void M() { WriteLine("IA.M"); }
}

class C : IA { } // OK

IA i = new C();
i.M(); // 인터페이스에 구현한 기본 메서드는 반드시 형변환을 해야 사용 가능

따라서 만약 구현 타입이 ref struct 유형이라면 인터페이스로의 형변환을 할 수 없으니 기본 메서드를 호출할 수 있는 방법이 없습니다.

결국, ref struct의 경우에는 반드시 해당 메서드를 재정의하도록 제한을 두었습니다.

interface IA
{
    void M() { WriteLine("IA.M"); } // 일반적으로 구현 클래스 측에서 M 메서드를 재정의하지 않아도 되지만,
}

ref struct C : IA 
{
    public void M() { WriteLine("C.M"); } // ref struct 타입은 인터페이스의 기본 메서드를 반드시 재정의해야 함
}

C i = new C(); 
i.M(); // 인터페이스로의 형변환 없이, 재정의한 메서드 M을 호출




그런데 좀 이상하지 않나요? ^^ 인터페이스를 상속해도 (다형성에 기반한 원칙으로) 쓸 수 없다면 왜 굳이 신규 문법으로 제공하는 걸까요?

왜냐하면, 그래도 딱 하나 유용한 사용처가 있기 때문입니다. 바로 제네릭 타입의 제약에 인터페이스를 명시했을 때 ref struct 타입의 사용이 가능해진다는 점입니다.

일례로 기존에는 다음과 같이 where 제약을 가하는 것이 불가능했지만,

// C# 12 이하에서는 ref strcut 타입은 인터페이스를 상속할 수 없으므로 PrintType<T>의 T 타입 인자로 사용할 수 없음
ref struct PrintType<T> where T : ILog
{
    T _instance;

    public PrintType(T instance)
    {
        _instance = instance; // ref struct 인스턴스를 보관하기 위해서는 PrintType<T> 자체도 ref struct 타입이어야 함
    }

    public void Log(TextWriter tw)
    {
        _instance.Log(tw); // 인터페이스로 형변환할 필요는 없으므로 interface를 상속한 ref struct 타입도 사용 가능
    }
}

C# 13부터는 가능해진 것입니다. 또한, "ref struct" 자체에 대해서도 제네릭에 제약을 가할 수 있게 되었는데요, 일례로 위의 PrintType의 T 타입 인자로 ILog를 구현한 것 외에도 반드시 ref struct 유형만 가능하게 하려면 다음과 같이 "allows ref struct" 제약을 추가할 수 있습니다.

ref struct PrintType<T> where T : ILog, allows ref struct // allows ref struct 제약은 반드시 가장 마지막 위치에 명시해야 함
{
    T _instance;

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

위와 같은 경우, ILog를 구현한 타입이라고 해도 class나 일반 struct 타입이라면 PrintType<T>의 T 타입 인자로 사용할 수 없습니다.




참고로 아래의 공식 문서를 보면,

Restrictions for ref struct types that implement an interface
; https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct#restrictions-for-ref-struct-types-that-implement-an-interface

인터페이스를 사용한 ref struct의 경우, 이후의 변화에 따라 1) 소스 레벨에서 컴파일 시 오류가 발생하거나 2) 바이너리 레벨에서 런타임 시 예외가 발생할 수 있으니 주의가 필요하다고 합니다.

예를 들면, 다음과 같이 인터페이스와 ref struct 구현체가 어셈블리를 나눠 존재한다고 가정해 보겠습니다.

// A_asm.dll
public interface ILog
{
    void Log(TextWriter tw);
}

// B_asm.dll
public ref struct MyStruct : ILog
{
    public void Log(TextWriter tw) => tw.WriteLine("MyStruct.Log");
}

저렇게 분리가 된 채로 관리가 되고 있는 상황에서, 저 상황을 모르는 A_asm.dll 개발자는 무심코 ILog 인터페이스에 기본 메서드를 추가할 수 있을 것입니다.

// A_asm.dll
public interface ILog
{
    void Log(TextWriter tw);

    int X => 0; // 기본 메서드 추가
}

그럼, 어느 순간 B_asm.dll을 유지/보수하던 개발자는 컴파일하는 순간 int X { get; }을 구현하지 않았다고 컴파일 오류가 발생할 것입니다.

또 다른 시나리오로는, 이미 잘 실행되고 있는 제품에 (기본 메서드 정의를 추가한) A_asm.dll을 업데이트해서 배포를 했다면, 그리고 이런 상황에서 어떤 식으로든 MyStruct.X를 접근하는 코드가 실행된다면 런타임 오류가 발생하게 됩니다.

물론 원칙상으로는, interface를 한번 구현했다면 철저하게 "계약(contact)"이라는 관점에서 그 구현을 바꿔서는 안 됩니다. 따라서, 위와 같은 경우 ILog2를 새로 만들어 제공했어야 합니다.

// A_asm.dll
public interface ILog
{
    void Log(TextWriter tw);
}

public interface ILog2 : ILog
{
    int X => 0; // 기본 메서드 추가
}

하지만 원칙이 그렇다 해도, 실제로는 저렇게 변화가 되는 것을 강제로 막는 장치가 없으므로 문제의 소지가 될 수 있음에 유의해야 합니다.




그나저나 이런 특수한 문법이 사용될 사례가 과연 어떤 것일까요? proposal 링크를 따라가보면 아래의 이슈가 나옵니다.

[API Proposal]: Utf8JsonReader should read from JsonNode #106047
; https://github.com/dotnet/runtime/issues/106047

예를 들어, 아래와 같은 코드는,

string dataJson = message.PayloadData.ToJsonString(); // typeof(PayloadData) == JsonNode
byte[] dataJsonBytes = Encoding.UTF8.GetBytes(dataJson);
var reader = new Utf8JsonReader(dataJsonBytes);

Utf8JsonReader를 초기화하는데 기존 JsonNode 인스턴스를 "문자열"로 바꾼 후 다시 "바이트 배열"로 변환하는 과정을 거치고 있습니다. 한 마디로, 쓸데없이 GC Heap을 사용하고 있는 것입니다.

그보다는, JsonNode 인스턴스를 바로 Utf8JsonReader로 전달해 초기화하는 것이 효율적일 텐데요, 질문자는 단순히 JsonNode를 인자로 받는 생성자를 Utf8JsonReader에 추가해 달라고 했지만, 답변자는 Utf8JsonReader에 (앞으로도 요구에 따라 늘어날지도 모르는) 고정된 타입 유형을 추가하기보다는, 그 중간을 추상화하는 것이 (시간이 좀 걸리더라도) 좀 더 나을 거라는 식으로 답변합니다.

가령 이런 식으로 구현하는 것인데,

public partial class JsonConverter<T>
{
    public virtual T? Read<TReader>(ref TReader reader, Type type, JsonSerializerOptions options) where TReader : allow ref struct, IJsonReader => throw new NotImplementedException();
}

위의 TReader가 바로 Utf8JsonReader가 올 수 있는 타입 인자입니다. 그런데 막상 이런 유형으로 구현하고 싶어도 현재 Utf8JsonReader는 ref struct 타입이기 때문에 IJsonReader를 지정할 수 없고, 또한 TReader에 ref struct만 지정하게 강제할 수도 없습니다. 따라서 C# 13을 통해, 인터페이스를 상속할 수 있도록 만들고, 제네릭 제약에 신규로 allows ref struct 제약도 추가해 저 psudo 코드를 실제로 구현할 수 있게 만들고 싶은 듯합니다. (하지만 .NET 9 BCL의 Utf8JsonReader는 아직 어떠한 인터페이스도 상속하지 않고 있습니다.)




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







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

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)
13812정성태11/11/20249오류 유형: 933. Active Directory - The forest functional level is not supported.
13811정성태11/11/20249Linux: 104. Linux - COLUMNS 환경변수가 언제나 80으로 설정되는 환경
13810정성태11/10/2024247Linux: 103. eBPF (bpf2go) - Tracepoint를 이용한 트레이스 (BPF_PROG_TYPE_TRACEPOINT)
13809정성태11/10/2024265Windows: 271. 윈도우 서버 2025 마이그레이션
13808정성태11/9/2024245오류 유형: 932. Linux - 커널 업그레이드 후 "error: bad shim signature" 오류 발생
13807정성태11/9/2024278Linux: 102. Linux - 커널 이미지 파일 서명 (Ubuntu 환경)
13806정성태11/8/2024278Windows: 270. 어댑터 상세 정보(Network Connection Details) 창의 내용이 비어 있는 경우
13805정성태11/8/2024268오류 유형: 931. Active Directory의 adprep 또는 복제가 안 되는 경우
13804정성태11/7/2024561Linux: 101. eBPF 함수의 인자를 다루는 방법
13803정성태11/7/2024476닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도
13802정성태11/6/2024833Windows: 269. GetSystemTimeAsFileTime과 GetSystemTimePreciseAsFileTime의 차이점파일 다운로드1
13801정성태11/5/2024867Linux: 100. eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
13800정성태11/3/20241081닷넷: 2308. C# - ICU 라이브러리를 활용한 문자열의 대소문자 변환파일 다운로드1
13799정성태11/2/2024862개발 환경 구성: 732. 모바일 웹 브라우저에서 유니코드 문자가 표시되지 않는 경우
13798정성태11/2/2024956개발 환경 구성: 731. 유니코드 - 출력 예시 및 폰트 찾기
13797정성태11/1/20241003C/C++: 185. C++ - 문자열의 대소문자를 변환하는 transform + std::tolower/toupper 방식의 문제점파일 다운로드1
13796정성태10/31/2024879C/C++: 184. C++ - ICU dll을 이용하는 예제 코드 (Windows)파일 다운로드1
13795정성태10/31/2024811Windows: 268. Windows - 리눅스 환경처럼 공백으로 끝나는 프롬프트 만들기
13794정성태10/30/2024917닷넷: 2307. C# - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
13793정성태10/28/2024925C/C++: 183. C++ - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
13792정성태10/27/2024833Linux: 99. Linux - 프로세스의 실행 파일 경로 확인
13791정성태10/27/2024893Windows: 267. Win32 API의 A(ANSI) 버전은 DBCS를 사용할까요?파일 다운로드1
13790정성태10/27/2024879Linux: 98. Ubuntu 22.04 - 리눅스 커널 빌드 및 업그레이드
13789정성태10/27/2024819Linux: 97. menuconfig에 CONFIG_DEBUG_INFO_BTF, CONFIG_DEBUG_INFO_BTF_MODULES 옵션이 없는 경우
13788정성태10/26/2024864Linux: 96. eBPF (bpf2go) - fentry, fexit를 이용한 트레이스
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...