在构造函数外部设置只读字段的可接受方法

时间:2013-10-10 05:58:48

标签: c# constructor switch-statement readonly

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

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的不变性问题。也许'如何执行复杂的,可能未知的初始化'现在是一个更好的问题标题。我现在完全明白,拥有一个巨大的开关是一个坏主意。工厂模式对我正在做的事情肯定很有吸引力,我想我回答了我的问题。我只想再给它几天。

非常感谢您对目前的想法!我将离开最初的示例代码以保持答案有意义。

9 个答案:

答案 0 :(得分:7)

您可以在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);
    }
}

答案 1 :(得分:5)

您可以使用auto-properties

public int Bar { get; private set; }。您已经将Bar大写,如果它是属性的话。其他课程可以获得Bar,但由于其Bar设置者,只有您的课程才能设置private set;

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

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

答案 2 :(得分:2)

如果有任何根本性的错误,没有更多的信息很难说,但我看起来并不完全错误(显示事实)。我会做每个案例我自己的方法或可能有自己的对象(取决于形式内容)。当然,您无法使用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();
}

答案 3 :(得分:2)

我宁愿选择Nahum的答案作为SOLID原则之一。如果你想扩展作为一部分的行为,开放/封闭原则将无法通过Switch语句实现。另一个要回答的是如何解决这个问题。这可以通过继承方法和Factory方法(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.

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

答案 4 :(得分:1)

也许我会忽略这一点,但您如何看待:

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

答案 5 :(得分:1)

我认为托马斯的方法是最简单的,并且维护了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: 
        }
    }
}

答案 6 :(得分:0)

跟进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。)

答案 7 :(得分:0)

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

  • 你想在施工期间改变和改变价值观。特别是,您希望在构造值时使用常规封装和代码重用技术,例如普通旧方法调用。
  • 构建完成后,您希望修复该值。

结构基本上只是一堆价值观;因此,它们很容易在构建过程中对突变进行突变和封装。然而,因为它们只是一个值,所以它们使用容器提供的任何存储信息。特别是,一旦将struct(value)存储在readonly字段中,该值就不能被变异(在构造函数之外)。如果struct本身存储在只读字段中,即使struct自己的方法也不能改变非readonly字段。

例如(可在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()(根据结构是否在可变或不可变的上下文中改变行为)这样的行为可能是微妙和令人惊讶的。为了保持理智,保持结构简单:避免方法和属性,并且永远不要改变成员中结构的内容。

答案 8 :(得分:-2)