Po co dodawanie metody dodawałoby niejednoznaczne wywołanie, jeśli nie byłoby to związane z niejednoznacznością

112

Mam tę klasę

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] something)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }
}

Jeśli nazywam to tak:

        var blah = new Overloaded();
        blah.ComplexOverloadResolution("Which wins?");

Pisze Normal Winnerdo konsoli.

Ale jeśli dodam inną metodę:

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }

Otrzymuję następujący błąd:

Wywołanie jest niejednoznaczne między następującymi metodami lub właściwościami:> „ Overloaded.ComplexOverloadResolution(params string[])” i „ Overloaded.ComplexOverloadResolution<string>(string)

Rozumiem, że dodanie metody może wprowadzić niejednoznaczność wywołania, ale jest to niejednoznaczność między dwiema już istniejącymi metodami (params string[])i <string>(string)! Oczywiście żadna z dwóch metod związanych z niejednoznacznością nie jest nowo dodaną metodą, ponieważ pierwsza to params, a druga to rodzajowa.

Czy to błąd? Jaka część specyfikacji mówi, że tak powinno być?

McKay
źródło
2
Myślę, że nie 'Overloaded.ComplexOverloadResolution(string)'odnosi się do <string>(string)metody; Myślę, że odnosi się to do (string, object)metody bez dostarczonego obiektu.
phoog
1
@phoog Och, te dane zostały wycięte przez StackOverflow, ponieważ jest to tag, ale komunikat o błędzie ma desygnator szablonu. Dodam to z powrotem.
McKay,
złapałeś mnie! W mojej odpowiedzi zacytowałem odpowiednie sekcje specyfikacji, ale nie spędziłem ostatnich pół godziny na czytaniu i rozumieniu!
phoog
@phoog, przeglądając te części specyfikacji, nie widzę nic o wprowadzaniu niejednoznaczności do metod innych niż sama i inna metoda, a nie dwie inne metody.
McKay,
Przyszło mi do głowy, że to tylko kamień-papier-nożyce : każdy zestaw dwóch różnych wartości ma zwycięzcę, ale kompletny zestaw trzech wartości nie.
phoog

Odpowiedzi:

107

Czy to błąd?

Tak.

Gratulacje, znalazłeś błąd w rozwiązaniu problemu z przeciążeniem. Błąd odtwarza się w C # 4 i 5; nie odtwarza się w wersji „Roslyn” analizatora semantycznego. Poinformowałem zespół testowy C # 5 i mam nadzieję, że uda nam się to zbadać i rozwiązać przed ostatecznym wydaniem. (Jak zawsze, żadnych obietnic.)

Następuje prawidłowa analiza. Kandydaci to:

0: C(params string[]) in its normal form
1: C(params string[]) in its expanded form
2: C<string>(string) 
3: C(string, object) 

Kandydat zerowy oczywiście nie stringma zastosowania, ponieważ nie można go zamienić na string[]. To pozostawia trzy.

Z tych trzech musimy określić jedyną najlepszą metodę. Robimy to, dokonując porównań parami trzech pozostałych kandydatów. Są trzy takie pary. Wszystkie z nich mają identyczne listy parametrów po usunięciu pominiętych parametrów opcjonalnych, co oznacza, że ​​musimy przejść do zaawansowanej rundy rozstrzygającej, opisanej w sekcji 7.5.3.2 specyfikacji.

Co jest lepsze, 1 czy 2? Istotną kwestią rozstrzygającą jest to, że metoda ogólna jest zawsze gorsza niż metoda nieogólna. 2 jest gorsze niż 1. Zatem 2 nie może być zwycięzcą.

Co jest lepsze, 1 czy 3? Istotna rozstrzygająca kwestia to: metoda stosowana tylko w rozszerzonej formie jest zawsze gorsza niż metoda stosowana w jej normalnej formie. Zatem 1 jest gorszy niż 3. Zatem 1 nie może być zwycięzcą.

Co jest lepsze, 2 czy 3? Istotną kwestią rozstrzygającą jest to, że metoda ogólna jest zawsze gorsza niż metoda nieogólna. 2 jest gorsze niż 3. Zatem 2 nie może być zwycięzcą.

Aby zostać wybranym spośród wielu odpowiednich kandydatów, kandydat musi (1) być niepokonany, (2) pokonać co najmniej jednego innego kandydata oraz (3) być jedynym kandydatem, który ma dwie pierwsze właściwości. Kandydat trzeci nie został pokonany przez żadnego innego kandydata i pokonuje co najmniej jednego innego kandydata; jest to jedyny kandydat z tą własnością. Dlatego kandydat trzeci jest jedynym najlepszym kandydatem . Powinien wygrać.

Kompilator C # 4 nie tylko popełnia błąd, jak słusznie zauważyłeś, zgłasza dziwny komunikat o błędzie. To, że kompilator źle analizuje rozdzielczość przeciążenia, jest nieco zaskakujące. To, że wyświetla nieprawidłowy komunikat o błędzie, nie jest zaskakujące; heurystyka błędu „niejednoznacznej metody” zasadniczo wybiera dowolne dwie metody ze zbioru kandydatów, jeśli nie można określić najlepszej metody. Niezbyt dobrze jest znaleźć „prawdziwą” dwuznaczność, jeśli w rzeczywistości taka istnieje.

Można rozsądnie zapytać, dlaczego tak jest. Trudno jest znaleźć dwie metody, które są „jednoznacznie niejednoznaczne”, ponieważ relacja „lepszości” jest nieprzechodnia . Można wymyślić sytuacje, w których kandydat 1 jest lepszy od 2, 2 jest lepszy od 3, a 3 jest lepszy od 1. W takich sytuacjach nie możemy zrobić nic lepszego niż wybranie dwóch z nich jako „niejednoznacznych”.

Chciałbym ulepszyć tę heurystykę dla Roslyn, ale ma ona niski priorytet.

(Ćwiczenie do czytelnika: „Opracowanie algorytmu czasu liniowego w celu zidentyfikowania unikalnego najlepszego członka zbioru n elementów, w którym relacja lepszości jest nieprzechodnia” było jednym z pytań, które zadano mi w dniu, w którym przeprowadzałem wywiad dla tego zespołu. bardzo trudny algorytm; daj mu szansę.)

Jednym z powodów, dla których odkładaliśmy dodawanie opcjonalnych argumentów do C # przez tak długi czas, była liczba złożonych niejednoznacznych sytuacji, które wprowadza do algorytmu rozwiązywania przeciążeń; najwyraźniej nie zrobiliśmy tego dobrze.

Jeśli chcesz zgłosić problem dotyczący połączenia, aby go śledzić, nie krępuj się. Jeśli chcesz tylko zwrócić na to naszą uwagę, pomyśl, że to zrobione. W przyszłym roku przejdę do testów.

Dziękuję za zwrócenie mi na to uwagi. Przepraszamy za błąd.

Eric Lippert
źródło
1
Dzięki za odpowiedzi. powiedziałeś "1 jest gorsze niż 2", ale wybiera metodę 1, jeśli mam tylko metodę 1 i 2?
McKay,
@ McKay: Ups, masz rację, powiedziałem to wstecz. Poprawię tekst.
Eric Lippert
1
Czuję się niezręcznie, czytając frazę „reszta roku”, biorąc pod uwagę, że nie zostało nawet pół tygodnia :)
BoltClock
2
@BoltClock rzeczywiście, stwierdzenie „wyjeżdżam na resztę roku” oznacza jeden dzień wolny.
phoog
1
Chyba tak. Przeczytałem „3) być jedynym kandydatem, który ma dwie pierwsze właściwości”, jako „być jedynym kandydatem, który (jest niepokonany i pokonuje co najmniej jednego innego kandydata)” . Ale twój najnowszy komentarz sprawia, że ​​myślę „(bądź jedynym kandydatem, który jest niepokonany) i pokonuje przynajmniej jednego innego kandydata” . Angielski naprawdę mógłby używać symboli grupowania. Jeśli to ostatnie jest prawdą, znowu to rozumiem.
default.kramer
5

Jaka część specyfikacji mówi, że tak powinno być?

Sekcja 7.5.3 (rozpoznawanie przeciążenia) wraz z sekcjami 7.4 (wyszukiwanie elementów) i 7.5.2 (wnioskowanie o typie).

Zwróć szczególną uwagę na sekcję 7.5.3.2 (lepsza składowa funkcji), w której po części stwierdza się, że „parametry opcjonalne bez odpowiadających im argumentów są usuwane z listy parametrów” oraz „Jeśli M (p) jest metodą inną niż generyczna amd M (q) jest metoda ogólna, wtedy M (p) jest lepsze niż M (q). "

Jednak nie rozumiem tych części specyfikacji wystarczająco dokładnie, aby wiedzieć, które części specyfikacji kontrolują to zachowanie, nie mówiąc już o ocenie, czy jest zgodne.

phoog
źródło
Ale to nie wyjaśnia, dlaczego dodanie członka powodowałoby niejednoznaczność między dwiema metodami, które już istniały.
McKay,
@McKay dość fair (patrz edycja). Będziemy musieli tylko poczekać, aż Eric Lippert powie nam, czy to jest prawidłowe zachowanie: ->
phoog
1
To są właściwe części specyfikacji, w porządku. Problem w tym, że mówią, że tak nie powinno być!
Eric Lippert
3

możesz uniknąć tej niejednoznaczności zmieniając nazwę pierwszego parametru w niektórych metodach i określając parametr, który chcesz przypisać

lubię to :

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] somethings)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Overloaded a = new Overloaded();
        a.ComplexOverloadResolution(something:"asd");
    }
}
MhdSyrwan
źródło
Och, wiem, że ten kod jest zły i istnieje kilka sposobów obejścia tego problemu, pytanie brzmiało: „dlaczego kompilator zachowuje się w ten sposób?”
McKay
1

Jeśli usuniesz paramsz pierwszej metody, to się nie stanie. Pierwsza i trzecia metoda mają oba prawidłowe wywołania ComplexOverloadResolution(string), ale jeśli jest pierwsza metoda, public void ComplexOverloadResolution(string[] something)nie będzie żadnych niejasności.

Podanie wartości dla parametru object somethingElse = nullsprawia, że ​​jest to parametr opcjonalny, a zatem nie trzeba go określać podczas wywoływania tego przeciążenia.

Edycja: kompilator robi tutaj szalone rzeczy. Jeśli przeniesiesz trzecią metodę w kodzie po pierwszej, zostanie ona prawidłowo zgłoszona. Wygląda więc na to, że bierze pierwsze dwa przeciążenia i zgłasza je, bez sprawdzania właściwego.

„ConsoleApplication1.Program.ComplexOverloadResolution (params string [])” i „ConsoleApplication1.Program.ComplexOverloadResolution (string, object)”

Edit2: nowe odkrycie. Usunięcie dowolnej metody z powyższych trzech nie spowoduje niejednoznaczności między nimi. Wygląda więc na to, że konflikt pojawia się tylko wtedy, gdy istnieją trzy metody, niezależnie od kolejności.

Tomislav Markovski
źródło
Ale to nie wyjaśnia, dlaczego dodanie członka powodowałoby niejednoznaczność między dwiema metodami, które już istniały.
McKay,
Niejednoznaczność występuje między twoją pierwszą a trzecią metodą, ale dlaczego kompilator zgłasza pozostałe dwie, jest poza mną.
Tomislav Markovski,
Ale jeśli usunę drugą metodę, nie mam dwuznaczności, pomyślnie wywołuje trzecią metodę. Więc nie wygląda na to, że kompilator ma niejednoznaczność między pierwszą a trzecią metodą.
McKay,
Zobacz moją edycję. Szalone kompilatory.
Tomislav Markovski,
W rzeczywistości dowolna kombinacja tylko dwóch metod nie powoduje żadnych dwuznaczności. To bardzo dziwne. Edycja 2.
Tomislav Markovski,
1
  1. Jeśli piszesz

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");

    lub po prostu napisz

    var blah = new Overloaded();
    blah.ComplexOverloadResolution();

    skończy się na tej samej metodzie , w metodzie

    public void ComplexOverloadResolution(params string[] something

    Jest to paramssłowo kluczowe przyczyna, które sprawia, że ​​jest ono najlepiej dopasowane również w przypadku, gdy nie określono parametru

  2. Jeśli spróbujesz dodać nową metodę, taką jak ta

    public void ComplexOverloadResolution(string something)
    {
        Console.WriteLine("Added Later");
    }

    Doskonale skompiluje i wywoła tę metodę, ponieważ idealnie pasuje do twojego wywołania z stringparametrem. Wtedy dużo silniejszy params string[] something.

  3. Deklarujesz drugą metodę, tak jak to zrobiłeś

    public void ComplexOverloadResolution(string something, object something=null);

    Kompilator, przeskakuje w całkowitym zamieszaniu między pierwszą metodą a tą, właśnie dodał jedną. Ponieważ nie wie, jaką funkcję powinien teraz wykonać w Twoim telefonie

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");

    W rzeczywistości, jeśli usuniesz parametr ciągu z wywołania, takiego jak następujący kod, wszystko kompiluje się poprawnie i działa jak wcześniej

    var blah = new Overloaded();
    blah.ComplexOverloadResolution(); // will be ComplexOverloadResolution(params string[] something) function called here, like a best match.
Tigran
źródło
Najpierw napisz dwa przypadki wywołań, które są takie same. Więc oczywiście będzie to dotyczyło tej samej metody, czy też chciałeś napisać coś innego?
McKay,
Ale znowu, jeśli dobrze rozumiem resztę twojej odpowiedzi, nie czytasz tego, co według kompilatora jest pomyłką, a mianowicie między pierwszą a drugą metodą, a nie nową trzecią, którą właśnie dodałem.
McKay,
ach, dzięki. Ale to wciąż pozostawia problem, o którym wspomniałem w moim drugim komentarzu do twojego postu.
McKay,
Żeby było jaśniej, napisałeś: „Kompilator, skacze w całkowitym zamieszaniu między pierwszą metodą a tą, właśnie dodaną”. Ale tak nie jest. Skacze w zamieszaniu do pozostałych dwóch metod. Metoda params i metoda ogólna.
McKay,
@ McKay: whell, to skok w zamieszaniu, mając dane statez trzech funkcji, a nie jednej lub kilku z nich, aby być dokładnym. W rzeczywistości wystarczy skomentować którekolwiek z nich, aby rozwiązać problem. Najlepszym dopasowaniem spośród dostępnych funkcji jest ta z params, druga to ta z genericsparametrem, kiedy dodamy trzecią, powoduje to zamieszanie w jednej setz funkcji. Myślę, że najprawdopodobniej nie jest to jasny komunikat o błędzie generowany przez kompilator.
Tigran