使用初始值设定项和复制构造函数实例化对象

已完成

对象初始值设定项和复制构造函数是实例化 C# 中的对象的两种方法。 对象初始值设定项允许在创建时将值分配给对象的任何可访问字段或属性,而无需调用构造函数后跟赋值语句行。 复制构造函数允许通过复制现有对象的值来创建新对象。

使用对象初始值设定项和复制构造函数可帮助你编写更简洁且可读的代码。

对象初始值设定项

对象初始值设定项允许在创建时将值分配给对象的任何可访问字段或属性,而无需调用构造函数后跟赋值语句行。 使用对象初始值设定项语法可以指定构造函数的参数或省略参数(和括号语法)。

可以使用对象初始值设定项以声明性方式初始化类型对象,而无需显式调用类型的构造函数。

编译器首先访问无参数实例构造函数,然后处理成员初始化来处理对象初始值设定项。 因此,如果无参数构造函数在类中声明为私有构造函数,则需要公共访问的对象初始值设定项将失败。

以下示例演示如何使用对象初始值设定项。

第一个代码示例显示名为 Cat的类的类定义。 定义包括两个构造函数,其中一个是无参数构造函数。 第二个代码示例演示如何使用对象初始值设定项实例化 Cat 对象。 对象初始值设定项将值分配给 Age 对象的 NameCat 属性。


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

下面是另一个示例,演示如何使用对象初始值设定项初始化新的 StudentName 类型。 此示例设置 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;
    }
}

通过对象初始值设定项语法,可以创建一个实例,并在该实例之后将新创建的对象及其分配的属性分配给赋值中的变量。

除了分配字段和属性之外,对象初始值设定项还可以设置索引器。 请考虑以下基本 Matrix 类:


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

可以使用以下代码初始化标识矩阵:


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

以下示例定义一个 BaseballTeam 类,该类使用索引器获取和设置位于不同位置的玩家。 初始值设定项可以根据位置的缩写或棒球记分卡上每个位置使用的编号来分配玩家:


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

具有所需修饰符的对象初始值设定项

使用必需的关键字强制调用方使用对象初始值设定项设置属性或字段的值。 不需要将所需的属性设置为构造函数参数。 编译器可确保所有调用方初始化这些值。

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

}

这是一种典型的做法,可以保证正确初始化对象,尤其是在有多个字段或属性要管理和不希望将它们全部包含在构造函数中时。

具有 init 访问器的对象初始值设定项

确保没有人可以使用 init 访问器来限制设计对象。 它有助于限制属性值的设置。


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!

必需的仅初始化属性支持不可变结构,同时允许类型用户使用自然语法。

具有类类型属性的对象初始值设定项

在初始化对象时,必须考虑类类型属性的含义:


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
}

下一个示例演示使用包含和不使用参数的构造函数执行构造函数和成员初始化的顺序:


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

复制构造函数

在下面的示例中,Person 类定义复制构造函数,该构造函数采用其参数作为 Person实例。 参数的属性值分配给 Person的新实例的属性。 该代码包含一个备用复制构造函数,该构造函数发送要复制到类的实例构造函数的 NameAge 属性。 Person 类是密封的,因此无法声明派生类型,仅复制基类可能会引发错误。


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