5

Я пишу код в функциональном стиле на C#. Многие из моих классов неизменяемы с методами возврата измененной копии экземпляра.Универсальные классы неизмененного класса в C#

Например:

sealed class A 
{ 
    readonly X x; 
    readonly Y y; 

    public class A(X x, Y y) 
    { 
     this.x = x; 
     this.y = y; 
    } 

    public A SetX(X nextX) 
    { 
     return new A(nextX, y); 
    } 

    public A SetY(Y nextY) 
    { 
     return new A(x, nextY); 
    } 
} 

Это тривиальный пример, но представьте себе гораздо больший класс, со многими более членов.

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

Есть ли образец или техника, чтобы избежать всей этой котельной плиты при построении неизменяемых классов с помощью методов модификатора?

Примечание: я не хочу использовать struct для reasons discussed elsewhere on this site.

Обновление: с тех пор я обнаружил, что это выражение «копирование и обновление записи» в F #.

+3

Я бы поставил под сомнение необходимость иметь эти «сеттеры», поскольку вы написали их для всего. Действительно, вы должны выполнять сложные операции над типом, а не просто «устанавливать» значение произвольно. Для просто общей пары ему не нужно ничего, кроме конструктора. – Servy

+1

@Servy Извините, это ограничение примера. Большинство модификаторов более сложны или зависят от конкретной задачи. Пожалуйста, представьте, что я дал лучший пример :) – sdgfsdh

+3

Да. Используйте F #. Серьезно сейчас. C# - это, прежде всего, язык ООП с состоянием. Хотя у него есть некоторые функциональные возможности, это далеко не хороший функциональный язык. – Euphoric

ответ

8

Для больших типов устрою With функцию, которая имеет аргументы, которые все по умолчанию null, если не предусмотрены:

public sealed class A 
{ 
    public readonly X X; 
    public readonly Y Y; 

    public A(X x, Y y) 
    { 
     X = x; 
     Y = y; 
    } 

    public A With(X X = null, Y Y = null) => 
     new A(
      X ?? this.X, 
      Y ?? this.Y 
     ); 
} 

Затем используйте названные аргументы особенность C# таким образом:

val = val.With(X: x); 

val = val.With(Y: y); 

val = val.With(X: x, Y: y); 

I находят более привлекательный подход, чем множество методов setter. Это означает, что null становится непригодным для использования, но если вы идете по функциональному маршруту, я предполагаю, что вы также избегаете null и используете опции.

Если у вас есть ценностные типы/Структура, как члены затем сделать их Nullable в With, например:

public sealed class A 
{ 
    public readonly int X; 
    public readonly int Y; 

    public A(int x, int y) 
    { 
     X = x; 
     Y = y; 
    } 

    public A With(int? X = null, int? Y = null) => 
     new A(
      X ?? this.X, 
      Y ?? this.Y 
     ); 
} 

Однако обратите внимание, это не приходит бесплатно, есть N операции нуля сравнения в звоните в With, где N - это количество аргументов. Я лично считаю, что удобство стоит затрат (что в конечном счете незначительно), однако, если у вас есть что-то особенно чувствительное к производительности, вы должны вернуться к методам setpoke setter.

0

Вы можете использовать следующий шаблон (не знаю, если это пройдет, но вы просили меньше избыточной версии, в любом случае вы можете получить идею):

public class Base 
    { 
     public int x { get; protected set; } 
     public int y { get; protected set; } 

     /// <summary> 
     /// One constructor which set all properties 
     /// </summary> 
     /// <param name="x"></param> 
     /// <param name="y"></param> 
     public Base(int x, int y) 
     { 
      this.x = x; 
      this.y = y; 
     } 

     /// <summary> 
     /// Constructor which init porperties from other class 
     /// </summary> 
     /// <param name="baseClass"></param> 
     public Base(Base baseClass) : this(baseClass.x, baseClass.y) 
     { 
     } 

     /// <summary> 
     /// May be more secured constructor because you always can check input parameter for null 
     /// </summary> 
     /// <param name="baseClass"></param> 
     //public Base(Base baseClass) 
     //{ 
     // if (baseClass == null) 
     // { 
     //  return; 
     // } 

     // this.x = baseClass.x; 
     // this.y = baseClass.y; 
     //} 
    } 

    public sealed class A : Base 
    { 
     // Don't know if you really need this one 
     public A(int x, int y) : base(x, y) 
     { 
     } 

     public A(A a) : base(a) 
     { 
     } 

     public A SetX(int nextX) 
     { 
      // Create manual copy of object and then set another value 
      var a = new A(this) 
      { 
       x = nextX 
      }; 

      return a; 
     } 

     public A SetY(int nextY) 
     { 
      // Create manual copy of object and then set another value 
      var a = new A(this) 
      { 
       y = nextY 
      }; 

      return a; 
     } 
    } 

Таким образом, вы уменьшаете количество параметров в конструкторе из A путем передачи ссылки на существующий объект, задайте все свойства и установите только одно новое внутри некоторого метода A.

+1

Но теперь вы мутируете типы вне своих конструкторов. – Servy

+0

Да, это действительно плохо для вас? Вы спросили о менее избыточной схеме, чтобы уменьшить количество параметров в конструкторе. Извините, если я вас неправильно понял. –

+0

Проблема с этим ответом заключается в том, что если вы собираетесь сделать вещи изменчивыми, тогда есть более простой способ реализовать это: просто клонируйте весь объект и задайте поля, которые вы хотите изменить. – sdgfsdh

2

Для этого точного случая я использую Object. MemberwiseClone(). Этот подход работает только для прямых обновлений свойств (из-за мелкого клонирования).

sealed class A 
{ 
    // added private setters for approach to work 
    public X x { get; private set;} 
    public Y y { get; private set;} 

    public class A(X x, Y y) 
    { 
     this.x = x; 
     this.y = y; 
    } 

    private A With(Action<A> update) 
    { 
     var clone = (A)MemberwiseClone(); 
     update(clone); 
     return clone; 
    } 

    public A SetX(X nextX) 
    { 
     return With(a => a.x = nextX); 
    } 

    public A SetY(Y nextY) 
    { 
     return With(a => a.y = nextY); 
    } 
} 
+0

Это не слишком хорошо, что ваши поля (теперь свойства) больше не 'readonly'. С несколькими членами он также будет иметь больше кода, чем версия OP, а производительность также немного хуже ('MemberwiseClone' немного медленнее сам по себе, кроме того, он копирует по крайней мере одно поле слишком много, что делегат также должен быть выделен, вызван и собраны). Возможно, все нерелевантные точки для большинства приложений, но хорошо иметь в виду, если вы разрабатываете систему с нуля. – Groo

+1

Собственность с частным сеттером может считаться «readonly» (не говоря о рефлексии). Эффективность следует рассматривать в более широком контексте. В случае, если это имеет значение, вы можете встроить метод 'With', избегая выделения лямбда. Код станет более утомительным, но все же менее подверженным ошибкам, чем ручное кодирование. Фактически в моем проекте я использую встроенную версию для класса с более чем 10 свойствами, и преимущества очевидны. – dadhi

0

Я бы использовал шаблон строителя в сочетании с некоторыми методами расширения. Основная идея состоит в том, чтобы использовать метод ToBuilder для инициализации A в ABuilder, модифицировать строителя с помощью свободного интерфейса, а затем завершить построитель, чтобы получить новый экземпляр. В некоторых случаях такой подход может даже уменьшить количество мусора.

непреложный класс:

public sealed class A 
{ 
    readonly int x; 

    public int X 
    { 
     get { return x; } 
    } 

    public A(int x) 
    { 
     this.x = x; 
    } 
} 

Класс строитель:

public sealed class ABuilder 
{ 
    public int X { get; set; } 

    public ABuilder(A a) 
    { 
     this.X = a.X; 
    } 

    public A Build() 
    { 
     return new A(X); 
    } 
} 

Полезные методы расширения:

public static class Extensions 
{ 
    public static ABuilder With(this ABuilder builder, Action<ABuilder> action) 
    { 
     action(builder); 

     return builder; 
    } 

    public static ABuilder ToBuilder(this A a) 
    { 
     return new ABuilder(a) { X = a.X }; 
    } 
} 

Он используется так:

var a = new A(10); 

a = a.ToBuilder().With(i => i.X = 20).Build(); 

Это не идеально. Вам нужно определить дополнительный класс со всеми свойствами оригинала, но синтаксис использования довольно чист и поддерживает простоту типа источника.