Perché le parentesi del costruttore dell'inizializzatore di oggetti C # 3.0 sono facoltative?


114

Sembra che la sintassi dell'inizializzatore di oggetti C # 3.0 consenta di escludere la coppia di parentesi aperta / chiusa nel costruttore quando esiste un costruttore senza parametri. Esempio:

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

Al contrario di:

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

Sono curioso di sapere perché la coppia di parentesi aperta / chiusa del costruttore è opzionale qui dopo XTypeName?


9
Per inciso, abbiamo trovato questo in una revisione del codice la scorsa settimana: var list = new List <Foo> {}; Se qualcosa può essere abusato ...
blu

@blu Questo è uno dei motivi per cui volevo fare questa domanda. Ho notato l'incongruenza nel nostro codice. L'incoerenza in generale mi dà fastidio, quindi ho pensato di vedere se ci fosse una buona motivazione dietro l'opzionalità nella sintassi. :)
James Dunne

Risposte:


143

Questa domanda è stata oggetto del mio blog il 20 settembre 2010 . Le risposte di Josh e Chad ("non aggiungono valore, quindi perché richiederle?" E "per eliminare la ridondanza") sono fondamentalmente corrette. Per arricchirlo un po 'di più:

La caratteristica di permettervi di elidere l'elenco degli argomenti come parte della "caratteristica più ampia" degli inizializzatori di oggetti ha incontrato la nostra barra per le caratteristiche "zuccherine". Alcuni punti che abbiamo considerato:

  • il costo di progettazione e specifica era basso
  • avremmo comunque modificato ampiamente il codice del parser che gestisce la creazione degli oggetti; il costo aggiuntivo di sviluppo per rendere opzionale l'elenco dei parametri non era elevato rispetto al costo della funzionalità più grande
  • l'onere del test era relativamente piccolo rispetto al costo della funzionalità più grande
  • l'onere della documentazione era relativamente piccolo rispetto ...
  • si prevedeva che l'onere della manutenzione fosse ridotto; Non ricordo alcun bug segnalato in questa funzione negli anni trascorsi dalla sua spedizione.
  • la funzionalità non pone rischi immediatamente evidenti per le funzionalità future in quest'area. (L'ultima cosa che vogliamo fare è creare una funzionalità facile ed economica ora che rende molto più difficile implementare una funzionalità più convincente in futuro.)
  • la funzionalità non aggiunge nuove ambiguità all'analisi lessicale, grammaticale o semantica della lingua. Non pone problemi per il tipo di analisi del "programma parziale" che viene eseguita dal motore "IntelliSense" dell'IDE durante la digitazione. E così via.
  • la caratteristica colpisce un comune "punto debole" per la caratteristica di inizializzazione di oggetti più grandi; tipicamente se stai usando un inizializzatore di oggetti è proprio perché il costruttore dell'oggetto non ti permette di impostare le proprietà che vuoi. È molto comune che tali oggetti siano semplicemente "borse di proprietà" che non hanno parametri nel ctor in primo luogo.

Perché allora non hai reso anche le parentesi vuote opzionali nella chiamata del costruttore predefinito di un'espressione di creazione di oggetti che non ha un inizializzatore di oggetto?

Dai un'altra occhiata all'elenco di criteri sopra. Uno di questi è che il cambiamento non introduce alcuna nuova ambiguità nell'analisi lessicale, grammaticale o semantica di un programma. La vostra proposta di modifica fa introdurre un'analisi ambiguità semantica:

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
    }
}

La riga 1 crea un nuovo C, chiama il costruttore predefinito e quindi chiama il metodo di istanza M sul nuovo oggetto. La riga 2 crea una nuova istanza di BM e chiama il suo costruttore predefinito.Se le parentesi sulla riga 1 fossero opzionali, la riga 2 sarebbe ambigua. Dovremmo quindi elaborare una regola che risolva l'ambiguità; non è stato possibile commettere un errore perché si tratterebbe di una modifica sostanziale che trasforma un programma C # legale esistente in un programma danneggiato.

Quindi la regola dovrebbe essere molto complicata: essenzialmente che le parentesi sono opzionali solo nei casi in cui non introducono ambiguità. Dovremmo analizzare tutti i possibili casi che introducono ambiguità e quindi scrivere codice nel compilatore per rilevarli.

In quella luce, torna indietro e guarda tutti i costi che ho menzionato. Quanti di loro ora diventano grandi? Le regole complicate hanno costi di progettazione, specifiche, sviluppo, test e documentazione elevati. È molto più probabile che le regole complicate causino problemi con interazioni impreviste con le funzionalità in futuro.

Tutto per cosa? Un piccolo vantaggio per il cliente che non aggiunge nuovo potere rappresentativo al linguaggio, ma aggiunge casi d'angolo pazzi che aspettano solo di gridare "gotcha" a qualche povera anima ignara che ci si imbatte. Funzionalità del genere vengono tagliate immediatamente e inserite nell'elenco "non farlo mai".

Come hai determinato quella particolare ambiguità?

Quello fu subito chiaro; Conosco abbastanza le regole in C # per determinare quando è previsto un nome puntato.

Quando si considera una nuova funzionalità, come si determina se causa ambiguità? A mano, con prove formali, con analisi automatiche, cosa?

Tutti e tre. Per lo più guardiamo solo le specifiche e la tagliatella, come ho fatto sopra. Ad esempio, supponiamo di voler aggiungere un nuovo operatore di prefisso a C # chiamato "frob":

x = frob 123 + 456;

(AGGIORNAMENTO: frobè ovviamente await; l'analisi qui è essenzialmente l'analisi che il team di progettazione ha eseguito durante l'aggiuntaawait .)

"frob" qui è come "nuovo" o "++" - viene prima di un'espressione di qualche tipo. Calcolavamo la precedenza e l'associatività desiderate e così via, quindi iniziavamo a fare domande come "e se il programma avesse già un tipo, un campo, una proprietà, un evento, un metodo, una costante o un locale chiamato frob?" Ciò porterebbe immediatamente a casi come:

frob x = 10;

significa "fai l'operazione frob sul risultato di x = 10, o crea una variabile di tipo frob chiamata x e assegnale 10?" (Oppure, se frobbing produce una variabile, potrebbe essere un assegnamento di 10 a frob x. Dopo tutto, *x = 10;analizza ed è legale se lo xè int*.)

G(frob + x)

Significa "frob il risultato dell'operatore più unario su x" o "aggiungi espressione frob a x"?

E così via. Per risolvere queste ambiguità potremmo introdurre l'euristica. Quando dici "var x = 10;" è ambiguo; potrebbe significare "dedurre il tipo di x" o potrebbe significare "x è di tipo var". Quindi abbiamo un'euristica: prima tentiamo di cercare un tipo chiamato var, e solo se non esiste deduciamo il tipo di x.

Oppure potremmo cambiare la sintassi in modo che non sia ambigua. Quando hanno progettato C # 2.0 hanno riscontrato questo problema:

yield(x);

Significa "restituire x in un iteratore" o "chiamare il metodo yield con l'argomento x?" Modificandolo in

yield return(x);

ora è inequivocabile.

Nel caso di parentesi opzionali in un inizializzatore di oggetto è semplice ragionare sul fatto che ci siano ambiguità introdotte o meno perché il numero di situazioni in cui è consentito introdurre qualcosa che inizia con {è molto piccolo . Fondamentalmente solo vari contesti di istruzioni, espressioni lambda di istruzioni, inizializzatori di array e questo è tutto. È facile ragionare su tutti i casi e dimostrare che non c'è ambiguità. Assicurarsi che l'IDE rimanga efficiente è un po 'più difficile ma può essere fatto senza troppi problemi.

Questo tipo di armeggiare con le specifiche di solito è sufficiente. Se è una funzionalità particolarmente complicata, estraiamo strumenti più pesanti. Ad esempio, durante la progettazione di LINQ, uno dei ragazzi del compilatore e uno dei ragazzi dell'IDE che hanno entrambi un background nella teoria del parser si sono costruiti un generatore di parser che potrebbe analizzare le grammatiche alla ricerca di ambiguità, e quindi ha alimentato le grammatiche C # proposte per la comprensione delle query in esso ; in questo modo sono stati riscontrati molti casi in cui le query erano ambigue.

Oppure, quando abbiamo eseguito l'inferenza di tipo avanzata su lambda in C # 3.0, abbiamo scritto le nostre proposte e poi le abbiamo inviate a Microsoft Research a Cambridge, dove il team linguistico era abbastanza bravo da elaborare una prova formale che la proposta di inferenza di tipo fosse teoricamente sano.

Ci sono ambiguità in C # oggi?

Sicuro.

G(F<A, B>(0))

In C # 1 è chiaro cosa significa. È lo stesso di:

G( (F<A), (B>0) )

Cioè, chiama G con due argomenti che sono bool. In C # 2, ciò potrebbe significare cosa significava in C # 1, ma potrebbe anche significare "passare 0 al metodo generico F che accetta i parametri di tipo A e B, quindi passare il risultato di F a G". Abbiamo aggiunto una complicata euristica al parser che determina quale dei due casi probabilmente intendevi.

Allo stesso modo, i cast sono ambigui anche in C # 1.0:

G((T)-x)

È "cast -x to T" o "sottract x from T"? Di nuovo, abbiamo un'euristica che fa una buona ipotesi.


3
Oh scusa, dimenticavo ... L'approccio del segnale di pipistrello, sebbene sembri funzionare, è preferito a (IMO) un mezzo di contatto diretto per cui non si otterrebbe l'esposizione pubblica richiesta per l'istruzione pubblica sotto forma di un post SO che è indicizzabile, ricercabile e facilmente consultabile. Dobbiamo invece contattare direttamente per coreografare un ballo post / risposta SO messo in scena? :)
James Dunne

5
Ti consiglio di evitare di mettere in scena un post. Non sarebbe giusto per gli altri che potrebbero avere ulteriori informazioni sulla domanda. Un approccio migliore sarebbe pubblicare la domanda, quindi inviare tramite e-mail un link ad essa chiedendo la partecipazione.
freddo

1
@ James: ho aggiornato la mia risposta per rispondere alla tua domanda di follow-up.
Eric Lippert

8
@ Eric, è possibile che tu possa scrivere un blog su questo elenco "non fare mai questo"? Sono curioso di vedere altri esempi che non faranno mai parte del linguaggio C # :)
Ilya Ryzhenkov

2
@ Eric: apprezzo davvero molto la tua pazienza con me :) Grazie! Molto informativo.
James Dunne

12

Perché è così che è stata specificata la lingua. Non aggiungono valore, quindi perché includerli?

È anche molto simile agli array tipizzati in modo implicito

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

Riferimento: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx


1
Non aggiungono valore nel senso che dovrebbe essere ovvio quale sia l'intento, ma rompe con coerenza in quanto ora abbiamo due diverse sintassi per la costruzione di oggetti, una con le parentesi richieste (e le espressioni di argomento delimitate da virgole) e una senza .
James Dunne

1
@ James Dunne, in realtà è una sintassi molto simile alla sintassi dell'array tipizzata in modo implicito, vedi la mia modifica. Non esiste un tipo, nessun costruttore e l'intento è ovvio, quindi non è necessario dichiararlo
CaffGeek

7

Questo è stato fatto per semplificare la costruzione degli oggetti. I progettisti del linguaggio non hanno (che io sappia) specificatamente detto perché ritengono che ciò sia utile, sebbene sia esplicitamente menzionato nella pagina delle specifiche della versione 3.0 di C # :

Un'espressione di creazione di oggetti può omettere l'elenco di argomenti del costruttore e racchiudere le parentesi, a condizione che includa un oggetto o un inizializzatore di raccolta. Omettere l'elenco di argomenti del costruttore e racchiudere le parentesi equivale a specificare un elenco di argomenti vuoto.

Suppongo che abbiano ritenuto che le parentesi, in questo caso, non fossero necessarie per mostrare l'intento dello sviluppatore, poiché l'inizializzatore dell'oggetto mostra invece l'intento di costruire e impostare le proprietà dell'oggetto.


4

Nel tuo primo esempio, il compilatore deduce che stai chiamando il costruttore predefinito (la specifica del linguaggio C # 3.0 afferma che se non vengono fornite parentesi, viene chiamato il costruttore predefinito).

Nel secondo, chiami esplicitamente il costruttore predefinito.

È inoltre possibile utilizzare tale sintassi per impostare le proprietà passando in modo esplicito i valori al costruttore. Se avessi la seguente definizione di classe:

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;
    }
}

Tutte e tre le affermazioni sono valide:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};

Destra. Nel primo e nel secondo caso nel tuo esempio sono funzionalmente identici, giusto?
James Dunne

1
@ James Dunne - Corretto. Questa è la parte specificata dalle specifiche della lingua. Le parentesi vuote sono ridondanti, ma puoi comunque fornirle.
Justin Niessner

1

Non sono Eric Lippert, quindi non posso dirlo con certezza, ma presumo che sia perché la parentesi vuota non è necessaria al compilatore per inferire il costrutto di inizializzazione. Pertanto diventa un'informazione ridondante e non necessaria.


Esatto, è ridondante, ma sono solo curioso di sapere perché l'improvvisa introduzione di loro sono opzionali? Sembra rompere con la coerenza della sintassi del linguaggio. Se non avevo la parentesi graffa aperta per indicare un blocco di inizializzazione, questa dovrebbe essere una sintassi illegale. Buffo che tu abbia menzionato il signor Lippert, stavo cercando pubblicamente la sua risposta in modo che io e gli altri potessimo trarre beneficio da una vaga curiosità. :)
James Dunne
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.