To pytanie było tematem mojego bloga 20 września 2010 roku . Odpowiedzi Josha i Chada („nie dodają żadnej wartości, więc po co ich wymagać?” I „aby wyeliminować nadmiarowość”) są w zasadzie prawidłowe. Aby to nieco bardziej wyrazić:
Możliwość usunięcia listy argumentów jako części „większej funkcji” inicjatorów obiektów spełniła naszą poprzeczkę dla „słodkich” funkcji. Rozważaliśmy kilka kwestii:
- koszt projektu i specyfikacji był niski
- zamierzaliśmy gruntownie zmienić kod parsera, który i tak obsługuje tworzenie obiektów; Dodatkowy koszt opracowania opcjonalnego wykazu parametrów nie był duży w porównaniu z kosztem większej funkcji
- obciążenie testami było stosunkowo niewielkie w porównaniu z kosztem większej funkcji
- obciążenie dokumentacją było stosunkowo niewielkie w porównaniu ...
- oczekiwano, że obciążenie alimentacyjne będzie niewielkie; Nie przypominam sobie żadnych błędów zgłoszonych w tej funkcji od czasu jej wprowadzenia.
- funkcja ta nie stanowi od razu oczywistego zagrożenia dla przyszłych funkcji w tym obszarze. (Ostatnią rzeczą, jaką chcemy teraz zrobić, jest stworzenie taniej i łatwej funkcji, która znacznie utrudni wdrożenie bardziej atrakcyjnej funkcji w przyszłości).
- funkcja ta nie dodaje nowych niejasności do leksykalnej, gramatycznej czy semantycznej analizy języka. Nie stwarza żadnych problemów w przypadku analizy „częściowego programu” wykonywanej przez aparat „IntelliSense” środowiska IDE podczas pisania. I tak dalej.
- funkcja trafia w wspólny „słodki punkt” dla funkcji inicjalizacji większych obiektów; zwykle jeśli używasz inicjatora obiektu, dzieje się tak właśnie dlatego, że konstruktor obiektu nie pozwala na ustawienie żądanych właściwości. Bardzo często takie obiekty są po prostu „torbami własności”, które w pierwszej kolejności nie mają parametrów w ktor.
Dlaczego więc nie ustawiłeś również pustych nawiasów jako opcjonalnych w domyślnym wywołaniu konstruktora wyrażenia tworzenia obiektu, które nie ma inicjatora obiektu?
Spójrz jeszcze raz na powyższą listę kryteriów. Jednym z nich jest to, że zmiana nie wprowadza żadnej nowej dwuznaczności w analizie leksykalnej, gramatycznej czy semantycznej programu. Twój Proponowana zmiana ma wprowadzić analizy semantycznej dwuznaczności:
class P
{
class B
{
public class M { }
}
class C : B
{
new public void M(){}
}
static void Main()
{
new C().M(); // 1
new C.M(); // 2
}
}
Linia 1 tworzy nowe C, wywołuje domyślny konstruktor, a następnie wywołuje metodę wystąpienia M na nowym obiekcie. Linia 2 tworzy nową instancję BM i wywołuje jej domyślny konstruktor. Gdyby nawiasy w linii 1 były opcjonalne, to linia 2 byłaby niejednoznaczna. Musielibyśmy wtedy wymyślić regułę rozwiązującą niejednoznaczność; nie moglibyśmy uczynić tego błędem, ponieważ byłaby to przełomowa zmiana, która zmienia istniejący legalny program C # w uszkodzony program.
Dlatego reguła musiałaby być bardzo skomplikowana: zasadniczo nawiasy są opcjonalne tylko w przypadkach, w których nie wprowadzają niejednoznaczności. Musielibyśmy przeanalizować wszystkie możliwe przypadki, które wprowadzają niejasności, a następnie napisać kod w kompilatorze, aby je wykryć.
W tym świetle wróć i spójrz na wszystkie wymienione przeze mnie koszty. Ile z nich staje się teraz dużych? Skomplikowane reguły wiążą się z dużymi kosztami projektowania, specyfikacji, rozwoju, testowania i dokumentacji. Skomplikowane reguły znacznie częściej spowodują problemy z nieoczekiwanymi interakcjami z funkcjami w przyszłości.
Wszystko za co? Drobna korzyść dla klienta, która nie dodaje językowi nowej mocy reprezentacyjnej, ale dodaje szalone przypadki, które tylko czekają, by krzyknąć „złapać” na jakąś biedną, niczego nie podejrzewającą duszę, która wpadnie na niego. Takie funkcje są natychmiast usuwane i umieszczane na liście „nigdy tego nie rób”.
Jak określiłeś tę konkretną dwuznaczność?
To było od razu jasne; Jestem dość dobrze zaznajomiony z regułami języka C # dotyczącymi określania, kiedy oczekiwana jest kropkowana nazwa.
Rozważając nową funkcję, w jaki sposób określasz, czy powoduje ona jakąkolwiek niejednoznaczność? Ręcznie, na podstawie formalnego dowodu, na podstawie analizy maszynowej, co?
Wszystkie trzy. Przeważnie patrzymy tylko na specyfikację i makaron, tak jak zrobiłem powyżej. Na przykład załóżmy, że chcemy dodać nowy operator prefiksu do C # o nazwie „frob”:
x = frob 123 + 456;
(AKTUALIZACJA: frob
jest oczywiście await
; analiza tutaj jest zasadniczo analizą, którą przeszedł zespół projektowy podczas dodawania await
).
„frob” jest tutaj jak „nowy” lub „++” - pojawia się przed jakimś wyrażeniem. Wypracowywalibyśmy pożądany priorytet i łączność itd., A następnie zaczynalibyśmy zadawać pytania typu „a co, jeśli program ma już typ, pole, właściwość, zdarzenie, metodę, stałą lub lokalną o nazwie frob?” To natychmiast doprowadziłoby do takich przypadków, jak:
frob x = 10;
czy to oznacza "wykonaj operację frob na wyniku x = 10, czy utwórz zmienną typu frob o nazwie x i przypisz jej 10?" (Lub, jeśli frobbing tworzy zmienną, może to być przypisanie od 10 do frob x
. W końcu *x = 10;
analizuje i jest poprawne, jeśli tak x
jest int*
.)
G(frob + x)
Czy to oznacza „frob wynik jednoargumentowego operatora plus na x” lub „dodaj wyrażenie frob do x”?
I tak dalej. Aby rozwiązać te dwuznaczności, możemy wprowadzić heurystykę. Kiedy mówisz „var x = 10;” to jest niejednoznaczne; mogłoby to oznaczać „wywnioskować typ x” lub „x jest typu zmiennego”. Mamy więc heurystykę: najpierw próbujemy znaleźć typ o nazwie var i tylko jeśli takiego nie ma, wnioskujemy o typie x.
Lub możemy zmienić składnię, aby nie była niejednoznaczna. Kiedy projektowali C # 2.0, mieli następujący problem:
yield(x);
Czy to oznacza „yield x w iteratorze” czy „wywołanie metody yield z argumentem x?” Zmieniając go na
yield return(x);
teraz jest to jednoznaczne.
W przypadku opcjonalnych parenów w inicjatorze obiektu łatwo jest wnioskować o tym, czy są wprowadzone niejasności, czy nie, ponieważ liczba sytuacji, w których dopuszczalne jest wprowadzenie czegoś, co zaczyna się od {, jest bardzo mała . Zasadniczo tylko różne konteksty instrukcji, wyrażenia lambda, inicjatory tablic i to wszystko. Łatwo jest uzasadnić wszystkie przypadki i pokazać, że nie ma dwuznaczności. Upewnienie się, że IDE pozostaje wydajne, jest nieco trudniejsze, ale można to zrobić bez większych problemów.
Ten rodzaj majstrowania przy specyfikacji zwykle jest wystarczający. Jeśli jest to szczególnie trudna funkcja, wyciągamy cięższe narzędzia. Na przykład, podczas projektowania LINQ, jeden z kompilatorów i jeden z IDE, którzy obaj mają doświadczenie w teorii parsera, zbudowali sobie generator parsera, który mógł analizować gramatyki w poszukiwaniu niejednoznaczności, a następnie wprowadzał proponowane gramatyki C # do zrozumienia zapytań ; w ten sposób znaleziono wiele przypadków, w których zapytania były niejednoznaczne.
Lub, kiedy przeprowadzaliśmy zaawansowane wnioskowanie o typie na lambdach w C # 3.0, pisaliśmy nasze propozycje, a następnie wysyłaliśmy je przez staw do Microsoft Research w Cambridge, gdzie zespół językowy był wystarczająco dobry, aby opracować formalny dowód, że propozycja wnioskowania o typie była teoretycznie rozsądne.
Czy istnieją obecnie niejasności w języku C #?
Pewnie.
G(F<A, B>(0))
W C # 1 jest jasne, co to oznacza. To jest to samo co:
G( (F<A), (B>0) )
Oznacza to, że wywołuje G z dwoma argumentami, które są bools. W języku C # 2 może to oznaczać to, co oznaczało w języku C # 1, ale może również oznaczać „przekazanie 0 do ogólnej metody F, która przyjmuje parametry typu A i B, a następnie przekazanie wyniku F do G”. Dodaliśmy skomplikowaną heurystykę do parsera, która określa, który z dwóch przypadków prawdopodobnie miałeś na myśli.
Podobnie rzuty są niejednoznaczne nawet w C # 1.0:
G((T)-x)
Czy to „rzut -x na T” czy „odejmij x od T”? Znowu mamy heurystykę, która pozwala na dobre przypuszczenie.