Laden...

Property, die nur einmal gesetzt werden kann

Erstellt von LaTino vor 6 Jahren Letzter Beitrag vor 6 Jahren 6.249 Views
LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren
Property, die nur einmal gesetzt werden kann

(@mods: falls das doch in die snippets soll, nur zu, ich war mir unsicher)


Das Schlüsselwort [tt]readonly[/tt] kann auf Felder einer Klasse angewendet werden. Felder, die so markiert werden, können für nicht-statische Klassen nur direkt bei der Deklaration, oder im Konstruktor der Klasse gesetzt werden. Danach können sie nicht mehr verändert werden.
[csharp]
class ExampleClass
{
    private readonly int _readonlyField;

    public int ReadonlyProperty => _readonlyField;

    public ExampleClass(int readonlyValue)
    {
        _readonlyField = readonlyValue; //einziges Mal, dass die Variable gesetzt werden kann.
    }
}
[/csharp]


Diese Möglichkeit eignet sich für Werttypen - Zahlen zum Beispiel. Das Feld, in dem ihr Wert gespeichert wird, ist ein Nullable<T>, und der Setter des Property prüft, ob das Feld bereits einen Wert hat.

[csharp]
class ExampleClass
{
    private int? _nullableField;

    public int? ReadonlyProperty
    {
        get
        {
            if(_nullableField.HasValue) return _nullableField;
            throw new InvalidOperationException($"Field {nameof(_nullableField)} has not been set (yet)!");
        }
        set
        {
            if(_nullableField.HasValue) throw new InvalidOperationException($"Field {nameof(_nullableField)} has already been set!");
            _nullableField = value;
        }
    }
}
[/csharp]


Ein Set-Memory ist einfach eine Variable, in der gespeichert wird, ob eine Property gesetzt wurde oder nicht. Diese Möglichkeit eignet sich für alle Typen, funktioniert aber nur, wenn der Set-Memory auch wirklich jedesmal gesetzt wird, wenn die Property sich ändert.
[csharp]
class ExampleClass
{
    private bool _setMemory;
    private int _readonlyProperty;

    public int ReadonlyProperty
    {
        get { return _readonlyProperty; }
        set
        {
            if (!_setMemory)
            {
                _readonlyProperty = value;
                _setMemory = true;
            }
            else throw new InvalidOperationException($"Field {nameof(_readonlyProperty)} has already been set!");
        }
    }
}
[/csharp]


Die in Möglichkeit drei beschriebene Vorgehensweise lässt sich natürlich noch in eine eigene Klasse kapseln, so dass die Übersicht in der benutzenden Klasse nicht verloren geht.

(Vorschlag T-Virus)
[csharp]
class ExampleClass
{
    private readonly SetAnywhereReadonly<int> _readonlyValue = new SetAnywhereReadonly<int>();

    public int ReadonlyProperty
    {
        get => _readonlyValue.Value;
        set => _readonlyValue.Value = value;
    }
}

class SetAnywhereReadonly<T>
{
    private bool _setMemory;
    private T _value;

    public T Value
    {
        get => _value;
        set
        {
            if (!_setMemory)
            {
                _value = value;
                _setMemory = true;
            }
            else throw new InvalidOperationException($"Field {nameof(_value)} has already been set!");
        }
    }
}
[/csharp]

LaTino
(ruhig ergänzen / diskutieren)
EDIT: Vorschlag von T-Virus ergänzt

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

T
2.219 Beiträge seit 2008
vor 6 Jahren

Wäre hilfreich die Klasse als Generic zu haben.
Dann kann man die Instanz der Klasse als Property verwenden, die diesen Zweck erfüllt.
Wird bei einer Klasse die mehrere solcher Properties braucht, sonst schnell unübersichtlich.
So kann ich einfach eine entsprechende Instanz erstellen und diese verwenden.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren

Stimmt. Eine Möglichkeit, das zu tun, habe ich mal ergänzt.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

16.806 Beiträge seit 2008
vor 6 Jahren

Was wäre denn ein konkreter Use Case?

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren

Für readonly?

Die anderen Möglichkeiten sind ausschließlich dafür gedacht, die Beschränkung von readonly (Setzen ausschließlich im c'tor oder Deklaration) zu umgehen. So was ähnliches ist bei uns zB in den Factories im Einsatz, um die Stellen handlicher zu machen, die IoC per Property Injection realisieren. Oder du setzt eine readonly-Variable, die Werte benötigt, die im Konstruktor noch nicht verfügbar sind.

Letzten Endes alles Fälle, um die Art und Weise, wie eine Property benutzt wird, weiter einzuschränken, als die Sprache selbst das erlauben würde.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

T
2.219 Beiträge seit 2008
vor 6 Jahren

@LaTino
Wäre es dann vielleicht noch hilfreich, auch deinen Fall über einen Konstruktor abzudecken?
Also beim setzen des Wertes über den Konstrutor gleich auch den Wert auf gesetzt zustellen?
Dann kann man den Wert entweder beim Initalisieren über den Kosntruktor oder durch entsprechenden Aufruf des Setter einmalig setzen.
Dann solltest du aber auch einen Default Konstruktor mitgeben der dann eben leer bleibt.
So kann man den Wert auch später setzen aber spart sich ggf. endlose null Prüfungen.

Ebenfalls wäre auch eine Getter für eine Abfrage ob der Wert bereits gesetzt wurde eine sinnvoll Erweiterung.
Somit würde man sich durch eine entsprechende Prüfung die Exception aus dem Setter sparen.
Wäre aber ggf. ein Sonderfall.

Nachtrag:
Würde reichen im Konstruktor den Setter aufzurufen.
Der erledigt schon alles.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.806 Beiträge seit 2008
vor 6 Jahren

Wäre es dann vielleicht noch hilfreich, auch deinen Fall über einen Konstruktor abzudecken?

An genau das würde ich denken, wenn ich diese Anforderung lese.
Wenn ich ein Property nur ein mal setzen will, und ich mach das über den Setter, dann stimmt doch was am Code nicht... 🤔

Daher die Frage des Anwendungsfalls:
welche Situation gibt es denn, die nicht über einen Konstruktor mit entsprechendem Design umsetzbar wäre?
So würde ich das persönlich als suboptimale Umsetzung empfinden.

2.078 Beiträge seit 2012
vor 6 Jahren

Eine konkrete Idee, wo das vielleicht hilft:

In einem ViewModel mit vielen Commands, die Commands sind bei mir immer readonly.
Allerdings kann das recht schnell den Konstruktor aufblähen, weshalb es zur Übersicht beitragen würde, wenn diese Command-Instantiierungen in einer eigenen Methode - z.B. InitCommands - statt finden.
Das geht natürlich nur, wenn die Property nicht readonly ist

Ein besseres Beispiel fällt mir aber auch nicht ein 😄

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren

Wie gesagt, wenn man etwas intensiver mit property injection arbeitet, kann das unter Umständen ganz hilfreich sein. Natürlich kann man property injection komplett mit c'tor injection abbilden, aber es ist nunmal situationsabhängig, welche Methode ich einsetze.

Bin etwas befremdet darüber, dass bestimmte Techniken abgelehnt werden, nur weil einem kein Beispiel einfällt, wofür man selbst das brauchen könnte. Wenn man xy erreichen will, hat man Möglichkeit a, b und c, das zu erreichen. Da muss ich doch nicht darübner diskutieren, dass man selbst noch nie xy erreichen wollte.

LaTino

EDIT:
Noch mal langsam:

  • sinnvoll, um einen Wert einmalig mit etwas zu füllen, dass zur Konstruktion noch nicht verfügbar ist. Nennt es write-once lazy loading, jedenfalls mit einer Mechanik, die sicherstellt, dass nur einmal gesetzt wird. Dass das hilfreich ist, wenn -zig Leute am Code rumbauen, von denen nicht jeder weiß, dass die Prop gesetzt sein muss, und dass sie nur einmal gesetzt werden sollte, muss ich nicht wirklich erläutern?
  • im Konstruktor einen Setter aufrufen? Soviel Masochismus habe ich dann doch nicht, danke 😄.
  • Ja, ich gestehe gern zu, dass das Sonderfälle sind. Es kann aber hilfreich sein, und wenn man es braucht, hat man die obigen drei Möglichkeiten, es einigermaßen sauber zu implementieren.

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

16.806 Beiträge seit 2008
vor 6 Jahren

Das ist keine ablehnende Haltung; das ist nur - Du sagst es ja so gern- aus meinem Empfinden eher ein Anti Patterm 😃

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren

Hm, jein. Das gewünschte Verhalten an sich ist nix schlimmes, die Verwendung birgt in sich aber ein hohes Risiko, Probleme zu bereiten. (siehe Setteraufruf im Konstruktor, was eben wirklich in die Hose gehen kann und nicht offensichtlich ist). Tatsächlich sind hier die Tests für die betreffenden Komponenten unter denen, die am häufigsten rot werden. Nur ist der Einsatz gerechtfertigt und auch nicht ohne weiteres ersetzbar 😃. Wenn man's braucht, sollte man zusehen, ob man sein Design nicht so ändern kann, dass Variante eins - simples readonly - benutzt werden kann. Dann ist man auf der sicheren Seite, und bekommt obendrein sogar Compilerfehler, wenn man's falsch macht.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

D
985 Beiträge seit 2014
vor 6 Jahren

@Palladin007

Wenn die Command-Properties über eine Methode gesetzt werden, dann deklariert man diese Properties als


public Foo Foo { get; private set; }
private bool _fooInitialized;
public void InitializeFoo( Foo foo )
{
    if ( _fooInitialized ) throw new InvalidOperationException();
    Foo = foo;
    _fooInitialized = true;
}

Wenn die Commands aber sowieso von aussen kontrolliert werden, warum dann diese erhöhte Komplikation?

Ich lasse die einfach beschreibbar und gut ist. Das wäre eher etwas was in einen Unit Test geprüft wird (was du sowieso machen musst um zu überprüfen, ob dort auch wirklich InitializeCommands aufgerufen wurde)

16.806 Beiträge seit 2008
vor 6 Jahren

Ernst gemeinte Frage...
Wieso kein Property ohne Setter und dafür eine TrySet-Methode, die false zurück gibt, wenn es im Readonly-Modus ist?

Für mich sieht das wirklich nach Konzept-Misshandlung von Eigenschaften aus.
Ich bemängle nicht die Anforderung, sondern die Umsetzung.

Wenn man's braucht, sollte man zusehen, ob man sein Design nicht so ändern kann, dass Variante eins - simples readonly - benutzt werden kann. Dann ist man auf der sicheren Seite, und bekommt obendrein sogar Compilerfehler, wenn man's falsch macht.

Eben.

2.078 Beiträge seit 2012
vor 6 Jahren

@SirRufo:

Ich meine es eher so:

public ICommand DoACommand { get; }
public ICommand DoBCommand { get; }
public ICommand DoCCommand { get; }

public MyClass()
{
    DoACommand = new DoACommand();
    DoBCommand = new DoBCommand();
    DoCCommand = new DoCCommand();
}

Sind das jetzt aber nicht nur 3 Commands, sondern z.B. 15, dann wird's schon voll.
Wenn im Konstruktor dann auch noch andere Dinge gemacht werden sollen, ist's schön, wenn das doch recht stumpfe erstellen der Commands in einer eigenen Methode liegt.

Das sähe dann so aus:

public ICommand DoACommand { get; private set; }
public ICommand DoBCommand { get; private set; }
public ICommand DoCCommand { get; private set; }

public MyClass()
{
    CreateCommands();
}

private void CreateCommands()
{
    DoACommand = new DoACommand();
    DoBCommand = new DoBCommand();
    DoCCommand = new DoCCommand();
}

Klar, kann ich den Setter auch einfach private lassen, ich versuche aber immer die Felder/Properties, die sich nicht ändern sollen/dürfen readonly zu machen, dann kann da gar nicht erst ein Fehler entstehen.
Da würde LaTinos Idee helfen. Die Setter sind zwar immer noch da, aber selbst wenn jemand auf die blöde Idee kommt, den ein zweites mal zu nutzen, dann fällt das spätestens bei den Tests auf.

16.806 Beiträge seit 2008
vor 6 Jahren

Und es ist nun besser mehrere Methoden zu haben statt die Arbeit in einen "vollen" Konstruktor zu packen...? 🤔

PS:

public ICommand DoACommand { get; } = new DoCAommand();
public ICommand DoBCommand { get; } = new DoBCommand();
public ICommand DoCCommand { get; } = new DoCCommand();

public MyClass() { }

PS: wenn ein Konstruktor voll ist, dann ist das für mich ein Anzeichen, dass eine oder mehrere Basisklassen zur Übersicht beitragen könnte.

2.078 Beiträge seit 2012
vor 6 Jahren

Das ist nur ein kleines Code-Beispiel ^^

Ich nutze z.B. ganz gerne das RelayCommand bzw. manchmal bringt ein "echtes" Command (eigene Klasse) einfach keinen Vorteil.
Oder wenn ein Command einen Zustand des ViewModels braucht, der im Konstruktor erst hergestellt wird.

Dann fliegt der direkte Feldinitializer (so wird's zumindest in der Fehlermeldung genannt) raus, der kann nicht auf Instanz-Member zugreifen.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Um ehrlich zu sein für mich wirkt es auch ein wenig wie ein Workaround, denn ich so eigendlich nicht verwenden sollte.

Grundlegend sollten ja bei der Initialisierung einer Klasse alle nötigen Abhänigkeiten vorhanden sein.
Wenn nicht initialisiere ich meist die Klasse an der falsche Stelle bzw. Zeitpunkt. Oder die Klasse hat vielleicht zu viele Verantwortlichkeiten (SRP).
Was meines Erachtens ein Design Fehler ist, der behoben werden sollte.

Dann wird noch die Instantiierung von der Initialisierung getrennt. Was meines Erachtens die Wartbarkeit verschlechtert. Ich muss immer an 2 Stellen nachschauen und ich muss wissen, das ich an 2 Stellen nachschauen muss.

Dass das hilfreich ist, wenn -zig Leute am Code rumbauen, von denen nicht jeder weiß, dass die Prop gesetzt sein muss, und dass sie nur einmal gesetzt werden sollte, muss ich nicht wirklich erläutern?

An dem Punkt hab ich die Erfahrung gemacht, dass es am besten ist es im Konstruktor zu machen. Dann können die Leute nichts anderes machen als es zu setzen.

Ich hatte bei einer meiner Anstellung einen Kollegen, der Klassen so ähnlich aufgebaut hatte (OK da gab es auch noch Design Probleme). Das war wirklich nicht schon die Klassen zu benutzen.
Man hat die Klasse Instantiiert, die Methode aufgerufen, die man verwenden wollte. Und beim Testen des eigenen Codes kam dann der Fehler, das ein Property nicht gesetzt ist. Also Property gesetzt, Compiler wieder an geschmissen und beim Testen den Fehler bekommen, das ein weiteres Property nicht gesetzt ist. Das Spiel hat man dann noch ein paar mal wiederholt, bis man genervt zum Kollegen gegangen ist und gefragt hat welche Propertys man denn alle setzen muss um die Methode der Klasse zu benutzen. Seine Standard Antwort war dann meistens erstmals, die Klasse wird ja auch von anderen Benutzt schau mal welche Propertys die gesetzte haben. Und hat diese dann auch einfach gesetzt. Compiler gestartet und eine Exception bekommen, weil ein Property nicht gesetzt war. Zurück zum Kollegen, der ist dann mit zum Arbeitsplatz gekommen und hat sich das Angeschaut. Dann kam dann meist so was wie:"Ach die Methode verwendest du, die benutzt ja noch keiner. Wenn du die Methode benutzen willst, musst du noch die Propertys XYZ, dafür brauchst du dann aber, diese hier nicht zu setzen. Ist doch ganz einfach." Nein, es war nicht einfach, es war einfach nur Frustrierend seine Klassen zu benutzen. Und das schlimmste war, er war für das Firmen interne Framework (Painwork ist da wohl der bessere Begriff) zuständig. Alle 20 .Net Entwickler der Firma mussten den seine Klassen benutzen und hatten die gleichen Probleme wie ich. Er war überigens auch nicht besonders Glücklich, weil die anderen "dummen" Entwickler andauernd bei ihm standen und Fragen hatten und er nicht mehr wirklich zum Arbeiten kam.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

W
955 Beiträge seit 2010
vor 6 Jahren

Um ehrlich zu sein für mich wirkt es auch ein wenig wie ein Workaround, denn ich so eigendlich nicht verwenden sollte. ... und für mich wie eine Demonstration von NIH anhand von Lazy<T>.

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 6 Jahren

und für mich wie eine Demonstration von NIH anhand von Lazy<T>.

Yep, war tatsächlich eine Motivation. Allerdings kein NIH - benötigt für net 2.0/CF.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)