我有一个构造函数,可以在这样的开关上执行初始化:
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
的不变性问题。也许'如何执行复杂的,可能未知的初始化'现在是一个更好的问题标题。我现在完全明白,拥有一个巨大的开关是一个坏主意。工厂模式对我正在做的事情肯定很有吸引力,我想我回答了我的问题。我只想再给它几天。
非常感谢您对目前的想法!我将离开最初的示例代码以保持答案有意义。
答案 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)
switch语句根本就错了!
这是一种方法:(基类) http://simpleprogrammer.com/2012/02/21/refactoring-switches-to-classes/