有没有一种方法只能在C#中设置一次属性


74

我正在寻找一种方法,以允许仅将C#对象中的属性设置一次。编写代码很容易做到这一点,但是如果存在的话,我宁愿使用一种标准的机制。

公共OneShot <int> SetOnceProperty {get; 组; }

我想发生的是,如果尚未设置属性,则可以设置该属性,但是如果之前已设置,则抛出异常。它的功能应类似于Nullable值,在这里我可以检查它是否已设置。


对我来说这很难闻。为什么不将值传递给构造函数?另外,您是否要提供反馈给呼叫者,以便他们可以在设置值之前进行检查,以确保未设置该值?
Binary Worrier

7
理想情况下,我会在构造函数中传递它,但是我必须在一段时间内建立对象。例如,一条记录提供信息A,下一条记录提供信息B和C。一旦我有了一套完整的信息,我便使用此信息将所有记录链接回去。我想要一个运行时机制来验证我只设置一次值,使它们psuedo只读!
尼克·兰德尔

是的-我也闻起来!
尼克·兰德尔

3
它闻起来只是因为您认为它闻到了。当我正在使用的框架调用到一个实例化的对象中时,我需要此方法,并且根本没有办法注入依赖项。因此,我让合成根将容器推送到对该对象定义的一次写入静态属性。
罗尼·欧弗比

Answers:


58

在.NET 4.0中的TPL中对此有直接支持。

(编辑:以上句子是在预期System.Threading.WriteOnce<T>当时存在的“预览”位中存在该句子的情况下编写的,但这似乎在TPL达到RTM / GA之前就已经消失了)

在那之前,请自己检查一下。根据我的回忆,这行并不多...

就像是:

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

然后使用,例如:

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

2
我忙于完善自己的答案,以至于没有注意到您(实际上)写了同样荒唐的话。我不再觉得特别了。
迈克尔·梅多斯

28
您是否介意添加此功能的直接支持版本(因为我们已经超越了.NET 4.0)。我似乎找不到有关此功能的任何信息。
Destrictor 2013年


3
@MarcGravell TPL中的确切功能是什么?TaskCompletionSource<T>
罗尼·欧弗比

1
@ArturoTorresSánchez在并行扩展框架(这是“当前”当我写这个,IIRC)的原CTP,有一个WriteOnce<T>类型-相关的数据流位。不知道这是否仍然存在或是否存在于RTM中:但是-在撰写本文时它已经存在。你仍然可以找到它们的踪迹-在这里在这里
马克Gravell

31

您可以自己动手(有关可靠的线程安全且支持默认值的更健壮的实现,请参见答案的结尾)。

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

您可以这样使用它:

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

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

下面更强大的实现

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

仅当您设置值时才未将this.set设置为true;;)
Tomas Aschan 09年

我们现在就去做,将其设置为true
JoshBerke,2009年

谢谢。我看到了,但随后也开始看到其他问题。我在答案的底部放了一个“生产准备就绪”的版本。
迈克尔·梅多斯

2
throwIfGet(在第一个构造函数中)未在任何地方定义
reggaeguitar 2014年

2
锁定财产获得的机会
Chris Marisic 2015年

11

可以通过摆弄flag来完成:

private OneShot<int> setOnce;
private bool setOnceSet;

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

        setOnce = value;
        setOnceSet = true;
    }
}

这不好,因为您可能会收到运行时错误。最好在编译时强制执行以下行为:

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

然后使用任何一个构造函数。


1
如果我尝试设置两次,我希望出现运行时错误。否则,我会创建一个设置只读成员变量的构造函数
Nick Randell,2009年

4

C#(从3.5版开始)没有此类功能。您必须自己编写代码。


1
Microsoft应该提供这种功能。
Syed Tayyab Ali 2009年

6
我不确定。如果某项仅应设置一次,则它可能应该是构造函数参数。
Vizu

4

正如Marc所说,在.Net中默认没有办法做到这一点,但是自己添加一个并不太困难。

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

然后,您可以将其用作特定属性的支持。


3

这是我的看法:

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的回答表明TPL提供了此功能,我认为这 TaskCompletionSource<T>可能就是他的意思,但我不确定。

我的解决方案的一些不错的属性:

  • TaskCompletionSource<T> 是官方支持的MS类,可简化实现。
  • 您可以选择同步或异步获取值。
  • 此类的实例将隐式转换为其存储的值的类型。当您需要传递值时,这可以整理一下代码。


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

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

注意,myFoo被声明为IFoo,但被实例化为Foo。

这意味着可以在初始化程序块内设置Bar,但不能通过以后对myFoo的引用来设置。


0

答案假定将来收到对象引用的对象将不会尝试对其进行更改。如果要防止这种情况,则需要使一次写入代码仅适用于实现ICloneable或原语的类型。例如,String类型实现ICloneable。那么您将返回数据的克隆或原语的新实例,而不是实际数据。

仅泛型适用于原语:T GetObject其中T:结构;

如果您知道获得对数据的引用的对象永远不会覆盖它,则不需要这样做。

另外,请考虑ReadOnlyCollection是否适用于您的应用程序。每当尝试对数据进行更改时,都会引发异常。


0

虽然公认的且评分最高的答案最直接地回答了这个(旧的)问题,但另一种策略是建立一个类层次结构,以便您可以通过父母以及新的属性来构造孩子:

public class CreatedAtPointA 
{
    public int ExamplePropOne { get; }
    public bool ExamplePropTwo { get; }

    public CreatedAtPointA(int examplePropOne, bool examplePropTwo)
    {
        ExamplePropOne = examplePropOne;
        ExamplePropTwo = examplePropTwo;
    }
}

public class CreatedAtPointB : CreatedAtPointA
{
    public string ExamplePropThree { get; }

    public CreatedAtPointB(CreatedAtPointA dataFromPointA, string examplePropThree) 
        : base(dataFromPointA.ExamplePropOne, dataFromPointA.ExamplePropTwo)
    {
        ExamplePropThree = examplePropThree;
    }
}

通过依赖构造函数,您可以在代码气味上喷洒一些Febreeze,尽管它仍然很繁琐并且可能是昂贵的策略。


0

我创建了一个类型,该类型允许在构造时设置一个值,然后此值只能设置/覆盖一次,否则将引发异常。

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

    public SetOnce(T init) =>
        this.value = init;

    public T Value
    {
        get => this.value;
        set
        {
            if (this.set) throw new AlreadySetException($"Not permitted to override {this.Value}.");
            this.set = true;
            this.value = value;
        }
    }

    public static implicit operator T(SetOnce<T> setOnce) =>
        setOnce.value;

    class AlreadySetException : Exception
    {
        public AlreadySetException(string message) : base(message){}
    }
}

0

C#9内置了此功能。它被称为Init only setters

public DateTime RecordedAt { get; init; }

这并不是真的一样,因为它只允许在初始化时设置值,并且不会创建异常,而是编译错误
Yegor Androsov

-2

您可以这样做,但不是一个明确的解决方案,并且代码可读性也不是最好的。如果您正在执行代码设计,则可以与AOP一起侦听单例实现以拦截设置器。实现只是123 :)


1
这就是为什么仅链接的答案不在主题范围之内...拦截链接已死:-(
t3chb0t
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.