2009-05-08 8 views
53

? Estoy buscando una manera de permitir que una propiedad en un objeto C# se establezca solo una vez. Es fácil escribir el código para hacer esto, pero prefiero usar un mecanismo estándar si existe.¿Hay alguna manera de establecer una propiedad una vez solo en C#

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

Lo que quiero que suceda es que la propiedad se puede establecer si no está ya establecido, pero una excepción si se ha puesto delante. Debería funcionar como un valor Nullable donde puedo verificar si se ha configurado o no.

+0

Esto huele a mí, lo siento. ¿Por qué no pasar el valor en un constructor? Además, ¿va a proporcionar retroalimentación a la persona que llama para que puedan verificar antes de establecer el valor, para asegurarse de que no se haya configurado? –

+6

Lo ideal sería pasarlo en el constructor, pero tengo que construir el objeto durante un período de tiempo. por ejemplo, un registro proporciona información A, el siguiente registro proporciona información B y C. Una vez que tengo un conjunto completo de información, entonces uso esta información para vincular todos los registros nuevamente. Quería un mecanismo de tiempo de ejecución para verificar que solo establecí valores una vez, ¡lo que hace que sean de solo lectura! –

+0

Y sí, ¡también huele a mí! –

Respuesta

43

Hay soporte directo para esto en el TPL en .NET 4.0; hasta entonces, sólo que el mismo compruebe ... es no muchas líneas, de lo que recuerdo ...

algo como:

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

A continuación, utilice, por ejemplo:

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

Estaba tan ocupada puliendo mi propia respuesta que no noté que escribiste (prácticamente) la misma maldita cosa. Ya no me siento tan especial. –

+27

¿Te importaría agregar la versión de soporte directo de esto (ya que ya estamos más allá de .NET 4.0). Parece que no puedo encontrar información sobre esta función. – Destrictor

+0

Es posible que Marc estuviera hablando de https://msdn.microsoft.com/ru-ru/library/hh194820(v=vs.110).aspx –

26

Puede rodar por su cuenta (consulte el final de la respuesta para obtener una implementación más robusta que sea segura para subprocesos y admita valores predeterminados).

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

Se puede utilizar de este modo:

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

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

aplicación más robusta a continuación

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 que no está estableciendo this.set en true cuando establece el valor ...;) –

+0

Ahí vamos ahora lo estamos configurando en true – JoshBerke

+0

Gracias. Vi eso, pero luego comencé a ver otros problemas también. Puse una versión más "lista para producción" en la parte inferior de la respuesta. –

3

No existe tal función en C# (a partir de 3,5). Tienes que codificarlo tú mismo.

+1

Microsoft debería proporcionar dicha característica. –

+5

No estoy seguro. Si algo debe establecerse solo una vez, entonces probablemente sea un parámetro constructor. – Vizu

11

Esto se puede hacer con cualquiera de tocar el violín con la bandera:

private OneShot<int> setOnce; 
private bool setOnceSet; 

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

     setOnce = value; 
     setOnceSet = true; 
    } 
} 

que no es bueno ya que potencialmente puede recibir un error de tiempo de ejecución. Es mucho mejor para hacer cumplir este comportamiento en tiempo de compilación:

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

}

y utilizar después constructor.

+1

Quiero que haya un error de tiempo de ejecución si intento establecerlo dos veces. De lo contrario, habría creado un constructor que establece una variable de miembro de solo lectura –

+0

+1 para establecer el valor en el constructor – Scoregraphic

4

Como Marc dijo que no hay forma de hacerlo por defecto en .Net, pero agregar uno no es demasiado difícil.

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

Puede usar esto como respaldo para su propiedad en particular.

0

Puede hacer esto pero no es una solución clara y la legibilidad del código no es la mejor. Si está haciendo un diseño de código, puede echar un vistazo a la realización de singleton en conjunto con AOP a intercept setters. La realización es 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 
    } 
} 

Observe que myFoo está declarado como IFoo, pero instanciado como un Foo.

Esto significa que Bar se puede establecer dentro del bloque de inicializador, pero no a través de una referencia posterior a myFoo.

0

Las respuestas asumen que los objetos que reciben una referencia a un objeto en el futuro no intentarán cambiarlo. Si desea protegerse de esto, debe hacer que su código de escritura única funcione para tipos que implementen ICloneable o que sean primitivos. el tipo String implementa ICloneable por ejemplo. luego devolvería un clon de los datos o una nueva instancia de la primitiva en lugar de los datos reales.

genéricos solo para primitivas: T GetObject donde T: struct;

Esto no es necesario si sabe que los objetos que obtienen una referencia a los datos nunca lo sobrescribirán.

Además, considere si ReadOnlyCollection funcionará para su aplicación. se lanza una excepción cada vez que se intenta un cambio en los datos.

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

Esta es mi opinión sobre esto:

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 sugiere el TPL proporciona esta funcionalidad y yo creo TaskCompletionSource<T> podría haber sido lo que quería decir, pero no puedo estar seguro.

Algunas propiedades agradables de mi solución:

  • TaskCompletionSource<T> es un soporte oficialmente clase MS que simplifica la implementación.
  • Puede elegir obtener el valor de forma sincrónica o asíncrona.
  • Una instancia de esta clase se convertirá implícitamente al tipo de valor que almacena. Esto puede arreglar tu código un poco cuando necesites pasar el valor.
Cuestiones relacionadas