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

(시리즈 글이 9개 있습니다.)
닷넷: 2342. C# 14 - (취소된 글)
; https://www.sysnet.pe.kr/2/0/13970

닷넷: 2343. C# 14 - (1) 속성 구문에서 문맥 키워드로 추가되는 field 예약어
; https://www.sysnet.pe.kr/2/0/13971

닷넷: 2346. C# 14 - (2) Span 타입과 배열 간의 암시적 형변환
; https://www.sysnet.pe.kr/2/0/13974

닷넷: 2347. C# 14 - (3) 형식 인자가 없는 제네릭 타입의 nameof 지원
; https://www.sysnet.pe.kr/2/0/13975

닷넷: 2349. C# 14 - (4) 문자열 리터럴을 utf-8 인코딩으로 저장
; https://www.sysnet.pe.kr/2/0/13977

닷넷: 2350. C# 14 - (5) 람다 매개 변수에 접근자가 있는 경우에도 타입 생략 가능
; https://www.sysnet.pe.kr/2/0/13986

닷넷: 2351. C# 14 - (6) event와 생성자에도 partial 메서드 적용
; https://www.sysnet.pe.kr/2/0/13987

닷넷: 2354. C# 14 - (7) 확장 메서드에 정적 메서드와 속성 지원을 위한 전용 구문 추가
; https://www.sysnet.pe.kr/2/0/13998

닷넷: 2355. C# 14 - (8) null 조건부 연산자 개선 - 대입문에도 사용 가능
; https://www.sysnet.pe.kr/2/0/13999




C# 14 - (7) 확장 메서드에 정적 메서드와 속성 지원을 위한 전용 구문 추가

C# 3.0에 확장 메서드가 구현된 이후, 제 기억으로는 초기부터 정적 메서드와 속성에 대해서도 확장 메서드를 지원해 달라는 요청이 있었습니다. 이에 대한 답을 C# 14에서야 해주는군요. ^^

[Proposal]: extensions #8697
; https://github.com/dotnet/csharplang/issues/8697

Extension members
; https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14#extension-members

그런데 한 가지 아쉬운 점은, 기존의 확장 메서드 문법 체계를 재사용하지 않고 아예 새로운 예약어를 도입했다는 점입니다. 그래서 다소 낯설을 수 있는데요, 그래도 문법이 어렵진 않으니 쉽게 익숙해질 수는 있을 것입니다. ^^

우선, 기존의 확장 메서드 문법은 여전히 유효한데요, 단지 C# 14부터는 새롭게 extension 예약어를 사용하는 방식도 추가된 것입니다. 예를 들어,

public static class StringExtension
{
    // C# 3.0부터 지원한 확장 메서드
    public static IPEndPoint ToEndPoint(this string value)
    {
        if (IPEndPoint.TryParse(value, out var endPoint) == false)
        {
            throw new ArgumentException($"Invalid endpoint format: {value}");
        }

        return endPoint;
    }
}

// 사용 예
// IPEndPoint ep = "192.168.100.50:8080".ToEndPoint();

위와 같이 정의한 확장 메서드를 신규 extension 예약어를 사용해 다음과 같이 대체할 수 있습니다.

/* 문법
    extension(확장할_타입 변수명)
    {
        public 반환형 메서드명(매개변수) { ... }
    }
*/

public static class StringExtension
{
    // 기존 확장 메서드 정의를 대체하는 신규 문법
    extension(string value)
    {
        public IPEndPoint ToEndPoint()
        {
            if (IPEndPoint.TryParse(value, out var endPoint) == false)
            {
                throw new ArgumentException($"Invalid endpoint format: {value}");
            }

            return endPoint;
        }
    }
}

기존에는 "this string value"로 "확장할 타입과 변수명"을 지정했지만, 이제는 "extension(string value)"와 같이 extension 괄호 안에 "확장할 타입과 변수명"을 지정하도록 바뀌었고, 메서드의 정의를 extension 블록 내부에 추가할 수 있습니다. 뭐랄까... 마치 "string value"라는 필드가 있는 클래스에서,

public class MyClass
{
    string value;
    
    public IPEndPoint ToEndPoint()
    {
        // ...[생략]...
    }
}

메서드를 정의하는 것과 비슷한 느낌마저 들 정도입니다.




신규 extension 구문에서는 속성도 정의할 수 있다고 하는데요, 사실 속성은 구현 과정에서 get/set 메서드로 바뀌는 것이므로 메서드와 구조적으로 다르지 않아 당연히 지원했어야 하던 구문입니다.

public static class IPAddressExtension
{
    extension(IPAddress address)
    {
        // 확장 메서드의 유형으로 속성(property) 정의 가능
        public uint IPv4Integer
        {
            get
            {
                if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                {
                    throw new InvalidOperationException("Only IPv4 addresses are supported.");
                }

                // Address 필드가 deprecated 되었으므로,
                // uint value = (uint)address.Address;

                return BitConverter.ToUInt32(address.GetAddressBytes(), 0);
            }
        }
    }
}

단지, 같은 속성 정의라고는 하지만 C# 컴파일러가 필드까지 생성해야만 하는 "자동 구현 속성(auto-implemented properties)"은 지원하지 않습니다.

public static class IPAddressExtension
{
    extension(IPAddress address)
    {
        // get/set 내부 코드를 구현하는 속성 정의는 지원하지만,
        public uint IPv4Integer
        {
            get { /* ...[생략]... */ }
            set { /* ...[생략]... */ }
        }

        // 자동 구현 속성(auto-implemented properties)은 지원하지 않습니다.
        public int Port { get; set; } // 컴파일 오류 - error CS9282: Extension declarations can include only methods or properties
    }
}

조금만 생각해 보면, 클래스 정의에서 결정된 인스턴스의 메모리 구조를 외부 어셈블리에서 정의할 수 있는 확장 메서드 구문에서 변경할 수는 없으므로 이해할 수 있는 부분입니다.




이번 신규 구문에서 특히 반가운 점이 있다면, 바로 인스턴스 메서드뿐만 아니라 정적 메서드 정의도 지원한다는 점입니다. 구문도 거의 비슷한데요, 인스턴스 멤버를 위한 extension과 거의 유사하지만 단지 extension 괄호 내에 타입명만 지정한다는 차이점이 하나 있습니다.

public static class IPAddressExtension
{
    // 인스턴스 메서드 정의를 위한 extension 구문 (타입명과 변수명 지정)
    extension(IPAddress address)
    {
        // ...[생략]...
    }

    public static IPAddress _minValue = new IPAddress(0x00000000);
    public static IPAddress _maxValue = new IPAddress(0xFFFFFFFF);

    // 정적 메서드 정의를 위한 extension 구문 (타입명만 지정)
    extension(IPAddress)
    {
        // 블록 내부에 정적 메서드/속성 정의 가능

        public static IPAddress MinValue => _minValue;
        public static IPAddress MaxValue => _maxValue;

        public static IPAddress FromUInt32(uint value)
        {
            return new IPAddress(value);
        }
    }
}

// 사용 예

Console.WriteLine(IPAddress.MinValue); // 출력: 0.0.0.0
Console.WriteLine(IPAddress.MaxValue); // 출력: 255.255.255.255

IPAddress addr = IPAddress.FromUInt32(845457600);
Console.WriteLine(addr); // 출력: 192.168.100.50

어떠세요? 언뜻 낯설어 보이지만 크게 어렵지 않으므로 금방 익숙해질만한 수준입니다. (제 개인적으로는 이번 C# 14에서 가장 마음에 드는 신규 문법으로 평가하고 싶습니다. ^^)




그나저나, 공식 문서에 보면 분명히 indexer에 대한 정의를 포함하고 있는데요,

// https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14#extension-members

public static class EnumerableExtension
{
    // Extension block
    extension<TSource>(IEnumerable<TSource> source) // extension members for IEnumerable<TSource>
    {
        // Extension property:
        public bool IsEmpty => !source.Any();
        // Extension indexer:
        public TSource this[int index] => source.Skip(index).First();
    }
}

하지만, 실습해 보면 컴파일 오류가 발생합니다.

IEnumerable<int> e = Enumerable.Range(1, 10);
int n = e[10]; // 컴파일 오류 - error CS0021: Cannot apply indexing with [] to an expression of type 'IEnumerable<int>'

문서만 먼저 만든 것 같기도 한데요, 어쨌든 C# 14 공식 출시에는 포함하지 않을까 싶습니다. 이와 관련해 문서에서는 다음의 순서로 기능 구현을 할 예정이라는 언급도 있는데,

  1. Properties and methods (instance and static)
  2. Operators
  3. Indexers (instance and static, may be done opportunistically at an earlier point)
  4. Anything else

순서상으로 보면 오히려 indexer보다 연산자 재정의가 먼저군요. ^^; 아마도 현재의 분위기로 보면 연산자 재정의는 C# 15에서나 지원하지 않을까 싶습니다.

마지막으로, 기존 문법으로 정의하든 신규 문법으로 정의하든 컴파일된 확장 메서드의 시그니처는 동일하기 때문에 저 2개의 메서드를 함께 정의할 수는 없습니다.

public static class StringExtension
{
    // 기존 확장 메서드 정의
    public static IPEndPoint ToEndPoint(this string value)
    {
        if (IPEndPoint.TryParse(value, out var endPoint) == false)
        {
            throw new ArgumentException($"Invalid endpoint format: {value}");
        }

        return endPoint;
    }

    extension(string value)
    {
        // 동시에 신규 확장 메서드 정의를 정의하면?
        // 컴파일 오류 - error CS0111: Type 'StringExtension' already defines a member called 'ToEndPoint' with the same parameter types
        public IPEndPoint ToEndPoint()
        {
            if (IPEndPoint.TryParse(value, out var endPoint) == false)
            {
                throw new ArgumentException($"Invalid endpoint format: {value}");
            }

            return endPoint;
        }
    }
}




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







[최초 등록일: ]
[최종 수정일: 8/14/2025]

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)
13900정성태3/8/20256101스크립트: 72. 파이썬 - SQLAlchemy + oracledb 연동
13899정성태3/7/20253504스크립트: 71. 파이썬 - asyncio의 ContextVar 전달
13898정성태3/5/20254566오류 유형: 948. Visual Studio - Proxy Authentication Required: dotnetfeed.blob.core.windows.net
13897정성태3/5/20256110닷넷: 2326. C# - PowerShell과 연동하는 방법 (두 번째 이야기)파일 다운로드1
13896정성태3/5/20255906Windows: 279. Hyper-V Manager - VM 목록의 CPU Usage 항목이 항상 0%로 나오는 문제
13895정성태3/4/20255807Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
13894정성태2/28/20255447Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
13893정성태2/27/20254511Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
13892정성태2/24/20256814닷넷: 2325. C# - PowerShell과 연동하는 방법파일 다운로드1
13891정성태2/23/20254736닷넷: 2324. C# - 프로세스의 성능 카운터용 인스턴스 이름을 구하는 방법파일 다운로드1
13890정성태2/21/20254397닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)파일 다운로드1
13889정성태2/20/20256455닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI) [1]파일 다운로드1
13888정성태2/17/20255208닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
13887정성태2/17/20257200닷넷: 2320. Blazor의 razor 페이지에서 code-behind 파일로 코드를 분리 및 DI 사용법
13886정성태2/15/20254851VS.NET IDE: 196. Visual Studio - Code-behind처럼 cs 파일을 그룹핑하는 방법
13885정성태2/14/20256993닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/20257416닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/20257118닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/20254958스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20255416닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/20257482오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20257184오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20257111오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20255837닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20257545닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20255298스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...