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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1856정성태2/15/201521281.NET Framework: 493. TypeRef 메타테이블에 등록되는 타입의 조건파일 다운로드1
1855정성태2/10/201520812개발 환경 구성: 256. WebDAV Redirector - Sysinternals 폴더 연결 시 "The network path was not found" 오류 해결 방법
1854정성태2/10/201521805Windows: 104. 폴더는 삭제할 수 없지만, 그 하위 폴더/파일은 생성/삭제/변경하는 보안 설정
1853정성태2/6/201552074웹: 29. 여신금융협회 웹 사이트의 "Netscape 6.0은 지원하지 않습니다." 오류 메시지 [5]
1852정성태2/5/201522491.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미파일 다운로드1
1851정성태2/5/201523416VC++: 88. 하룻밤의 꿈 - 인텔 하스웰의 TSX Instruction 지원 [2]
1850정성태2/4/201544319Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미 [4]
1849정성태2/4/201524195기타: 51. DropBox의 CPU 100% 현상 [1]파일 다운로드1
1848정성태2/4/201519471.NET Framework: 491. 닷넷 Generic 타입의 메타 데이터 토큰 값 알아내는 방법 [2]
1847정성태2/3/201522833기타: 50. C# - 윈도우에서 dropbox 동기화 폴더 경로 및 종료하는 방법
1846정성태2/2/201532035Windows: 102. 제어판의 프로그램 추가/삭제 항목을 수동으로 실행하고 싶다면? [1]
1845정성태1/26/201532916Windows: 101. 제어판의 "Windows 자격 증명 관리(Manage your credentials)"를 금지시키는 방법
1844정성태1/26/201530870오류 유형: 269. USB 메모리의 용량이 비정상적으로 보여진다면? [7]
1843정성태1/24/201521936VC++: 87. 무시할 수 없는 Visual C++ 런타임 함수 성능
1842정성태1/23/201544474개발 환경 구성: 255. 노트북 키보드에 없는 BREAK 키를 다른 키로 대체하는 방법
1841정성태1/21/201519425오류 유형: 268. Win32 핸들 관련 CLR4 보안 오류 사례
1840정성태1/8/201527640오류 유형: 267. Visual Studio - CodeLens 사용 시 CPU 100% 현상
1839정성태1/5/201520539디버깅 기술: 69. windbg 분석 사례 - cpu 100% 현상 (2)
1838정성태1/4/201540246기타: 49. 윈도우 내레이터(Narrator) 기능 끄는 방법(윈도우에 파란색의 굵은 테두리 선이 나타난다면?) [4]
1837정성태1/4/201526363디버깅 기술: 68. windbg 분석 사례 - 메모리 부족 [1]
1836정성태1/4/201526375디버깅 기술: 67. windbg - 덤프 파일과 handle 정보
1835정성태1/3/201526864개발 환경 구성: 254. SQL 서버 역시 SSL 3.0/TLS 1.0만을 지원하는 듯!
1834정성태1/3/201551509개발 환경 구성: 253. TLS 1.2를 적용한 IIS 웹 사이트 구성
1833정성태1/3/201527587.NET Framework: 490. System.Data.SqlClient는 SSL 3.0/TLS 1.0만 지원하는 듯! [3]
1832정성태1/2/201520658오류 유형: 266. Azure에 응용 프로그램 게시 중 로그인 오류
1831정성태1/1/201528541디버깅 기술: 66. windbg 분석 사례 - cpu 100% 현상 (1) [1]
... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...