2013-10-10 4 views
7

У меня есть конструктор, который выполняет инициализацию на переключателе, как это:Приемлемый способ установить только для чтения поле вне конструктора

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

Я до сих пор осуществляет это, но когда-то сделали это легко будет несколько сотен линий. Я не поклонник создания такого конструктора, но я не понимаю, как либо безопасно обойти эту языковую функцию (и обходить вообще - это то, что я не хочу делать). Может быть, должно быть намек на то, что есть что-то принципиально неправильное с тем, что я пытаюсь сделать, но я не уверен.

В принципе, я хочу выполнить сложную инициализацию в своем собственном неизменяемом типе. Каков наилучший способ сделать это? Является ли gazillion конструктором строк ужасным в этом случае?

Обновление: Просто для уточнения, я хочу сохранить неизменность в классе, который бы инициализировал экземпляры сложным образом наилучшим образом Я пишу класс, который представляет случайное сгенерированное ken, 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

Readonly означает ТОЛЬКО для чтения ... вы не сможете его установить. Но почему бы не использовать публичный геттер и частный сеттер для вашей собственности? Вы также можете использовать метод с параметром out-parameter для вашего свойства: Init (out this.bar) – HimBromBeere

+2

Это не строго верно, значения readonly могут быть инициализированы в конструкторе. – codemonkeh

+1

Какую инициализацию мы говорим здесь? Просто установка значений по умолчанию может быть выставлена ​​со свойствами. – codemonkeh

ответ

-2
+0

Ты просто дал мне прозрение. Объем корпусов коммутаторов будет * определенно * расти с течением времени. Я также вернусь с тобой. – jdphenix

+4

Я не вижу, как эта ссылка объясняет, как операторы 'switch' всегда плохие. При правильном использовании они могут просто условный код, особенно когда переключатель «enum» включен побитовыми флагами. – codemonkeh

+1

Корпусы переключателей плохие - это правило ** tumb ** в 99% случаев, когда вы переключаете код, вы написали плохой код. кроме базового кода конвертации, как вы вняли, и некоторые другие случаи, связанные с случаями, являются злыми! – Nahum

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(); 
} 
+1

Методы, то есть 'methodFoo()', 'methodBar()' и т. Д., Не смогут изменять значения readonly, потому что они не являются конструктором. – jdphenix

+0

@jdphenix: Да, это правильно. Я редактирую свой вопрос несколько минут назад. Если потерять «readonly» приемлемо или нет, это ваше решение. Я не знаю, что касается вашего использования «readonly». – Micha

+0

Даунвитинг из-за? – Micha

5

Вы можете использовать auto-properties:

public int Bar { get; private set; }. Вы уже зачисляете Bar, если это свойство.Другие классы могут получить Bar, но только ваш класс может установить Bar из-за его установщика private set;.

Однако вы можете установить значение Bar несколько раз для каждого объекта.

Вы можете установить auto-properties в методах (но не можете использовать readonly), если вы сделаете путь конструктора Micha (https://stackoverflow.com/a/19288211/303939).

+0

Другими словами, сохранить неизменность, не подвергая открытый интерфейс мутатора? Я вернусь с вами, когда я сделаю пару изменений ... – jdphenix

+0

Если единственная причина, по которой вы используете readonly, - это то, что вы не знали об автоматических свойствах, определенно используете их. Однако обратите внимание, что использование полей readonly может иметь хорошее влияние на производительность и безопасность, если это вообще проблема. Кроме того, поля readonly гарантируют, что поле не изменится во время жизни объекта (которое является определением мутируемости), в то время как свойство private set гарантирует, что изменения произойдут внутри класса. – Gilthans

7

Вы можете использовать статический заводский метод в классе Foo в c ombination с помощью частного конструктора. Метод фабрики должен отвечать за то, чтобы ваш большой коммутатор определял требуемые значения 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

В частности, теперь вы можете разделить 'CreateFoo', вызывая другие статические методы, которые возвращают соответствующие ссылки Foo. –

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

Это именно тот подход, который я принимаю, когда мне нужно инициализировать значение в конструкторе в сложном виде. Просто потому, что ваш конструктор «void» не означает, что любая другая функция init должна быть! – flipchart

0

Вслед за rasmusgreve и Джон Скит:

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, вы можете поместить функции статической конструкции в отдельный класс (например, Fo oMaker.)

2

Я бы предпочел пойти с ответом Нахума, поскольку один из принципов открытого/закрытого принципа 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, мы можем иметь переменную bool, установленную в true в методе Initalize, а в других методах мы можем проверить, установлено ли это значение true, иначе мы можем вызвать исключение, указывающее объект необходимо инициализировать, вызвав метод Initialize.

Теперь код клиента будет выглядеть примерно так.

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

Проблема, которая будет иметь место, если мы используем статический метод внутри класса является то, что эти методы косяка членов экземпляров доступа, если это необходимо. Так вот где вместо статических методов внутри одного класса мы можем выделить его как метод Factory для создания экземпляра (но да, хотя Singleton работает таким образом, я больше подчеркиваю это поведение для текущего поведения, упомянутого здесь, потому что он обращается к другим соответствующие статические методы для выполнения своей работы).

1

Я думаю, что подход Томаса является самым простым и поддерживает API-интерфейс конструктора, который уже имеет jdphenix.

Альтернативный подход заключается в использовании 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: 
     } 
    } 
} 
0

Возможно, вы захотите использовать поле readonly, сохраняющее измененную структуру. Зачем? Давайте вскипятим это до необходимого уровня:

  • Вы хотите мутировать и перемешать вокруг значений во время строительства. В частности, вы хотите использовать обычные методы инкапсуляции и повторного использования кода, такие как простые старые вызовы методов при построении ваших значений.
  • После строительства вы хотите, чтобы значение было исправлено.

Структуры - по существу всего лишь мешок ценностей; поэтому они легко допускают мутацию и инкапсуляцию этой мутации во время строительства. Однако, поскольку они являются просто ценностью, они используют любую складскую семантику, которую предоставляет их контейнер. В частности, как только вы храните struct (value) в своем поле readonly, значение не может быть изменено (вне конструктора). Даже собственные методы структуры не могут мутировать нечитанные поля, если сама структура хранится в поле readonly.

Например (pastable в 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() (которое изменяет поведение в зависимости от того, находится ли структура в изменяемом или неизменяемом контексте) может быть тонким и неожиданным. Чтобы оставаться в здравом уме, храните простые структуры: избегайте методов и свойств и никогда не мутируйте содержимое структуры в члене.

Смежные вопросы