属性,属性
无参属性
属性是类或对象中的一种智能字段形式。从对象外部,它们看起来像对象中的字段。 如下定义了一个属性Name,属性包含get和set访问器的声明。
public class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
为什么不直接使用类型的成员字段?
面向对象设计和编程的重要原则之一就是数据封装,类型的字段不应该公开,否则很容易因为不恰当使用字段而破坏对
象的状态。所以应提供公有的方法访问私有的字段,封装了字段访问的方法通常叫做访问器方法,访问器方法可以
对数据的合理性进行检查,确保对象的状态永远不被破坏。
下面,通过查看ildasm工具,了解编译器在定义属性时做了哪些工作。
如上图所示,编译器生成了get和set访问器方法,并且在属性名之前自动附加了get_和set_前缀来命名方法。除此之外,还生成了一个属性的定义项(IL代码如下),在这个记录项中包含了一些flag、属性的类型以及对get和set访问器方法的引用,这些元数据信息可供编译器和其他工具使用。
.property instance string Name()
{
.get instance string Property.Person::get_Name()
.set instance void Property.Person::set_Name(string)
} // end of property Person::Name
自动属性
如果只是为了封装一个支持字段而创建属性,C#提供了自动实现的属性(Automatically Implemented Property,AIP),语法如下,也可以键入prop+两个tab键快速生成自动属性。
public string Name { get; set; }
编译器会自动声明一个string类型的私有字段,并实现get_Name和set_Name方法。注意:使用AIP,属性是可读可写的。另外,如果需要显示实现get或set访问器方法,就必须同时显示实现。在C#6和更高版本,可以像字段一样初始化自动实现属性。
public string FirstName { get; set; } = "Mike";
对象和集合初始化器
一般要构造一个新对象并设置对象的一些公共属性的代码如下:
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Name = "Mike";
person.Age = 24;
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
为了简化这个常见的编程模型,C#提供了一种简单的初始化语法:
Person person = new Person { Name = "Mike", Age = 24 };
由于调用的是Person类的无参构造函数,这里省略了小括号,并在语句的结尾处的大括号内设置了person对象的Name和Age属性。
如果属性的类型实现了IEnumerable或IEnumerable<T>接口,属性就被认作是集合,为Person类添加Hobbies属性。
private List<string> hobbies = new List<string>();
public List<string> Hobbies
{
get { return hobbies; }
set { hobbies = value; }
}
构造Person对象,设置Name、Age和Hobbies属性:
Person person = new Person { Name = "Mike", Age = 24, Hobbies = { "reading", "swimming" } };
编译上述代码时,编译器发现Hobbies属性的类型是List<string>,该类型实现了IEnumerable<string>接口,然后生成代码来调用集合的Add方法。因此,上述代码会转换成:
Person person = new Person();
person.Name = "Mike";
person.Age = 24;
person.Hobbies.Add("reading");
person.Hobbies.Add("swimming");
匿名类型
使用C#的匿名类型,可以用简洁的语法来声明类型,如上节中定义的Person类型,可以使用如下代码:
var person = new { Name = "Mike", Age = 24, Hobbies = new List<string>() { "reading", "swimming" } };
下面通过ildasm工具查看这段程序的元数据信息,看看编译器都做了什么。
编译器会推断出每个表达式的类型,创建私有字段,为每个字段创建公共只读属性,并创建一个构造函数来接受这些表达式,在构造函数代码中(IL代码如下),会用传入表达式的求值结果来初始化私有的只读字段。除此之外,编译器还会重写Object类的Equals、GetHashCode和ToString方法。
.method public hidebysig specialname rtspecialname
instance void .ctor(!'<Name>j__TPar' Name,
!'<Age>j__TPar' Age,
!'<Hobbies>j__TPar' Hobbies) cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 )
// Code size 28 (0x1c)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld !0 class '<>f__AnonymousType0`3'<!'<Name>j__TPar',!'<Age>j__TPar',!'<Hobbies>j__TPar'>::'<Name>i__Field'
IL_000d: ldarg.0
IL_000e: ldarg.2
IL_000f: stfld !1 class '<>f__AnonymousType0`3'<!'<Name>j__TPar',!'<Age>j__TPar',!'<Hobbies>j__TPar'>::'<Age>i__Field'
IL_0014: ldarg.0
IL_0015: ldarg.3
IL_0016: stfld !2 class '<>f__AnonymousType0`3'<!'<Name>j__TPar',!'<Age>j__TPar',!'<Hobbies>j__TPar'>::'<Hobbies>i__Field'
IL_001b: ret
} // end of method '<>f__AnonymousType0`3'::.ctor
到这里我们已经大概了解了匿名类型的使用方法,下面请看这段代码:
var mike = new { Name = "Mike", Age = 24, Hobbies = new List<string>() { "reading", "swimming" } };
var jack = new { Name = "Jack", Age = 31, Hobbies = new List<string>() { "coding", "playing basketball" } };
那么问题来了,这段代码究竟会生成几个类呢?查看元数据信息,答案是一个。看来编译器还是很智能的,对于具有相同结构的匿名类型,只会创建一个类型的定义。这时我们再来做个试验,把Name和Age的属性调换一下位置。
var mike = new { Name = "Mike", Age = 24, Hobbies = new List<string>() { "reading", "swimming" } };
var jack = new { Age = 31, Name = "Jack", Hobbies = new List<string>() { "coding", "playing basketball" } };
编译后再次查看元数据,发现这时生成了两个类型的定义。
结论:当定义多个匿名类型时,如果每个属性都有相同的类型和名称,并且这些属性指定的顺序完全相同,那么它只会创建一个匿名类型的定义。
由于编译器还有这种操作,为我们使用匿名类型定义一组对象构成集合提供了可能性,没错,我说的就是配合LINQ的select操作符构建对象的集合,示例如下:
class Employee
{
public string EmployeeId { get; private set; }
public string EmployeeName { get; private set; }
public int DepartmentId { get; private set; }
public int Age { get; private set; }
public int Salary { get; private set; }
public Employee(string employeeId, string employeeName, int departmentId, int age, int salary)
{
this.EmployeeId = employeeId;
this.EmployeeName = employeeName;
this.DepartmentId = departmentId;
this.Age = age;
this.Salary = salary;
}
}
class Department
{
public int DepartmentId { get; private set; }
public string DepartmentName { get; private set; }
public Department(int departmentId, string departmentName)
{
this.DepartmentId = departmentId;
this.DepartmentName = departmentName;
}
}
static void Main(string[] args)
{
List<Employee> employees = new List<Employee>
{
new Employee("001","Mike",2,24,30000),
new Employee("002","Jack",1,34,40998),
new Employee("003","Altony",2,26,58888),
new Employee("004","Carl",3,32,350000),
new Employee("005","Duncan",1,34,100000)
};
List<Department> departments = new List<Department>
{
new Department(1,"HR"),
new Department(2,"IT"),
new Department(3,"FT"),
};
var query = from q in employees
join t in departments on q.DepartmentId equals t.DepartmentId
where q.Age > 30
select new
{
Name = q.EmployeeName, Age = q.Age, DepartmentName = t.DepartmentName
}; //匿名类型的对象构成的集合
foreach (var employee in query)
{
Console.WriteLine($"EmployeeName= {employee.Name},Age={employee.Age},DepartmentName={employee.DepartmentName}");
}
Console.ReadLine();
}
有参属性
上一节中,属性的get访问器方法不接受参数,因此称为无参属性。除此之外,C#还支持有参属性(也称为索引器)。
class SampleCollection
{
private int[] arr = new int[10];
public int this[int i]
{
get
{
return arr[i] * 100 + 300;
}
set
{
arr[i] = value;
}
}
}
static void Main(string[] args)
{
SampleCollection sample = new SampleCollection();
sample[0] = 100;
Console.WriteLine(sample[0]); //Output:10300
Console.ReadLine();
}
根据以上示例,能够看出:
- 和无参属性类似,索引器也需要定义get,set访问器,并且在set访问器中可以使用value关键字。
- 索引器的get访问器需要接受参数。
- 使用this关键字定义索引器。
老样子,查看程序集元数据信息。
编译器生成的get/set访问器方法的默认名称为get/set_Item,C#允许向索引器应用定制特性来重命名这些方法。
class SampleCollection
{
private int[] arr = new int[10];
[System.Runtime.CompilerServices.IndexerName("Answer")]
public int this[int i]
{
get
{
return arr[i] * 100 + 300;
}
set
{
arr[i] = value;
}
}
}