2016-07-25 17 views
5

Ich schreibe Code in einem funktionalen Stil in C#. Viele meiner Klassen sind unveränderbar mit Methoden zum Zurückgeben einer modifizierten Kopie einer Instanz.Universal unveränderliche Klassen in C#

Zum Beispiel:

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

Dies ist ein triviales Beispiel, aber vorstellen, eine viel größere Klasse, mit vielen weiteren Mitgliedern.

Das Problem ist, dass das Erstellen dieser modifizierten Kopien sehr ausführlich ist. Die meisten Methoden ändern nur einen Wert, aber ich muss alle der unveränderten Werte in den Konstruktor übergeben.

Gibt es ein Muster oder eine Technik, um all diese Kesselplatte zu vermeiden, wenn unveränderliche Klassen mit Modifikatormethoden konstruiert werden?

Hinweis: Ich möchte kein struct für reasons discussed elsewhere on this site verwenden.

Update: Ich habe festgestellt, dass dies in F # ein "Kopieren und Aktualisieren von Datensatzausdruck" genannt wird.

+3

Ich würde die Notwendigkeit fragen, diese "Setter" zu haben, wie Sie sie für alles geschrieben haben. Wirklich sollten Sie komplexe Operationen auf dem Typ durchführen, nicht nur einen Wert willkürlich "setzen". Für ein gewöhnliches Paar braucht es nichts anderes als einen Konstruktor. – Servy

+1

@Servy Dies ist eine Einschränkung des Beispiels. Die meisten Modifikatoren sind komplexer oder aufgabenspezifisch. Bitte stell dir vor, ich hätte ein besseres Beispiel gegeben :) – sdgfsdh

+3

Ja. Verwende F #. Ernsthaft jetzt. C# ist in erster Linie eine Stateful OOP-Sprache. Obwohl es einige funktionale Merkmale hat, ist es weit davon entfernt, eine gute funktionale Sprache zu sein. – Euphoric

Antwort

8

Für größere Arten werde ich eine With Funktion erstellen, die Argumente, die alle standardmäßig null wenn nicht zur Verfügung gestellt hat:

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

Dann nutzen Sie die benannten Argumente verfügen von C# so:

val = val.With(X: x); 

val = val.With(Y: y); 

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

I Finden Sie einen viel attraktiveren Ansatz als viele Setter-Methoden. Es bedeutet, dass null ein unbrauchbarer Wert wird, aber wenn Sie die funktionale Route gehen, dann nehme ich an, dass Sie versuchen, null zu vermeiden und Optionen zu verwenden.

Wenn Sie Wert-Typen haben/structs als Mitglieder sie dann machen Nullable im With, zum Beispiel:

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

Hinweis dies jedoch nicht umsonst kommen, gibt es N Nullvergleichsoperationen pro Rufen Sie With an, wobei N die Anzahl der Argumente ist. Ich persönlich finde die Bequemlichkeit die Kosten wert (was letztlich vernachlässigbar ist), aber wenn Sie etwas haben, das besonders leistungsempfindlich ist, dann sollten Sie zurück zu maßgeschneiderten Setter-Methoden zurückgreifen.

0

Sie verwenden Muster folgende kann (weiß nicht, ob es passieren, aber man bat um weniger redundante Version, trotzdem kann man eine Vorstellung bekommen):

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

diese Weise können Sie Menge von Parametern in Konstruktor verringern von A durch Übergabe der Referenz des vorhandenen Objekts, setze alle Eigenschaften und setze dann nur eine neue in einer A-Methode.

+1

Aber jetzt mutieren Sie Typen außerhalb ihrer Konstruktoren. – Servy

+0

Ja, ist es wirklich schlecht für dich? Sie haben nach weniger redundanten Mustern gefragt, um die Anzahl der Parameter im Konstruktor zu reduzieren. Entschuldigung, wenn ich dich falsch verstanden habe. –

+0

Das Problem mit dieser Antwort ist, dass, wenn Sie die Dinge veränderbar machen wollen, es einen einfacheren Weg gibt, dies zu implementieren: klonen Sie einfach das ganze Objekt und legen Sie die Felder fest, die Sie ändern wollen. – sdgfsdh

2

Für diesen genauen Fall verwende ich Object. MemberwiseClone(). Der Ansatz funktioniert nur für direkte Eigenschaftsaktualisierungen (wegen eines flachen Klonens).

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

Es ist nicht zu groß, dass Ihre Felder (jetzt Eigenschaften) nicht mehr "readonly" sind. Mit wenigen Mitgliedern wird es auch mehr Code als die OP-Version haben, und auch die Performance ist etwas schlechter ('MemberwiseClone' ist etwas langsamer, zusätzlich kopiert es mindestens ein Feld zu viel, dieser Delegate muss auch zugewiesen, aufgerufen und gesammelt). Vielleicht alle irrelevanten Punkte für die meisten Anwendungen, aber gut zu beachten, wenn man ein System von Grund auf neu entwickelt. – Groo

+1

Die Eigenschaft mit privatem Setter kann "readonly" (nicht über Reflexion) betrachtet werden. Die Leistung sollte in einem größeren Zusammenhang betrachtet werden. Wenn es darauf ankommt, können Sie die 'With'-Methode verwenden, um die Lambda-Zuweisung zu vermeiden. Der Code wird mühsamer, aber immer noch weniger fehleranfällig als die manuelle Codierung. Tatsächlich verwende ich in meinem Projekt die Inline-Version für die Klasse mit mehr als 10 Eigenschaften, und die Vorteile sind offensichtlich. – dadhi

0

Ich würde das Builder-Muster in Kombination mit einigen Erweiterungen Methoden verwenden. Die Grundidee ist, eine ToBuilder Methode zu haben, um eine A in eine ABuilder zu initialisieren, den Erbauer mit einer fließenden Schnittstelle zu modifizieren, dann den Erbauer zu vervollständigen, um die neue Instanz zu erhalten. Dieser Ansatz kann in einigen Fällen sogar Müll reduzieren.

Die unveränderliche Klasse:

public sealed class A 
{ 
    readonly int x; 

    public int X 
    { 
     get { return x; } 
    } 

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

Der Builder-Klasse:

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

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

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

Nützliche Erweiterungsmethoden:

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

Es wird wie folgt verwendet:

var a = new A(10); 

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

Es ist nicht perfekt. Sie müssen eine zusätzliche Klasse mit allen Eigenschaften des Originals definieren, aber die Verwendungssyntax ist ziemlich sauber und behält die Einfachheit des Originaltyps bei.