Trình biên dịch Lỗi gọi mơ hồ - phương thức và nhóm phương thức ẩn danh với Func <> hoặc Action


102

Tôi có một tình huống mà tôi muốn sử dụng cú pháp nhóm phương thức thay vì các phương thức ẩn danh (hoặc cú pháp lambda) để gọi một hàm.

Hàm có hai lần nạp chồng, một hàm nhận một Action, một nạp a Func<string>.

Tôi có thể vui vẻ gọi hai quá tải bằng cách sử dụng các phương thức ẩn danh (hoặc cú pháp lambda), nhưng gặp lỗi trình biên dịch của Lời gọi mơ hồ nếu tôi sử dụng cú pháp nhóm phương thức. Tôi có thể giải quyết bằng cách truyền rõ ràng đến Actionhoặc Func<string>, nhưng không nghĩ rằng điều này là cần thiết.

Bất cứ ai có thể giải thích tại sao cần phải có phôi rõ ràng.

Mẫu mã bên dưới.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

C # 7.3 Cập nhật

Theo nhận xét của 0xcde bên dưới vào ngày 20 tháng 3 năm 2019 (chín năm sau khi tôi đăng câu hỏi này!), Mã này biên dịch từ C # 7.3 nhờ các ứng viên quá tải được cải thiện .


Tôi đã thử mã của bạn và tôi gặp phải lỗi thời gian biên dịch bổ sung: 'void test.ClassWithSimpleMethods.DoNothing ()' có kiểu trả về không chính xác (nằm ở dòng 25, là nơi có lỗi không rõ ràng)
Matt Ellen

@Matt: Tôi cũng thấy lỗi đó. Các lỗi mà tôi đã trích dẫn trong bài đăng của mình là các vấn đề biên dịch mà VS nêu bật trước khi bạn thậm chí thử biên dịch đầy đủ.
Richard Ev

1
Nhân tiện, đây là một câu hỏi tuyệt vời. Tôi yêu bất cứ điều gì mà lực lượng tôi vào thông số kỹ thuật :)
Jon Skeet

1
Lưu ý rằng mã mẫu của bạn sẽ được biên dịch nếu bạn sử dụng C # 7.3 ( <LangVersion>7.3</LangVersion>) hoặc mới hơn nhờ các ứng cử viên quá tải được cải thiện .
0xced

Câu trả lời:


97

Trước hết, tôi xin nói rằng câu trả lời của Jon là đúng. Đây là một trong những phần xù xì nhất của thông số kỹ thuật, rất tốt cho Jon vì đã đi sâu vào đầu tiên.

Thứ hai, hãy để tôi nói rằng dòng này:

Một chuyển đổi ngầm tồn tại từ một nhóm phương thức sang một loại đại biểu tương thích

(nhấn mạnh thêm) là sai lầm sâu sắc và đáng tiếc. Tôi sẽ nói chuyện với Mads về việc xóa từ "tương thích" ở đây.

Lý do điều này gây hiểu lầm và đáng tiếc là vì có vẻ như điều này đang gọi đến phần 15.2, "Khả năng tương thích của đại diện". Phần 15.2 đã mô tả mối quan hệ tương thích giữa các phương thức và kiểu đại biểu , nhưng đây là một câu hỏi về khả năng chuyển đổi của các nhóm phương thức và kiểu đại biểu , điều này là khác nhau.

Bây giờ chúng ta đã hiểu rõ điều đó, chúng ta có thể xem qua phần 6.6 của thông số kỹ thuật và xem những gì chúng ta nhận được.

Để thực hiện giải quyết quá tải, trước tiên chúng ta cần xác định xem quá tải nào là ứng viên có thể áp dụng . Một ứng cử viên có thể áp dụng nếu tất cả các đối số có thể chuyển đổi hoàn toàn thành các kiểu tham số chính thức. Hãy xem xét phiên bản đơn giản này của chương trình của bạn:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

Vì vậy, chúng ta hãy đi qua nó từng dòng một.

Một chuyển đổi ngầm tồn tại từ một nhóm phương thức sang một loại đại biểu tương thích.

Tôi đã thảo luận về cách không may từ "tương thích" ở đây. Tiếp tục. Chúng tôi đang tự hỏi khi thực hiện giải quyết quá tải trên Y (X), nhóm phương thức X có chuyển đổi thành D1 không? Nó có chuyển đổi thành D2 không?

Cho một kiểu đại biểu D và một biểu thức E được phân loại là một nhóm phương thức, một phép chuyển đổi ngầm định tồn tại từ E sang D nếu E chứa ít nhất một phương thức có thể áp dụng [...] cho danh sách đối số được xây dựng bằng cách sử dụng tham số các loại và bổ ngữ của D, như được mô tả trong phần sau.

Càng xa càng tốt. X có thể chứa một phương thức có thể áp dụng với danh sách đối số của D1 hoặc D2.

Ứng dụng thời gian biên dịch của việc chuyển đổi từ nhóm phương thức E sang kiểu đại biểu D được mô tả như sau.

Dòng này thực sự không nói lên điều gì thú vị.

Lưu ý rằng sự tồn tại của chuyển đổi ngầm định từ E sang D không đảm bảo rằng ứng dụng thời gian biên dịch của chuyển đổi sẽ thành công mà không có lỗi.

Dòng này thật hấp dẫn. Nó có nghĩa là có những chuyển đổi ngầm tồn tại, nhưng có thể bị biến thành lỗi! Đây là một quy tắc kỳ lạ của C #. Để lạc đề một chút, đây là một ví dụ:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

Hoạt động tăng dần là bất hợp pháp trong cây biểu thức. Tuy nhiên, lambda vẫn có thể chuyển đổi sang kiểu cây biểu thức, ngay cả khi việc chuyển đổi đã từng được sử dụng, đó là một lỗi! Nguyên tắc ở đây là chúng ta có thể muốn thay đổi các quy tắc của những gì có thể đi trong cây biểu thức sau này; thay đổi các quy tắc đó không nên thay đổi các quy tắc hệ thống kiểu . Chúng tôi muốn buộc bạn phải làm cho các chương trình của bạn trở nên rõ ràng ngay bây giờ , để khi chúng tôi thay đổi các quy tắc cho cây biểu thức trong tương lai để làm cho chúng tốt hơn, chúng tôi sẽ không đưa ra các thay đổi vi phạm trong độ phân giải quá tải .

Dù sao, đây là một ví dụ khác về loại quy tắc kỳ lạ này. Một chuyển đổi có thể tồn tại cho mục đích giải quyết quá tải, nhưng thực sự là một lỗi khi sử dụng. Mặc dù trên thực tế, đó không chính xác là tình huống mà chúng ta đang ở đây.

Tiếp tục:

Một phương thức duy nhất M được chọn tương ứng với một lệnh gọi phương thức có dạng E (A) [...] Danh sách đối số A là danh sách các biểu thức, mỗi biểu thức được phân loại là một biến [...] của tham số tương ứng trong biểu thức -parameter-list of D.

ĐỒNG Ý. Vì vậy, chúng tôi giải quyết quá tải trên X đối với D1. Danh sách tham số chính thức của D1 trống, vì vậy chúng tôi giải quyết quá tải trên X () và joy, chúng tôi tìm thấy một phương thức "string X ()" hoạt động. Tương tự, danh sách tham số chính thức của D2 trống. Một lần nữa, chúng tôi thấy rằng "string X ()" cũng là một phương thức hoạt động ở đây.

Nguyên tắc ở đây là việc xác định khả năng chuyển đổi của nhóm phương pháp đòi hỏi phải chọn một phương thức từ một nhóm phương pháp sử dụng độ phân giải quá tảigiải quyết quá tải không xem xét các kiểu trả về .

Nếu thuật toán [...] tạo ra lỗi, thì lỗi thời gian biên dịch sẽ xảy ra. Nếu không, thuật toán tạo ra một phương pháp tốt nhất M có cùng số lượng tham số với D và chuyển đổi được coi là tồn tại.

Chỉ có một phương pháp trong nhóm phương pháp X nên nó phải là phương pháp tốt nhất. Chúng tôi đã chứng minh thành công rằng chuyển đổi tồn tại từ X sang D1 và từ X sang D2.

Bây giờ, dòng này có liên quan không?

Phương pháp M đã chọn phải tương thích với kiểu đại biểu D, nếu không, xảy ra lỗi thời gian biên dịch.

Thực ra, không, không có trong chương trình này. Chúng tôi không bao giờ đi xa được khi kích hoạt dòng này. Bởi vì, hãy nhớ rằng, những gì chúng ta đang làm ở đây là cố gắng giải quyết quá tải trên Y (X). Chúng ta có hai ứng cử viên Y (D1) và Y (D2). Cả hai đều có thể áp dụng. Cái nào tốt hơn ? Không nơi nào trong đặc điểm kỹ thuật mà chúng tôi mô tả sự tốt hơn giữa hai chuyển đổi có thể có này .

Bây giờ, người ta chắc chắn có thể tranh luận rằng một chuyển đổi hợp lệ tốt hơn một chuyển đổi tạo ra lỗi. Điều đó thực sự có thể nói rằng, trong trường hợp này, việc giải quyết quá tải KHÔNG xem xét các kiểu trả về, đó là điều chúng ta muốn tránh. Sau đó, câu hỏi đặt ra là nguyên tắc nào tốt hơn: (1) duy trì bất biến mà giải pháp quá tải không xem xét các kiểu trả về, hoặc (2) cố gắng chọn một chuyển đổi mà chúng ta biết sẽ hoạt động trên một chuyển đổi mà chúng ta biết là không?

Đây là một cuộc gọi phán xét. Với lambdas , chúng tôi làm xem xét các kiểu trả về trong những loại chuyển đổi, trong phần 7.4.3.3:

E là một hàm ẩn danh, T1 và T2 là kiểu đại biểu hoặc kiểu cây biểu thức có danh sách tham số giống hệt nhau, kiểu trả về suy ra X tồn tại cho E trong ngữ cảnh của danh sách tham số đó và một trong các giá trị sau là:

  • T1 có kiểu trả về Y1 và T2 có kiểu trả về Y2 và chuyển đổi từ X sang Y1 tốt hơn chuyển đổi từ X sang Y2

  • T1 có kiểu trả về Y và T2 là kiểu trả về vô hiệu

Thật không may là chuyển đổi nhóm phương pháp và chuyển đổi lambda không nhất quán về mặt này. Tuy nhiên, tôi có thể sống với nó.

Dù sao, chúng tôi không có quy tắc "tốt hơn" để xác định chuyển đổi nào tốt hơn, X thành D1 hoặc X thành D2. Do đó, chúng tôi đưa ra một lỗi không rõ ràng về độ phân giải của Y (X).


8
Bẻ khóa - cảm ơn rất nhiều vì cả câu trả lời và (hy vọng) kết quả cải thiện về thông số kỹ thuật :) Cá nhân tôi nghĩ rằng sẽ hợp lý khi giải quyết quá tải để tính đến loại trả về cho các chuyển đổi nhóm phương thức để làm cho hành vi trực quan hơn, nhưng Tôi hiểu nó sẽ làm như vậy với cái giá phải trả là nhất quán. (Điều này cũng có thể nói chung chung suy luận kiểu như áp dụng cho chuyển đổi nhóm phương pháp khi chỉ có một phương pháp trong nhóm phương pháp, như tôi nghĩ chúng ta đã thảo luận trước đó.)
Jon Skeet

35

CHỈNH SỬA: Tôi nghĩ rằng tôi đã hiểu nó.

Như zinglon nói, đó là bởi vì có một sự chuyển đổi ngầm định từ GetStringthành Actionmặc dù ứng dụng thời gian biên dịch sẽ không thành công. Đây là phần giới thiệu phần 6.6, với một số điểm nhấn (của tôi):

Một chuyển đổi ngầm định (§6.1) tồn tại từ một nhóm phương thức (§7.1) sang một kiểu đại biểu tương thích. Cho một kiểu đại biểu D và một biểu thức E được phân loại là một nhóm phương thức, một chuyển đổi ngầm định tồn tại từ E sang D nếu E chứa ít nhất một phương thức có thể áp dụng ở dạng bình thường của nó (§7.4.3.1) cho danh sách đối số được xây dựng bằng cách sử dụng các loại tham số và công cụ sửa đổi của D , như được mô tả trong phần sau.

Bây giờ, tôi đã bị bối rối bởi câu đầu tiên - nói về việc chuyển đổi sang một loại đại biểu tương thích. Actionkhông phải là đại biểu tương thích cho bất kỳ phương thức nào trong GetStringnhóm phương thức, nhưng GetString()phương thức này có thể áp dụng ở dạng bình thường cho danh sách đối số được xây dựng bằng cách sử dụng các kiểu tham số và bổ ngữ của D. Lưu ý rằng điều này không nói về kiểu trả về của D. Đó là lý do tại sao nó trở nên nhầm lẫn ... bởi vì nó sẽ chỉ kiểm tra tính tương thích của đại biểu GetString()khi áp dụng chuyển đổi, không kiểm tra sự tồn tại của nó.

Tôi nghĩ rằng việc loại bỏ quá tải ra khỏi phương trình một cách ngắn gọn là điều dễ hiểu và xem sự khác biệt này giữa sự tồn tại của một chuyển đổi và khả năng áp dụng của nó có thể biểu hiện như thế nào. Đây là một ví dụ ngắn gọn nhưng đầy đủ:

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Không có biểu thức gọi phương thức nào trong các Mainbiên dịch, nhưng các thông báo lỗi là khác nhau. Đây là cái dành cho IntMethod(GetString):

Test.cs (12,9): error CS1502: Phương thức được nạp chồng tốt nhất cho 'Program.IntMethod (int)' có một số đối số không hợp lệ

Nói cách khác, phần 7.4.3.1 của thông số kỹ thuật không thể tìm thấy bất kỳ thành viên chức năng áp dụng nào.

Bây giờ đây là lỗi cho ActionMethod(GetString):

Test.cs (13,22): lỗi CS0407: 'string Program.GetString ()' có kiểu trả về sai

Lần này nó đã tìm ra phương thức mà nó muốn gọi - nhưng không thực hiện được chuyển đổi cần thiết. Rất tiếc, tôi không thể tìm ra một chút thông số kỹ thuật nơi thực hiện kiểm tra cuối cùng - có vẻ như nó thể nằm trong 7.5.5.1, nhưng tôi không thể biết chính xác vị trí.


Câu trả lời cũ đã bị xóa, ngoại trừ phần này - bởi vì tôi hy vọng Eric có thể làm sáng tỏ "tại sao" của câu hỏi này ...

Vẫn đang tìm kiếm ... trong thời gian ngắn, nếu chúng tôi nói "Eric Lippert" ba lần, bạn có nghĩ rằng chúng tôi sẽ có một chuyến thăm (và do đó là một câu trả lời)?


@Jon - có thể như vậy classWithSimpleMethods.GetStringclassWithSimpleMethods.DoNothingkhông phải là đại biểu?
Daniel A. White,

@Daniel: Không - những biểu thức đó là biểu thức nhóm phương thức và các phương thức được nạp chồng chỉ nên được coi là có thể áp dụng khi có sự chuyển đổi ngầm định từ nhóm phương thức sang loại tham số có liên quan. Xem phần 7.4.3.1 của thông số kỹ thuật.
Jon Skeet

Đọc phần 6.6, có vẻ như quá trình chuyển đổi từ classWithSimpleMethods.GetString thành Action được coi là tồn tại vì danh sách tham số tương thích, nhưng chuyển đổi (nếu được cố gắng) không thành công tại thời điểm biên dịch. Do đó, một chuyển đổi ngầm không tồn tại để cả hai loại đại biểu và các cuộc gọi là mơ hồ.
zinglon

@zinglon: Làm cách nào để bạn đọc §6.6 để xác định rằng một chuyển đổi từ ClassWithSimpleMethods.GetStringsang Actionlà hợp lệ? Để một phương thức Mtương thích với kiểu đại biểu D(§15.2) "tồn tại một chuyển đổi tham chiếu ngầm định hoặc chuyển đổi từ kiểu trả về Msang kiểu trả về của D."
jason

@Jason: Thông số kỹ thuật không nói rằng chuyển đổi là hợp lệ, nó nói rằng nó tồn tại . Trên thực tế, nó không hợp lệ vì nó không thành công tại thời điểm biên dịch. Hai điểm đầu tiên của §6.6 xác định liệu chuyển đổi có tồn tại hay không. Các điểm sau đây xác định liệu chuyển đổi có thành công hay không. Từ điểm 2: "Nếu không, thuật toán tạo ra một phương pháp tốt nhất M có cùng số tham số với D và chuyển đổi được coi là tồn tại." §15.2 được gọi trong điểm 3.
zinglon

1

Sử dụng Func<string>Action<string>(rõ ràng là rất khác với ActionFunc<string>) trong ClassWithDelegateMethodsxóa bỏ sự mơ hồ.

Sự không rõ ràng cũng xảy ra giữa ActionFunc<int>.

Tôi cũng gặp lỗi không rõ ràng với điều này:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

Thử nghiệm sâu hơn cho thấy rằng khi tự nó truyền vào một nhóm phương thức, kiểu trả về hoàn toàn bị bỏ qua khi xác định quá tải nào sẽ sử dụng.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 

0

Quá tải với FuncActiontương tự (vì cả hai đều là đại biểu) cho

string Function() // Func<string>
{
}

void Function() // Action
{
}

Nếu bạn để ý, trình biên dịch không biết phải gọi cái nào vì chúng chỉ khác nhau ở kiểu trả về.


Tôi không nghĩ nó thực sự hoàn toàn như vậy - bởi vì bạn không thể chuyển đổi a Func<string>thành Action... và bạn không thể chuyển đổi nhóm phương thức chỉ bao gồm một phương thức trả về một chuỗi thành một Actiontrong hai.
Jon Skeet

2
Bạn không thể truyền một đại biểu không có tham số và trả về stringmột Action. Tôi không hiểu tại sao lại có sự mơ hồ.
jason

3
@dtb: Có, việc loại bỏ quá tải sẽ loại bỏ vấn đề - nhưng điều đó không thực sự giải thích tại sao có vấn đề.
Jon Skeet
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.