Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 8개 있습니다.)

C# 7.3 - 개선된 메서드 선택 규칙 3가지(Improved overload candidates)

C# 7.3 (1) - 개선된 문법 4개(Support == and != for tuples, Ref Reassignment, Constraints, Stackalloc initializers)
; https://www.sysnet.pe.kr/2/0/11552

C# 7.3 (2) - 개선된 메서드 선택 규칙 3가지(Improved overload candidates)
; https://www.sysnet.pe.kr/2/0/11553

C# 7.3 (3) - 자동 구현 속성에 특성 적용 가능(Attribute on backing field)
; https://www.sysnet.pe.kr/2/0/11554

C# 7.3 (4) - 사용자 정의 타입에 fixed 적용 가능(Custom fixed)
; https://www.sysnet.pe.kr/2/0/11555

C# 7.3 (5) - 구조체의 고정 크기를 갖는 fixed 배열 필드에 대한 직접 접근 가능(Indexing movable fixed buffers)
; https://www.sysnet.pe.kr/2/0/11556

C# 7.3 (6) - blittable 제네릭 제약(blittable)
; https://www.sysnet.pe.kr/2/0/11558

C# 7.3 (7) - 초기화 식에서 변수 사용 가능(expression variables in initializers)
; https://www.sysnet.pe.kr/2/0/11560




C# 6.0에서도 한 차례 개선이 있었지만 메서드 오버로드에 대한 좀 더 정확한 식별 작업이 C# 7.3에도 추가되었습니다.

하나씩 예를 들어볼까요? ^^

우선 정적/인스턴스 멤버의 구분입니다. 정적/인스턴스 메서드가 함께 정의되어 있을 때 C# 7.2 이전까지는 아래의 코드를 컴파일할 수 없었습니다.

class StaticButInstanceMatchFirst
{
    public static void Do(object obj)
    {
    }

    public void Do(string txt)
    {
    }
}

static void Main(string[] args)
{
    // C# 7.2까지 컴파일 에러 - Error CS0120 An object reference is required for the non-static field, method, or property 'StaticButInstanceMatchFirst.Do(string)'
    StaticButInstanceMatchFirst.Do("TEST"); // 우회적으로 (object)"TEST"로 인자를 전달하면 OK
}

왜냐하면, 시그니처가 일치하는 메서드가 우선순위가 높고 일단 매칭이 되면 더 이상의 검색을 하지 않기 때문입니다. 즉, 정적/비정적 호출과 관계없이 "void Do(string txt)"와 일치하는 메서드를 호출한 것으로 판단을 해버렸고 그에 대해 인스턴스 호출이 아니라는 규칙을 나중에 적용해 오류라고 판단하는 것입니다.

이와 반대의 경우도 있습니다.

class InstanceButStaticMatchFirst
{
    public static void Do(string txt)
    {
    }

    public void Do(object obj)
    {
    }
}

static void Main(string[] args)
{
    InstanceButStaticMatchFirst t = new InstanceButStaticMatchFirst();

    // C# 7.2까지 컴파일 에러 - Error CS0176 Member 'InstanceButStaticMatchFirst.Do(string)' cannot be accessed with an instance reference; qualify it with a type name instead
    t.Do("TEST");  // 우회적으로 (object)"TEST"로 인자를 전달하면 OK
}

이번 역시 마찬가지로 static/instance 모든 멤버를 대상으로 정확히 일치하는 시그니처를 가진 메서드(static void Do)가 검색되었지만 인스턴스 멤버가 아니므로 오류가 발생한 것입니다. 이에 대해, 단순하게 "클래스 이름"으로 시작하는 호출은 정적 메서드에서, 그렇지 않은 경우에는 인스턴스 메서드를 대상으로 검색하면 된다고 정할 수도 있습니다. 만약 그렇게 규칙을 정하면 (현재 잘 빌드가 되는) 다음의 코드에서 다시 문제가 발생합니다.

class A
{
    public static void M(string txt)
    {
        System.Console.WriteLine("static M(string)");
    }

    public void M(object obj)
    {
        System.Console.WriteLine("M(object)");
    }
}

class B
{
    public static void M(object obj)
    {
        System.Console.WriteLine("static M(object)");
    }

    public void M(string txt)
    {
        System.Console.WriteLine("M(string)");
    }
}

class C
{
    // 참고: the Color Color problem
    // https://blogs.msdn.microsoft.com/ericlippert/2009/07/06/color-color/

    public A A = new A(); // 클래스 명과 필드의 이름이 같음
    public B B = new B(); // 클래스 명과 필드의 이름이 같음

    public void N()
    {
        // 아래의 코드를 어떻게 처리하는지는 C# 3.0 문서의 7.5.4.1 절을 참고
        A.M("hello");
        B.M("hello");
    }
}

/*
출력 결과:

static M(string)
M(string)
*/

기존에 위의 메서드는 [A].M, [B].M 호출에서 [A], [B]가 클래스 이름인지, 인스턴스 변수 이름인지에 상관없이 대상 타입의 모든 메서드를 검색 대상으로 하기 때문에 적절한 선택을 할 수 있습니다.

이 모든 상황을 고려하기 위해 C# 7.3부터 도입한 규칙이 다음의 내용입니다.

1. When a method group contains both instance and static members, we discard the instance members if invoked without an instance receiver or context, and discard the static members if invoked with an instance receiver. When there is no receiver, we include only static members in a static context, otherwise both static and instance members. When the receiver is ambiguously an instance or type due to a color-color situation, we include both. A static context, where an implicit this instance receiver cannot be used, includes the body of members where no this is defined, such as static members, as well as places where this cannot be used, such as field initializers and constructor-initializers.

정리해 보면 다음과 같습니다.

1) "an instance receiver or context" 없이 호출되면 인스턴스 멤버를 제외
2) "an instance receiver"와 함께 호출되면 정적 멤버를 제외

3) receiver가 없으면,
    static 문맥인 경우 static 멤버만 포함
    그 외의 경우 변함없이 모든 멤버(instance/static) 포함

4) receiver가 모호한 경우 변함없이 모든 멤버(instance/static) 포함

1)번 규칙으로 인해 StaticButInstanceMatchFirst의 정적 Do 메서드의 호출이 정상적으로 컴파일됩니다.

static void Main(string[] args)
{
    // C# 7.3부터 정상 컴파일
    StaticButInstanceMatchFirst.Do("TEST");
}

마찬가지로 2)번 규칙으로 InstanceButStaticMatchFirst의 비정적 Do 메서드 호출이 잘 됩니다.

static void Main(string[] args)
{
    InstanceButStaticMatchFirst t = new InstanceButStaticMatchFirst();

    // C# 7.3부터 정상 컴파일
    t.Do("TEST");
}

3)번 규칙으로 인해 다음의 메서드 호출이,

class StaticButInstanceMatchFirst
{
    public static void Do(object obj)
    {
        System.Console.WriteLine("static Do(object)");
    }

    public void Do(string txt)
    {
        System.Console.WriteLine("Do(string)");
    }

    public static void StaticDone()
    {
        // C# 7.2까지 컴파일 오류 - Error CS0120 An object reference is required for the non-static field, method, or property 'StaticButInstanceMatchFirst.Do(string)'
        Do("test");
    }
}

C# 7.3부터는 receiver가 명시되지 않은 상태에서 "static" 문맥에서 호출되었으므로 대상이 static 멤버만 고려되므로 "public static void Do(object obj)" 메서드가 선택됩니다. 이로 인해 경우에 따라 receiver의 유무로 선택이 달라지는 실행이 있습니다. 가령 다음의 경우,

class InstanceButStaticMatchFirst
{
    public static void Do(string txt)
    {
        System.Console.WriteLine("static Do(string)");
    }

    public void Do(object obj)
    {
        System.Console.WriteLine("Do(object)");
    }

    public void InstanceDone()
    {
        Do("test"); // receiver 명시가 없으므로.
    }
}

InstanceDone에서 Do 메서드에 대한 receiver를 명시하지 않았고, static 문맥이 아니므로 모든 메서드를 대상으로 검색이 되므로 "static void Do" 메서드가 선택됩니다. 반면 receiver를 다음과 같이 명시하면,

public void InstanceDone()
{
    this.Do("test");
}

이제 2)번 규칙이 적용되므로 static 멤버가 제외되어 "public void Do(object obj)" 메서드가 선택됩니다.

마지막으로, 4)번 규칙으로 인해 위와 같은 규칙들을 세웠어도 "the Color Color problem" 예제도 컴파일이 잘 됩니다.




다음으로 개선된 것이 제네릭 메서드와의 모호함입니다. 설명을 위해 다음의 예제를 보면,

using System;
using System.Linq;
using System.Threading.Tasks;

public class AmbiguousMethods
{
    public static GenericResult<T> Do<T>(Func<T> func)
    {
        Console.WriteLine("GenericResult<T> Do");
        T inst = func();
        return new GenericResult<T>(inst);
    }

    public static int Do(Func<int> func)
    {
        Console.WriteLine("int Do");
        return func();
    }
}

public class GenericResult<T>
{
    T _inst;
    public GenericResult(T inst)
    {
        _inst = inst;
    }
}

class Program
{
    static void Main(string[] args)
    {
        AmbiguousMethods.Do(GetInteger);
    }

    private static int GetInteger()
    {
        return 5;
    }
}

AmbiguousMethods.Do 메서드 호출에서 int를 반환하는 메서드가 찾아지므로 당연히 public static int Do(Func<int> func)가 호출이 됩니다. 반면, 다음과 같이 호출하면 어떻게 될까요?

class Program
{
    static void Main(string[] args)
    {
        // C# 7.2까지 컴파일 오류 - Error CS0121 The call is ambiguous between the following methods or properties: 'AmbiguousMethods.Do<T>(Func<T>)' and 'AmbiguousMethods.Do(Func<GenericResult>)'
        AmbiguousMethods.Do(GetString);
    }

    private static string GetString()
    {
        return "test";
    }
}

일단, GetString은 int를 반환하지 않으므로 "public static int Do(Func<int> func)" 메서드와 맞지 않으므로 그다음 public static GenericResult<T> Do<T>(Func<T> func) 메서드를 선택해 빌드해야 합니다. 하지만 C# 7.2까지는 이런 경우 모호함 오류가 발생했고, 이를 보완하기 위해 다음과 같은 개선된 규칙이 C# 7.3부터 적용됩니다.

2. When a method group contains some generic methods whose type arguments do not satisfy their constraints, these members are removed from the candidate set.

이로 인해 혜택받는 코드가 바로 Task.Run입니다.

static void Main(string[] args)
{
    // C# 7.2까지 컴파일 오류 - Error CS0121 The call is ambiguous between the following methods or properties: 'Task.Run<TResult>(Func<TResult>)' and 'Task.Run(Func<Task>)'
    Task.Run(SumOfIntegers);
}

private static int SumOfIntegers()
{
    return Enumerable.Range(1, 1000).Sum();
}

C# 7.3부터는 위와 같은 코드가 정상적으로 컴파일 됩니다.




자, 이제 마지막 규칙입니다. ^^ C# 7.2까지 다음의 코드는 오류가 발생했습니다.

using System;

class Program
{
    public delegate int IntStringDelegate(string txt);
    public delegate string StringObjectDelegate(object obj);

    static void Main(string[] args)
    {
        // C# 7.2까지 컴파일 오류 - Error CS0121 The call is ambiguous between the following methods or properties: 'Program.Call(Program.IntStringDelegate)' and 'Program.Call(Program.StringObjectDelegate)'
        Call(RetIntArgObject);

        // C# 7.2까지 컴파일 오류 - Error CS0121 The call is ambiguous between the following methods or properties: 'Program.Call(Program.IntStringDelegate)' and 'Program.Call(Program.StringObjectDelegate)'
        Call(RetStringArgObject);
    }

    public static void Call(IntStringDelegate func)
    {
        Console.WriteLine("IntStringDelegate");
    }

    public static void Call(StringObjectDelegate func)
    {
        Console.WriteLine("StringObjectDelegate");
    }

    private static int RetIntArgObject(object txt)
    {
        return 5;
    }

    private static string RetStringArgObject(object txt)
    {
        return txt.ToString();
    }
}

이유는, Call 메서드의 인자로 전달한 메서드가 2개의 후보로 선정된 delegate 중에 "반환값을 제외하고" 인자 타입이 일치하는 것이 선택된 탓입니다. 이 문제를 피하기 위해 C# 7.3부터는 다음의 규칙이 적용됩니다.

3. For a method group conversion, candidate methods whose return type doesn't match up with the delegate's return type are removed from the set.

따라서 반환 타입이 다르다면 후보 선정에서 탈락하므로 Call(RetIntArgObject); 호출은 "public delegate int IntStringDelegate(string txt);" 만이 고려가 되어 string 인자는 object로 처리됩니다. 반면, Call(RetStringArgObject); 호출은 "public delegate string StringObjectDelegate(object obj);" 만이 후보로 선정되므로 역시 정상적으로 컴파일이 됩니다.




(첨부 파일은 이 글의 예제 코드를 포함합니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/16/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)
13917정성태4/30/202544VS.NET IDE: 199. Directory.Build.props에 정의한 속성에 대해 Condition 제약으로 값을 변경하는 방법
13916정성태4/23/2025446디버깅 기술: 221. WinDbg 분석 사례 - ASP.NET HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상 - 두 번째 이야기
13915정성태4/13/20251652닷넷: 2331. C# - 실행 시에 메서드 가로채기 (.NET 9)파일 다운로드1
13914정성태4/11/20251975디버깅 기술: 220. windbg 분석 사례 - x86 ASP.NET 웹 응용 프로그램의 CPU 100% 현상 (4)
13913정성태4/10/20251204오류 유형: 950. Process Explorer - 64비트 윈도우에서 32비트 프로세스의 덤프를 뜰 때 "Error writing dump file: Access is denied." 오류
13912정성태4/9/2025863닷넷: 2330. C# - 실행 시에 메서드 가로채기 (.NET 5 ~ .NET 8)파일 다운로드1
13911정성태4/8/20251091오류 유형: 949. WinDbg - .NET Core/5+ 응용 프로그램 디버깅 시 sos 확장을 자동으로 로드하지 못하는 문제
13910정성태4/8/20251257디버깅 기술: 219. WinDbg - 명령어 내에서 환경 변수 사용법
13909정성태4/7/20251725닷넷: 2329. C# - 실행 시에 메서드 가로채기 (.NET Framework 4.8)파일 다운로드1
13908정성태4/2/20252138닷넷: 2328. C# - MailKit: SMTP, POP3, IMAP 지원 라이브러리
13907정성태3/29/20251937VS.NET IDE: 198. (OneDrive, Dropbox 등의 공유 디렉터리에 있는) C# 프로젝트의 출력 경로 변경하기
13906정성태3/27/20252191닷넷: 2327. C# - 초기화되지 않은 메모리에 접근하는 버그?파일 다운로드1
13905정성태3/26/20252221Windows: 281. C++ - Windows / Critical Section의 안정화를 위해 도입된 "Keyed Event"파일 다운로드1
13904정성태3/25/20251902디버깅 기술: 218. Windbg로 살펴보는 Win32 Critical Section파일 다운로드1
13903정성태3/24/20251522VS.NET IDE: 197. (OneDrive, Dropbox 등의 공유 디렉터리에 있는) C++ 프로젝트의 출력 경로 변경하기
13902정성태3/24/20251737개발 환경 구성: 742. Oracle - 테스트용 hr 계정 및 데이터 생성파일 다운로드1
13901정성태3/9/20252112Windows: 280. Hyper-V의 3가지 Thread Scheduler (Classic, Core, Root)
13900정성태3/8/20252346스크립트: 72. 파이썬 - SQLAlchemy + oracledb 연동
13899정성태3/7/20251806스크립트: 71. 파이썬 - asyncio의 ContextVar 전달
13898정성태3/5/20252124오류 유형: 948. Visual Studio - Proxy Authentication Required: dotnetfeed.blob.core.windows.net
13897정성태3/5/20252367닷넷: 2326. C# - PowerShell과 연동하는 방법 (두 번째 이야기)파일 다운로드1
13896정성태3/5/20252178Windows: 279. Hyper-V Manager - VM 목록의 CPU Usage 항목이 항상 0%로 나오는 문제
13895정성태3/4/20252220Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
13894정성태2/28/20252250Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
13893정성태2/27/20252197Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...