Dlaczego nawiasy konstruktora inicjatora obiektu języka C # 3.0 są opcjonalne?

114

Wydaje się, że składnia inicjatora obiektu C # 3.0 pozwala wykluczyć parę nawiasów otwierających / zamykających w konstruktorze, gdy istnieje konstruktor bez parametrów. Przykład:

var x = new XTypeName { PropA = value, PropB = value };

W przeciwieństwie do:

var x = new XTypeName() { PropA = value, PropB = value };

Jestem ciekawy, dlaczego para nawiasów otwierających / zamykających konstruktora jest tutaj opcjonalna XTypeName?

James Dunne
źródło
9
Na marginesie, znaleźliśmy to podczas przeglądu kodu w zeszłym tygodniu: var list = new List <Foo> {}; Jeśli coś może być nadużywane ...
blu
@blu To jeden z powodów, dla których chciałem zadać to pytanie. Zauważyłem niespójność w naszym kodzie. Niespójność generalnie przeszkadza mi, więc pomyślałem, że sprawdzę, czy za opcjonalnością w składni kryje się dobre uzasadnienie. :)
James Dunne,

Odpowiedzi:

143

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: frobjest 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 xjest 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.

Eric Lippert
źródło
3
O, przepraszam, zapomniałem ... Podejście oparte na sygnale nietoperza, choć wydaje się działać, jest preferowane niż (IMO) bezpośredni sposób kontaktu, dzięki któremu nie można uzyskać publicznego ujawnienia pożądanego dla publicznej edukacji w postaci postu SO, który jest indeksowalny, przeszukiwalny i łatwy do odniesienia. Czy zamiast tego powinniśmy skontaktować się bezpośrednio w celu choreografii inscenizowanego tańca typu post / odpowiedź SO? :)
James Dunne,
5
Radziłbym unikać publikowania postów. To nie byłoby sprawiedliwe dla innych, którzy mogą mieć dodatkowy wgląd w to pytanie. Lepszym podejściem byłoby opublikowanie pytania, a następnie wysłanie e-maila z linkiem do niego z prośbą o udział.
chilltemp,
1
@James: Zaktualizowałem moją odpowiedź, aby odpowiedzieć na Twoje pytanie uzupełniające.
Eric Lippert,
8
@Eric, czy możesz pisać na blogu o liście „nigdy tego nie rób”? Jestem ciekawy innych przykładów, które nigdy nie będą częścią języka C # :)
Ilya Ryzhenkov
2
@Eric: Naprawdę, naprawdę doceniam twoją cierpliwość do mnie :) Dzięki! Bardzo informujące.
James Dunne,
12

Ponieważ tak określono język. Nie dodają żadnej wartości, więc po co je uwzględniać?

Jest również bardzo podobny do tablic o typie implicity

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

Źródła: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
źródło
1
Nie dodają żadnej wartości w tym sensie, że powinno być oczywiste, jaki jest zamiar, ale łamie to spójność, ponieważ mamy teraz dwie odmienne składnie konstrukcji obiektów, jedną z wymaganymi nawiasami (i wyrażeniami argumentów rozdzielanymi przecinkami), a drugą bez .
James Dunne,
1
@James Dunne, w rzeczywistości jest to bardzo podobna składnia do niejawnie wpisanej składni tablicy, zobacz moją edycję. Nie ma typu, nie ma konstruktora, a zamiar jest oczywisty, więc nie ma potrzeby go deklarować
CaffGeek
7

Zrobiono to w celu uproszczenia konstrukcji obiektów. Projektanci języka nie powiedzieli (według mojej wiedzy) konkretnie, dlaczego uważają, że jest to przydatne, chociaż jest to wyraźnie wymienione na stronie specyfikacji C # w wersji 3.0 :

Wyrażenie tworzenia obiektu może pomijać listę argumentów konstruktora i otaczające nawiasy, pod warunkiem, że zawiera inicjator obiektu lub kolekcji. Pominięcie listy argumentów konstruktora i zamknięcie nawiasów jest równoznaczne z określeniem pustej listy argumentów.

Przypuszczam, że w tym przypadku uważali, że nawiasy nie są konieczne, aby pokazać zamiary programisty, ponieważ inicjator obiektu pokazuje zamiar skonstruowania i ustawienia właściwości obiektu.

Reed Copsey
źródło
4

W pierwszym przykładzie kompilator wnioskuje, że wywołujesz domyślny konstruktor (specyfikacja języka C # 3.0 stwierdza, że ​​jeśli nie podano nawiasów, wywoływany jest konstruktor domyślny).

W drugim jawnie wywołujesz domyślny konstruktor.

Możesz również użyć tej składni do ustawiania właściwości podczas jawnego przekazywania wartości do konstruktora. Gdybyś miał następującą definicję klasy:

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

Wszystkie trzy stwierdzenia są ważne:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};
Justin Niessner
źródło
Dobrze. W pierwszym i drugim przypadku w twoim przykładzie są one funkcjonalnie identyczne, prawda?
James Dunne,
1
@James Dunne - poprawnie. To jest część określona w specyfikacji języka. Puste nawiasy są zbędne, ale nadal możesz je podać.
Justin Niessner,
1

Nie jestem Ericem Lippertem, więc nie mogę powiedzieć na pewno, ale zakładam, że dzieje się tak, ponieważ kompilator nie potrzebuje pustego nawiasu, aby wywnioskować konstrukcję inicjalizacyjną. Dlatego informacje stają się zbędne i niepotrzebne.

Josh
źródło
Racja, to jest zbędne, ale jestem po prostu ciekawy, dlaczego nagłe wprowadzenie ich jest opcjonalne? Wydaje się, że zrywa ze spójnością składni języka. Gdybym nie miał otwartego nawiasu klamrowego do wskazania bloku inicjalizatora, to powinna być to niedozwolona składnia. Zabawne, że wspomniał pan Lippert, że publicznie szukałem jego odpowiedzi, żeby ja i inni mogli skorzystać z bezczynnej ciekawości. :)
James Dunne,