Instanciar objetos usando inicializadores e construtores de cópia

Concluído

Inicializadores de objeto e construtores de cópia são duas maneiras de instanciar objetos em C#. Inicializadores de objeto permitem atribuir valores a quaisquer campos ou propriedades acessíveis de um objeto no momento da criação sem precisar invocar um construtor seguido por linhas de instruções de atribuição. Os construtores de cópia permitem que você crie um novo objeto copiando os valores de um objeto existente.

O uso de inicializadores de objeto e construtores de cópia pode ajudá-lo a escrever um código mais conciso e legível.

Inicializadores de objeto

Inicializadores de objeto permitem atribuir valores a quaisquer campos ou propriedades acessíveis de um objeto no momento da criação sem precisar invocar um construtor seguido por linhas de instruções de atribuição. A sintaxe do inicializador de objeto permite que você especifique argumentos para um construtor ou omita os argumentos (e sintaxe de parênteses).

Você pode usar inicializadores de objeto para inicializar objetos de tipo de maneira declarativa sem invocar explicitamente um construtor para o tipo.

O compilador processa inicializadores de objeto acessando primeiro o construtor de instância sem parâmetros e processando as inicializações de membro. Portanto, se o construtor sem parâmetros for declarado como privado na classe, os inicializadores de objeto que exigem acesso público falharão.

O exemplo a seguir demonstra como usar um inicializador de objeto.

O primeiro exemplo de código mostra a definição de classe para uma classe chamada Cat. A definição inclui dois construtores, um dos quais é um construtor sem parâmetros. O segundo exemplo de código mostra como criar uma instância de um objeto Cat usando um inicializador de objeto. O inicializador de objeto atribui valores às propriedades Age e Name do objeto Cat.


public class Cat
{
    // Automatically implemented properties.
    public int Age { get; set; }
    public string? Name { get; set; }

    public Cat()
    {
    }

    public Cat(string name)
    {
        this.Name = name;
    }
}


public class Program
{
    public static void Main()
    {
        // Declare and instantiate a new Cat object by using an object initializer.
        Cat cat = new Cat { Age = 10, Name = "Fluffy" };

        // Declare and instantiate a new Cat object by using an object initializer.
        Cat sameCat = new Cat("Fluffy") { Age = 10 };
    }
}

Aqui está outro exemplo que mostra como inicializar um novo tipo de StudentName usando inicializadores de objeto. Este exemplo define propriedades no tipo StudentName:


public class HowToObjectInitializers
{
    public static void Main()
    {
        // Declare a StudentName by using the constructor that has two parameters.
        StudentName student1 = new StudentName("Lisa", "Yeh");

        // Make the same declaration by using an object initializer and sending
        // arguments for the first and last names. The parameterless constructor is
        // invoked in processing this declaration, not the constructor that has
        // two parameters.
        StudentName student2 = new StudentName
        {
            FirstName = "Sandy",
            LastName = "Zoeng"
        };

        // Declare a StudentName by using an object initializer and sending
        // an argument for only the ID property. No corresponding constructor is
        // necessary. Only the parameterless constructor is used to process object
        // initializers.
        StudentName student3 = new StudentName
        {
            ID = 183
        };

        // Declare a StudentName by using an object initializer and sending
        // arguments for all three properties. No corresponding constructor is
        // defined in the class.
        StudentName student4 = new StudentName
        {
            FirstName = "Thomas",
            LastName = "Margand",
            ID = 116
        };

        Console.WriteLine(student1.ToString());
        Console.WriteLine(student2.ToString());
        Console.WriteLine(student3.ToString());
        Console.WriteLine(student4.ToString());
    }
    // Output:
    // Lisa  0
    // Sandy  0
    //   183
    // Thomas  116

    public class StudentName
    {
        // This constructor has no parameters. The parameterless constructor
        // is invoked in the processing of object initializers.
        // You can test this by changing the access modifier from public to
        // private. The declarations in Main that use object initializers will
        // fail.
        public StudentName() { }

        // The following constructor has parameters for two of the three
        // properties.
        public StudentName(string first, string last)
        {
            FirstName = first;
            LastName = last;
        }

        // Properties.
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public int ID { get; set; }

        // Override the ToString method of the Object class.
        public override string ToString() => FirstName + "  " + ID;
    }
}

A sintaxe dos inicializadores de objeto permite que você crie uma instância e, depois disso, atribui o objeto recém-criado, com suas propriedades atribuídas, à variável na atribuição.

Além de atribuir campos e propriedades, os inicializadores de objeto podem definir indexadores. Considere esta classe de Matrix básica:


public class Matrix
{
    private double[,] storage = new double[3, 3];

    public double this[int row, int column]
    {
        // The embedded array will throw out of range exceptions as appropriate.
        get { return storage[row, column]; }
        set { storage[row, column] = value; }
    }
}

Você pode inicializar a matriz de identidade com o seguinte código:


var identity = new Matrix
{
    [0, 0] = 1.0,
    [0, 1] = 0.0,
    [0, 2] = 0.0,

    [1, 0] = 0.0,
    [1, 1] = 1.0,
    [1, 2] = 0.0,

    [2, 0] = 0.0,
    [2, 1] = 0.0,
    [2, 2] = 1.0,
};

O exemplo a seguir define uma classe BaseballTeam que usa um indexador para obter e definir jogadores em posições diferentes. O inicializador pode atribuir jogadores, com base na abreviação da posição ou no número usado para cada posição em um scorecard de beisebol:


public class HowToIndexInitializer
{
    public class BaseballTeam
    {
        private string[] players = new string[9];
        private readonly List<string> positionAbbreviations = new List<string>
        {
            "P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"
        };

        public string this[int position]
        {
            // Baseball positions are 1 - 9.
            get { return players[position-1]; }
            set { players[position-1] = value; }
        }
        public string this[string position]
        {
            get { return players[positionAbbreviations.IndexOf(position)]; }
            set { players[positionAbbreviations.IndexOf(position)] = value; }
        }
    }

    public static void Main()
    {
        var team = new BaseballTeam
        {
            ["RF"] = "Lisa Yeh",
            [4] = "Sandy Zoeng",
            ["CF"] = "Thomas Margand"
        };

        Console.WriteLine(team["2B"]);
    }
}

Inicializadores de objeto com o modificador necessário

Use a palavra-chave necessária para forçar os chamadores a definir o valor de uma propriedade ou campo usando um inicializador de objeto. As propriedades necessárias não precisam ser definidas como parâmetros de construtor. O compilador garante que todos os chamadores inicializem esses valores.

// The `FirstName` property is optional and has a default value of an empty string.        
// The `LastName` property is required and must be initialized during object creation.
// You can create a new instance of the Person class using both properties.
var friend1 = new Person() { FirstName = "Lisa", LastName = "Yeh" };
Console.WriteLine($"Hello, {friend1.FirstName} {friend1.LastName}!");

// You can create a new instance of the Person class using only the LastName property.
var friend2 = new Person() { LastName = "Yeh"};
Console.WriteLine($"Hello, {friend2.FirstName} {friend2.LastName}!");

// You can assign a different value to the properties after the object is created.
friend2.FirstName = "Sandy";
friend2.LastName = "Chen";
Console.WriteLine($"Hello, {friend2.FirstName} {friend2.LastName}!");

// Compiler error: Required property 'LastName' must be initialized.
//var friend3 = new Person() { FirstName = "Lisa"};
//var friend4 = new Person();

// Output:
// Hello, Lisa Yeh!
// Hello,  Yeh!
// Hello, Sandy Chen!

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

}

É uma prática típica garantir que seu objeto seja inicializado corretamente, especialmente quando você tem vários campos ou propriedades para gerenciar e não deseja incluí-los todos no construtor.

Inicializadores de objeto com o acessador de inicialização

Garantir que ninguém altere o objeto projetado pode ser limitado usando um acessador de inicialização. Ele ajuda a restringir a configuração do valor da propriedade.


public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; init; }
}

// You can create a new instance of the Person class any combination of the properties.
var friend0 = new Person();
var friend1 = new Person() { FirstName = "Lisa" };
var friend2 = new Person() { LastName = "Yeh" };
var friend3 = new Person() { FirstName = "Lisa", LastName = "Yeh" };

Console.WriteLine($"Hello, {friend0.FirstName} {friend0.LastName}!");
Console.WriteLine($"Hello, {friend1.FirstName} {friend1.LastName}!");
Console.WriteLine($"Hello, {friend2.FirstName} {friend2.LastName}!");
Console.WriteLine($"Hello, {friend3.FirstName} {friend3.LastName}!");

// You can assign a different value to the FirstName property after the object is created, but not the LastName property.
friend3.FirstName = "Sandy";

// Compiler error:
// Error CS8852  Init - only property or indexer 'Person.LastName' can only be assigned in an object initializer,
//               or on 'this' or 'base' in an instance constructor or an 'init' accessor.
//friend3.LastName = "Chen";

Console.WriteLine($"Hello, {friend3.FirstName} {friend3.LastName}!");


// Output:
// Hello,  unknown!
// Hello, Lisa unknown!
// Hello,  Yeh!
// Hello, Lisa Yeh!
// Hello, Sandy Yeh!

As propriedades somente init necessárias dão suporte a estruturas imutáveis, permitindo a sintaxe natural para os usuários do tipo.

Inicializadores de objeto com propriedades tipadas por classe

É crucial considerar as implicações para propriedades tipadas em classe ao inicializar um objeto:


public class HowToClassTypedInitializer
{
    public class EmbeddedClassTypeA
    {
        public int I { get; set; }
        public bool B { get; set; }
        public string S { get; set; }
        public EmbeddedClassTypeB ClassB { get; set; }

        public override string ToString() => $"{I}|{B}|{S}|||{ClassB}";

        public EmbeddedClassTypeA()
        {
            Console.WriteLine($"Entering EmbeddedClassTypeA constructor. Values are: {this}");
            I = 3;
            B = true;
            S = "abc";
            ClassB = new() { BB = true, BI = 43 };
            Console.WriteLine($"Exiting EmbeddedClassTypeA constructor. Values are: {this})");
        }
    }

    public class EmbeddedClassTypeB
    {
        public int BI { get; set; }
        public bool BB { get; set; }
        public string BS { get; set; }

        public override string ToString() => $"{BI}|{BB}|{BS}";

        public EmbeddedClassTypeB()
        {
            Console.WriteLine($"Entering EmbeddedClassTypeB constructor. Values are: {this}");
            BI = 23;
            BB = false;
            BS = "BBBabc";
            Console.WriteLine($"Exiting EmbeddedClassTypeB constructor. Values are: {this})");
        }
    }

    public static void Main()
    {
        var a = new EmbeddedClassTypeA
        {
            I = 103,
            B = false,
            ClassB = { BI = 100003 }
        };
        Console.WriteLine($"After initializing EmbeddedClassTypeA: {a}");

        var a2 = new EmbeddedClassTypeA
        {
            I = 103,
            B = false,
            ClassB = new() { BI = 100003 } //New instance
        };
        Console.WriteLine($"After initializing EmbeddedClassTypeA a2: {a2}");
    }

    // Output:
    //Entering EmbeddedClassTypeA constructor Values are: 0|False||||
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)
    //After initializing EmbeddedClassTypeA: 103|False|abc|||100003|True|BBBabc
    //Entering EmbeddedClassTypeA constructor Values are: 0|False||||
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //After initializing EmbeddedClassTypeA a2: 103|False|abc|||100003|False|BBBabc
}

O exemplo a seguir mostra a ordem de execução de inicializações de construtor e membro usando construtores com e sem parâmetros:


public class ObjectInitializersExecutionOrder
{
    public static void Main()
    {
        new Person { FirstName = "Lisa", LastName = "Yeh", City = "unknown" };
        new Dog(2) { Name = "Oscar" };
    }

    public class Dog
    {
        private int age;
        private string name;

        public Dog(int age)
        {
            Console.WriteLine("Hello from Dog's non-parameterless constructor");
            this.age = age;
        }

        public required string Name
        {
            get { return name; }

            set
            {
                Console.WriteLine("Hello from setter of Dog's required property 'Name'");
                name = value;
            }
        }
    }

    public class Person
    {
        private string firstName;
        private string lastName;
        private string city;

        public Person()
        {
            Console.WriteLine("Hello from Person's parameterless constructor");
        }

        public required string FirstName
        {
            get { return firstName; }

            set
            {
                Console.WriteLine("Hello from setter of Person's required property 'FirstName'");
                firstName = value;
            }
        }

        public string LastName
        {
            get { return lastName; }

            init
            {
                Console.WriteLine("Hello from setter of Person's init property 'LastName'");
                lastName = value;
            }
        }

        public string City
        {
            get { return city; }

            set
            {
                Console.WriteLine("Hello from setter of Person's property 'City'");
                city = value;
            }
        }
    }

    // Output:
    // Hello from Person's parameterless constructor
    // Hello from setter of Person's required property 'FirstName'
    // Hello from setter of Person's init property 'LastName'
    // Hello from setter of Person's property 'City'
    // Hello from Dog's non-parameterless constructor
    // Hello from setter of Dog's required property 'Name'
}

Copiar construtores

No exemplo a seguir, a classe Person define um construtor de cópia que usa, como argumento, uma instância de Person. Os valores das propriedades do argumento são atribuídos às propriedades da nova instância de Person. O código contém um construtor de cópia alternativo que envia as propriedades Name e Age da instância que você deseja copiar para o construtor de instância da classe. A classe Person é lacrada, portanto, nenhum tipo derivado pode ser declarado que pode introduzir erros copiando apenas a classe base.


public sealed class Person
{
    // Copy constructor.
    public Person(Person previousPerson)
    {
        Name = previousPerson.Name;
        Age = previousPerson.Age;
    }

    //// Alternate copy constructor calls the instance constructor.
    //public Person(Person previousPerson)
    //    : this(previousPerson.Name, previousPerson.Age)
    //{
    //}

    // Instance constructor.
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public int Age { get; set; }

    public string Name { get; set; }

    public string Details()
    {
        return Name + " is " + Age.ToString();
    }
}

class TestPerson
{
    static void Main()
    {
        // Create a Person object by using the instance constructor.
        Person person1 = new Person("Lisa", 40);

        // Create another Person object, copying person1.
        Person person2 = new Person(person1);

        // Change each person's age.
        person1.Age = 39;
        person2.Age = 41;

        // Change person2's name.
        person2.Name = "Sandy";

        // Show details to verify that the name and age fields are distinct.
        Console.WriteLine(person1.Details());
        Console.WriteLine(person2.Details());

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
// Output:
// Lisa is 39
// Sandy is 41