【问题标题】:Acceptable way to set readonly field outside of a constructor在构造函数之外设置只读字段的可接受方法
【发布时间】:2014-07-29 19:24:01
【问题描述】:

我有一个像这样在开关上执行初始化的构造函数:

class Foo {
    public readonly int Bar; 
    public readonly object Baz; 

    public Foo(int bar, string baz) { 
        this.Bar = bar; 
        switch (bar) { 
        case 1: 
            // Boatload of initialization code
            this.Bar = /* value based upon initialization code */
            this.Baz = /* different value based upon initialization code */
        case 2:
            // Different boatload of initialization code
            this.Bar = /* value based upon initialization code */
            this.Baz = /* different value based upon initialization code */
        case 3: 
            // Yet another...
            this.Bar = /* value based upon initialization code */
            this.Baz = /* different value based upon initialization code */ 
        default: 
            // handle unexpected value 
        } 
    }
}

我仍在实现这一点,但一旦完成,它很容易变成几百行。我不喜欢有这么大的构造函数,但我不知道如何安全地绕过这个语言特性(而且绕过是我不想做的事情)。也许应该是暗示我正在尝试做的事情存在根本性错误,但我不确定。

基本上,我想在我自己的自定义不可变类型中执行复杂的初始化。最好的方法是什么?在这种情况下,庞大的行数构造函数是一件可怕的事情吗?

更新: 只是为了澄清,我想要做的是在一个类中保持不变性,该类将以可能的最佳方式以复杂的方式初始化实例。我正在编写一个代表随机生成的令牌FormatToken 的类,它通常是一个字符。

复杂的初始化是解析一个格式字符串(注意,我不是试图解析一个正则表达式来生成一个随机字符串,我不想在接下来的 20 辈子中这样做:) )。我最初是在写一些可以通过构造函数参数接受输入的东西,例如

+        /// Format tokens
+        /// c{l} Lowercase Roman character in the ASCII range. 
+        /// v{L} Uppercase Roman character in the ASCII range. 
+        /// c Roman character in the ASCII range.
+        /// d Decimal.
+        /// d{0-9} Decimal with optional range, both minimum and maximum inclusive.    

var rand = new RandomString("c{l}C{L}ddd{0-4}d{5-9}"); 
rand.Value == /* could equal "fz8318" or "dP8945", but not "f92781". 

最终产生这个问题的类是代表这些标记中的每一个的类。初始化问题来自能够支持各种格式(ASCII字符、罗马字母、小数、符号等)

这是有问题的实际代码:

internal class FormatToken {
    public TokenType Specifier { get; private set; }  
    public object Parameter { get; private set; }  

    public FormatToken(TokenType _specifier, string _parameter) { 
        // discussion of this constructor at 
        // http://stackoverflow.com/questions/19288131/acceptable-way-to-set-readonly-field-outside-of-a-constructor/
        Specifier = _specifier; 
        _init(_specifier, _parameter); 
    }

    private void _init(TokenType _specifier, string _parameter) { 
        switch (_specifier) { 
        case TokenType.Decimal:
            _initDecimalToken(_parameter); 
            break;
        case TokenType.Literal:
            Parameter = _parameter; 
            break; 
        case TokenType.Roman:
        case TokenType.LowerRoman:
        case TokenType.UpperRoman:
            _initRomanToken(_specifier, _parameter); 
            break;
        default: 
            throw new ArgumentOutOfRangeException("Unexpected value of TokenType."); 
        }
    }

我最初使用readonly 是因为我误解了使用它的原因。只需删除 readonly 并替换为自动属性(即 { get; private set; } 即可解决我的不变性问题。

这个问题更多的是关于初始化任务的问题,而不是关于FormatToken 的不变性的问题。也许“如何执行复杂的、可能未知的初始化”现在是一个更好的问题标题。现在对我来说很明显,拥有一个巨大的开关是一个坏主意。工厂模式对于我正在做的事情当然很有趣,我认为回答了我的问题。我只想再给它几天。

非常感谢您到目前为止的想法!我将最初的示例代码留在此处,以使答案有意义。

【问题讨论】:

  • Readonly 表示只读...您将无法设置它。但是为什么不为您的财产使用公共 getter 和私人 setter 呢?您还可以使用将 out 参数设置为您的属性的方法:Init(out this.bar)
  • 严格来说这不是真的,只读值可以在构造函数中初始化。
  • 我们在这里谈论什么样的初始化?只需设置默认值即可通过属性公开。
  • 最简单的选择是将解析和验证移到单独的方法中,我是小型构造函数的粉丝。我仍然会担心构造函数中的逻辑过多可能会影响性能。也许您可以通过限制输入选项来最小化所需的逻辑,例如对 bar 等使用枚举。如果没有此类使用的更具体示例,就很难说。
  • @codemonkeh:我同意将事情转移到一个单独的方法中 - 但在得到证明之前我不会担心性能。如果工作需要完成,就必须完成 - 在构造函数中比在其他任何地方都没有更多的性能负担。

标签: c# constructor switch-statement readonly


【解决方案1】:

您可以将 Foo 类的静态工厂方法与私有构造函数结合使用。工厂方法应该负责进行大开关,找出所需的 Bar 和 Baz 值,然后简单地将计算值传递给私有构造函数。

当然,这并没有摆脱巨大的开关,但它把它完全移出了构造函数,在构造函数中我们通常被告知进行大型计算是不好的。

这样你最终会得到类似的东西

class Foo {
    public readonly int Bar; 
    public readonly object Baz; 

    private Foo(int bar, string baz) { 
        this.Bar = bar; 
        this.Bas = baz;
    }

    public static Foo CreateFoo(int bar, string baz)
    {
        int tbar;
        string tbaz;
        switch (bar) { 
        case 1: 
            // Boatload of initialization code
            tbar = /* value based upon initialization code */
            tbaz = /* different value based upon initialization code */
        case 2:
            // Different boatload of initialization code
            tbar = /* value based upon initialization code */
            tbaz = /* different value based upon initialization code */
        //...
        default: 
            // handle unexpected value 
        }
        return new Foo(tbar, tbaz);
    }
}

【讨论】:

  • 特别是,您现在可以通过调用返回适当的Foo 引用的其他静态方法来拆分CreateFoo
【解决方案2】:

你可以使用auto-properties:

public int Bar { get; private set; }。你已经在大写Bar,就像它是一个属性一样。其他类可以获取Bar,但只有您的类可以设置Bar,因为它的private set; 设置器。

但是,您可以为每个对象多次设置Bar 的值。

如果按照构造函数 Micha 的方式 (https://stackoverflow.com/a/19288211/303939),可以在方法中设置自动属性(但不能使用 readonly)。

【讨论】:

  • 换句话说,通过不公开一个公共的mutator接口来保持不变性?当我进行一些更改时,我会尽快回复您...
  • 如果您使用只读的唯一原因是因为您不了解自动属性,请务必使用它们。但是请注意,如果这是一个问题,使用只读字段会对性能和安全性产生很好的影响。此外,只读字段保证该字段在对象的生命周期内不会更改(这是可变性的定义),而私有 set 属性仅保证更改将在类内部发生。
【解决方案3】:

如果没有更多信息,很难判断是否存在根本性错误,但我看起来并不完全错误(根据所显示的事实)。我会用自己的方法或自己的对象(取决于表单内容)来处理每种情况。当然,你不能使用readonly,而是使用public int Bar { get; private set; }public object Baz { get; private set; } 的属性。

public Foo(int bar, string baz) { 
     this.Bar = bar; 
     switch (bar) { 
        case 1: 
            methodFoo();
        case 2:
            methodBar();
        case 3: 
            methodFooBar();
        default: 
            ExceptionHandling();
}

【讨论】:

  • methodFoo()methodBar() 等方法无法更改只读值,因为它们不是构造函数。
  • @jdphenix:是的,没错。几分钟前我编辑了我的问题。是否可以接受readonly 是您的决定。我不知道您使用readonly 背后的意图。
【解决方案4】:

也许我没抓住重点,但你怎么看:

class Foo
{
    public readonly int Bar;
    public readonly object Baz;

    public Foo(int bar, string baz) { 
        this.Bar = GetInitBar(bar); 
    }

    private int GetInitBar(int bar)
    {
        int result;
         switch (bar) { 
            case 1: 
                // Boatload of initialization code
                result = /* value based upon initialization code */
                result = /* different value based upon initialization code */
            case 2:
                // Different boatload of initialization code
                result = /* value based upon initialization code */
                result = /* different value based upon initialization code */
            case 3: 
                // Yet another...
                result = /* value based upon initialization code */
                result = /* different value based upon initialization code */ 
            default: 
                // handle unexpected value 
        }
        return result;
    }
}

【讨论】:

  • 这正是我必须在构造函数中以复杂方式初始化值时所采用的方法。仅仅因为你的构造函数是“void”并不意味着其他所有的 init 函数都必须是!
【解决方案5】:

我也宁愿接受 Nahum 的回答,因为如果您想扩展作为一部分的行为,那么使用 Switch 语句将无法实现打开/关闭原则之一。要回答的另一部分是如何解决这个问题。这可以通过继承方法和通过工厂方法 (http://en.wikipedia.org/wiki/Factory_method_pattern) 创建适当的实例并对成员进行延迟初始化 (http://en.wikipedia.org/wiki/Lazy_initialization) 来完成。

    class FooFactory
    {
        static Foo CreateFoo(int bar,string baz)
        {
              if(baz == "a")
                  return new Foo1(bar,baz);
              else if(baz == "b")
                  return new Foo2(bar,baz);
              ........
        }
    }

    abstract class Foo
    {
          public int bar{get;protected set;}
          public string baz{get;protected set;}
          //this method will be overriden by all the derived class to do
          //the initialization
          abstract void Initialize();
    }

让 Foo1 和 Foo2 从 Foo 派生并重写 Initialize 方法以提供适当的实现。由于我们需要先初始化 Foo 中的其他方法才能工作,我们可以在 Initalize 方法中将 bool 变量设置为 true ,在其他方法中我们可以检查该值是否设置为 true 否则我们可以抛出指示对象的异常需要通过调用 Initialize 方法进行初始化。

现在客户端代码将如下所示。

   Foo obj = FooFactory.CreateFoo(1,"a");
   obj.Initialize();
   //now we can do any operation with Foo object.

如果我们在类中使用静态方法会出现一个问题,如果需要,这些方法无法访问实例成员。所以这是我们可以将它作为工厂方法分离出来而不是同一个类中的静态方法来创建实例的地方(但是,虽然 Singleton 以这种方式工作,但我更强调这里提到的当前行为的这种行为,因为它访问其他适当的静态方法来完成它的工作)。

【讨论】:

    【解决方案6】:

    我认为 Thomas 的方法是最简单的,并且维护了 jdphenix 已有的构造函数 API。

    另一种方法是使用Lazy 将设置实际推迟到使用值的时间。我喜欢在构造函数不是非常简单的时候使用Lazy,因为 1) 从未使用过的变量的设置逻辑永远不会执行,并且 2) 它确保创建对象永远不会慢得令人惊讶。

    在这种情况下,我认为设置逻辑并不复杂或缓慢,好处 1 随着类变得越来越大和越来越复杂,确实很明显。

    class Foo {
        public readonly Lazy<int> Bar; 
        public readonly Lazy<object> Baz; 
    
        public Foo(int bar, string baz) { 
            this.Bar = new Lazy<int>(() => this.InitBar(bar));
            this.Baz = new Lazy<object>(() => this.InitBaz(bar));
        }
    
        private int InitBar(int bar)
        {
            switch (bar) { 
            case 1: 
                // Bar for case 1
            case 2:
                // Bar for case 2
            case 3: 
                // etc..
            default: 
            }
        }
    
        private object InitBaz(int bar)
        {
            switch (bar) { 
            case 1: 
                // Baz for case 1
            case 2:
                // Baz for case 2
            case 3: 
                // etc..
            default: 
            }
        }
    }
    

    【讨论】:

      【解决方案7】:

      跟进 rasmusgreve 和 Jon Skeet:

      class Foo
      {
        public readonly int Bar; 
        public readonly object Baz; 
      
        private Foo(int bar, string baz) { 
            this.Bar = bar; 
            this.Baz = baz;
        }
      
        private static Foo _initDecimalToken(string _parameter)
        {
          int calculatedint = 0;
          string calculatedstring = _parameter;
          //do calculations
          return new Foo(calculatedint, calculatedstring);
        }
        private static Foo _initRomanToken(int bar, string _parameter)
        {
          int calculatedint = 0;
          string calculatedstring = _parameter;
          //do calculations
          return new Foo(calculatedint, calculatedstring);
        }
        public static Foo CreateFoo(int bar, string baz)
        {
          switch (bar) 
          { 
            case 1:
              return _initDecimalToken(baz);
            case 2:
              return _initRomanToken(bar, baz);
            default: 
              // handle unexpected value...
              return null;
          }
        }
      }
      

      如果您想保持 Foo 的轻量级,您可以将静态构造函数放入一个单独的类中(例如 FooMaker。)

      【讨论】:

        【解决方案8】:

        您可以考虑使用存储可变结构的只读字段。为什么?让我们把它归结为要点:

        • 您希望在构造过程中对值进行变异和洗牌。特别是,您希望在构造值时使用普通封装和代码重用技术,例如普通的旧方法调用。
        • 构建完成后,您希望固定值。

        结构本质上只是一袋值;因此它们很容易在构造过程中允许突变和封装该突变。但是,由于它们只是一个值,因此它们使用容器提供的任何存储语义。特别是,一旦将结构(值)存储在只读字段中,该值就不能被改变(在构造函数之外)。如果结构本身存储在只读字段中,即使结构自己的方法也不能改变非只读字段。

        例如(在 LINQpad 中可粘贴):

        void Main() {
            MyImmutable o = new MyImmutable(new MyMutable { Message = "hello!", A = 2});
            Console.WriteLine(o.Value.A);//prints 3
            o.Value.IncrementA();        //compiles & runs, but mutates a copy
            Console.WriteLine(o.Value.A);//prints 3 (prints 4 when Value isn't readonly)
            //o.Value.B = 42;            //this would cause a compiler error.
            //Consume(ref o.Value.B);    //this also causes a compiler error.
        }
        struct MyMutable {
            public string Message;
            public int A, B, C, D;
            //avoid mutating members such as the following:
            public void IncrementA() { A++; } //safe, valid, but really confusing...
        }
        class MyImmutable{
            public readonly MyMutable Value;
            public MyImmutable(MyMutable val) {
                this.Value=val;
                Value.IncrementA();
            }
        }
        void Consume(ref int variable){}
        

        这种技术的优点是您可以拥有大量字段和很好地分解的变异逻辑,但一旦完成,就可以轻松地修复该值。它还使复制和细微变化的复制变得非常容易:

        var v2 = o.Value;
        v2.D = 42;
        var d = new MyImmutable(v2);
        

        缺点是 C# 可变结构不常见,有时甚至令人惊讶。如果您的初始化逻辑变得复杂,您将使用具有复制语义的参数和返回值,这完全不同,您可能会意外引入错误。特别是像IncrementA() 这样的行为(根据结构是在可变上下文中还是在不可变上下文中改变行为)可能是微妙而令人惊讶的。为了保持理智,保持结构简单:避免方法和属性,并且永远不要改变成员中结构的内容。

        【讨论】:

          【解决方案9】:

          【讨论】:

          • 你刚刚给了我一个顿悟。随着时间的推移,switch case 的数量肯定会增长。我也会回来陪你的。
          • 我看不到该链接如何解释 switch 语句总是不好的。正确使用它们可以简单的条件代码,尤其是当被切换的enum 包含按位标志时。
          • switch case is bad 是 tumb 规则,在 99% 的情况下 switch 意味着你编写了糟糕的代码。除了像你提到的基本转换代码和其他一些类似的情况切换情况是邪恶的!
          • @NahumLitvin 最近的一次经历告诉我,你所说的背后的基本思想是合理的。您想进一步扩展它吗?
          • @im 在国外,只有我的手机。不幸的是,很快就会消失。虽然我有很多话要说
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-04-07
          • 1970-01-01
          • 2013-10-21
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多