2010-02-04 14 views
5

Wszyscy zdajemy sobie sprawę z zalet niezmiennych typów, szczególnie w przypadku wielowątkowych scenariuszy. (A przynajmniej wszyscy powinniśmy to zrozumieć, zobacz np. System.String.)Konstrukcja o niezmiennej konstrukcji klasy

Jednak nie widziałem wiele dyskusji tworząc niezmienne instancje, w szczególności wytyczne projektowania.

Na przykład, załóżmy, że chcemy mieć następujące klasy niezmienna:

class ParagraphStyle { 
    public TextAlignment Alignment {get;} 
    public float FirstLineHeadIndent {get;} 
    // ... 
} 

Najczęstszym podejście Widziałem to mieć zmienny/niezmienne „parę” typów, na przykład zmienne typy List<T> i niezmienne ReadOnlyCollection<T> lub typy zmienne i niezmienne String.

celu naśladowania tych istniejących wzoru wymaga wprowadzenia pewnego rodzaju „zmienny” ParagraphStyle typu, „powtórzone” elementy (w celu ustawiające), a następnie dostarczenie ParagraphStyle konstruktora który akceptuje zmienny typ jako argument

// Option 1: 
class ParagraphStyleCreator { 
    public TextAlignment {get; set;} 
    public float FirstLineIndent {get; set;} 
    // ... 
} 

class ParagraphStyle { 
    // ... as before... 
    public ParagraphStyle (ParagraphStyleCreator value) {...} 
} 

// Usage: 
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator { 
    TextAlignment = ..., 
    FirstLineIndent = ..., 
}); 

Działa to, obsługuje uzupełnianie kodu w IDE i sprawia, że ​​rzeczy oczywiste, jak budować rzeczy ... ale wydaje się dość powielanie.

Czy istnieje lepszy sposób?

Na przykład, C# anonimowe typy są niezmienne, i pozwalają za pomocą „normalnych” ustawiaczy własności dla inicjalizacji:

var anonymousTypeInstance = new { 
    Foo = "string", 
    Bar = 42; 
}; 
anonymousTypeInstance.Foo = "another-value"; // compiler error 

Niestety, najbliższa droga do powielenia tych semantykę w C# jest wykorzystanie parametrów Konstruktor:

// Option 2: 
class ParagraphStyle { 
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent, 
      /* ... */) {...} 
} 

Ale to nie "dobrze" skaluje; jeśli twój typ ma np. 15 właściwości, konstruktor z 15 parametrami nie jest przyjazny, a zapewnienie "użytecznego" przeciążenia dla wszystkich 15 właściwości jest receptą na koszmar. Odrzucam to wprost.

Jeśli staramy się naśladować anonimowych typów, wydaje się, że możemy użyć „set-once” właściwości w „niezmiennego” typu, a tym samym spadek „zmienny” Wariant:

// Option 3: 
class ParagraphStyle { 
    bool alignmentSet; 
    TextAlignment alignment; 

    public TextAlignment Alignment { 
     get {return alignment;} 
     set { 
      if (alignmentSet) throw new InvalidOperationException(); 
      alignment = value; 
      alignmentSet = true; 
     } 
    } 
    // ... 
} 

Problem z to jest to, że nie jest oczywiste, że właściwości można ustawić tylko raz (kompilator na pewno nie będzie narzekał), a inicjalizacja nie jest bezpieczna dla wątków. W związku z tym kuszące staje się dodanie metody Commit(), aby obiekt mógł wiedzieć, że programista jest wykonywany ustawiając właściwości (powodując w ten sposób wszystkie właściwości, które nie zostały wcześniej ustawione do rzucenia, jeśli ich wywołujący jest wywoływany), ale wydaje się to być sposób na pogorszenie sytuacji, nie na lepsze.

Czy istnieje lepszy projekt niż podział zmiennej/niezmienny? Czy jestem skazany na powielanie członków?

+0

Zdaję sobie sprawę, że może to być trudne do uwierzenia, ale pracuję z istniejącą bazą kodu C#. Przeniesienie całej rzeczy do F # nie jest opcją, a nawet gdyby była to opcja, licencja MSR-SSLA, w której udostępniane są biblioteki F #, nie jest odpowiednia do mojego użytku, konkretnie (ii) (c). – jonp

+0

To zdaje się być duplikatem: http://stackoverflow.com/questions/263585/immutable-object-pattern-in-c-what-do-you-think – jonp

+0

Może nie być odpowiedzią, ale miałem do czynienia z podobnymi wymaganiami w przeszłości poprzez implementację ISupportInitialize. Po wywołaniu EndInit() stajesz się niezmienny i rzucasz, gdy wywoływane są setery. Nie jest to bezpieczna kompilacja, ale dokumentacja i wczesne rzucanie często pomagają. – Will

Odpowiedz

2

W kilku projektach stosowałem płynne podejście. To znaczy. większość ogólnych właściwości (np. nazwa, lokalizacja, tytuł) jest zdefiniowanych przez ctor, podczas gdy inne są aktualizowane za pomocą Set metod zwracających nową niezmienną instancję.

class ParagraphStyle { 
    public TextAlignment Alignment {get; private set;} 
    public float FirstLineHeadIndent {get; private set;} 
    // ... 
    public ParagraphStyle WithAlignment(TextAlignment ta) { 
     var newStyle = (ParagraphStyle)MemberwiseClone(); 
     newStyle.TextAlignment = ta; 
    } 
    // ... 
} 

MemberwiseClone jest ok, jak tylko nasza klasa jest naprawdę głęboko niezmienna.

+1

To działa i jest użyteczne, ale nie jest skalowalnym rozwiązaniem. Jeśli musisz ustawić np. 15 wartości, metody mutatora WithFoo() powodują utworzenie 14 "tymczasowych" instancji, zanim uzyskasz ostateczny wynik. W zależności od tego, jak "ciężki" jest twój typ, może to być znaczna ilość śmieci. WithFoo() również nie obsługuje inicjalizatorów kolekcji, co może nie być ogromną stratą, ale bardzo ich kocham ;-). Idiom Foo + FooBuilder wydaje się rozsądnym kompromisem. – jonp

+0

Próbowałem tego podejścia innym razem w jednym z ostatnich projektów i całkowicie się z tym zgadzam, nudne jest tworzyć wszystkie te opakowania. Zgadzam się, że Builder jest znacznie bardziej elegancki i czytelny. A co z głęboko zagnieżdżonymi strukturami? Struktury danych FP promują najmniejszy możliwy ślad pamięci przez ponowne użycie niezmienionych członków. – olegz

+0

Możesz również dostarczyć konstruktora płynnego budowniczego, który pobiera niezmienny obiekt jako argument, aby zainicjować wewnętrzne pola za jego pomocą. Tak więc możesz użyć 'WithFoo()' tylko na tych 'Foos' które musisz zmodyfikować :) –

Powiązane problemy