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

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);" 만이 후보로 선정되므로 역시 정상적으로 컴파일이 됩니다.




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




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

[연관 글]





[최초 등록일: ]
[최종 수정일: 6/25/2018 ]

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

비밀번호

댓글 쓴 사람
 




... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
11986정성태7/18/20191092오류 유형: 557. 드라이브 문자를 할당하지 않은 파티션을 탐색기에서 드라이브 문자와 함께 보여주는 문제
11985정성태7/17/20191065개발 환경 구성: 452. msbuild - csproj에 환경 변수 조건 사용
11984정성태7/9/20192099개발 환경 구성: 451. Microsoft Edge (Chromium)을 대상으로 한 Selenium WebDriver 사용법
11983정성태7/8/20191145오류 유형: 556. nodemon - 'mocha' is not recognized as an internal or external command, operable program or batch file.
11982정성태7/8/20191119오류 유형: 555. Visual Studio 빌드 오류 - result: unexpected exception occured (-1002 - 0xfffffc16)
11981정성태7/7/20191445Math: 64. C# - 3층 구조의 신경망(분류)파일 다운로드1
11980정성태7/7/20196183개발 환경 구성: 450. Visual Studio Code의 Java 확장을 이용한 간단한 프로젝트 구축파일 다운로드1
11979정성태7/8/20191687개발 환경 구성: 449. TFS에서 gitlab/github등의 git 서버로 마이그레이션하는 방법
11978정성태7/6/20191440Windows: 161. 계정 정보가 동일하지 않은 PC 간의 인증을 수행하는 방법 [1]
11977정성태7/6/20192733오류 유형: 554. git push - error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large
11976정성태7/4/20191583오류 유형: 553. (잘못 인증 한 후) 원격 git repo 재인증 시 "remote: HTTP Basic: Access denied" 오류 발생
11975정성태7/4/20194136개발 환경 구성: 448. Visual Studio Code에서 콘솔 응용 프로그램 개발 시 "입력"받는 방법
11974정성태7/4/20192360Linux: 22. "Visual Studio Code + Remote Development"로 윈도우 환경에서 리눅스(CentOS 7) C/C++ 개발
11973정성태7/4/20191615Linux: 21. 리눅스에서 공유 라이브러리가 로드되지 않는다면?
11972정성태7/3/20191983.NET Framework: 847. JAVA와 .NET 간의 AES 암호화 연동파일 다운로드1
11971정성태7/3/20191781개발 환경 구성: 447. Visual Studio Code에서 OpenCvSharp 개발 환경 구성
11970정성태7/2/20191453오류 유형: 552. 웹 브라우저에서 파일 다운로드 후 "Running security scan"이 끝나지 않는 문제
11969정성태7/7/20191424Math: 63. C# - 3층 구조의 신경망파일 다운로드1
11968정성태7/1/20193863오류 유형: 551. Visual Studio Code에서 Remote-SSH 연결 시 "Opening Remote..." 단계에서 진행되지 않는 문제 [1]
11967정성태7/1/20191527개발 환경 구성: 446. Synology NAS를 Windows 10에서 iSCSI로 연결하는 방법
11966정성태6/30/20191473Math: 62. 활성화 함수에 따른 뉴런의 출력을 그리드 맵으로 시각화파일 다운로드1
11965정성태6/30/20191546.NET Framework: 846. C# - 2차원 배열을 1차원 배열로 나열하는 확장 메서드파일 다운로드1
11964정성태6/30/20192004Linux: 20. C# - Linux에서의 Named Pipe를 이용한 통신
11963정성태6/29/20191882Linux: 19. C# - .NET Core Unix Domain Socket 사용 예제
11962정성태6/27/20191215Math: 61. C# - 로지스틱 회귀를 이용한 선형분리 불가능 문제의 분류파일 다운로드1
11961정성태6/27/2019989Graphics: 37. C# - PLplot - 출력 모음(Family File Output)
... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...