9

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

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. 也许“如何执行复杂的、可能未知的初始化”现在是一个更好的问题标题。现在对我来说很明显,拥有一个巨大的开关是一个坏主意。工厂模式对于我正在做的事情当然很有趣,我认为回答了我的问题。我只想再给它几天。

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

4

9 回答 9

9

您可以将 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);
    }
}
于 2013-10-14T22:44:44.273 回答
6

您可以使用自动属性

public int Bar { get; private set; }. 你已经在资本化Bar,就像它是一个财产一样。其他类可以 get ,但由于它的设置器Bar,只有您的类可以设置。Barprivate set;

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

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

于 2013-10-10T06:11:44.820 回答
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();
}
于 2013-10-10T06:04:21.607 回答
2

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

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;
    }
}
于 2013-10-18T10:54:43.150 回答
2

我也宁愿接受 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 以这种方式工作,但我更强调这里提到的当前行为的这种行为,因为它访问其他适当的静态方法来完成它的工作)。

于 2013-10-20T19:13:29.013 回答
1

我认为 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: 
        }
    }
}
于 2013-10-20T22:51:54.333 回答
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。)

于 2013-10-18T16:46:07.020 回答
0

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

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

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

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

于 2014-07-29T11:24:01.193 回答