Propriedades

As propriedades são cidadãos de primeira classe em C#. A linguagem define a sintaxe que permite aos desenvolvedores escrever código que expressa com precisão sua intenção de design.

As propriedades se comportam como campos quando são acessadas. No entanto, ao contrário dos campos, as propriedades são implementadas com acessadores que definem as instruções executadas quando uma propriedade é acessada ou atribuída.

Sintaxe da propriedade

A sintaxe das propriedades é uma extensão natural dos campos. Um campo define um local de armazenamento:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

Uma definição de propriedade contém declarações para um get e set acessador que recupera e atribui o valor dessa propriedade:

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

    // Omitted for brevity.
}

A sintaxe mostrada acima é a sintaxe de propriedade auto. O compilador gera o local de armazenamento para o campo que faz backup da propriedade. O compilador também implementa o corpo dos get e set acessadores.

Às vezes, você precisa inicializar uma propriedade para um valor diferente do padrão para seu tipo. O C# permite isso definindo um valor após a chave de fechamento para a propriedade. Você pode preferir que o valor inicial da FirstName propriedade seja a cadeia de caracteres vazia em vez de null. Você deve especificar isso como mostrado abaixo:

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

    // Omitted for brevity.
}

A inicialização específica é mais útil para propriedades somente leitura, como você verá mais adiante neste artigo.

Você também pode definir o armazenamento por conta própria, como mostrado abaixo:

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

    // Omitted for brevity.
}

Quando uma implementação de propriedade é uma única expressão, você pode usar membros com corpo de expressão para o getter ou setter:

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

    // Omitted for brevity.
}

Esta sintaxe simplificada será utilizada quando aplicável ao longo deste artigo.

A definição de propriedade mostrada acima é uma propriedade de leitura-gravação. Observe a palavra-chave value no acessador definido. O set acessador sempre tem um único parâmetro chamado value. O get acessador deve retornar um valor que seja conversível para o tipo da propriedade (string neste exemplo).

Esse é o básico da sintaxe. Existem muitas variações diferentes que suportam várias expressões de design diferentes. Vamos explorar e aprender as opções de sintaxe para cada um.

Validação

Os exemplos acima mostraram um dos casos mais simples de definição de propriedade: uma propriedade de leitura-gravação sem validação. Ao escrever o código desejado no get e set acessadores, você pode criar muitos cenários diferentes.

Você pode escrever código no set acessador para garantir que os valores representados por uma propriedade sejam sempre válidos. Por exemplo, suponha que uma regra para a Person classe é que o nome não pode estar em branco ou espaço em branco. Você escreveria o seguinte:

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

O exemplo anterior pode ser simplificado usando uma throw expressão como parte da validação do setter de propriedades:

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

O exemplo acima impõe a regra de que o primeiro nome não deve estar em branco ou espaço em branco. Se um desenvolvedor escrever

hero.FirstName = "";

Essa atribuição lança um ArgumentException. Como um acessador de conjunto de propriedades deve ter um tipo de retorno vazio, você relata erros no acessador de conjunto lançando uma exceção.

Você pode estender essa mesma sintaxe para qualquer coisa necessária em seu cenário. Você pode verificar as relações entre diferentes propriedades ou validar em relação a quaisquer condições externas. Todas as instruções C# válidas são válidas em um acessador de propriedade.

Controlo de acesso

Até este ponto, todas as definições de propriedade que você viu são propriedades de leitura/gravação com acessadores públicos. Essa não é a única acessibilidade válida para propriedades. Você pode criar propriedades somente leitura ou dar acessibilidade diferente ao conjunto e obter acessadores. Suponha que sua Person classe só deve habilitar a FirstName alteração do valor da propriedade de outros métodos nessa classe. Você pode dar ao acessor private definido acessibilidade em vez de public:

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

    // Omitted for brevity.
}

Agora, a FirstName propriedade pode ser acessada a partir de qualquer código, mas só pode ser atribuída a partir de outro código na Person classe.

Você pode adicionar qualquer modificador de acesso restritivo ao conjunto ou obter acessadores. Qualquer modificador de acesso colocado no acessador individual deve ser mais limitado do que o modificador de acesso na definição de propriedade. O acima é legal porque a FirstName propriedade é public, mas o acessador conjunto é private. Não foi possível declarar um private imóvel com acessório public . As declarações de propriedade também podem ser declaradas protected, internal, protected internal, ou, mesmo private.

Também é legal colocar o modificador mais restritivo no get acessador. Por exemplo, você pode ter uma public propriedade, mas restringir o get acessador a private. Esse cenário raramente é feito na prática.

Só de leitura

Você também pode restringir modificações a uma propriedade para que ela só possa ser definida em um construtor. Você pode modificar a classe da Person seguinte maneira:

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

    public string FirstName { get; }

    // Omitted for brevity.
}

Somente inicialização

O exemplo anterior requer que os chamadores usem o construtor que inclui o FirstName parâmetro. Os chamadores não podem usar inicializadores de objeto para atribuir um valor à propriedade. Para suportar inicializadores, você pode tornar o set acessador um init acessador, conforme mostrado no código a seguir:

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

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

O exemplo anterior permite que um chamador crie um Person usando o construtor padrão, mesmo quando esse código não define a FirstName propriedade. A partir do C# 11, você pode exigir que os chamadores definam essa propriedade:

public class Person
{
    public Person() { }

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

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

O código anterior faz duas adições à Person classe. Primeiro, a declaração de FirstName propriedade inclui o required modificador. Isso significa que qualquer código que crie um novo Person deve definir essa propriedade. Em segundo lugar, o construtor que usa um firstName parâmetro tem o System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute atributo. Este atributo informa o compilador que este construtor define todos osrequired membros.

Importante

Não confunda required com não-anulável. É válido definir uma required propriedade como null ou default. Se o tipo não for anulável, como string nesses exemplos, o compilador emitirá um aviso.

Os chamadores devem usar o construtor com SetsRequiredMembers ou definir a FirstName propriedade usando um inicializador de objeto, conforme mostrado no código a seguir:

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

Propriedades computadas

Uma propriedade não precisa simplesmente retornar o valor de um campo membro. Você pode criar propriedades que retornam um valor calculado. Vamos expandir o Person objeto para retornar o nome completo, calculado concatenando os nomes e sobrenomes:

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

    public string? LastName { get; set; }

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

O exemplo acima usa o recurso de interpolação de cadeia de caracteres para criar a cadeia de caracteres formatada para o nome completo.

Você também pode usar um membro com corpo de expressão, que fornece uma maneira mais sucinta de criar a propriedade computada FullName :

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

    public string? LastName { get; set; }

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

Membros com corpo de expressão usam a sintaxe de expressão lambda para definir métodos que contêm uma única expressão. Aqui, essa expressão retorna o nome completo do objeto pessoa.

Propriedades avaliadas em cache

Você pode misturar o conceito de uma propriedade computada com armazenamento e criar uma propriedade avaliada em cache. Por exemplo, você pode atualizar a FullName propriedade para que a formatação da cadeia de caracteres só tenha acontecido na primeira vez que foi acessada:

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

O código acima contém um bug embora. Se o FirstName código atualizar o valor da propriedade ou LastName , o campo avaliado fullName anteriormente será inválido. Você modifica os set acessadores da FirstName propriedade e LastName para que o fullName campo seja calculado novamente:

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

Esta versão final avalia a FullName propriedade apenas quando necessário. Se a versão calculada anteriormente for válida, ela será usada. Se outra alteração de estado invalidar a versão calculada anteriormente, ela será recalculada. Os desenvolvedores que usam essa classe não precisam saber os detalhes da implementação. Nenhuma dessas alterações internas afeta o uso do objeto Person. Esse é o principal motivo para usar Propriedades para expor membros de dados de um objeto.

Anexando atributos a propriedades implementadas automaticamente

Os atributos de campo podem ser anexados ao campo de suporte gerado pelo compilador em propriedades implementadas automaticamente. Por exemplo, considere uma revisão para a Person classe que adiciona uma propriedade inteira Id exclusiva. Você escreve a Id propriedade usando uma propriedade implementada automaticamente, mas seu design não exige a persistência da Id propriedade. O NonSerializedAttribute só pode ser anexado a campos, não propriedades. Você pode anexar o NonSerializedAttribute ao campo de suporte da Id propriedade usando o field: especificador no atributo, conforme mostrado no exemplo a seguir:

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

Essa técnica funciona para qualquer atributo anexado ao campo de suporte na propriedade implementada automaticamente.

Implementando INotifyPropertyChanged

Um cenário final em que você precisa escrever código em um acessador de propriedade é oferecer suporte INotifyPropertyChanged à interface usada para notificar clientes de vinculação de dados de que um valor foi alterado. Quando o valor de uma propriedade muda, o objeto gera o INotifyPropertyChanged.PropertyChanged evento para indicar a alteração. As bibliotecas de vinculação de dados, por sua vez, atualizam os elementos de exibição com base nessa alteração. O código abaixo mostra como você implementaria INotifyPropertyChanged para a FirstName propriedade dessa classe de pessoa.

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

O ?. operador é chamado de operador condicional nulo. Ele verifica se há uma referência nula antes de avaliar o lado direito do operador. O resultado final é que, se não houver inscritos para o PropertyChanged evento, o código para gerar o evento não será executado. Nesse caso, lançaria um NullReferenceException sem esse cheque. Para obter mais informações, veja events. Este exemplo também usa o novo nameof operador para converter do símbolo de nome da propriedade para sua representação de texto. O uso nameof pode reduzir os erros em que você digitou incorretamente o nome da propriedade.

Novamente, a implementação INotifyPropertyChanged é um exemplo de um caso em que você pode escrever código em seus acessadores para dar suporte aos cenários necessários.

Resumindo

As propriedades são uma forma de campos inteligentes em uma classe ou objeto. De fora do objeto, eles aparecem como campos no objeto. No entanto, as propriedades podem ser implementadas usando a paleta completa da funcionalidade C#. Você pode fornecer validação, acessibilidade diferente, avaliação preguiçosa ou quaisquer requisitos que seus cenários precisem.