Gibt es eine Möglichkeit, eine Eigenschaft nur einmal in C # festzulegen?

73

Ich suche nach einer Möglichkeit, eine Eigenschaft in einem C # -Objekt nur einmal festzulegen. Es ist einfach, den Code dafür zu schreiben, aber ich würde lieber einen Standardmechanismus verwenden, falls einer existiert.

public OneShot <int> SetOnceProperty {get; einstellen; }}

Was ich passieren möchte, ist, dass die Eigenschaft festgelegt werden kann, wenn sie nicht bereits festgelegt ist, aber eine Ausnahme auslösen kann, wenn sie zuvor festgelegt wurde. Es sollte wie ein Nullable-Wert funktionieren, bei dem ich überprüfen kann, ob er festgelegt wurde oder nicht.

Nick Randell
quelle
Das riecht für mich, sorry. Warum nicht den Wert an einen Konstruktor übergeben? Werden Sie dem Anrufer auch eine Rückmeldung geben, damit er vor dem Festlegen des Werts überprüfen kann, ob er nicht festgelegt wurde?
Binary Worrier
7
Idealerweise würde ich es im Konstruktor übergeben, aber ich muss das Objekt über einen bestimmten Zeitraum aufbauen. Beispiel: Ein Datensatz enthält Informationen A, der nächste Datensatz enthält Informationen B und C. Sobald ich einen vollständigen Satz von Informationen habe, verwende ich diese Informationen, um alle Datensätze wieder miteinander zu verknüpfen. Ich wollte einen Laufzeitmechanismus, um zu überprüfen, ob ich Werte nur einmal festgelegt habe, sodass sie nur schreibgeschützt sind!
Nick Randell
Und ja - es riecht auch für mich!
Nick Randell
2
Es riecht nur, weil Sie annehmen, dass es riecht. Ich brauchte dies, wenn ein von mir verwendetes Framework in ein Objekt aufgerufen wurde, das instanziiert wurde, und mir überhaupt keine Möglichkeit gab, Abhängigkeiten einzufügen. Also ließ ich meinen Kompositionsstamm den Container zu einem Schreibvorgang verschieben, sobald eine statische Eigenschaft für dieses Objekt definiert wurde.
Ronnie Overby

Antworten:

58

Dies wird in der TPL in .NET 4.0 direkt unterstützt.

(Bearbeiten: Der obige Satz wurde in Erwartung System.Threading.WriteOnce<T>dessen geschrieben, was in den zu diesem Zeitpunkt verfügbaren "Vorschau" -Bits vorhanden war, aber dies scheint verdunstet zu sein, bevor die TPL RTM / GA traf.)

bis dahin mach den Check einfach selbst ... es sind nicht viele Zeilen, soweit ich mich erinnere ...

etwas wie:

public sealed class WriteOnce<T>
{
    private T value;
    private bool hasValue;
    public override string ToString()
    {
        return hasValue ? Convert.ToString(value) : "";
    }
    public T Value
    {
        get
        {
            if (!hasValue) throw new InvalidOperationException("Value not set");
            return value;
        }
        set
        {
            if (hasValue) throw new InvalidOperationException("Value already set");
            this.value = value;
            this.hasValue = true;
        }
    }
    public T ValueOrDefault { get { return value; } }

    public static implicit operator T(WriteOnce<T> value) { return value.Value; }
}

Dann verwenden Sie zum Beispiel:

readonly WriteOnce<string> name = new WriteOnce<string>();
public WriteOnce<string> Name { get { return name; } }
Marc Gravell
quelle
2
Ich war so beschäftigt, meine eigene Antwort zu polieren, dass ich nicht bemerkte, dass Sie (praktisch) dasselbe verdammte Ding geschrieben haben. Ich fühle mich nicht mehr so ​​besonders.
Michael Meadows
28
Würde es Ihnen etwas ausmachen, die direkte Support-Version davon hinzuzufügen (da wir bereits weit hinter .NET 4.0 zurück sind)? Ich kann anscheinend keine Informationen zu dieser Funktion finden.
Destrictor
Es ist möglich, dass Marc über msdn.microsoft.com/ru-ru/library/hh194820(v=vs.110).aspx
Dmitry Dovgopoly
3
@MarcGravell Was genau in der TPL ermöglicht dies? TaskCompletionSource<T>?
Ronnie Overby
1
@ ArturoTorresSánchez Im ursprünglichen CTP des Frameworks für parallele Erweiterungen (das "aktuell" war, als ich dies schrieb, IIRC) gab es einen WriteOnce<T>Typ, der nicht mit den Datenflussbits zusammenhängt. Ich bin mir nicht sicher, ob dies noch existiert oder es zu RTM geschafft hat: aber - es existierte zum Zeitpunkt des Schreibens. Sie können immer noch Spuren von ihnen finden - hier , hier
Marc Gravell
31

Sie können Ihre eigenen rollen (siehe das Ende der Antwort für eine robustere Implementierung, die threadsicher ist und Standardwerte unterstützt).

public class SetOnce<T>
{
    private bool set;
    private T value;

    public T Value
    {
        get { return value; }
        set
        {
            if (set) throw new AlreadySetException(value);
            set = true;
            this.value = value;
        }
    }

    public static implicit operator T(SetOnce<T> toConvert)
    {
        return toConvert.value;
    }
}

Sie können es so verwenden:

public class Foo
{
    private readonly SetOnce<int> toBeSetOnce = new SetOnce<int>();

    public int ToBeSetOnce
    {
        get { return toBeSetOnce; }
        set { toBeSetOnce.Value = value; }
    }
}

Robustere Implementierung unten

public class SetOnce<T>
{
    private readonly object syncLock = new object();
    private readonly bool throwIfNotSet;
    private readonly string valueName;
    private bool set;
    private T value;

    public SetOnce(string valueName)
    {
        this.valueName = valueName;
        throwIfGet = true;
    }

    public SetOnce(string valueName, T defaultValue)
    {
        this.valueName = valueName;
        value = defaultValue;
    }

    public T Value
    {
        get
        {
            lock (syncLock)
            {
                if (!set && throwIfNotSet) throw new ValueNotSetException(valueName);
                return value;
            }
        }
        set
        {
            lock (syncLock)
            {
                if (set) throw new AlreadySetException(valueName, value);
                set = true;
                this.value = value;
            }
        }
    }

    public static implicit operator T(SetOnce<T> toConvert)
    {
        return toConvert.value;
    }
}


public class NamedValueException : InvalidOperationException
{
    private readonly string valueName;

    public NamedValueException(string valueName, string messageFormat)
        : base(string.Format(messageFormat, valueName))
    {
        this.valueName = valueName;
    }

    public string ValueName
    {
        get { return valueName; }
    }
}

public class AlreadySetException : NamedValueException
{
    private const string MESSAGE = "The value \"{0}\" has already been set.";

    public AlreadySetException(string valueName)
        : base(valueName, MESSAGE)
    {
    }
}

public class ValueNotSetException : NamedValueException
{
    private const string MESSAGE = "The value \"{0}\" has not yet been set.";

    public ValueNotSetException(string valueName)
        : base(valueName, MESSAGE)
    {
    }
}
Michael Meadows
quelle
Nur setzen Sie this.set nicht auf true, wenn Sie den Wert setzen ...;)
Tomas Aschan
Jetzt setzen wir es auf wahr
JoshBerke
Vielen Dank. Ich habe das gesehen, aber dann auch andere Probleme gesehen. Ich habe am Ende der Antwort eine "produktionsbereite" Version eingefügt.
Michael Meadows
2
throwIfGet (im ersten Konstruktor) ist nirgendwo definiert
reggaeguitar
2
Autsch beim Sperren für eine Immobilie bekommen
Chris Marisic
11

Dies kann entweder mit der Flagge geschehen:

private OneShot<int> setOnce;
private bool setOnceSet;

public OneShot<int> SetOnce
{
    get { return setOnce; }
    set
    {
        if(setOnceSet)
            throw new InvalidOperationException();

        setOnce = value;
        setOnceSet = true;
    }
}

Das ist nicht gut, da Sie möglicherweise einen Laufzeitfehler erhalten können. Es ist viel besser, dieses Verhalten beim Kompilieren durchzusetzen:

public class Foo
{
    private readonly OneShot<int> setOnce;        

    public OneShot<int> SetOnce
    {
        get { return setOnce; }
    }

    public Foo() :
        this(null)
    {
    }

    public Foo(OneShot<int> setOnce)
    {
        this.setOnce = setOnce;
    }
}

und verwenden Sie dann einen der beiden Konstruktoren.

Anton Gogolev
quelle
1
Ich möchte, dass ein Laufzeitfehler auftritt, wenn ich versuche, ihn zweimal einzustellen. Andernfalls hätte ich einen Konstruktor erstellt, der eine schreibgeschützte Mitgliedsvariable setzt
Nick Randell
4

Keine solche Funktion in C # (ab 3.5). Sie müssen es selbst codieren.

Vizu
quelle
1
Microsoft sollte eine solche Funktion bereitstellen.
Syed Tayyab Ali
6
Ich bin mir nicht sicher. Wenn etwas nur einmal gesetzt werden soll, sollte es wahrscheinlich ein Konstruktorparameter sein.
Vizu
4

Wie Marc sagte, gibt es in .Net standardmäßig keine Möglichkeit, dies zu tun, aber es ist nicht allzu schwierig, selbst eine hinzuzufügen.

public class SetOnceValue<T> { 
  private T m_value;
  private bool m_isSet;
  public bool IsSet { get { return m_isSet; }}
  public T Value { get {
    if ( !IsSet ) {
       throw new InvalidOperationException("Value not set");
    }
    return m_value;
  }
  public T ValueOrDefault { get { return m_isSet ? m_value : default(T); }}
  public SetOnceValue() { }
  public void SetValue(T value) {
    if ( IsSet ) {
      throw new InvalidOperationException("Already set");
    }
    m_value = value;
    m_isSet = true;
  }
}

Sie können dies dann als Hintergrund für Ihre bestimmte Eigenschaft verwenden.

JaredPar
quelle
3

Hier ist meine Meinung dazu:

public class ReadOnly<T> // or WriteOnce<T> or whatever name floats your boat
{
    private readonly TaskCompletionSource<T> _tcs = new TaskCompletionSource<T>();

    public Task<T> ValueAsync => _tcs.Task;
    public T Value => _tcs.Task.Result;

    public bool TrySetInitialValue(T value)
    {
        try
        {
            _tcs.SetResult(value);
            return true;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    public void SetInitialValue(T value)
    {
        if (!TrySetInitialValue(value))
            throw new InvalidOperationException("The value has already been set.");
    }

    public static implicit operator T(ReadOnly<T> readOnly) => readOnly.Value;
    public static implicit operator Task<T>(ReadOnly<T> readOnly) => readOnly.ValueAsync;
}

Marc's Antwort legt nahe, dass die TPL diese Funktionalität bietet, und ich denke, TaskCompletionSource<T> dass dies das war, was er meinte, aber ich kann nicht sicher sein.

Einige schöne Eigenschaften meiner Lösung:

  • TaskCompletionSource<T> ist eine offiziell unterstützte MS-Klasse, die die Implementierung vereinfacht.
  • Sie können wählen, ob der Wert synchron oder asynchron abgerufen werden soll.
  • Eine Instanz dieser Klasse konvertiert implizit in den Wertetyp, den sie speichert. Dies kann Ihren Code ein wenig aufräumen, wenn Sie den Wert weitergeben müssen.
Ronnie Overby
quelle
2
/// <summary>
/// Wrapper for once inizialization
/// </summary>
public class WriteOnce<T>
{
    private T _value;
    private Int32 _hasValue;

    public T Value
    {
        get { return _value; }
        set
        {
            if (Interlocked.CompareExchange(ref _hasValue, 1, 0) == 0)
                _value = value;
            else
                throw new Exception(String.Format("You can't inizialize class instance {0} twice", typeof(WriteOnce<T>)));
        }
    }

    public WriteOnce(T defaultValue)
    {
        _value = defaultValue;
    }

    public static implicit operator T(WriteOnce<T> value)
    {
        return value.Value;
    }
}
Smagin Alexey
quelle
0
interface IFoo {

    int Bar { get; }
}

class Foo : IFoo {

    public int Bar { get; set; }
}

class Program {

    public static void Main() {

        IFoo myFoo = new Foo() {
            Bar = 5 // valid
        };

        int five = myFoo.Bar; // valid

        myFoo.Bar = 6; // compilation error
    }
}

Beachten Sie, dass myFoo als IFoo deklariert, aber als Foo instanziiert ist.

Dies bedeutet, dass Bar innerhalb des Initialisierungsblocks festgelegt werden kann, jedoch nicht durch einen späteren Verweis auf myFoo.

Allonhadaya
quelle
0

Die Antworten gehen davon aus, dass Objekte, die in Zukunft einen Verweis auf ein Objekt erhalten, nicht versuchen, es zu ändern. Wenn Sie sich davor schützen möchten, müssen Sie dafür sorgen, dass Ihr einmaliger Schreibcode nur für Typen funktioniert, die ICloneable implementieren oder primitiv sind. Der String-Typ implementiert beispielsweise ICloneable. Dann würden Sie einen Klon der Daten oder eine neue Instanz des Grundelements anstelle der tatsächlichen Daten zurückgeben.

Generika nur für Grundelemente: T GetObject wobei T: struct;

Dies ist nicht erforderlich, wenn Sie wissen, dass Objekte, die einen Verweis auf die Daten erhalten, diese niemals überschreiben.

Überlegen Sie auch, ob die ReadOnlyCollection für Ihre Anwendung funktioniert. Eine Ausnahme wird ausgelöst, wenn versucht wird, die Daten zu ändern.

VoteCoffee
quelle
0

Während die akzeptierten und am besten bewerteten Antworten diese (ältere) Frage am direktesten beantworten, besteht eine andere Strategie darin, eine Klassenhierarchie so zu erstellen, dass Sie Kinder über Eltern sowie die neuen Eigenschaften erstellen können:

public class CreatedAtPointA 
{
    public int ExamplePropOne { get; }
    public bool ExamplePropTwo { get; }

    public CreatedAtPointA(int examplePropOne, bool examplePropTwo)
    {
        ExamplePropOne = examplePropOne;
        ExamplePropTwo = examplePropTwo;
    }
}

public class CreatedAtPointB : CreatedAtPointA
{
    public string ExamplePropThree { get; }

    public CreatedAtPointB(CreatedAtPointA dataFromPointA, string examplePropThree) 
        : base(dataFromPointA.ExamplePropOne, dataFromPointA.ExamplePropTwo)
    {
        ExamplePropThree = examplePropThree;
    }
}

Wenn Sie sich auf Konstruktoren verlassen, können Sie Febreeze auf den Codegeruch sprühen, obwohl dies immer noch mühsam und eine potenziell teure Strategie ist.

Sternfuchs
quelle
0

Ich habe einen Typ erstellt, mit dem ein Wert bei der Erstellung festgelegt werden kann. Danach kann der Wert nur einmal festgelegt / überschrieben werden, andernfalls wird eine Ausnahme ausgelöst.

public class SetOnce<T>
{
    bool set;
    T value;

    public SetOnce(T init) =>
        this.value = init;

    public T Value
    {
        get => this.value;
        set
        {
            if (this.set) throw new AlreadySetException($"Not permitted to override {this.Value}.");
            this.set = true;
            this.value = value;
        }
    }

    public static implicit operator T(SetOnce<T> setOnce) =>
        setOnce.value;

    class AlreadySetException : Exception
    {
        public AlreadySetException(string message) : base(message){}
    }
}
Pellet
quelle
0

In C # 9 ist diese Funktion integriert. Sie wird nur als Init-Setter bezeichnet

public DateTime RecordedAt { get; init; }
Tono Nam
quelle
Dies ist nicht wirklich dasselbe, da es das Einstellen des Werts nur bei der Initialisierung erlaubt und keine Ausnahme erzeugt, stattdessen ein Kompilierungsfehler
Yegor Androsov
-2

Sie können dies tun, dies ist jedoch keine klare Lösung, und die Lesbarkeit des Codes ist nicht die beste. Wenn Sie Code-Design ausführen, können Sie sich die Singleton- Realisierung zusammen mit AOP ansehen , um Setter abzufangen . Die Realisierung ist nur 123 :)

Ruslander
quelle
1
Das ist der Grund, warum Antworten nur auf Links nicht zum Thema gehören ... der Intercept- Link ist tot :-(
t3chb0t