Egenskaper

Egenskaper är förstklassiga medborgare i C#. Språket definierar syntax som gör det möjligt för utvecklare att skriva kod som korrekt uttrycker deras designavsikt.

Egenskaper fungerar som fält när de används. Men till skillnad från fält implementeras egenskaper med accessorer som definierar de instruktioner som körs när en egenskap används eller tilldelas.

Egenskapssyntax

Syntaxen för egenskaper är ett naturligt tillägg till fält. Ett fält definierar en lagringsplats:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

En egenskapsdefinition innehåller deklarationer för en get och-accessor set som hämtar och tilldelar värdet för den egenskapen:

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

    // Omitted for brevity.
}

Syntaxen som visas ovan är syntaxen för automatisk egenskap . Kompilatorn genererar lagringsplatsen för fältet som säkerhetskopierar egenskapen. Kompilatorn implementerar också brödtexten för get och set -accessorerna.

Ibland måste du initiera en egenskap till ett annat värde än standardvärdet för dess typ. C# aktiverar detta genom att ange ett värde efter den avslutande klammerparentesen för egenskapen. Du kanske föredrar att det ursprungliga värdet för egenskapen är den tomma strängen i stället nullför FirstName . Du anger det enligt nedan:

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

    // Omitted for brevity.
}

Specifik initiering är mest användbar för skrivskyddade egenskaper, som du ser senare i den här artikeln.

Du kan också definiera lagringen själv, enligt nedan:

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

    // Omitted for brevity.
}

När en egenskapsimplementering är ett enda uttryck kan du använda uttrycksbaserade medlemmar för getter eller setter:

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

    // Omitted for brevity.
}

Den här förenklade syntaxen används där det är tillämpligt i hela den här artikeln.

Egenskapsdefinitionen som visas ovan är en skrivskyddad egenskap. Lägg märke till nyckelordet value i den angivna accessorn. Accessorn set har alltid en enda parameter med namnet value. Accessorn get måste returnera ett värde som är konvertibelt till egenskapens typ (string i det här exemplet).

Det är grunderna i syntaxen. Det finns många olika varianter som stöder olika design-idiom. Nu ska vi utforska och lära oss syntaxalternativen för var och en.

Validering

Exemplen ovan visade ett av de enklaste fallen med egenskapsdefinition: en skrivskyddad egenskap utan validering. Genom att skriva den kod som du vill använda i get och set kan du skapa många olika scenarier.

Du kan skriva kod i set accessorn för att säkerställa att värdena som representeras av en egenskap alltid är giltiga. Anta till exempel att en regel för Person klassen är att namnet inte får vara tomt eller tomt utrymme. Du skulle skriva det på följande sätt:

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

Föregående exempel kan förenklas med hjälp av ett uttryck som en throw del av verifieringen av egenskapsuppsättningen:

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

Exemplet ovan tillämpar regeln att förnamnet inte får vara tomt eller tomt utrymme. Om en utvecklare skriver

hero.FirstName = "";

Tilldelningen genererar en ArgumentException. Eftersom en egenskapsuppsättningsåtkomst måste ha en ogiltig returtyp rapporterar du fel i den angivna accessorn genom att utlösa ett undantag.

Du kan utöka samma syntax till allt som behövs i ditt scenario. Du kan kontrollera relationerna mellan olika egenskaper eller verifiera mot eventuella externa villkor. Giltiga C#-instruktioner är giltiga i en egenskapsåtkomst.

Åtkomstkontroll

Fram tills nu är alla egenskapsdefinitioner som du har sett läs-/skrivegenskaper med offentliga accessorer. Det är inte den enda giltiga tillgängligheten för egenskaper. Du kan skapa skrivskyddade egenskaper eller ge olika hjälpmedel till uppsättningen och få åtkomst. Anta att klassen Person endast ska aktivera ändring av värdet för FirstName egenskapen från andra metoder i den klassen. Du kan ge den inställda åtkomstorn private hjälpmedel i stället publicför :

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

    // Omitted for brevity.
}

Nu kan egenskapen FirstName nås från valfri kod, men den kan bara tilldelas från annan kod i Person klassen.

Du kan lägga till alla restriktiva åtkomstmodifierare i antingen uppsättningen eller hämta accessorer. Alla åtkomstmodifierare som du placerar på den enskilda åtkomstgivaren måste vara mer begränsade än åtkomstmodifieraren i egenskapsdefinitionen. Ovanstående är lagligt eftersom egenskapen FirstName är public, men den angivna åtkomstorn är private. Det gick inte att deklarera en private egenskap med en public accessor. Egenskapsdeklarationer kan också deklareras protected, internal, protected internaleller till och med private.

Det är också lagligt att placera den mer restriktiva modifieraren på get accessorn. Du kan till exempel ha en public egenskap, men begränsa åtkomsten get till private. Det scenariot görs sällan i praktiken.

Skrivskydd

Du kan också begränsa ändringar av en egenskap så att den bara kan anges i en konstruktor. Du kan ändra klassen så Person här:

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

    public string FirstName { get; }

    // Omitted for brevity.
}

Endast init

Föregående exempel kräver att anropare använder konstruktorn som innehåller parametern FirstName . Anropare kan inte använda objektinitierare för att tilldela egenskapen ett värde. Om du vill stödja initialiserare kan du göra set accessorn till en init accessor, som du ser i följande kod:

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

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

I föregående exempel kan en anropare skapa en Person med hjälp av standardkonstruktorn, även om koden inte anger FirstName egenskapen. Från och med C# 11 kan du kräva att anropare anger den egenskapen:

public class Person
{
    public Person() { }

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

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Föregående kod gör två tillägg till Person klassen. Först innehåller egenskapsdeklarationen FirstNamerequired modifieraren. Det innebär att all kod som skapar en ny Person måste ange den här egenskapen. För det andra har konstruktorn som tar en firstName parameter attributet System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute . Det här attributet informerar kompilatorn om att konstruktorn anger allarequired medlemmar.

Viktigt!

Blanda inte ihop required med icke-nullable. Det är giltigt att ange en required egenskap till null eller default. Om typen inte är nullbar, till exempel string i de här exemplen, utfärdar kompilatorn en varning.

Anropare måste antingen använda konstruktorn med SetsRequiredMembers eller ange FirstName egenskapen med hjälp av en objektinitierare, enligt följande kod:

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

Beräknade egenskaper

En egenskap behöver inte bara returnera värdet för ett medlemsfält. Du kan skapa egenskaper som returnerar ett beräknat värde. Nu ska vi expandera Person objektet för att returnera det fullständiga namnet, beräknat genom att sammanfoga för- och efternamnen:

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

    public string? LastName { get; set; }

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

Exemplet ovan använder funktionen för stränginterpolering för att skapa den formaterade strängen för det fullständiga namnet.

Du kan också använda en expression-bodied-medlem, vilket ger ett mer kortfattat sätt att skapa den beräknade FullName egenskapen:

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

    public string? LastName { get; set; }

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

Uttrycksbaserade medlemmar använder lambda-uttryckssyntaxen för att definiera metoder som innehåller ett enda uttryck. Här returnerar uttrycket det fullständiga namnet på personobjektet.

Cachelagrade utvärderade egenskaper

Du kan blanda begreppet beräknad egenskap med lagring och skapa en cachelagrad utvärderad egenskap. Du kan till exempel uppdatera FullName egenskapen så att strängformateringen bara skedde första gången den användes:

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

Koden ovan innehåller dock ett fel. Om koden uppdaterar värdet för antingen FirstName egenskapen eller LastName är det tidigare utvärderade fullName fältet ogiltigt. Du ändrar åtkomsten setFirstName till egenskapen och LastName så att fältet fullName beräknas igen:

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

Den här slutliga versionen utvärderar endast egenskapen när det FullName behövs. Om den tidigare beräknade versionen är giltig används den. Om en annan tillståndsändring ogiltigförklarar den tidigare beräknade versionen beräknas den om. Utvecklare som använder den här klassen behöver inte känna till informationen om implementeringen. Ingen av dessa interna ändringar påverkar användningen av personobjektet. Det är den viktigaste orsaken till att använda Egenskaper för att exponera datamedlemmar i ett objekt.

Koppla attribut till automatiskt implementerade egenskaper

Fältattribut kan kopplas till det kompilatorgenererade bakgrundsfältet i automatiskt implementerade egenskaper. Överväg till exempel en revision av Person klassen som lägger till en unik heltalsegenskap Id . Du skriver Id egenskapen med hjälp av en automatiskt implementerad egenskap, men designen anropar inte för att Id bevara egenskapen. Kan NonSerializedAttribute endast kopplas till fält, inte egenskaper. Du kan koppla NonSerializedAttribute till bakgrundsfältet för Id egenskapen med hjälp field: av specificeraren för attributet, som du ser i följande exempel:

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

Den här tekniken fungerar för alla attribut som du kopplar till bakgrundsfältet på den automatiskt implementerade egenskapen.

Implementera INotifyPropertyChanged

Ett sista scenario där du behöver skriva kod i en egenskapsåtkomst är att stödja det gränssnitt som används för att meddela databindningsklienter INotifyPropertyChanged om att ett värde har ändrats. När värdet för en egenskap ändras genererar INotifyPropertyChanged.PropertyChanged objektet händelsen för att indikera ändringen. Databindningsbiblioteken uppdaterar i sin tur visningselement baserat på den ändringen. Koden nedan visar hur du implementerar INotifyPropertyChanged för egenskapen för den FirstName här personklassen.

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

Operatorn ?. kallas för villkorsoperatorn null. Den söker efter en null-referens innan du utvärderar operatorns högra sida. Slutresultatet är att om det inte finns några prenumeranter på PropertyChanged händelsen körs inte koden för att generera händelsen. Det skulle kasta en NullReferenceException utan denna kontroll i så fall. Mer information finns i events. Det här exemplet använder också den nya nameof operatorn för att konvertera från egenskapsnamnsymbolen till dess textrepresentation. Om du använder nameof kan du minska felen där du har skrivit fel namn på egenskapen.

Återigen är implementering INotifyPropertyChanged ett exempel på ett fall där du kan skriva kod i dina åtkomstorer för att stödja de scenarier du behöver.

Sammanfattningsvis

Egenskaper är en form av smarta fält i en klass eller ett objekt. Utanför objektet visas de som fält i objektet. Egenskaper kan dock implementeras med hjälp av den fullständiga paletten med C#-funktioner. Du kan tillhandahålla validering, olika hjälpmedel, lat utvärdering eller eventuella krav som dina scenarier behöver.