Eigenschaften

Eigenschaften sind Bürger erster Klasse in C#. Die Sprache definiert die Syntax, mit der Entwickler Code schreiben können, der genau ihre Entwurfsabsicht ausdrückt.

Eigenschaften verhalten sich wie Felder, wenn darauf zugegriffen wird. Jedoch sind Eigenschaften im Gegensatz zu Feldern mit Accessoren implementiert, die die ausgeführten Anweisungen definieren, wenn auf eine Eigenschaft zugegriffen oder sie zugewiesen wird.

Eigenschaftssyntax

Die Syntax für Eigenschaften ist eine natürliche Erweiterung von Feldern. Ein Feld definiert einen Speicherort:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

Eine Eigenschaftendefinition enthält Deklarationen für einen get- und set-Accessor, der den Wert dieser Eigenschaft abruft oder zuweist:

public class Person
{
    public string? FirstName { get; set; }

    // Omitted for brevity.
}

Die oben dargestellte Syntax ist die Auto-Eigenschaft-Syntax. Der Compiler generiert den Speicherort für das Feld, das die Eigenschaft sichert. Der Compiler implementiert außerdem den Text der get- und set-Accessoren.

In einigen Fällen müssen Sie eine Eigenschaft auf einen anderen Wert als den Standardwert für seinen Datentyp initialisieren. C# ermöglicht dies, indem nach der schließenden Klammer für die Eigenschaft ein Wert festgelegt wird. Möglicherweise bevorzugen Sie den Anfangswert für die Eigenschaft FirstName als leere Zeichenfolge und nicht null. Sie würden dies wie unten dargestellt angeben:

public class Person
{
    public string FirstName { get; set; } = string.Empty;

    // Omitted for brevity.
}

Die bestimmte Initialisierung eignet sich am besten für schreibgeschützte Eigenschaften, wie Sie später in diesem Artikel sehen werden.

Sie können den Speicher auch selbst definieren, wie unten dargestellt:

public class Person
{
    public string? FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
    private string? _firstName;

    // Omitted for brevity.
}

Wenn die Implementierung einer Eigenschaft ein einzelner Ausdruck ist, können Sie Ausdruckskörpermember für die Getter oder Setter verwenden:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = value;
    }
    private string? _firstName;

    // Omitted for brevity.
}

Diese vereinfachte Syntax wird in diesem Artikel immer dann verwendet, wenn es sich anbietet.

Die oben gezeigte Eigenschaftendefinition ist eine Schreib-Lese-Eigenschaft. Beachten Sie das Schlüsselwort value im set-Accessor. Der set-Accessor verfügt immer über einen einzelnen Parameter namens value. Der get-Accessor muss einen Wert zurückgeben, der in den Typ der Eigenschaft konvertiert werden kann (string in diesem Beispiel).

Das sind die Grundlagen der Syntax. Es gibt viele verschiedene Varianten, die eine Vielzahl von verschiedenen Entwürfen unterstützen. Lassen Sie uns diese erforschen, und lernen Sie die Syntaxoptionen für jede kennen.

Überprüfen

In den Beispielen oben wurde eine der einfachsten Fälle von Eigenschaftendefinition gezeigt: eine Schreib-Lese-Eigenschaft ohne Überprüfung. Durch das Schreiben des Codes, den Sie in den get- und set-Accessoren möchten, können Sie viele verschiedene Szenarios erstellen.

Sie können Code im set-Accessor schreiben, um sicherzustellen, dass die durch eine Eigenschaft dargestellten Werte immer gültig sind. Angenommen, eine Regel für die Person-Klasse besagt, dass der Name nicht leer sein und keinen Leerraum enthalten darf. Sie würden das wie folgt schreiben:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            _firstName = value;
        }
    }
    private string? _firstName;

    // Omitted for brevity.
}

Das vorausgehende Beispiel kann unter Verwendung eines throw-Ausdrucks als Teil der Validierung des Eigenschaftensetters vereinfacht werden:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
    }
    private string? _firstName;

    // Omitted for brevity.
}

Das obige Beispiel erzwingt die Regel, dass der Vorname nicht leer sein und keinen Leerraum enthalten darf. Wenn ein Entwickler schreibt

hero.FirstName = "";

Die Zuweisung löst eine ArgumentException aus. Da der set-Accessor einen void-Rückgabetyp aufweisen muss, melden Sie Fehler im set-Accessor durch Auslösen einer Ausnahme.

Sie können die gleiche Syntax auf alles andere in Ihrem Szenario erweitern, was benötigt wird. Sie können die Beziehungen zwischen unterschiedlichen Eigenschaften oder gegen externe Bedingungen überprüfen. Gültige C#-Anweisungen sind in einem Eigenschaftenaccessor gültig.

Zugriffssteuerung

Bis zu diesem Zeitpunkt sind alle Eigenschaftsdefinitionen, die Sie gesehen haben Lese-/Schreibeigenschaften mit öffentlichen Accessoren. Dies sind nicht die einzige gültigen Eingabehilfen für Eigenschaften. Sie können schreibgeschützte Eigenschaften erstellen, oder den set- und get-Accessoren verschiedene Eingabehilfen geben. Angenommen, Ihre Person-Klasse sollte nur die Änderung des Werts der FirstName-Eigenschaft von anderen Methoden in dieser Klasse ermöglichen. Sie konnten dem set-Accessor private-Eingabehilfen anstelle von public geben:

public class Person
{
    public string? FirstName { get; private set; }

    // Omitted for brevity.
}

Nun, kann auf die FirstName-Eigenschaft von einem beliebigen Code zugegriffen werden, aber es kann nur von einem anderem Code in der Person-Klasse zugewiesen werden.

Sie können einen restriktiven Zugriffsmodifizierer zum set- oder get-Accessor hinzufügen. Jeder Zugriffsmodifizierer, den Sie auf den einzelnen Accessor platzieren, muss eingeschränkter sein als der Zugriffsmodifizierer für die Eigenschaftsdefinition. Das Obige ist zulässig, da die FirstName-Eigenschaft public ist, aber der set-Accessor ist private. Sie können keine private-Eigenschaft mit einer public-Zugriffsmethode deklarieren. Eigenschaftendeklarationen können ebenfalls als protected, internal, protected internal oder sogar private deklariert werden.

Es ist auch zulässig, den restriktiveren Modifizierer in die get-Zugriffsmethode einzufügen. Sie verfügen z.B. über eine public-Eigenschaft, schränken jedoch den get-Accessor auf private ein. Dieses Szenario wird in der Praxis nur selten ausgeführt.

Schreibgeschützt

Sie können auch Änderungen an einer Eigenschaft beschränken, sodass sie nur in einem Konstruktor oder einem Eigenschafteninitialisierer festgelegt werden kann. Sie können die Person-Klasse daher wie folgt ändern:

public class Person
{
    public Person(string firstName) => FirstName = firstName;

    public string FirstName { get; }

    // Omitted for brevity.
}

Nur „init“

Im obigen Beispiel müssen Aufrufer den Konstruktor verwenden, der den FirstName-Parameter enthält. Aufrufer können keine Objektinitialisierer verwenden, um der Eigenschaft einen Wert zuzuweisen. Um Initialisierer zu unterstützen, können Sie die set-Zugriffsmethode zu einer init-Zugriffsmethode machen, wie im folgenden Code gezeigt:

public class Person
{
    public Person() { }
    public Person(string firstName) => FirstName = firstName;

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

Im obigen Beispiel kann ein Aufrufer mit dem Standardkonstruktor eine Person erstellen, auch wenn der betreffende Code nicht die FirstName-Eigenschaft festgelegt. Ab C# 11 können Sie vorschreiben, dass Aufrufer diese Eigenschaft festlegen:

public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName) => FirstName = firstName;

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Der obige Code nimmt zwei Ergänzungen an der Person-Klasse vor. Erstens enthält die Deklaration der FirstName-Eigenschaft den required-Modifizierer. Das bedeutet, dass jeder Code, der eine neue Person erstellt, diese Eigenschaft festlegen muss. Zweitens verfügt der Konstruktor, der einen firstName-Parameter annimmt, über das System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute-Attribut. Dieses Attribut informiert den Compiler darüber, dass dieser Konstruktor allerequired-Member festlegt.

Wichtig

Verwechseln Sie required nicht mit non-nullable. Es ist zulässig, eine required-Eigenschaft auf null oder default festzulegen. Wenn der Typ non-nullable ist, wie string in diesen Beispielen, gibt der Compiler eine Warnung aus.

Aufrufer müssen entweder den Konstruktor mit SetsRequiredMembers verwenden oder die FirstName-Eigenschaft mit einem Objektinitialisierer festlegen, wie im folgenden Code gezeigt:

var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//person = new VersionNinePoint2.Person();

Berechnete Eigenschaften

Eine Eigenschaft muss nicht einfach den Wert eines Memberfelds zurückgeben. Sie können Eigenschaften erstellen, die einen berechneten Wert zurückgeben. Lassen Sie uns das Person-Objekt so erweitern, dass es den vollständigen Namen zurückgibt, berechnet durch die Verkettung des ersten und letzten Namens:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName { get { return $"{FirstName} {LastName}"; } }
}

Im Beispiel oben wird das Feature Zeichenfolgeninterpolation verwendet, um die formatierte Zeichenfolge für den vollständigen Namen zu erstellen.

Sie können auch einen Ausdruckskörpermember verwenden, was eine kompaktere Möglichkeit zum Erstellen der berechneten FullName-Eigenschaft darstellt:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

Ausdruckskörpermember verwenden die Syntax von Lambdaausdrücken zum Definieren einer Methode, die einen einzelnen Ausdruck enthält. Hier gibt dieser Ausdruck den vollständigen Namen für das Person-Objekt zurück.

Zwischengespeicherte ausgewertete Eigenschaften

Sie können das Konzept einer berechneten Eigenschaft mit dem Speicher mischen und eine zwischengespeicherte ausgewertete Eigenschaft erstellen. Sie können z.B. die FullName-Eigenschaft so aktualisieren, dass die Zeichenfolgenformatierung nur beim ersten Zugriff umgesetzt wird:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

Der obige Code enthält jedoch einen Fehler. Wenn der Code den Wert der FirstName- oder LastName-Eigenschaft aktualisiert, ist das zuvor ausgewertete fullName-Feld ungültig. Sie müssen die set-Accessoren der FirstName- und LastName-Eigenschaft aktualisieren, damit das fullName-Feld erneut berechnet wird:

public class Person
{
    private string? _firstName;
    public string? FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

    private string? _lastName;
    public string? LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            _fullName = null;
        }
    }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

Diese endgültige Version wertet die FullName-Eigenschaft nur bei Bedarf aus. Wenn die zuvor berechnete Version gültig ist, wird sie verwendet. Wenn eine andere Änderung des Zustands die zuvor berechnete Version ungültig macht, wird sie neu berechnet. Entwickler, die diese Klasse verwenden, müssen die Details der Implementierung nicht kennen. Keine dieser interne Änderungen hat Einfluss auf die Verwendung des Person-Objekts. Das ist der Hauptgrund für die Verwendung von Eigenschaften, um Datenmember eines Objekts verfügbar zu machen.

Anfügen von Attributen an automatisch implementierte Eigenschaften

Es ist möglich, Feldattribute an das vom Compiler generierte Unterstützungsfeld in den automatisch implementierten Eigenschaften anzufügen. Sie sollten z.B. eine Überarbeitung der Person-Klasse hinzufügen, die eine eindeutige Eigenschaft des Integers Id hinzufügt. Schreiben Sie die Id-Eigenschaft unter Verwendung einer automatisch implementierten Eigenschaft. Für Ihren Entwurf ist es jedoch nicht erforderlich, die Id-Eigenschaft zu speichern. Das NonSerializedAttribute-Attribut kann nur an Felder, aber nicht an Eigenschaften angefügt werden. Sie können wie im folgenden Beispiel dargestellt das NonSerializedAttribute-Attribut für die Id-Eigenschaft an das Unterstützungsfeld unter Verwendung des field:-Spezifizierers des Attributs anfügen:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    [field:NonSerialized]
    public int Id { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

Diese Technik funktioniert für jedes beliebige Attribut, das Sie an das Unterstützungsfeld für die automatisch implementierte Eigenschaft anfügen.

Implementiert INotifyPropertyChanged

Ein abschließendes Szenario, in dem Sie Code in einem Eigenschaftenaccessor schreiben müssen, ist zur Unterstützung der INotifyPropertyChanged-Schnittstelle, die verwendet wird, um Clients mit Datenbindung zu benachrichtigen, dass ein Wert geändert wurde. Wenn sich der Wert einer Eigenschaft ändert, löst das Objekt das INotifyPropertyChanged.PropertyChanged-Ereignis aus, um die Änderung anzuzeigen. Die Bibliotheken mit Datenbindung wiederum aktualisieren die auf dieser Änderung basierenden Anzeigeelemente. Der folgende Code zeigt, wie Sie INotifyPropertyChanged für die FirstName-Eigenschaft dieser Person-Klasse implementieren würden.

public class Person : INotifyPropertyChanged
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            if (value != _firstName)
            {
                _firstName = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(FirstName)));
            }
        }
    }
    private string? _firstName;

    public event PropertyChangedEventHandler? PropertyChanged;
}

Der Operator ?. wird bedingter NULL-Operator genannt. Es sucht vor der Auswertung der rechten Seite des Operators nach einem NULL-Verweis. Das Endergebnis bedeutet, dass, wenn sich keine Abonnenten im PropertyChanged-Ereignis befinden, der Code zum Auslösen des Ereignisses nicht ausgeführt wird. Er würde eine NullReferenceException auslösen, ohne diese in diesem Fall zu überprüfen. Weitere Informationen finden Sie unter events. In diesem Beispiel wird auch der neue nameof-Operator verwendet, um vom Symbol des Eigenschaftennamen in die Textdarstellung zu konvertieren. Mithilfe von nameof können Fehler reduziert werden, bei denen Sie den Namen der Eigenschaft falsch eingegeben haben.

Erneut ist die Implementierung von INotifyPropertyChanged ein Beispiel für einen Fall, in dem Sie Code in Ihren Accessoren schreiben können, um die benötigten Szenarios zu unterstützen.

Zusammenfassung

Eigenschaften sind eine Form intelligenter Felder in einer Klasse oder einem Objekt. Außerhalb des Objekts wirken diese wie Felder in dem Objekt. Eigenschaften können jedoch mithilfe der vollständigen Palette der C#-Funktionalität implementiert werden. Sie können Überprüfung, verschiedene Eingabehilfen, verzögerte Auswertung oder alle Anforderungen, die Ihre Szenarios benötigen, bereitstellen.