深入理解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;
    }
}
View Code

但如果像下面这样显式地调用无参构造函数,就完全可以了。

public struct Foo
{
    public int Value
    {
        get;
        private set;
    }

    public Foo(int value) :   this()
    {
        this.Value = value;
    }
}
View Code

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" }
        }
    }
};
View Code

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) }
posted @ 2019-10-16 00:19  FH1004322  阅读(131)  评论(0)    收藏  举报