2010-02-04 14 views
5

Così tutti ci rendiamo conto dei vantaggi dei tipi immutabili, in particolare negli scenari multithread. (O almeno dovremmo tutti rendercene conto; vedere ad esempio System.String.)Design della struttura a classe immutabile

Tuttavia, quello che non ho visto è molto discussione per creazione di istanze immutabili, in particolare le linee guida di progettazione.

Per esempio, supponiamo di voler avere la seguente classe immutabile:

class ParagraphStyle { 
    public TextAlignment Alignment {get;} 
    public float FirstLineHeadIndent {get;} 
    // ... 
} 

L'approccio più comune che ho visto è quello di avere mutevole/immutabili "coppie" di tipi, per esempio il mutabile List<T> e immutable ReadOnlyCollection<T> tipi o il mutabile StringBuilder e immutable String tipi.

di imitare questo modello esistente richiederebbe l'introduzione di un certo tipo di "mutabile" ParagraphStyle tipo che "duplicati" i membri (a fornire incastonatori), e quindi fornire un costruttore ParagraphStyle che accetta il tipo mutabile come argomento

// Option 1: 
class ParagraphStyleCreator { 
    public TextAlignment {get; set;} 
    public float FirstLineIndent {get; set;} 
    // ... 
} 

class ParagraphStyle { 
    // ... as before... 
    public ParagraphStyle (ParagraphStyleCreator value) {...} 
} 

// Usage: 
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator { 
    TextAlignment = ..., 
    FirstLineIndent = ..., 
}); 

Quindi, questo funziona, supporta il completamento del codice all'interno dell'IDE e rende le cose ragionevolmente ovvie su come costruire le cose ... ma sembra piuttosto duplicato.

C'è un modo migliore?

Ad esempio, C# tipi anonimi sono immutabili, e permettono l'utilizzo di "normali" setter di proprietà per l'inizializzazione:

var anonymousTypeInstance = new { 
    Foo = "string", 
    Bar = 42; 
}; 
anonymousTypeInstance.Foo = "another-value"; // compiler error 

Purtroppo, la strada più vicina per duplicare questa semantica in C# è quello di utilizzare parametri del costruttore:

// Option 2: 
class ParagraphStyle { 
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent, 
      /* ... */) {...} 
} 

Ma questo non "scala" bene; se il tuo tipo ha, ad es. 15 proprietà, un costruttore con 15 parametri è tutt'altro che amichevole e fornire sovraccarichi "utili" per tutte e 15 le proprietà è una ricetta per un incubo. Sto respingendo questo a titolo definitivo.

Se cerchiamo di imitare i tipi anonimi, sembra che potremmo usare "set-una volta" le proprietà nel tipo "immutabili", e quindi rilasciare la variante "mutabile":

// Option 3: 
class ParagraphStyle { 
    bool alignmentSet; 
    TextAlignment alignment; 

    public TextAlignment Alignment { 
     get {return alignment;} 
     set { 
      if (alignmentSet) throw new InvalidOperationException(); 
      alignment = value; 
      alignmentSet = true; 
     } 
    } 
    // ... 
} 

Il problema con questo è che non è ovvio che le proprietà possano essere impostate una sola volta (il compilatore certamente non si lamenterà) e l'inizializzazione non è thread-safe. Diventa quindi tentato di aggiungere un metodo Commit() in modo che l'oggetto possa sapere che lo sviluppatore ha finito di impostare le proprietà (causando così tutte le proprietà che non sono state precedentemente impostate per il lancio se il loro setter è invocato), ma questo sembra essere un modo per peggiorare le cose, non meglio.

Esiste un design migliore rispetto alla divisione di classe mutevole/immutabile? O sono condannato a gestire la duplicazione dei membri?

+0

Mi rendo conto che potrebbe essere difficile da credere, ma sto lavorando con una base di codice C# esistente. Portare l'intera cosa a F # non è un'opzione, e anche se fosse un'opzione la licenza MSR-SSLA che le librerie F # sono rilasciate sotto non sembra appropriato per il mio uso, in particolare (ii) (c). – jonp

+0

Questo sembra un duplicato di: http://stackoverflow.com/questions/263585/immutable-object-pattern-in-c-what-do-you-think – jonp

+0

Potrebbe non essere una risposta, ma ho affrontato requisiti simili in passato implementando ISupportInitialize. Dopo una chiamata a EndInit() diventi immutabile e lancia quando vengono chiamati i setter. Non è sicuro in fase di compilazione, ma la documentazione e il lancio precoce aiutano spesso. – Will

risposta

2

In un paio di progetti stavo usando un approccio fluente. Cioè le proprietà più generiche (ad esempio nome, posizione, titolo) sono definite tramite ctor mentre altre sono aggiornate con i metodi Set che restituiscono una nuova istanza immutabile.

class ParagraphStyle { 
    public TextAlignment Alignment {get; private set;} 
    public float FirstLineHeadIndent {get; private set;} 
    // ... 
    public ParagraphStyle WithAlignment(TextAlignment ta) { 
     var newStyle = (ParagraphStyle)MemberwiseClone(); 
     newStyle.TextAlignment = ta; 
    } 
    // ... 
} 

MemberwiseClone è ok qui non appena la nostra classe è veramente profondamente immutabile.

+1

Sebbene funzioni e sia utile, anche questa non è una soluzione scalabile. Se è necessario impostare ad es. 15 valori, i metodi del mutatore WithFoo() determinano la creazione di 14 istanze "temporanee" prima di ottenere il risultato finale. A seconda di quanto "pesante" è il tuo tipo, questa potrebbe essere una notevole quantità di spazzatura. WithFoo() inoltre non supporta gli inizializzatori di raccolta, che potrebbero non essere una perdita enorme ma li amo teneramente ;-). L'idioma di Foo + FooBuilder sembra essere un ragionevole compromesso. – jonp

+0

Ho provato questo approccio un'altra volta in uno dei progetti recenti e sono completamente d'accordo, è noioso creare tutti questi wrapper. Sono d'accordo che Builder è molto più elegante e praticamente leggibile. A proposito di strutture profondamente annidate? Le strutture di dati FP promuovono il minor impatto possibile sulla memoria riutilizzando membri immutati. – olegz

+0

È anche possibile fornire un costruttore del generatore fluente che accetta l'oggetto immutabile come argomento per inizializzare i campi interni con esso. Quindi, puoi usare 'WithFoo()' solo su quei 'Foos' che devi modificare :) –

Problemi correlati