深入理解C#(第3版)-- 【C#3】第8章 用智能的编译器来防错(学习笔记)
自动实现的属性——编写由字段直接支持的简单属性,不再显得臃肿不堪;
隐式类型的局部变量——根据初始值推断变量的类型,从而简化局部变量的声明;
对象和集合初始化程序——用一个表达式就能轻松创建和初始化对象;
隐式类型的数组——根据内容推断数组的类型,从而简化数组的创建表达式;
匿名类型——允许你创建新的“临时”类型来包含简单的属性。
8.1 自动实现的属性

写自己的结构(struct)时,如果使用自动属性,那么会有一个小问题:所有构造函数都需要显式地调用一下无参构造函数this() ,只有这样,编译器才知道所有字段都被明确赋值了。由于字段是匿名的,所以不能直接设置它们。同时,在所有字段都被设置之前,也不能使用这些属性。如果要使用属性,唯一的办法就是调用无参构造函数,它会将字段设成它们的默认值。
例如,如果想写一个有单个整数属性的结构,将是无效的。
public struct Foo { public int Value { get; private set; } public Foo(int value) { this.Value = value; } }
但如果像下面这样显式地调用无参构造函数,就完全可以了。
public struct Foo { public int Value { get; private set; } public Foo(int value) : this() { this.Value = value; } }
8.2 隐式类型的局部变量
8.2.1 用var 声明局部变量
编译器所做的工作很简单,就是获取初始化表达式在编译时的类型,并使变量也具有那种类型。类型可以是任何普通的.NET类型,包括泛型、委托和接口。变量仍然是静态类型的,只是你在代码中没有写类型的名称而已。
var stringVariable = "Hello, world."; stringVariable = 0;
上述代码是无法编译的,因为stringVariable的类型是System.String ,而你不能将值0赋给一个字符串变量。在许多动态语言中,上述代码是可以编译的,造成在编译器、IDE 或运行时环境的眼中,变量的类型变得无关紧要。使用var 和使用COM或VB6 中的VARIANT类型不一样。
变量是静态类型的,只是类型要由编译器推断。
8.2.2 隐式类型的限制
不是在所有情况下都能为所有变量使用隐式类型,只有在以下情况下才能用它:
被声明的变量是一个局部变量,而不是静态字段和实例字段;
变量在声明的同时被初始化;
初始化表达式不是方法组,也不是匿名函数(如果不进行强制类型转换);
初始化表达式不是null;
语句中只声明了一个变量;
你希望变量拥有的类型是初始化表达式的编译时类型;
初始化表达式不包含正在声明的变量。
第3点和第4点比较有趣。你不能像下面这样写:
var starter = delegate() { Console.WriteLine(); };
因为编译器不知道使用什么类型。但可以像这样写:
var starter = (ThreadStart) delegate() { Console.WriteLine(); };
8.2.3 隐式类型的优缺点
读取代码时,相同的长类型名称在同一行上没必要出现两次——如果它们显然应该是同一个类型的话。
对于下面声明的每一个变量,你能以多快的速度判断出它们的类型?
var a = 2147483647; var b = 2147483648; var c = 4294967295; var d = 4294967296; var e = 9223372036854775807; var f = 9223372036854775808;
答案依次是:int 、uint、uint、long、long和ulong ——具体使用什么类型要取决于表达式的值。
8.2.4 建议
如果让读代码的人一眼就能看出变量的类型是很重要的,就使用显式类型。
如果变量直接用一个构造函数初始化,而且类型名称很长(用泛型时经常会这样),就考虑使用隐式类型。
如果变量的确切类型不重要,而且它的本质在当前上下文中已很清楚,就用隐式类型,从而不去强调代码具体是如何达到其目标的,而是关注它想要达到什么目标这个更高级别的主题。
在开发一个新项目的时候,与团队成员就这件事情进行商议。
如有疑虑,一行代码用两种方式都写一写,根据直觉选一个最“顺眼”的。
8.3 简化的初始化
8.3.1 定义示例类型
代码清单8-2 一个相当简单的 Person类,用于进一步演示
public class Person { public int Age { get; set; } public string Name { get; set; } List<Person> friends = new List<Person>(); public List<Person> Friends { get { return friends; } } Location home = new Location(); public Location Home { get { return home; } } public Person() { } public Person(string name) { Name = name; } } public class Location { public string Country { get; set; } public string Town { get; set; } }
8.3.2 设置简单属性
在C# 3 之前,可以采取两种方式:
Person tom1 = new Person(); tom1.Name = "Tom"; tom1.Age = 9; Person tom2 = new Person("Tom"); tom2.Age = 9;
第一种方式直接使用无参构造函数,然后设置这两个属性。第二种方式则使用了一个能设置名字的重载的构造函数,然后再设置年龄。当然,这两种方法在C# 3 中仍然可以使用,但现在多了另外三种方案:
Person tom3 = new Person() { Name = "Tom", Age = 9 }; Person tom4 = new Person { Name = "Tom", Age = 9 }; Person tom5 = new Person("Tom") { Age = 9 };
在每一行末尾,用大括号括的就是对象初始化程序。同样,这是编译器耍的一个花招。用于初始化tom3和tom4的IL 是完全一样的,并且与初始化tom1的IL 也几乎
完全一样。可以预见,tom5的IL 和tom2的IL 也几乎完全一样。
8.3.3 为嵌入对象设置属性
以下C# 1 代码:
Person tom = new Person("Tom"); tom.Age = 9; tom.Home.Country = "UK"; tom.Home.Town = "Reading";
C# 3 可以在一个表达式中完成这些工作,如下例所示:
Person tom = new Person("Tom") { Age = 9, Home = { Country = "UK", Town = "Reading" } };
8.3.4 集合初始化程序
1 .使用集合初始化程序来创建新集合
以下是C# 2 代码
List<string> names = new List<string>(); names.Add("Holly"); names.Add("Jon"); names.Add("Tom"); names.Add("Robin"); names.Add("William");
等价的C# 3 代码
var names = new List<string> { "Holly", "Jon", "Tom", "Robin", "William" };
集合初始化列表并非只能应用于列表。任何实现了IEnumerable 的类型,只要它为初始化列表中出现的每个元素都提供了一个恰当的公有的Add 方法,就可以使用这个特性。
Dictionary<string, int> nameAgeMap = new Dictionary<string, int> { { "Holly", 36 }, { "Jon", 36 }, { "Tom", 9 } };
2 .在其他对象初始化程序中填充集合
代码清单8-3 使用对象和集合初始化程序来构建一个“富对象”
Person tom = new Person { Name = "Tom", Age = 9, Home = { Town = "Reading", Country = "UK" }, Friends = { new Person { Name = "Alberto" }, new Person("Max"), new Person { Name = "Zak", Age = 7 }, new Person("Ben"), new Person("Alice"), { Age = 9, Home = { Town = "Twyford", Country = "UK" } } } };
8.4 隐式类型的数组
在C# 1 和C# 2 中,作为变量声明和初始化的一部分,初始化数组的语句是相当整洁的。如果想在其他地方声明并初始化数组,就必须指定具体数组类型。例如,以下语句编译起来没有任何问题:
string[] names = {"Holly", "Jon", "Tom", "Robin", "William"};
但这种写法不适用于参数。假定要调用MyMethod 方法,该方法被声明为void MyMethod (string[] names) ,那么以下代码是无法编译的:
MyMethod({"Holly", "Jon", "Tom", "Robin", "William"});
相反,你必须告诉编译器你想要初始化的数组是什么类型:
MyMethod(new string[] {"Holly", "Jon", "Tom", "Robin", "William"});
C# 3 允许介于这两者之间的一种写法:
MyMethod(new[] {"Holly", "Jon", "Tom", "Robin", "William"});
编译器必须自己判断要使用什么类型的数组。它首先构造一个集合,其中包括大括号内所有表达式的编译时类型。在这个类型的集合中,如果其他所有类型都能隐式转换为其中一种类型,该类型即为数组的类型。否则(或者所有值都是无类型的表达式,比如不变的null值或者匿名方法,而且不存在强制类型转换),代码就无法编译。
注意,只有表达式的类型才会成为一个候选的数组类型。
8.5 匿名类型
8.5.1 第一次邂逅匿名类型
代码清单8-4 创建具有 Name和Age 属性的匿名类型的对象
var tom= new { Name = "Tom", Age = 9 }; var holly= new { Name = "Holly", Age = 36 }; var jon = new { Name = "Jon", Age = 36 } ; Console.WriteLine("{0} is {1} yearsold", jon.Name, jon.Age);
代码清单8-5 用匿名类型填充数组,并计算总年龄
var family = new[] { new { Name = "Holly", Age = 36 }, new { Name = "Jon", Age = 36 }, new { Name = "Tom", Age = 9 }, new { Name = "Robin", Age = 6 }, new { Name = "William", Age = 6 } }; int totalAge = 0; foreach (var person in family) { totalAge += person.Age; } Console.WriteLine("Total age: {0}", totalAge);
8.5.2 匿名类型的成员
匿名类型包含以下成员:
一个获取所有初始值的构造函数。参数的顺序和它们在匿名对象初始化程序中的顺序一样,名称和类型也一样;
公有的只读属性;
属性的私有只读字段;
重写的Equals、GetHashCode 和ToString 。
由于所有属性都是只读的,所以只要这些属性是不易变的,那么匿名类型就是不易变的。
8.5.3 投影初始化程序
如下代码:
new { Name = person.Name , IsAdult = (person.Age >= 18) }
C# 3 支持一种简化的语法:如果不指定属性名称,而是只指定用于求值的表达式,它就会使用表达式的最后一个部分作为名称——前提是它只能是一个简单字段或属性。这就是所谓的投影初始化程序(projection initializer)。换言之,上述代码可重写为:
new { person.Name, IsAdult = (person.Age >= 18) }
浙公网安备 33010602011771号