non è necessario assegnare parametri di tipo struct


9

Ho notato alcuni comportamenti bizzarri nel mio codice quando commentavo accidentalmente una riga in una funzione durante la revisione del codice. È stato molto difficile riprodurlo, ma qui descriverò un esempio simile.

Ho questa lezione di prova:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

non vi è alcuna assegnazione a e-mail in GetOutcui normalmente si genererebbe un errore:

Il parametro out "email" deve essere assegnato prima che il controllo lasci il metodo corrente

Tuttavia, se EmailAddress si trova in una struttura in un assieme separato, non viene creato alcun errore e tutto viene compilato correttamente.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Perché il compilatore non impone che Email debba essere assegnato? Perché questo codice viene compilato se la struttura viene creata in un assembly separato, ma non viene compilata se la struttura è definita nell'assembly esistente?


2
Se stai usando una classe devi "nuovo" un'istanza dell'oggetto. Non è richiesto per le strutture. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… (cerca questo testo su quella pagina in particolare: A differenza delle classi, le strutture possono essere istanziate senza usare il nuovo operato)
Dortimer

1
Non appena la struttura del tuo cane ottiene una variabile, questa non verrà compilata :)
André Sanson,

In questo esempio, con struct Dog{}, va tutto bene.
Henk Holterman,

2
@ johnny5 Quindi mostra l'esempio.
André Sanson,

1
OK, questo è interessante. Riprodotto con un'app console Core 3 e una libreria di classe standard.
Henk Holterman,

Risposte:


12

TLDR: questo è un bug noto di vecchia data. Ne ho scritto per la prima volta nel 2010:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

È innocuo e puoi tranquillamente ignorarlo e congratularti con te stesso per aver trovato un bug piuttosto oscuro.

Perché il compilatore non impone che Emaildebba essere definitivamente assegnato?

Oh, lo fa, in un modo. Ha solo un'idea sbagliata di quale condizione implica che la variabile sia definitivamente assegnata, come vedremo.

Perché questo codice viene compilato se la struttura viene creata in un assembly separato, ma non viene compilata se la struttura è definita nell'assembly esistente?

Questo è il nocciolo del bug. Il bug è una conseguenza dell'intersezione di come il compilatore C # esegue un controllo delle assegnazioni definito sulle strutture e di come il compilatore carica i metadati dalle librerie.

Considera questo:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, a questo punto cosa sappiamo? fè un alias per una variabile di tipo Foo, quindi l'archiviazione è già stata allocata ed è sicuramente almeno nello stato in cui è uscita dall'allocatore di archiviazione. Se il chiamante ha inserito un valore nella variabile, quel valore è presente.

Cosa ci serve? Richiediamo che fsiano definitivamente assegnati in qualsiasi punto in cui il controllo lascia Mnormalmente. Quindi ti aspetteresti qualcosa di simile:

void M(out Foo f)
{
  f = new Foo();
}

quale imposta f.xe f.yai loro valori predefiniti. Ma che dire di questo?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Anche questo dovrebbe andare bene. Ma, ed ecco il kicker, perché dobbiamo assegnare i valori predefiniti solo per farli saltare via un momento dopo? Il controllore di assegnazione definito di C # verifica se ogni campo è assegnato! Questo è legale:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

E perché non dovrebbe essere legale? È un tipo di valore. fè una variabile e contiene già un valore valido di tipo Foo, quindi impostiamo semplicemente i campi e abbiamo finito, giusto?

Giusto. Quindi qual è il bug?

Il bug che hai scoperto è: come un risparmio sui costi, il compilatore C # non carica i metadati per i campi privati ​​delle strutture che si trovano nelle librerie di riferimento . Quei metadati possono essere enormi , e rallenterebbe il compilatore per pochissime vittorie caricando tutto in memoria ogni volta.

E ora dovresti essere in grado di dedurre la causa del bug che hai trovato. Quando il compilatore verifica se il parametro out è definitivamente assegnato, confronta il numero di campi noti con il numero di campi definiti inizializzati e nel tuo caso conosce solo i campi pubblici zero perché i metadati del campo privato non sono stati caricati . Il compilatore conclude "zero campi obbligatori, zero campi inizializzati, siamo a posto".

Come ho detto, questo bug esiste da più di un decennio e persone come te lo riscoprono e lo segnalano di tanto in tanto. È innocuo ed è improbabile che venga risolto perché ripararlo ha un vantaggio quasi nullo ma un costo elevato per le prestazioni.

E ovviamente il bug non si ripropone per i campi privati ​​di strutture che sono nel codice sorgente nel progetto, perché ovviamente il compilatore ha già informazioni sui campi privati ​​a portata di mano.


@ johnny5: non dovresti ricevere errori. Vedi dotnetfiddle.net/ZEKiUk . Puoi pubblicare una semplice riproduzione?
Eric Lippert,

1
Grazie per il violino è stato perché ho definito xey come proprietà anziché membri
johnny 5

1
@ johnny5: se hai appena definito una normale proprietà di stile C # 1.0, dal punto di vista del controllo di assegnazione definito, questo è un metodo, non un campo. Se hai definito una proprietà automatica di stile C # 3.0+, il compilatore sa che esiste un campo privato che la supporta; le regole per l'assegnazione definitiva di quella cosa sono state modificate nel corso degli anni e non ricordo ora le regole esatte.
Eric Lippert,

Se usi System.TimeSpaninvece un , gli errori arrivano: error CS0269: Use of unassigned out parameter 'email'e error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. C'è solo un campo non statico di TimeSpan, vale a dire _ticks. È internalal suo assemblaggio mscorlib. Questa assemblea è speciale? Lo stesso System.DateTimevale e il suo campo èprivate
Jeppe Stig Nielsen il

@JeppeStigNielsen: non so che succede! Se lo capisci, fammi sapere.
Eric Lippert,

1

Anche se sembra un bug, ha un senso.

L '"errore mancante" appare solo quando si utilizza una libreria di classi. E una libreria di classi potrebbe essere stata scritta in un altro linguaggio .net, ad esempio VB.Net. Il "tracciamento definito delle assegnazioni" è una caratteristica di C #, non del framework.

Quindi nel complesso non penso che sia un bug ma non conosco una dichiarazione autorevole per questo.


Se stai importando un assembly in C #, anche se la struttura potrebbe trovarsi in un assembly scritto in un'altra lingua, il codice che lo utilizza è ancora in c #, quindi non dovrebbe usare un tracciamento di assegnazione definito.
johnny, 5

1
Non necessariamente. C # non ti permetterà di usare una variabile unitaria (locale) ma allo stesso tempo il framework garantisce che sarà impostato su 0 ( default(T)). Quindi non vi è alcuna violazione della sicurezza della memoria o qualcosa di simile.
Henk Holterman,

3
Posso fare una dichiarazione così autorevole. :) È un bug noto di vecchia data.
Eric Lippert,

1
@EricLippert Grazie, speravo che l'avresti visto ad un certo punto
johnny 5
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.