Sprawdzanie automatycznie zaimplementowanych właściwości

Zakończone

Standardowa składnia definiowania właściwości w języku C# zawiera get akcesorium i set akcesorium. Jeśli jednak w metodzie dostępu nie jest wymagana żadna inna logika, można użyć automatycznie zaimplementowanych właściwości.

Automatycznie zaimplementowane właściwości

Automatycznie zaimplementowane właściwości sprawiają, że deklaracja właściwości jest bardziej zwięzła, gdy żadna inna logika nie jest wymagana w metodach dostępu do właściwości. Umożliwiają one również kodowi klienta tworzenie obiektów. Po zadeklarowaniu właściwości, jak pokazano w poniższym przykładzie, kompilator tworzy prywatne, anonimowe pole zapasowe, do którego można uzyskać dostęp tylko za pośrednictwem get właściwości i set metod dostępu. init metody dostępu można również zadeklarować jako automatycznie zaimplementowane właściwości.

W poniższym przykładzie przedstawiono prostą klasę, która ma pewne automatycznie zaimplementowane właściwości:


// This class is mutable. Its data can be modified from
// outside the class.
public class Customer
{
    // Auto-implemented properties for trivial get and set
    public double TotalPurchases { get; set; }
    public string Name { get; set; }
    public int CustomerId { get; set; }

    // Constructor
    public Customer(double purchases, string name, int id)
    {
        TotalPurchases = purchases;
        Name = name;
        CustomerId = id;
    }

    // Methods
    public string GetContactInfo() { return "ContactInfo"; }
    public string GetTransactionHistory() { return "History"; }

    // .. Other methods, events, etc.
}

class Program
{
    static void Main()
    {
        // Initialize a new object.
        Customer cust1 = new Customer(4987.63, "Northwind", 90108);

        // Modify a property.
        cust1.TotalPurchases += 499.99;
    }
}

Automatycznie zaimplementowane właściwości z kopiami zapasowymi pól i deklarują pole kopii zapasowej wystąpienia prywatnego. Możesz zainicjować automatycznie zaimplementowane właściwości podobne do pól:


public string FirstName { get; set; } = "FirstName";

Klasa Customer pokazana w poprzednim przykładzie jest modyfikowalna. Kod klienta może zmienić wartości w obiektach po utworzeniu. W złożonych klasach, które zawierają znaczące zachowanie (metody) i dane, często konieczne jest posiadanie właściwości publicznych. Jednak w przypadku małych klas lub struktur, które po prostu hermetyzują zestaw wartości (danych) i mają niewielkie lub żadne zachowania, należy użyć jednej z następujących opcji, aby obiekty stały się niezmienne:

  • Zadeklaruj tylko metodę pobierania (niezmienną wszędzie z wyjątkiem konstruktora).
  • Zadeklaruj metodę dostępu get i akcesorium init (niezmienne wszędzie z wyjątkiem podczas budowy obiektu).
  • Zadeklaruj metodę dostępu zestawu jako prywatną (niezmienną dla odbiorców).

Może być konieczne dodanie walidacji do automatycznie zaimplementowanych właściwości. Język C# 13 dodaje właściwości oparte na polach jako funkcję w wersji zapoznawczej. Słowo kluczowe pola służy do uzyskiwania dostępu do syntetyzowanego pola zapasowego kompilatora automatycznie zaimplementowanej właściwości. Na przykład można upewnić się, że właściwość FirstName w poprzednim przykładzie nie może być ustawiona na wartość null lub pusty ciąg:


public string FirstName 
{ 
    get; 
    set 
    { 
        field = (string.IsNullOrWhiteSpace(value) is false
            ? value
            : throw new ArgumentException(nameof(value), "First name can't be whitespace or null"));
    }
} = "FirstName";

Ta funkcja umożliwia dodawanie logiki do metod dostępu bez konieczności jawnego deklarowania pola zapasowego. Słowo kluczowe pola służy do uzyskiwania dostępu do pola zapasowego wygenerowanego przez kompilator.

Ważny

Słowo kluczowe field to funkcja w wersji zapoznawczej w języku C# 13. Musisz użyć platformy .NET 9 i ustawić element <LangVersion>, aby preview w pliku projektu, aby użyć field kontekstowego słowa kluczowego.

Należy zachować ostrożność przy użyciu funkcji słowa kluczowego field w klasie, która ma pole o nazwie field. Nowe słowo kluczowe field cieniuje pole o nazwie field w zakresie metody dostępu właściwości. Możesz zmienić nazwę zmiennej field lub użyć tokenu @, aby odwołać się do identyfikatora field jako @field.

Właściwości z polami zapasowymi

Można mieszać koncepcję obliczonej właściwości z polem prywatnym i utworzyć keszowaną ocenioną właściwość. Na przykład zaktualizuj właściwość FullName, aby formatowanie ciągu odbywało się przy pierwszym dostępie:


public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public required string FirstName { get; init; }
    public required string LastName { get; init; }

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

Ta implementacja działa, ponieważ właściwości FirstName i LastName są tylko do odczytu. Użytkownicy mogą zmienić swoje imię i nazwisko. Zaktualizowanie właściwości FirstName i LastName w celu umożliwienia dostępu set wymaga unieważnienia wszystkich buforowanych wartości dla fullName. Zmodyfikujesz metody dostępu właściwości set i FirstName, aby pole LastName 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 wersja ocenia właściwość FullName tylko wtedy, gdy jest to konieczne. Poprzednio obliczona wersja jest używana, jeśli jest prawidłowa. W przeciwnym razie obliczenie aktualizuje buforowane wartości. 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.

Począwszy od języka C# 13, można utworzyć partial properties w klasach częściowych. Deklaracja implementowania właściwości partial nie może być automatycznie zaimplementowaną właściwością. Właściwość zaimplementowana automatycznie używa tej samej składni co deklarowanie deklaracji właściwości częściowej.

Implementowanie lekkiej klasy z automatycznie zaimplementowanymi właściwościami

Mogą wystąpić sytuacje, w których trzeba utworzyć niezmienną lekką klasę, która służy tylko do hermetyzacji zestawu automatycznie zaimplementowanych właściwości. Tego rodzaju konstrukcji zamiast struct należy używać semantyki typu odwołania.

Właściwość niezmienną można ustawić w następujący sposób:

  • Zadeklaruj tylko metodę dostępu get, która sprawia, że właściwość jest niezmienna wszędzie, z wyjątkiem konstruktora typu.
  • Zadeklaruj metodę dostępu init zamiast metody dostępu set, co sprawia, że właściwość ustawiana jest tylko w konstruktorze lub za pomocą inicjatora obiektu.
  • Zadeklaruj metodę dostępu set, aby private. Właściwość jest ustawiana w obrębie typu, ale jest niezmienna dla użytkowników.

Do deklaracji właściwości można dodać modyfikator required, aby wymusić ustawienie właściwości w ramach inicjowania nowego obiektu.

W poniższym przykładzie pokazano, jak właściwość z metodą get get różni się od właściwości get i private set.


class Contact
{
    public string Name { get; }
    public string Address { get; private set; }

    public Contact(string contactName, string contactAddress)
    {
        // Both properties are accessible in the constructor.
        Name = contactName;
        Address = contactAddress;
    }

    // Name isn't assignable here. This will generate a compile error.
    //public void ChangeName(string newName) => Name = newName;

    // Address is assignable here.
    public void ChangeAddress(string newAddress) => Address = newAddress;
}

W poniższym przykładzie przedstawiono dwa sposoby implementowania niezmiennej klasy przy użyciu automatycznie zaimplementowanych właściwości. Każdy sposób deklaruje jedną z właściwości z prywatnym set i jedną z właściwości tylko z get. Pierwsza klasa używa konstruktora tylko do inicjowania właściwości, a druga klasa używa statycznej metody fabryki, która wywołuje konstruktora.


// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// constructor to initialize its properties.
class Contact
{
    // Read-only property.
    public string Name { get; }

    // Read-write property with a private set accessor.
    public string Address { get; private set; }

    // Public constructor.
    public Contact(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;
    }
}

// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// static method and private constructor to initialize its properties.
public class Contact2
{
    // Read-write property with a private set accessor.
    public string Name { get; private set; }

    // Read-only property.
    public string Address { get; }

    // Private constructor.
    private Contact2(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;
    }

    // Public factory method.
    public static Contact2 CreateContact(string name, string address)
    {
        return new Contact2(name, address);
    }
}

public class Program
{
    static void Main()
    {
        // Some simple data sources.
        string[] names = ["Person One","Person Two", "Person Three",
                            "Person Four", "Person Five"];
        string[] addresses = ["123 Main St.", "345 Cypress Ave.", "678 1st Ave",
                                "12 108th St.", "89 E. 42nd St."];

        // Simple query to demonstrate object creation in select clause.
        // Create Contact objects by using a constructor.
        var query1 = from i in Enumerable.Range(0, 5)
                    select new Contact(names[i], addresses[i]);

        // List elements can't be modified by client code.
        var list = query1.ToList();
        foreach (var contact in list)
        {
            Console.WriteLine("{0}, {1}", contact.Name, contact.Address);
        }

        // Create Contact2 objects by using a static factory method.
        var query2 = from i in Enumerable.Range(0, 5)
                        select Contact2.CreateContact(names[i], addresses[i]);

        // Console output is identical to query1.
        var list2 = query2.ToList();

        // List elements can't be modified by client code.
        // CS0272:
        // list2[0].Name = "Person Six";
    }
}

/* Output:
    Person One, 123 Main St.
    Person Two, 345 Cypress Ave.
    Person Three, 678 1st Ave
    Person Four, 12 108th St.
    Person Five, 89 E. 42nd St.
*/

Kompilator tworzy pola zapasowe dla każdej automatycznie zaimplementowanej właściwości. Pola nie są dostępne bezpośrednio z kodu źródłowego.