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 인터페이스를 구현한 struct/class 타입만 가능했는데 이제는 다음과 같이 "allows ref struct" 제약을 추가하면,

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

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

ILog를 상속한 ref struct 타입까지 제네릭 인자로 전달할 수 있습니다. 따라서, .NET 8에 있었던 많은 제네릭 타입들이 .NET 9부터는 "allows ref struct" 제약이 추가로 붙게 되었습니다.

[API Proposal]: Employ allows ref struct in libraries #102671
; https://github.com/dotnet/runtime/issues/102671

public delegate void Action<in T>(T obj) where T : allows ref struct;

그런데, 개인적으로 궁금한 점이 있습니다. 왜? 굳이 "allows ref struct"를 추가했을까요? 기존의 class와 struct가 "allows class", "allows struct"와 같은 것 없이도 받아들인 것처럼 ref struct도 그냥 C# 13부터 자연스럽게 지원해도 좋지 않았을까요? 혹시 이에 대해 의견이 있으신 분은 덧글 부탁드립니다. ^^




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

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는 아직 어떠한 인터페이스도 상속하지 않고 있습니다.)




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







[최초 등록일: ]
[최종 수정일: 4/11/2025]

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

비밀번호

댓글 작성자
 



2025-04-09 10시21분
안녕하세요~~
항상 글 잘 보고 많이 배우고있습니다.
해당 내용에 오류가 있는 것 같아서 제보드립니다!
allows ref struct 제약 조건에 대한 설명이 반대로 되어있는 것 같은데요.
allows ref struct를 걸어야지만 제네릭인자에 ref struct를 넣을 수 있는 것으로 파악을 했는데, 예시에는 allows ref struct를 사용하면 ref struct만 허용한다는 느낌으로 작성되어있는 것 같습니다.
https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/ref-struct
해당문서 참고했습니다
김호준
2025-04-09 11시05분
@김호준 혹시, 혹시 아래의 문장을,

------------------------------------------------------------
또한, "ref struct" 자체에 대해서도 제네릭에 제약을 가할 수 있게 되었는데요
------------------------------------------------------------

이렇게 바꾸는 것을 원하는 걸까요?

------------------------------------------------------------
또한, 타입의 제약에도 ref struct를 지정하는 것이 가능해졌는데요,
------------------------------------------------------------
정성태
2025-04-10 02시28분
제가 말을 두서없게 했었네요 ㅎㅎ
-----------------
위와 같은 경우, ILog를 구현한 타입이라고 해도 class나 일반 struct 타입이라면 PrintType<T>의 T 타입 인자로 사용할 수 없습니다.
-----------------
이 부분을 말씀드렸습니다. 언 직접 테스트 해본결과 오히려 allows ref struct를 제약으로 걸지않으면 ILog를 상속 받은 ref struct 타입을 제네릭인자로 넣지못하고
allows ref struct를 해야지만 가능했었어요. 정리하자면 allows ref struct를 추가하면 class, struct, ref struct 가 제네릭인자로 들어갈 수있었고 해당 제약이 없으면 struct와 class 타입만 가능했습니다.
즉, allows ref struct 라는 제약조건이 “제네릭 인자로 ref struct 를 “추가로” 허용한다” 라는 의미로 파악을 했었는데 본문에는 allows ref struct를 넣으면 "ref struct "만“ 제네릭 인자로 허용한다”는 의미로 적혀있는 것 같습니다.

--------
C# 13 이전에는 ref struct이(가) 형식 인수가 될 수 없습니다. C# 13부터 ref struct 형식 매개 변수가 allows ref struct 절에 where을(를) 지정할 때 형식 인수가 될 수 있습니다.
--------
위는 msdn의 내용을 인용한 것인데 이 부분을 보아도 ref struct를 추가로 허용한다는 느낌이 강한 것 같습니다.

제가 틀렸을 수도 있습니다 ㅜㅜ
항상 감사드립니다.
김호준
2025-04-10 02시40분
static void TestFunc<T>() where T : IA, allows ref struct
{
}

static void Main(string[] args)
{
    // A는 struct
    // B는 class
    // C는 ref struct
    
    TestFunc<A>(); // allows ref struct 유무에 상관없이 컴파일 가능
    TestFunc<B>(); // allows ref struct 유무에 상관없이 컴파일 가능
    TestFunc<C>(); // allows ref struct이 있어야만 컴파일 가능
}

위와 같은 코드로 테스트 해보았었는데요~ 혹시나 잘못된 부분이 있거나 제가 잘못 이해하고 있다면 지적 부탁드립니다.
김호준
2025-04-10 08시22분
@hhh 지적해 주신 것이 맞습니다. 제가 습관적으로 코드 테스트를 했군요. ^^;

where 제약이 and 조건이라 당연히 그렇게 예상하고 테스트를 한 것이었는데, allows ref struct의 경우에는 기존의 class, struct에서 "ref struct"까지 허용한다는 것이 맞습니다.

그러고 보니 일반적으로는 기존의 관례대로라면 단순히 제약을 "ref struct"로 했을 텐데, 이번 경우에만 "allows"가 추가로 붙는 이유가 아마도 그런 이유 때문인 듯합니다.
정성태
2025-04-11 08시09분
@hhh 관련해서 본문의 내용을 수정했습니다. ^^
정성태

... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13541정성태1/29/20249611VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20249499Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20249306개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20249902닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/202410695닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/202410309닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/202410732닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/202410140닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/202410122닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
13532정성태1/17/202410175닷넷: 2205. C# - SuperSimpleTcp 사용 시 주의할 점파일 다운로드1
13531정성태1/16/202410556닷넷: 2204. C# - TCP KeepAlive에 새로 추가된 Retry 옵션파일 다운로드1
13530정성태1/15/20249835닷넷: 2203. C# - Python과의 AES 암호화 연동파일 다운로드1
13529정성태1/15/202410035닷넷: 2202. C# - PublishAot의 glibc에 대한 정적 링킹하는 방법
13528정성태1/14/202410196Linux: 68. busybox 컨테이너에서 실행 가능한 C++, Go 프로그램 빌드
13527정성태1/14/202410286오류 유형: 892. Visual Studio - Failed to launch debug adapter. Additional information may be available in the output window.
13526정성태1/14/202410613닷넷: 2201. C# - Facebook 연동 / 사용자 탈퇴 처리 방법
13525정성태1/13/20249751오류 유형: 891. Visual Studio - Web Application을 실행하지 못하는 IISExpress
13524정성태1/12/20249780오류 유형: 890. 한국투자증권 KIS Developers OpenAPI - GW라우팅 중 오류가 발생했습니다.
13523정성태1/12/20249769오류 유형: 889. Visual Studio - error : A project with that name is already opened in the solution.
13522정성태1/11/202410613닷넷: 2200. C# - HttpClient.PostAsJsonAsync 호출 시 "Transfer-Encoding: chunked" 대신 "Content-Length" 헤더 처리
13521정성태1/11/202410258닷넷: 2199. C# - 한국투자증권 KIS Developers OpenAPI의 WebSocket Ping, Pong 처리
13520정성태1/10/20249930오류 유형: 888. C# - Unable to resolve service for type 'Microsoft.Extensions.ObjectPool.ObjectPool`....' [1]
13519정성태1/10/20249639닷넷: 2198. C# - Reflection을 이용한 ClientWebSocket의 Ping 호출파일 다운로드1
13518정성태1/9/202410370닷넷: 2197. C# - ClientWebSocket의 Ping, Pong 처리
13517정성태1/8/20249502스크립트: 63. Python - 공개 패키지를 이용한 위성 이미지 생성 (pystac_client, odc.stac)
13516정성태1/7/20249664닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...