Właściwości

Właściwości są obywatelami pierwszej klasy w języku C#. Język definiuje składnię, która umożliwia deweloperom pisanie kodu, który dokładnie wyraża ich intencję projektowania.

Właściwości zachowują się jak pola, gdy są one dostępne. Jednak w przeciwieństwie do pól właściwości są implementowane za pomocą metod dostępu, które definiują instrukcje wykonywane, gdy właściwość jest uzyskiwana lub przypisywana.

Składnia właściwości

Składnia właściwości to naturalne rozszerzenie pól. Pole definiuje lokalizację magazynu:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

Definicja właściwości zawiera deklaracje metody get i set , która pobiera i przypisuje wartość tej właściwości:

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

    // Omitted for brevity.
}

Składnia pokazana powyżej to składnia właściwości automatycznej. Kompilator generuje lokalizację magazynu dla pola, które tworzy kopię zapasową właściwości. Kompilator implementuje również treść elementów get i set metod dostępu.

Czasami należy zainicjować właściwość na wartość inną niż domyślna dla jej typu. Język C# umożliwia ustawienie wartości po zamykającym nawiasie klamrowym dla właściwości . Możesz preferować początkową wartość FirstName właściwości jako pusty ciąg, a nie null. Należy to określić, jak pokazano poniżej:

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

    // Omitted for brevity.
}

Konkretna inicjalizacja jest najbardziej przydatna w przypadku właściwości tylko do odczytu, jak zobaczysz w dalszej części tego artykułu.

Możesz również zdefiniować magazyn samodzielnie, jak pokazano poniżej:

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

    // Omitted for brevity.
}

Gdy implementacja właściwości jest pojedynczym wyrażeniem, można użyć składowych wyrażeń dla elementu getter lub setter:

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

    // Omitted for brevity.
}

Ta uproszczona składnia będzie używana w stosownych przypadkach w tym artykule.

Definicja właściwości pokazana powyżej jest właściwością read-write. Zwróć uwagę na słowo kluczowe value w zestawie dostępu. Akcesorium set zawsze ma jeden parametr o nazwie value. Akcesorium get musi zwrócić wartość, która jest konwertowana na typ właściwości (string w tym przykładzie).

To są podstawy składni. Istnieje wiele różnych odmian, które obsługują różne idiomy projektowe. Przyjrzyjmy się i poznajmy opcje składni dla każdego z nich.

Walidacja

W powyższych przykładach pokazano jeden z najprostszych przypadków definicji właściwości: właściwość read-write bez walidacji. Pisząc odpowiedni kod w elementach get i set metod dostępu, można utworzyć wiele różnych scenariuszy.

Możesz napisać kod w metodzie set dostępu, aby upewnić się, że wartości reprezentowane przez właściwość są zawsze prawidłowe. Załóżmy na przykład, że jedną regułą dla Person klasy jest to, że nazwa nie może być pusta ani nie może zawierać odstępu. Napiszesz to w następujący sposób:

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

Powyższy przykład można uprościć przy użyciu throw wyrażenia w ramach weryfikacji ustawiającego właściwość:

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

Powyższy przykład wymusza regułę, że imię nie może być puste ani białe znaki. Jeśli deweloper pisze

hero.FirstName = "";

To przypisanie zgłasza błąd ArgumentException. Ponieważ metoda dostępu zestawu właściwości musi mieć typ zwracany void, zgłaszasz błędy w metodzie dostępu zestawu, zgłaszając wyjątek.

Tę samą składnię można rozszerzyć na wszystko, co jest potrzebne w danym scenariuszu. Relacje między różnymi właściwościami można sprawdzić lub zweryfikować pod kątem dowolnych warunków zewnętrznych. Wszystkie prawidłowe instrukcje języka C# są prawidłowe we właściwości dostępu.

Kontrola dostępu

Do tego momentu wszystkie widoczne definicje właściwości to właściwości odczytu/zapisu z publicznymi metodami dostępu. To nie jest jedyna prawidłowa dostępność właściwości. Można utworzyć właściwości tylko do odczytu lub zapewnić różne ułatwienia dostępu do zestawu i uzyskać metody dostępu. Załóżmy, że klasa Person powinna włączać tylko zmianę wartości FirstName właściwości z innych metod w tej klasie. Można nadać zestaw dostępu dostępu private zamiast public:

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

    // Omitted for brevity.
}

FirstName Teraz dostęp do właściwości można uzyskać z dowolnego kodu, ale można go przypisać tylko z innego kodu w Person klasie.

Możesz dodać dowolny restrykcyjny modyfikator dostępu do zestawu lub uzyskać metody dostępu. Każdy modyfikator dostępu umieszczany na poszczególnych metod dostępu musi być bardziej ograniczony niż modyfikator dostępu w definicji właściwości. Powyższe jest legalne, ponieważ FirstName właściwość to public, ale zestaw metod dostępu to private. Nie można zadeklarować private właściwości za pomocą public metody dostępu. Deklaracje właściwości można również zadeklarować protected, internal, protected internal, lub nawet private.

Legalne jest również umieszczenie bardziej restrykcyjnego modyfikatora na akcesorium get . Można na przykład mieć właściwość , ale ograniczyć metodę publicget dostępu do privatemetody . Ten scenariusz jest rzadko wykonywany w praktyce.

Tylko do odczytu

Można również ograniczyć modyfikacje do właściwości, aby można było ją ustawić tylko w konstruktorze. Klasę Person można zmodyfikować w następujący sposób:

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

    public string FirstName { get; }

    // Omitted for brevity.
}

Tylko inicjowanie

Powyższy przykład wymaga, aby wywołujący używali konstruktora zawierającego FirstName parametr . Obiekt wywołujący nie może użyć inicjatorów obiektów do przypisania wartości do właściwości. Aby obsługiwać inicjatory, można ustawić metodę setinit dostępu, jak pokazano w poniższym kodzie:

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

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

W poprzednim przykładzie obiekt wywołujący może utworzyć Person obiekt wywołujący przy użyciu konstruktora domyślnego, nawet jeśli ten kod nie ustawia FirstName właściwości. Począwszy od języka C# 11, można wymagać od wywołujących ustawienia tej właściwości:

public class Person
{
    public Person() { }

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

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Powyższy kod tworzy dwa dodatki do Person klasy. FirstName Najpierw deklaracja właściwości zawiera required modyfikator. Oznacza to, że każdy kod tworzący nowy Person musi ustawić tę właściwość. Po drugie, konstruktor, który przyjmuje firstName parametr, ma System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute atrybut . Ten atrybut informuje kompilator, że ten konstruktor ustawia wszystkierequired elementy członkowskie.

Ważne

Nie należy mylić required z wartością niepustą. Prawidłowe jest ustawienie required właściwości na null lub default. Jeśli typ jest niepusty, taki jak string w tych przykładach, kompilator wyświetla ostrzeżenie.

Wywołujące muszą użyć konstruktora z SetsRequiredMembers lub ustawić FirstName właściwość przy użyciu inicjatora obiektu, jak pokazano w poniższym kodzie:

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

Obliczone właściwości

Właściwość nie musi po prostu zwracać wartości pola członkowskiego. Możesz utworzyć właściwości zwracające obliczoną wartość. Rozwińmy obiekt, Person aby zwrócić pełną nazwę obliczoną przez połączenie imienia i nazwiska:

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

    public string? LastName { get; set; }

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

W powyższym przykładzie użyto funkcji interpolacji ciągów do utworzenia sformatowanego ciągu dla pełnej nazwy.

Można również użyć elementu członkowskiego wyrażeń, który zapewnia bardziej zwięzły sposób tworzenia obliczonej FullName właściwości:

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

    public string? LastName { get; set; }

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

Składowe typu wyrażenie-bodied używają składni wyrażenia lambda do definiowania metod zawierających pojedyncze wyrażenie. W tym miejscu to wyrażenie zwraca pełną nazwę obiektu osoby.

Właściwości ocenione w pamięci podręcznej

Można mieszać koncepcję obliczonej właściwości z magazynem i utworzyć buforowane oceniane właściwości. Można na przykład zaktualizować FullName właściwość tak, aby formatowanie ciągu miało miejsce tylko przy pierwszym uzyskiwaniu do niego dostępu:

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

Powyższy kod zawiera jednak usterkę. Jeśli kod aktualizuje wartość właściwości FirstName lub LastName , wcześniej oceniane fullName pole jest nieprawidłowe. Zmodyfikujesz setFirstName metody dostępu właściwości i LastName , aby fullName pole zostało ponownie obliczone:

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

Ta ostateczna FullName wersja ocenia właściwość tylko wtedy, gdy jest to konieczne. Jeśli wcześniej obliczona wersja jest prawidłowa, jest używana. Jeśli inna zmiana stanu unieważni poprzednio obliczoną wersję, zostanie ona ponownie obliczona. Deweloperzy korzystający z tej klasy nie muszą znać szczegółów implementacji. Żadne z tych zmian wewnętrznych nie ma wpływu na użycie obiektu Person. Jest to kluczowa przyczyna użycia właściwości w celu uwidocznienia elementów członkowskich danych obiektu.

Dołączanie atrybutów do właściwości implementowanych automatycznie

Atrybuty pól można dołączyć do pola kopii zapasowej wygenerowanego przez kompilator we właściwościach implementowanych automatycznie. Rozważmy na przykład poprawkę klasy Person , która dodaje unikatową właściwość całkowitą Id . Właściwość jest Id zapisywana przy użyciu właściwości zaimplementowanej automatycznie, ale projekt nie wywołuje utrwalania Id właściwości. Element NonSerializedAttribute może być dołączony tylko do pól, a nie właściwości. Możesz dołączyć NonSerializedAttribute element do pola tworzenia kopii zapasowej dla Id właściwości przy użyciu field: specyfikatora atrybutu, jak pokazano w poniższym przykładzie:

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

Ta technika działa w przypadku dowolnego atrybutu dołączanego do pola zapasowego we właściwości zaimplementowanej automatycznie.

Implementowanie metody INotifyPropertyChanged

Ostatnim scenariuszem, w którym należy napisać kod w metodzie dostępu do właściwości, jest obsługa interfejsu używanego INotifyPropertyChanged do powiadamiania klientów powiązania danych o zmianie wartości. Gdy wartość właściwości zmieni się, obiekt zgłasza INotifyPropertyChanged.PropertyChanged zdarzenie, aby wskazać zmianę. Biblioteki powiązań danych z kolei aktualizują elementy wyświetlania na podstawie tej zmiany. Poniższy kod pokazuje, jak zaimplementować INotifyPropertyChanged dla FirstName właściwości tej klasy osoby.

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

Operator ?. jest nazywany operatorem warunkowym o wartości null. Sprawdza odwołanie o wartości null przed oceną prawej strony operatora. Wynikiem końcowym jest to, że jeśli nie ma subskrybentów zdarzenia PropertyChanged , kod zgłaszający zdarzenie nie jest wykonywany. Zgłosiłoby NullReferenceException to bez tego sprawdzenia w tym przypadku. Aby uzyskać więcej informacji, zobacz events. W tym przykładzie użyto również nowego nameof operatora do konwersji z symbolu nazwy właściwości na jego reprezentację tekstową. Użycie nameof metody może zmniejszyć błędy, w których błędnie wtypowaliśmy nazwę właściwości.

Ponownie implementacja INotifyPropertyChanged to przykład przypadku, w którym można napisać kod w metodzie dostępu do obsługi potrzebnych scenariuszy.

Sumowanie

Właściwości są formą pól inteligentnych w klasie lub obiekcie. Z zewnątrz obiektu są one wyświetlane jak pola w obiekcie. Można jednak zaimplementować właściwości przy użyciu pełnej palety funkcji języka C#. Możesz zapewnić walidację, różne ułatwienia dostępu, leniwą ocenę lub wszelkie wymagania wymagane przez scenariusze.