用智能的编译器来防错
本章的主要内容:
- 自动实现的属性:编写由字段直接支持的简单属性, 不再显得臃肿不堪;
- 隐式类型的局部变量:根据初始值推断类型,简化局部变量的声明;
- 对象和集合初始化程序:用一个表达式就能创建和初始化对象;
- 隐式类型的数组:根据内容推断数组的类型,从而简化数组的创建过程;
- 匿名类型:允许创建新的临时类型来包含简单的属性;
自动实现的属性
这个特性简单的我都不想描述,但是为了保持内容的完整性,放一张图:
和匿名方法还有迭代器一样,它在编译器的帮助下会生成一个后备字段。
自动实现的属性是赋值和取值方法都是共有的,当然你还可以继续使用C#2私有赋值方法。
在实现自己的结构(struct)时,所有构造函数都必须显式的调用一下无参的构造函数,只有这样,编译器才知道所有的字段都被明确赋值了。因为这里有个类型初始化的顺序:
类或结构在初始化时的执行顺序,依次如下:
1: 子类静态变量
2: 子类静态构造函数
3: 子类非静态变量
4: 父类静态变量
5: 父类静态构造函数
6: 父类非静态变量
7: 父类构造函数
8: 子类构造函数
可以看到除了构造函数以外其他东西都是先初始化本身在初始化基类的。
隐式类型的局部变量
首先需要说明一点的时隐式类型只能用于局部变量,不能用于字段变量。
第二点是,C#仍然是一门静态的语言,只是要求编译器为你来推断变量的类型,在编译时仍然是类型静态的。
用var关键字来声明一个隐式类型的局部变量。
小小的总结:不是在所有情况下都能为所有变量使用隐式类型, 只有在以下情况下才能用它:
- 被声明的变量是一个局部变量, 而不是静态字段和实例字段;
- 变量在声明的同时被初始化;
- 初始化表达式不是方法组, 也不是匿名函数( 如果不进行强制类型转换);
- 初始化表达式不是 null;
- 语句中只声明了一个变量;
- 你希望变量拥有的类型是初始化表达式的编译时类型;
- 初始化表达式不包含正在声明的变量 。
隐式类型的局部变量也有一些不好的地方,有的时候你不得不仔细的判断它的类型。例如:
- var a = 2147483647;
- var b = 2147483648;
- var c = 4294967295;
- var d = 4294967296;
- var e = 9223372036854775807;
- var f = 9223372036854775808;
上面的这些变量的类型都不好在第一时间就判断出来。但是,有的时候你不得不用,比如要返回一个匿名的类型,只能这样写:var a=new {name="pangjianxin",age=10};这样的表达式你要用什么类型来引用呢?
简化的初始化
直接上代码。
public class Person { public string Name { get; } public int Age { get; set; } public List<Person> Persons { get; } = new List<Person>(); public Location Location { get; } = new Location(); public Person() { } public Person(string name) { this.Name = name; } } public class Location { public string City { get; set; } public string Street { get; set; } }
上面定义两个类,一个Person类,一个Location类。Person类中维护两个自动属性Name和Age,另外,还维护了两个只读属性Persons和Location。还有一个无参的构造函数和一个有参数的构造函数。
前面已经说过,类会在静态的和非静态的字段初始化后才会执行构造函数,属性本质上来说是一对get/set方法,不存在初始化。
看一下调用情况:
static void Main(string[] args) { Person p = new Person("pangianxin") { Age = 18, Location = { City = "baotou", Street = "gangtiedajie" } }; Console.WriteLine(p.Location.City); //p.Location=new Location();无法对Location进行初始化,因为他是只读的。
p.Location.City = "baotou ";
p.Location.Street = "gangtiedajie ";
Console.ReadKey();
}
上面使用了对象初始化程序来对对象进行初始化。
首先注意到的是p.Location是一个只读的属性。我们不能直接该给属性进行赋值,但是可以在取到这个属性的引用后,再对其进行赋值,在C#语言规范里面,这个叫做“设置一个嵌入对象的属性”。就是设置属性的属性。这样却没有了限制。
第二点是Location = { City = "baotou", Street = "gangtiedajie" }这句。编译器发现等号右侧的是另一个对象初始化程序, 所以会适当地将属性应用到嵌入对象。
集合初始化程序
var names = new List { "Holly", "Jon", "Tom", "Robin", "William" };
就是这样。
同样是编译器在后台调用add方法来将元素add进集合。
集合初始化程序必须要遵循以下两点:
- 实现IEnumerale
- 具有add方法
对于第一点来说,要求实现IEnumerable是合理的,因为编译器必须得知道是某种意义上的集合。对于第二点,因为编译器会在后台调用add方法来存放元素,所以,你初始化的这个集合必须得保证有这个add方法。
隐式类型的数组
string[] names = {"Holly", "Jon", "Tom", "Robin", "William"};
这种方式看起来很简洁,但是不能将大括号里面的东西直接传递给方法:MyMethod({" Holly", "Jon", "Tom", "Robin", "William"});这样会报错。要这样:MyMethod( new string[] {"Holly", "Jon", "Tom", "Robin", "William"});
匿名类型
这个玩意儿才是今天的重点。
匿名类型在与更高级的特性结合起来才会更有用。
用这个东西初始化的类型也只能用var来承接了:var myInfo=new {name="pangjianxin“,age=19};当然。除了object以外。
如果两个匿名对象初始化程序包含相同数量的属性, 且这些属性具有相同的名称和类型, 而且以相同的顺序出现, 就认为它们是同一个类型。
static void Main(string[] args) { var persons = new[] { new { name = "pangjianxin", age = 19 }, new { name = "pangjianxin", age = 19 }, new { name = "pangjianxin", age = 19 }, new { name = "pangjianxin", age = 19 }, new { name = "pangjianxin", age = 19 } }; Console.ReadKey(); }
如果上面的匿名类型的类型不一致,比如吧其中一个的属性的顺序颠倒,额外增加一个属性等等,编译器会报错:”找不到隐式类型数组的最佳类型“。
编译器在后台为匿名类型生成了一个泛型的类型来帮助匿名类型进行初始化,这个泛型类被放到了一个单独的程序集中。它在后台生成的类名超级变态:
它将匿名类型中的name和age的类型作为泛型类型的类型参数,然后,main方法中变成了这样:
编译器厉害吧?
返回来看一下匿名类型具有哪些成员:
首先它是继承了object。废话
它有后备字段和只读的属性
有获取所有初始值的构造函数。
重写了object的Equals、GetHashCode、ToString
由于所有属性都是只读的,所以只要这些属性是不易变的, 那么匿名类型就是不易变的。 这就为你提供了“ 不易变” 这一特性所具有全部常规性的优势—— 能放心向方法传递值, 不用害怕这些值被改变; 能在线程之间共享数据, 等等。
投影初始化程序
var person = new {name = "pangjianxin", age = 19};
var anotherPerson = new {name = person.name, isAdult = (person.age > 18)};
anotherPerson利用person的属性形成了一个新的匿名类型。这就是投影初始化程序。
不过匿名类型最大的用处在于linq中。利用select或selectmany等操作可以从横向的缩小要查找的范围。