【问题标题】:Is there a way of setting a property once only in C#有没有办法只在 C# 中设置一次属性
【发布时间】:2010-10-24 19:08:47
【问题描述】:

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

公共 OneShot SetOnceProperty { 获取;放; }

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

【问题讨论】:

  • 这对我来说很臭,对不起。为什么不将值传递给构造函数?另外,您是否要向调用者提供反馈,以便他们在设置值之前进行检查,以确保它没有被设置?
  • 理想情况下我会在构造函数中传递它,但我必须在一段时间内构建对象。例如,一条记录提供信息 A,下一条记录提供信息 B 和 C。一旦我有了一套完整的信息,我就可以使用这些信息将所有记录重新链接在一起。我想要一个运行时机制来验证我只设置了一次值,使它们成为伪只读!
  • 是的 - 我也闻到了!
  • 它闻起来只是因为你认为它闻起来。当我使用的框架调用它实例化的对象并且根本无法注入依赖项时,我需要这个。因此,我让我的组合根将容器推送到该对象上定义的一次性写入静态属性。

标签: c# .net


【解决方案1】:

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

【讨论】:

  • 我正忙着完善自己的答案,以至于我没有注意到您(实际上)写了同样该死的东西。我不再觉得自己很特别了。
  • 您是否介意添加它的直接支持版本(因为我们已经超过了 .NET 4.0)。我似乎找不到有关此功能的任何信息。
  • @MarcGravell TPL 中究竟是什么实现了这一点? TaskCompletionSource&lt;T&gt;?
  • @ArturoTorresSánchez 在并行扩展框架的原始 CTP 中(在我写这篇文章时是“当前的”,IIRC),有一个 WriteOnce&lt;T&gt; 类型 - not 相关到数据流位。不确定这是否仍然存在或是否已进入 RTM:但是 - 它在撰写本文时已经存在。你仍然可以找到他们的踪迹——herehere
【解决方案2】:

您可以自己滚动(请参阅答案的末尾以获取更强大的线程安全实现并支持默认值)。

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... ;)
  • 现在我们将其设置为 true
  • 谢谢。我看到了,但随后也开始看到其他问题。我在答案的底部放了一个更“生产就绪”的版本。
  • throwIfGet(在第一个构造函数中)未在任何地方定义
  • Ouch on 锁定属性获取
【解决方案3】:

这可以通过摆弄标志来完成:

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

然后使用任一构造函数。

【讨论】:

  • 如果我尝试设置两次,我希望出现运行时错误。否则我会创建一个设置只读成员变量的构造函数
【解决方案4】:

C# 9 内置了这个特性。它被称为Init only setters

public DateTime RecordedAt { get; init; }

【讨论】:

  • 这不太一样,因为它只允许在初始化时设置值并且不会创建异常,而是编译错误
【解决方案5】:

C# 中没有这样的功能(从 3.5 开始)。您必须自己编写代码。

【讨论】:

  • 微软应该提供这样的功能。
  • 我不确定。如果某些东西应该只设置一次,那么它可能应该是一个构造函数参数。
【解决方案6】:

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

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

【讨论】:

    【解决方案7】:

    你考虑过只读吗? http://en.csharp-online.net/const,_static_and_readonly

    它只能在初始化期间设置,但可能是您正在寻找的。​​p>

    【讨论】:

      【解决方案8】:

      这是我的看法:

      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 建议 TPL 提供此功能,我认为 TaskCompletionSource&lt;T&gt; 可能是他的意思,但我不能确定。

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

      • TaskCompletionSource&lt;T&gt; 是官方支持的 MS 类,它简化了实现。
      • 您可以选择同步或异步获取值。
      • 此类的实例将隐式转换为它存储的值的类型。当您需要传递值时,这可以稍微整理您的代码。

      【讨论】:

        【解决方案9】:
        /// <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;
            }
        }
        

        【讨论】:

          【解决方案10】:
          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 的引用来设置。

          【讨论】:

            【解决方案11】:

            答案假设对象在将来接收到对象的引用不会尝试更改它。如果您想防止这种情况发生,您需要使您的一次性编写代码仅适用于实现 ICloneable 或原语的类型。例如,String 类型实现了 ICloneable。那么您将返回数据的克隆或原始数据的新实例,而不是实际数据。

            仅用于基元的泛型: T GetObject where T: struct;

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

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

            【讨论】:

              【解决方案12】:

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

              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,尽管它仍然很乏味并且可能是一种昂贵的策略。

              【讨论】:

                【解决方案13】:

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

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

                【讨论】:

                  【解决方案14】:
                      public string Name
                      {
                          get => _name;
                          set
                          {
                              //if value already set, raise alarm
                              if (_name != "") throw new InvalidOperationException("Only set once!");
                              _name = value;
                          }
                      }
                  

                  【讨论】:

                  • 这个问题已经有近 13 年的历史了,已经有 14 个现有的答案,其中一个已经被接受,还有几个超过 10 票。您是否完全确定现有答案尚未涵盖这一点?如果是这样,请edit您的回答清楚地解释为什么这是一个更好的方法。
                  【解决方案15】:

                  您可以这样做,但不是一个明确的解决方案,代码可读性不是最好的。 如果你正在做代码设计,你可以看看singleton 实现与AOP 到intercept 设置器。实现只有 123 :)

                  【讨论】:

                  • 这就是为什么仅链接的答案是题外话... intercept 链接已死:-(
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-07-13
                  • 1970-01-01
                  • 2021-08-14
                  • 2023-03-09
                  • 2021-02-15
                  • 1970-01-01
                  相关资源
                  最近更新 更多