Propriedades

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

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

Sintaxe de propriedade

A sintaxe para propriedades é uma extensão natural para os 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 acessador get e set 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 da propriedade automática. O compilador gera o local de armazenamento para o campo que dá suporte à propriedade. O compilador também implementa o corpo dos acessadores get e set.

À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 da propriedade. Você pode preferir que o valor inicial para a propriedade FirstName seja a cadeia de caracteres vazia em vez de null. Você deve especificar isso conforme 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á adiante neste artigo.

Você mesmo também pode definir o armazenamento, conforme 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 aptos para expressão para o getter ou setter:

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

    // Omitted for brevity.
}

Essa sintaxe simplificada será usada quando aplicável ao longo deste artigo.

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

Essas são as noções básicas sobre a sintaxe. Há muitas variações diferentes que oferecem suporte a uma variedade de linguagens de design diferentes. Vamos explorá-las e conhecer as opções de sintaxe para cada uma.

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 que você deseja nos acessadores get e set, você pode criar vários cenários diferentes.

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

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 expressão throw como parte da validação do setter da propriedade:

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 aplica a regra de que o nome não pode ser em branco ou espaço em branco. Se um desenvolvedor escreve

hero.FirstName = "";

Essa atribuição lança uma ArgumentException. Como um acessador set de propriedade deve ter um tipo de retorno void, você relata erros no acessador set 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 qualquer condição externa. Todas as instruções de C# válidas são válidas em um acessador de propriedade.

Controle de acesso

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

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

    // Omitted for brevity.
}

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

Você pode adicionar qualquer modificador de acesso restritivo aos acessadores get ou set. Nenhum modificador de acesso que você colocar no acessador individual deve ser mais limitado que o modificador de acesso da definição de propriedade. O que está acima é válido porque a propriedade FirstName é public, mas o acessador set é private. Você não poderia declarar uma propriedade private com um acessador public. As declarações de propriedade também podem ser declaradas protected, internal, protected internal ou até mesmo private.

Também é válido colocar o modificador mais restritivo no acessador get. Por exemplo, você poderia ter uma propriedade public, mas restringir o acessador get como private. Esse cenário raramente acontece na prática.

Somente leitura

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

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

    public string FirstName { get; }

    // Omitted for brevity.
}

Somente init

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

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 que esse código não defina a propriedade FirstName. Começando com o 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 à classe Person. Primeiro, a declaração de propriedade FirstName inclui o modificador required. Isso significa que qualquer código que cria um novo Person deve definir essa propriedade. Em segundo lugar, o construtor que usa um parâmetro firstName tem o atributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute. Esse atributo informa ao compilador que esse construtor define todosrequired os membros.

Importante

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

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

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 de membro. Você pode criar propriedades que retornam um valor computado. Vamos expandir o objeto Person para retornar o nome completo, computado pela concatenação dos 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 do nome completo.

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

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

    public string? LastName { get; set; }

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

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

Propriedades avaliadas armazenadas em cache

Combine o conceito de uma propriedade computada com o armazenamento e crie uma propriedade avaliada armazenada em cache. Por exemplo, você poderia atualizar a propriedade FullName para que a formatação da cadeia de caracteres só acontecesse na primeira vez que ela 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;
        }
    }
}

No entanto, o código acima contém um bug. Se o código atualizar o valor das propriedades FirstName ou LastName, o campo fullName, anteriormente avaliado, será inválido. Modifique os acessadores set das propriedades FirstName e LastName para que o campo fullName 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 propriedade FullName 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 dos detalhes da implementação. Nenhuma dessas alterações internas afetam o uso do objeto Person. Esse é o motivo principal para o uso de propriedades para expor os membros de dados de um objeto.

Anexando atributos a propriedades autoimplementadas

Os atributos de campo podem ser anexados ao campo de suporte gerado pelo compilador em propriedades autoimplementadas. Por exemplo, considere uma revisão da classe Person que adiciona uma propriedade Id de inteiro exclusivo. A propriedade Id é escrita usando uma propriedade autoimplementada, mas o design não exige a persistência da propriedade Id. O NonSerializedAttribute pode ser anexado apenas a campos, não a propriedades. Anexe o NonSerializedAttribute ao campo de suporte da propriedade Id usando o especificador field: no atributo, conforme mostrado no seguinte exemplo:

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

Implementando INotifyPropertyChanged

A última situação em que você precisa escrever código em um acessador de propriedade é para oferecer suporte à interface INotifyPropertyChanged, usada para notificar os clientes de vinculação de dados que um valor foi alterado. Quando o valor de uma propriedade for alterado, o objeto aciona o evento INotifyPropertyChanged.PropertyChanged 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 a seguir mostra como você implementaria INotifyPropertyChanged para a propriedade FirstName dessa classe person.

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 uma referência nula antes de avaliar o lado direito do operador. O resultado final é que, se não houver nenhum assinante para o evento PropertyChanged, o código para acionar o evento não é executado. Ela lançaria uma NullReferenceException sem essa verificação, nesse caso. Para obter mais informações, consulte events. Este exemplo também usa o novo operador nameof para converter o símbolo de nome da propriedade em sua representação de texto. O uso de nameof pode reduzir erros no local em que o nome da propriedade foi digitado errado.

Novamente, a implementação de INotifyPropertyChanged é um exemplo de um caso em que você pode escrever o código nos 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, elas parecem como campos no objeto. No entanto, as propriedades podem ser implementadas usando a paleta completa de funcionalidades do C#. Você pode fornecer validação, acessibilidade diferente, avaliação lenta ou quaisquer requisitos necessários aos seus cenários.