2009-05-08 15 views
53

Sto cercando un modo per consentire a una proprietà in un oggetto C# di essere impostata una sola volta. È facile scrivere il codice per farlo, ma preferirei usare un meccanismo standard se ne esiste uno.C'è un modo per impostare una proprietà una sola volta in C#

 
public OneShot<int> SetOnceProperty { get; set; } 

Quello che voglio che accada è che la proprietà può essere impostata, se non è già impostato, ma un'eccezione se è stato impostato prima. Dovrebbe funzionare come un valore Nullable in cui posso verificare se è stato impostato o meno.

+0

Questo mi odora, mi dispiace. Perché non passare il valore in un costruttore? Inoltre, hai intenzione di fornire il feed al chiamante in modo che possano controllare prima di impostare il valore, per assicurarsi che non sia stato impostato? –

+6

Idealmente lo passerei nel costruttore, ma devo costruire l'oggetto su per un periodo di tempo. ad esempio un record fornisce l'informazione A, il record successivo fornisce le informazioni B e C. Una volta che ho un set completo di informazioni, quindi uso queste informazioni per collegare tutti i record di nuovo insieme. Volevo un meccanismo di runtime per verificare di aver impostato i valori solo una volta, rendendoli psuedo in sola lettura! –

+0

E sì - puzza anche per me! –

risposta

43

Esiste un supporto diretto per questo in TPL in .NET 4.0; fino ad allora solo fare il verificare voi stessi ... non è molte linee, da quello che mi ricordo ...

qualcosa di simile:

public sealed class WriteOnce<T> 
{ 
    private T value; 
    private bool hasValue; 
    public override string ToString() 
    { 
     return hasValue ? Convert.ToString(value) : ""; 
    } 
    public T Value 
    { 
     get 
     { 
      if (!hasValue) throw new InvalidOperationException("Value not set"); 
      return value; 
     } 
     set 
     { 
      if (hasValue) throw new InvalidOperationException("Value already set"); 
      this.value = value; 
      this.hasValue = true; 
     } 
    } 
    public T ValueOrDefault { get { return value; } } 

    public static implicit operator T(WriteOnce<T> value) { return value.Value; } 
} 

Poi utilizzare, ad esempio:

readonly WriteOnce<string> name = new WriteOnce<string>(); 
public WriteOnce<string> Name { get { return name; } } 
+2

Ero così occupato a lucidare la mia risposta che non ho notato che hai scritto (in modo pratico) la stessa cosa dannata. Non mi sento più così speciale. –

+27

Ti dispiacerebbe aggiungere la versione di supporto diretto di questo (dato che siamo già passati a .NET 4.0). Non riesco a trovare alcuna informazione su questa funzione. – Destrictor

+0

È possibile che Marc stia parlando di https://msdn.microsoft.com/ru-ru/library/hh194820(v=vs.110).aspx –

26

È possibile eseguire il rollover (vedere la fine della risposta per un'implementazione più solida che sia thread-safe e supporti valori predefiniti).

public class SetOnce<T> 
{ 
    private bool set; 
    private T value; 

    public T Value 
    { 
     get { return value; } 
     set 
     { 
      if (set) throw new AlreadySetException(value); 
      set = true; 
      this.value = value; 
     } 
    } 

    public static implicit operator T(SetOnce<T> toConvert) 
    { 
     return toConvert.value; 
    } 
} 

Si può usare in questo modo:

public class Foo 
{ 
    private readonly SetOnce<int> toBeSetOnce = new SetOnce<int>(); 

    public int ToBeSetOnce 
    { 
     get { return toBeSetOnce; } 
     set { toBeSetOnce.Value = value; } 
    } 
} 

più robusta implementazione di sotto

public class SetOnce<T> 
{ 
    private readonly object syncLock = new object(); 
    private readonly bool throwIfNotSet; 
    private readonly string valueName; 
    private bool set; 
    private T value; 

    public SetOnce(string valueName) 
    { 
     this.valueName = valueName; 
     throwIfGet = true; 
    } 

    public SetOnce(string valueName, T defaultValue) 
    { 
     this.valueName = valueName; 
     value = defaultValue; 
    } 

    public T Value 
    { 
     get 
     { 
      lock (syncLock) 
      { 
       if (!set && throwIfNotSet) throw new ValueNotSetException(valueName); 
       return value; 
      } 
     } 
     set 
     { 
      lock (syncLock) 
      { 
       if (set) throw new AlreadySetException(valueName, value); 
       set = true; 
       this.value = value; 
      } 
     } 
    } 

    public static implicit operator T(SetOnce<T> toConvert) 
    { 
     return toConvert.value; 
    } 
} 


public class NamedValueException : InvalidOperationException 
{ 
    private readonly string valueName; 

    public NamedValueException(string valueName, string messageFormat) 
     : base(string.Format(messageFormat, valueName)) 
    { 
     this.valueName = valueName; 
    } 

    public string ValueName 
    { 
     get { return valueName; } 
    } 
} 

public class AlreadySetException : NamedValueException 
{ 
    private const string MESSAGE = "The value \"{0}\" has already been set."; 

    public AlreadySetException(string valueName) 
     : base(valueName, MESSAGE) 
    { 
    } 
} 

public class ValueNotSetException : NamedValueException 
{ 
    private const string MESSAGE = "The value \"{0}\" has not yet been set."; 

    public ValueNotSetException(string valueName) 
     : base(valueName, MESSAGE) 
    { 
    } 
} 
+0

Solo tu non stai impostando this.set su true quando imposti il ​​valore ...;) –

+0

Ora andiamo impostandolo su true – JoshBerke

+0

Grazie. L'ho visto, ma poi ho iniziato a vedere anche altri problemi. Ho messo una versione più "pronta per la produzione" nella parte inferiore della risposta. –

3

Nessuna tale funzione in C# (come di 3.5). Devi codificarlo da solo.

+1

Microsoft dovrebbe fornire tale funzionalità. –

+5

Non ne sono sicuro. Se qualcosa dovrebbe essere impostato solo una volta, allora probabilmente dovrebbe essere un parametro costruttore. – Vizu

11

questo può essere fatto sia con giocherellare con bandiera:

private OneShot<int> setOnce; 
private bool setOnceSet; 

public OneShot<int> SetOnce 
{ 
    get { return setOnce; } 
    set 
    { 
     if(setOnceSet) 
      throw new InvalidOperationException(); 

     setOnce = value; 
     setOnceSet = true; 
    } 
} 

che non è buono perché si può potenzialmente ricevere un errore di run-time. E 'molto meglio per far rispettare questo comportamento al momento della compilazione:

public class Foo 
{ 
    private readonly OneShot<int> setOnce;   

    public OneShot<int> SetOnce 
    { 
     get { return setOnce; } 
    } 

    public Foo() : 
     this(null) 
    { 
    } 

    public Foo(OneShot<int> setOnce) 
    { 
     this.setOnce = setOnce; 
    } 

}

e quindi utilizzare uno costruttore.

+1

Voglio che ci sia un errore di run time se provo ad impostarlo due volte. Altrimenti avrei creato un costruttore che imposta una variabile membro readlyly –

+0

+1 per l'impostazione del valore nel costruttore – Scoregraphic

4

Come ha detto Marc, non c'è modo di farlo in .Net ma aggiungerne uno non è troppo difficile.

public class SetOnceValue<T> { 
    private T m_value; 
    private bool m_isSet; 
    public bool IsSet { get { return m_isSet; }} 
    public T Value { get { 
    if (!IsSet) { 
     throw new InvalidOperationException("Value not set"); 
    } 
    return m_value; 
    } 
    public T ValueOrDefault { get { return m_isSet ? m_value : default(T); }} 
    public SetOnceValue() { } 
    public void SetValue(T value) { 
    if (IsSet) { 
     throw new InvalidOperationException("Already set"); 
    } 
    m_value = value; 
    m_isSet = true; 
    } 
} 

È quindi possibile utilizzare questo come il supporto per la proprietà particolare.

0

È possibile farlo ma non è una soluzione chiara e la leggibilità del codice non è la migliore. Se stai progettando il codice, puoi dare un'occhiata alla realizzazione di in tandem con AOP a intercept setter. La realizzazione è solo 123 :)

0
interface IFoo { 

    int Bar { get; } 
} 

class Foo : IFoo { 

    public int Bar { get; set; } 
} 

class Program { 

    public static void Main() { 

     IFoo myFoo = new Foo() { 
      Bar = 5 // valid 
     }; 

     int five = myFoo.Bar; // valid 

     myFoo.Bar = 6; // compilation error 
    } 
} 

Si noti che myFoo è dichiarato come IFoo, ma istanziato come Foo.

Ciò significa che la barra può essere impostata all'interno del blocco di inizializzazione, ma non tramite un successivo riferimento a myFoo.

0

Le risposte presuppongono che gli oggetti che ricevono un riferimento a un oggetto in futuro non cercheranno di cambiarlo. Se vuoi proteggerti da questo, devi fare in modo che il tuo codice di sola scrittura funzioni solo per i tipi che implementano ICloneable o sono primitivi. per esempio, il tipo String implementa ICloneable. quindi restituiresti un clone dei dati o una nuova istanza del primitivo invece dei dati effettivi.

Generici solo per primitive: T GetObject dove T: struct;

Questo non è necessario se si sa che gli oggetti che ottengono un riferimento ai dati non lo sovrascriveranno mai.

Inoltre, considerare se ReadOnlyCollection funzionerà per l'applicazione. viene generata un'eccezione ogni volta che viene tentata una modifica sui dati.

1
/// <summary> 
/// Wrapper for once inizialization 
/// </summary> 
public class WriteOnce<T> 
{ 
    private T _value; 
    private Int32 _hasValue; 

    public T Value 
    { 
     get { return _value; } 
     set 
     { 
      if (Interlocked.CompareExchange(ref _hasValue, 1, 0) == 0) 
       _value = value; 
      else 
       throw new Exception(String.Format("You can't inizialize class instance {0} twice", typeof(WriteOnce<T>))); 
     } 
    } 

    public WriteOnce(T defaultValue) 
    { 
     _value = defaultValue; 
    } 

    public static implicit operator T(WriteOnce<T> value) 
    { 
     return value.Value; 
    } 
} 
1

Ecco il mio prendere su questo:

public class ReadOnly<T> // or WriteOnce<T> or whatever name floats your boat 
{ 
    private readonly TaskCompletionSource<T> _tcs = new TaskCompletionSource<T>(); 

    public Task<T> ValueAsync => _tcs.Task; 
    public T Value => _tcs.Task.Result; 

    public bool TrySetInitialValue(T value) 
    { 
     try 
     { 
      _tcs.SetResult(value); 
      return true; 
     } 
     catch (InvalidOperationException) 
     { 
      return false; 
     } 
    } 

    public void SetInitialValue(T value) 
    { 
     if (!TrySetInitialValue(value)) 
      throw new InvalidOperationException("The value has already been set."); 
    } 

    public static implicit operator T(ReadOnly<T> readOnly) => readOnly.Value; 
    public static implicit operator Task<T>(ReadOnly<T> readOnly) => readOnly.ValueAsync; 
} 

Marc's answer suggerisce la TPL fornisce questa funzionalità e io penso TaskCompletionSource<T> avrebbe potuto essere quello che voleva dire, ma non posso essere sicuro.

Alcune belle proprietà della mia soluzione:

  • TaskCompletionSource<T> è un supporto ufficialmente classe di MS, che semplifica l'implementazione.
  • È possibile scegliere di ottenere il valore in modo sincrono o asincrono.
  • Un'istanza di questa classe verrà convertita implicitamente nel tipo di valore memorizzato. Questo può riordinare un po 'il tuo codice quando devi passare il valore.
Problemi correlati