《C#与.NET4高级程序设计(第五版)》学习札记

第一章 .NET之道

.Net可以理解为一个运行库环境和一个全面的基础类库。

运行库正式命名为CLR,用于管理.NET类型、处理一些底层细节等。

CLR(公共运行时引擎)为所有支持.Net的语言共享。

CTS(Common Type System 公共类型系统)规范完整的描述了运行库所支持的所有的可能的类型和编程结构;一个特定的.NET语言可能不支持CTS中定义的所有特性。

CLS(公共语言规范Common Language Specification)规范定义了一个让所有.NET语言都支持的公共类型和编程结构的子集,使用任何一种.NET语言构造的类型都必须与CLS定义的特性兼容。

BCL(Base Class Library)或FCL:.NET提供的用于全部.NET语言的基础类库。

 

在C#中property(属性)是一种特殊字段(内部用get/set方法实现),而attribute(特性)是一种为语言构造添加元数据信息的方式。

 

.NET2.0中新增特性:

1、泛型,用于构建高效且类型安全的代码;

2、匿名方法,在定义委托实例时传入一段代码块(内联函数,即匿名方法)替代方法指针(名称);

3、partial关键字,提供在多个代码文件中创建单一类型的能力。

.NET3.5中新增特性:

1、支持强类型查询(LINQ);

2、支持匿名类型,如

new关键字之后就直接为对象定义了属性,并且为这些属性赋值
 var obj = new {Guid.Empty, myTitle = "匿名类型", myOtherParam = new int[] { 1, 2, 3, 4 } };

3、扩展方法

//定义扩展方法,用this关键字指定要扩展的对象
public static void PrintString(this string a, string b)
{
    Console.Writeline(a);
}
//消费扩展方法string b = "xxx";
a.PrintString(b);

4、lambda表达式,替换委托类型中传入的匿名方法

5、对象初始化器

//不同于匿名类型的是此处对象属性都是先定义好的
var myObj1 = new MyObj() { id = Guid.NewGuid(), Title = "allen" };
//集合初始化器
var arr = new List<int>() { 1, 2, 3, 4, 5, 6 };

 

必须在.NET运行库中执行的代码成为托管代码,反之,不能直接在.NET运行库中承载的代码称为非托管代码

包含托管代码的二进制单元称为程序集

第二章 构建C#应用程序

.NET二进制文件(.dll或.exe文件)只包含平台无关的CIL(中间语言)、类型元数据(metadata元数据就是对某一类型或对象、事物进行特征描述的信息数据)以及程序集清单(也是用元数据描述);

一个程序集可能只包含一个.NET二进制文件(称为单文件程序集),也可能包含多个.NET二进制文件(多文件程序集)。

任何.net语言编译成的中间语言都是基本相同的。

将CIL编译成CPU指令(机器代码)的称为JIT(编译器),即jitter,针对不同的CPU .NET运行库会配备不同的JIT。

操作系统会以适当的方式缓存Jitter编辑器生成的机器代码,方便下次使用,从而不用再编译JIT。

.net程序集中的元数据描述了每一个二进制文件中的类型(类、结构、枚举)以及每个类型的成员(属性、方法、事件等)。

 

任何一个可执行C#应用程序(控制台应用程序、window桌面程序或者windows服务)都必须包含一个含有Main()方法的类型,这个Main()方法作为应用程序的入口点,定义Main()方法的类叫做应用程序对象。

 

所谓签名就是指一个方法的名称、放回类型和参数列表。

 

在vs命令提示窗口下编译c#文件:

编译单个文件:

1、打开vs命令提示符工具;

2、切换(cd)到c#文件所在目录,好像只能切换到c盘下的目录;

3、输入命令:csc /t:exe filename.cs 或者 csc /t:exe c:\filename.cs(编译为控制台应用程序(exe格式程序集))

csc /t:dll filename.cs(编译为dll程序集)

csc /t:exe filename.cs也可以写成 csc /target:exe filename.cs或者 csc filename.cs

还可以使用/out命令指定程序集的名称,如:csc /t:exe /out:HelloWorld.exe filename.cs

编译多个文件:

csc /out:appName.exe a.cs b.cs或者csc /out:appName.exe *.cs(编译当前目录下的所有cs文件)

csc /out:appName.exe d:\*.cs(编译d盘根目录下的所有cs文件,appName.exe会被保存到你之前切换的目录下)

csc /out:appName.dll d:\*.cs

第三章 C#核心编程结构I

每个数值类型都是System中的一个结构,继承自System.ValueType;

从System.ValueType派生的类型都是结构或枚举,不是类;

String类型变量一旦分配之后,所有操作都是返回一个新的string对象,在内存中重新分配一个区域;

System.Enviroment获取当前应用程序的系统详细信息

第四章 C#核心编程结构II

[assembly:System.CLS.Compliant(true)]指示编译器检测代码是否遵循CLS规则(所有遵循CLS规则的.net代码都能被其它.net语言调用);

枚举类型
结构类型

第五章 定义封装的类类型

构造函数:

每一个C#类都提供了默认的构造函数(没有参数,可以重新定义),其作用的是把对象分配到内存,并确保所有的字段数据都设置为正确的默认值,默认构造函数域内应该没有现实;

一旦定义了自定义的构造函数,默认构造函数就会被移除失效(除非显示的定义默认无参构造函数)。

使用this关键字实现串联构造函数:
public class TestApp
{
    class Car
    {
        private string _name;
        private int _speed;
        private string _category;

        public Car(string name)
            : this(name, 0)
        {
            Console.WriteLine("name:{0}", name);
        }

        public Car(string name, int speed)
            : this(name, speed, "")
        {
            Console.WriteLine("name:{0}; speed:{1}", name, speed);
        }

        public Car(string name, int speed, string category)
        {
            this._name = name;
            this._speed = speed;
            this._category = category;
            Console.WriteLine("name:{0}; speed:{1}; category:{2}", name, speed, category);
        }
    }

    static void Main(string[] args)
    {
        Car car = new Car("benz");
        Console.ReadLine();
    }
}

 

串联构造函数逻辑流程如下:

1、通过调用只有单个参数的构造函数来创建对象;

2、构造函数将传递的数据转发给其串联的构造函数,并提供调用者没有提供的参数(提供其默认值),以此类推,最终将参数传递给主构造函数;

3、主构造函数调用完成之后,将依次返回执行其前面的构造函数中的代码,最终回到调用者调用的构造函数执行。

串联构造函数的好处在于只需要关心主构造函数的实现。使用可选参数和自定义参数同样可以实现串联构造函数的功能,但是需要.NET 4版本支持。

使用可选参数和自定义参数实现串联构造函数的功能:

public class TestApp
{
    class Car
    {
        private string _name;
        private int _speed;
        private string _category;

        public Car(string name, int speed = 0, string category = "")
        {
            this._name = name;
            this._speed = speed;
            this._category = category;
            Console.WriteLine("name:{0}; speed:{1}; category:{2}", name, speed, category);
        }
    }

    static void Main(string[] args)
    {
        Car car = new Car("benz", speed : 120, category : "housing car");
        Console.ReadLine();
    }
}

 

关于C#修饰符:

internal表示其修改的类型或类型成员只能在当前程序集中被访问;

默认情况下,类型是internal的,类型成员是private的;

public和internal不能修改嵌套类型;

关于静态成员的理解:

静态成员是类级别成员,非实例级别成员。静态成员只能操作静态数据或调用类的静态方法;

静态数据只分配一次并在相同类型的所有对象实例之间共享,在第一次创建对象实例时分配,在非静态构造函数中不应对静态数据进行设置。

静态构造函数:

用于初始化在编译时未知的静态数据值;

一个类只能定义一个静态构造函数,不能被重载;

静态构造函数不允许访问修饰符并且不能有参数;

无论创建多少个对象实例,静态构造函数只执行一次;

首次创建类的实例或者调用者首次访问静态成员之前,运行库会调用静态函数;

静态构造函数的执行优先于任何其他非静态构造函数;

第六章 继承和多态

OOP:

封装(encapsulation)

为对象用户隐藏类型实现细节,即对象内部数据不应该从对象实例处直接访问;

继承(inheritance,is-a关系)

基于已有类创建新类定义,子类继承基类核心功能,并扩展基类(注意:子类不继承父类构造函数)

.net平台不允许多重继承,即不能同时继承多个基类。但是允许某一类(或结构)类型同时实现多个独立接口(.net中的接口实现非继承机制);

使用sealed关键字修饰的类型不允许被继承,使用sealed修饰的方法不能被重写;

结构不能用于继承;

创建派生类的实例时不会同时创建基类的实例,但是有可能调用基类的构造函数;

聚合(has-a关系)

允许类型中义嵌套类型成员变量;嵌套类型可以定义为private的,而其它类型则不可以;

继承和聚合为代码重用的两种方式;

多态(polymorphism)

以同一种方式处理相关对象的能力,即用基类类型变量保存派生类型对象引用。

相关对象继承基类,基类也称为多态接口,多态接口为派生类型定义一组由任意virtual或abstract成员组成的集合,virtual成员提供默认实现,abstract成员不提供默认实现,只提供签名,派生类型必须重写abstract成员;

不能创建抽象类的实例;

子类可以不重写基类虚方法(virtual),但是必须重写基类的抽象方法(abstract);

第八章 对象的生命周期

.NET中的对象都是分配在一块叫做托管堆的内存区域中。

使用new关键字创建对象时返回的都是对象的引用,非对象本身,这个引用变量保存在栈内。

结构是值类型,分配在栈上。

垃圾回收器会在对象不需要时将其删除,如何判断对象在什么时候不再需要呢?简短不完全的回答是:一个对象从代码库的任何位置都不可访问。不能保证对象不可用时会立即被回收,但可以肯定的是,垃圾回收器会在下一次垃圾回收时将其删除。

托管堆中分配着一个对象指针(内存单元地址),指向下一个将要分配的对象的位置。

CLR在托管堆中分配对象的步骤:

1、计算分配对象所需要的总内存数(包括数据成员和基类所需要的内存);

2、检查托管堆剩余内存总数,如果空间足够,则将对象分配到托管堆中对象指针所指位置,返回对象引用给调用者,然后移动对象指针指向下一个可用区域;如果空间不够将进行一次垃圾回收;

在代码中将对象引用设置为null并不意味着强制垃圾回收器立即回收对象,可能会等到下一次垃圾回收时回收。

应用程序根:一个存储位置,保存着对托管堆中一个对象的引用。垃圾回收器判断托管堆上的对象是否可用就判断这些对象是否还有根的。

CLR在进行垃圾回收时会建立一个对象图,再检查对象图中的对象是否有活动根,若没有,则将其标记为垃圾。一旦对象被标记终结后,将会从内存中被清除。这时,堆上剩余空间将会被压缩,继而引用修改活动应用程序根的集合,最后,下一个对象指针被调整,指向下一个可用位置。

 

垃圾回收器仅当它不能从托管堆上获取所需内存时才运行(或是当指定应用程序域从内存卸载时)。

 

对象的代:

第0代:从未被标记为回收的对象;

第1代:在上一次垃圾回收中未被回收的对象(也就是说,在上一次垃圾回收时被标记了回收,但是因为堆上有足够的空间而未被回收的对象。)

第2代:一次以上垃圾回收后任然没有被回收的对象。

在进行垃圾回收时,算上第0代可回收对象后,仍然没有足够的空间,就会检查第1代对象的可访问性并进行相应回收。没有被回收的第1代对象就会被提升至第2代。如果仍然需要更多的内存,就会检查第2代对象的可访问性。这时,如果一个第二代对象仍然存在,那么它仍然是第2代对象。第2代对象是上限。

 

在结构类型上重写Finalize()方法是不合法的,因为结构是值类型,不分配在托管堆上。如果结构类型中包含了非托管资源,可以实现IDisposiabe接口来释放非托管资源。

在.NET平台上,非托管资源是通过PInvoke服务调用windows API,或者使用一些com互操作性任务获得的。

因此重写Finalize()的唯一原因是使用了非托管资源(一般是通过System.Runtime.Interopservices.Marshal类型定义的各成员)。

在为自定义类型重写Finalize()方法时不能使用如下预期方式,将无法编译通过:

protected override void Finalize(){…}

可用终结器的方式替代:

//终结器以波浪线开头,名称和类名一样,不能有访问修饰符、参数,不能被重载

~ClassName()

{

   //释放非托管资源

   ……

}

垃圾回收器在回收该类的实例时,CLR会调用终结器方法释放资源。

自定义终结器的缺点:

1、在终结器中不能使用任何托管对象,因为不能确保在垃圾回收时该托管对象是否存在;

2、因不能确定垃圾回收器何时回收,所以不能确保非托管资源及时释放;

3、终结对象时会增加CLR后台工作量,速度变慢,影响性能。

 

为了能够及时的释放宝贵的非托管资源,类可以实现IDisposable接口,重写Dispose()方法,在Dispose()中添加释放资源逻辑,然后调用者可以手动调用Dispose()方法。基础类库中许多类都实现了IDisposable接口。

在Dispose中还能处置任何的托管对象,因为垃圾回收器并不支持IDisposable接口,当对象调用Dispose方法时,对象仍在托管堆上。

使用using(…){…}能确保支持IDisposable接口的对象在结束using作用域或出现运行时异常时始终能够调用Dispose()方法。

 

在自定义类型中同时设置Finalize()和Dispose()是可行的,如果用户调用了Dispose方法,可以使用GC.SuppressFinalize()通知垃圾回收器跳过终结过程,如果用户忘记了调用Dispose(),那么在垃圾回收时CLR会自动终结对象。对象的内部非托管资源会用其中一种方式释放。

image

上面的代码会导致Dispose()和终结器中清理非托管资源的代码重复,增加维护难度,下面的代码解决了此问题:

image

image

第9章 接口

接口是一组抽象成员的集合。一个类可以支持任意数量的接口,也就支持了多种行为。

接口和抽象类的比较:

相同点:都能定义许多抽象成员;

不同点:

1、抽象类还能定义许多构造函数、字段数据、非抽象成员等,而接口只能包含抽象成员。

2、抽象类不支持多重继承,而接口可以。

3、抽象类中的抽象成员必须被派生类实现(虚成员可重写可不重写),而接口则是要么全部被实现要么全部不被实现。

4、抽象类中只有虚成员和抽象成员才能被派生类重写。

自定义接口:

接口成员不能有访问修饰符,所有接口成员都是隐式公共和抽象的。接口不能有字段、构造函数和方法实现。可以包括方法定义、属性、事件以及索引器定义。

接口本身不能被实例化;

实现接口:

自定义类型的直接基类必须在冒号操作符后第一个,然后才是类型支持的接口,如下:

//Shpe为Triangle的父类,必须放在接口IDraw之前

public class Triangle : Shape,IDraw

{…}

 

实现接口成员的两种方式:

1、接口成员被实现为共有的,可在对象级别调用,如下:

image

2、显式接口实现,显式接口成员总是被实现为隐式私有的,不能从对象级别访问,但是解决了不同接口存在同名成员的命名冲突问题,如下:

image

获取接口引用:

使用as关键字

image

使用is关键字

image

接口作为参数:

如果将接口作为参数,就能传递任何实现了该接口的对象。

接口数组:接口类型数组能包含任何实现了该接口的对象;

接口的继承:

如果希望扩展既有接口功能而又不希望改变现有接口代码,可以通过接口继承实现,派生接口不会真正去实现父类接口,而是通过增加抽象成员扩展父类接口。

当一个类型支持了一个派生接口,那么同时其父接口也会自动被支持。在类型中必须对派生接口和父接口的成员都进行实现。

一个接口可以同时扩展多个接口,即支持多重继承。

.NET内置常见接口:

1、IEnumerable、IEnumerator(System.Collections命名空间中)

image

任何支持GetEnumerator()方法的类型都可以通过foreach结构进行运算。

IEnumerator提供基础设施,调用方可以用来移动IEnumerable兼容容器包含的内部对象。

image

自定义类型实现IEnumerable接口:

image

image

用yield关键字构建迭代器方法

yield关键字用来向调用方的foreach结构返回值,迭代器方法名必须是GetEnumerator,返回类型必须是IEnumerator类型。

image

构建命名迭代器

命名迭代器方法返回类型必须是IEnumerable类型

image

关于.NET中浅复制与深复制的理解:

首先这两个概念是针对分配在托管堆上的对象的(string对象除外),对于基本类型变量(在栈上分配空间)还包括string对象(即使其在堆上分配,也可以当作栈空间上变量对待)在克隆自身时都是拷贝自身的一个完整副本。

浅复制:只是复制了对象的引用,两个引用仍然指向同一对象;

深复制:完全拷贝了整个对象,副本对象与原对象具有相同数据和处理能力。

IComparable

image

System.Array有一个静态方法重载方法Sort(object obj),所有通过Sort方法排序的对象都必须实现IComparable接口。所有内置数据类型都实现了IComparable接口。

自定义类型实现IComparable接口:

image

CompareTo()方法的返回值被用来判断当前对象是否大于、小于、等于所比较的对象:

image

 

由于C#int类型实现了IComparable接口,所以可以按如下修改程序:

image

IComparer接口

另一个重载方法System.Array.Sort(object obj1, object obj2):obj1支持IComparable接口,obj2支持IComparer接口,obj1指定当前要排序的对象,obj2作为一个辅助排序类对象(辅助类型定制了排序方式)。

image

image

image

使用自定义静态属性辅助对用用户排序:

image

第10章 泛型

装箱和拆箱

.net中存在两大数据类型:值类型和引用类型,有时需要将一种类型的变量分配给另一种类型的变量,为此引用了装箱和拆箱操作。

装箱:将值类型变量分配给引用类型变量的过程;

当我们进行装箱时,会首先在托管堆上分配一个对象,然后将值类型数据复制到分配的对象上,最后将该对象的引用返回。

拆箱:将引用类型变量转换成装箱之前的值类型的过程。

 

由于装箱/拆箱操作会在栈空间(值类型数据分配在栈上)和托管堆之间来回移动操作,拆箱完之后无用的对象会被回收,因此会导致运行速度低下,性能降低。另外,在进行拆箱操作时,无法完全保证引用类型数据会正确的转换成相应的值类型,因此还存在一个类型安全问题。

 

由于System.Collections命名空间中绝大多数非泛型集合类操作的都是System.Object类型数据,因此在使用这些集合类容器时会出现大量的装箱拆箱操作,从而导致性能和类型安全问题。

使用泛型集合类能解决所有上述问题,由于泛型集合类指定了容器要操作的数据类型,因此不存在出现拆箱装箱的操作。

注意:只有结构、类、接口、委托可以使用泛型,枚举不可以。

非泛型结构、类同样支持泛型成员。

image

image

 

对象初始化语法:允许我们在构建变量时设置其属性。

image

image

集合初始器语法:允许你用填充数组的方法来填充非泛型或泛型容器。

image

List<int> myGenericList = new List<int>() {…}也是可行的。

 

在调用泛型方法时,当且仅当泛型方法需要参数时,可以选择省略类型参数,因为编译器会基于成员参数推断类型参数。

泛型中的default关键字:

image

泛型基类

如果一个非泛型类继承了一个泛型基类,非泛型类必须指定一个类型参数。

image

类型参数的约束

image

同样的也可以为多个类型参数指定约束条件:

image

image

 

还有一定要注意的是:不能在泛型类型中对类型参数使用(. + – * ==)操作符,因为编译器无法预先知道类型参数的具体类型。

第11章 委托、事件和lambda

.NET委托类型

普通调用是高层代码(如应用程序)调用底层代码(如系统函数、库函数),回调则是底层函数执行时调用高层代码。.NET中的回调则是指对象自身向调用者发送信息,执行调用者定义的程序代码。CLR通过委托实现这一回调机制,委托对象指向一个或多个匹配的调用者定义的方法。

委托类型定义:

image

编译器会为委托类型自动生成一个派生自System.MutilCastDelegate(System.MutilCastDelegate又派生自基类System.Delegate)的密封类,其中定义了三个公共方法:

//核心方法,以同步方式调用委托对象维护的方法

public int Invoke(int x, int y);

//BeginInvoke()和EndInvoke()用于在第二个执行线程上异步执行当前方法

public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state);

public int EndInvoke(IAsyncResult result);

 

System.MutilCastDelegate和System.Delegate为委托对象提供基础设施,维护包含由委托对象维护的方法地址列表,并提供一些处理调用方法列表的附加方法。

image

 

给委托类型指定方法的两种方式:

1、构造委托对象,将方法名称作为参数传入委托对象构造函数。

image

2、直接为委托类型变量指定方法名称,这种方式称为方法组转换:

DelegateType delegateVar = methodName;

 

可以使用+=或-=操作符为类型注册或者注销方法绑定;

 

委托协变:委托对象指向的方法的返回值可以是同一类层次结构上具有继承关系的类型;

委托逆变:委托对象指向的方法的参数可以是同一类层次结构上具有继承关系的类型;

事件

事件为.NET类型(包括内置类型和自定义类型)的对象被触发时的某一动作特性,外部实体触发对象事件时需要指定事件处理程序,CLR通过委托协作为其指定事件处理程序。类型中事件定义:

image

为事件指定外部处理程序等同于为委托类型变量指定方法列表的方式:

Exploded += MethodName;

Exploded –= MethodName.

 

很多.NET内置类型事件使用内置EventHandler委托声明:

//System.Object表示发送事件的对象本身;

//System.EventArgs表示对象要给外部处理程序传递的消息

public delegate void EventHandler(System.Object, System.EventArgs);

lambda表达式:

匿名方法:

image

lambda表达式是匿名方法的更简化形式。

第12章 高级C#语言特性

1、索引器方法:

image

在给自定义类型实现了索引器方法的同时可以支持IEnumerable接口,方便在foreach结构中使用索引器。

同时在接口上也可以定义索引器:

image

2、操作符重载:

image

 

重载二元操作符:

//重载方法必须是静态的,必须使用operator关键字,operator关键字只可与静态方法联合使用。

image

重载一元操作符:

image

 

对于比较操作符的重载必须成对重载,如重载了<操作符,就必须重载>操作符。

基础类库中的很多类型已经重载过许多操作符。

3、自定义类型转换

关于显示强制转换和隐式转换的理解:

值类型间的转换:在较小容器类存储较大的值时需要显示强制转换,小值放在大容器内则可以隐式转换,如下所示:

image

类类型间的转换:

派生类型引用转换为基类类型引用可以隐式转换,基类类型引用放在派生类型引用中需要显示强制转换,如下:

image

对于不同类层次结构中不具有继承关系的类型若需要相互转换,则需要显示指定转换例程:

image

自定义转换例程需注意以下几点:

1、方法必须是静态的;

2、explicit/implicit必须结合operator关键字使用;

3、方法名称是转换后的对象类型名称,参数是要转换的类型引用;

4、不能在类型中同时定义显示和隐式转换例程,另外,若定义了隐式转换例程,用户对其进行显示强制转换操作也是合法的,反之,若定义显示强制转换例程,用户再进行隐式转换操作则非法,不能通过编译。

 

当然,自定义类型和其他非自定义类型之间也是存在转换可能的:

image

实际上只要代码语法正确,编译器并不关心由什么转换成什么!

4、扩展方法

扩展方法允许为现有程序集中的类型(已编译好)添加新功能,同时扩展方法将单独存放。

扩展方法的定义:

image

扩展方法的定义有几个限制:

1、必须定义在静态类中,方法必须是静态的;

2、第一参数必须使用this关键字修饰,第一个参数表示要扩展的类型名称。

 

在实例层次上调用扩展方法:

image

image

静态调用扩展方法:

image

 

在扩展方法内不能直接访问扩展类型的成员,须通过this限定的参数引用来访问其公共成员(仅是公共成员)。

在使用扩展方法的程序集中必须引用(using)定义扩展方法的程序集。

推荐把扩展方法放在独立的程序集中(独立命名空间)。

 

使用扩展方法扩展接口类型:在扩展接口类型时必须提供方法的实现,意思就是告诉调用者,所有支持该接口的类型都有一个扩展方法定义的方法。如下:

image

5、分部方法

分部方法允许在一个代码文件中定义方法原型(即方法签名),在另一个代码文件中实现方法。

分部方法的定义有以下几个限制:

1、分部方法都是隐式私有的;

2、分部方法必须返回void;

3、只能定义在分部类中。

如果分部方法只提供原型而无具体实现,编译器在编译时会除去该分部方法的定义和调用。

 

使用分部方法定义轻量级事件处理程序是很常见,如果不想让事件得到什么通知,只是简单的将事件与方法挂钩,那么可以使用分部方法只提供原型,如果希望触发事件时处理一些事情,那么就可以提供该方法的实现。

6、匿名类型

匿名类型的定义:

image

匿名类型特点:

1、不能定义匿名类型的名称,编译器会自动为其生成一个名称,当两个匿名类型使用完全相同的属性名称和值时,编译器只会生成一个类型。

2、匿名类型继承自System.Object;

3、匿名类型的字段和属性总是只读的;

4、匿名类型不支持自定义事件、自定义方法、自定义操作符和自定义重写;

5、匿名类型是隐式封闭的;

6、匿名类型重写了Equals()(定义了基于值比较的语义)、ToString()(输出匿名类型定义的样式)、GetHashCode()方法;

应用场景:只是用于当前应用程序中,不需要在其他项目中重用。

第13章 linq to object

linq提供了一种简明、对称、强类型的方式访问各种数据存储。核心LINQ API定义在System.Core.dll程序集中(System.Linq命名空间)。

为什么linq结果集要用var来定义?

因为linq结果集会用很多来自System.Linq命名空间中的类型表示,在很多情形下我们并不知道结果集对应的具体底层类型,所以应将结果集定义为var,让编译器自动判断。另外,在绝大多数情况下,结果集返回类型都是兼容IEnumberable<T>接口类型的。

为什么System.Array没有实现IEnumberable<T>,但仍可以应用linq查询表达式?

在Sytem.Linq下有一个静态类型Enumerable,这是一个扩展工具类,在这个类中为System.Array定义了扩展方法,使其能够得到一个兼容IEnumberable<T>的对象。

这就是为什么linq能应用与原始数组的原因。

这个工具类还定义了许多泛型扩展方法。

延迟执行

所有的linq查询表达式在定义的时候都不会执行,只有当利用查询结果集进行运算时才会执行,这样能保证多次查询同一容器时能得到最新的数据。

 

如果希望一个方法返回一个查询结果集,而又不希望返回时结果集被执行,可以参考如下:

image

立即执行返回结果集:

image

怎么样才能将linq应用与非泛型容器?

我们知道linq只能应用于支持IEnumerable<T>接口的容器,而非泛型容器不支持此接口。在Enumerable中有一个扩展方法OfType<T>()可以为非泛型容器提取出一个兼容IEnumerable<T>接口的对象,这样我们就可以通过这个方法将linq操作应用与非泛型容器了。

image

利用OfType<T>()方法的特点我们还可以过滤、筛选数据:

image

使用匿名类型使用Linq从现有数据源中投影新的数据类型:

image

在这种情况下由于无法知道linq结果集的类型,就必须使用隐式类型var定义。

c#LINQ查询操作符:

所有的c#LINQ查询操作符都是定义在Enumerable工具类中扩展方法的简写形式。

Enumerable中提供的联合操作多个结果集的方法(查询操作符):

1、except:找出A结果集中不同于B结果集的元素。

image

2、intersect:找出A结果集和B结果集中共同的元素。

3、union:合并多个结果集中的元素,如果存在相同的元素,将只返回一个。

4、concat:连接所有结果集的所有成员。

5、distinct:过滤掉重复的元素。

Enumerable中的大部分方法都要求传入强类型泛型委托参数,很多都是要求Func<>泛型委托。

image

下面通过简单的实例从最简化方式一步一步到最原始方式介绍建立查询表达式,其中每一步实例在编译后的实现原理都是一样的:

1、通过LINQ查询表达式实现查询:

image

2、使用Enumerable中的扩展方法和lambda表达式建立查询:

image

3、使用Enumerable中的扩展方法和匿名方法建立查询:

image

4、使用Enumerable中的扩展方法和委托对象建立查询:

//Filter和ProcessItem为委托目标方法

image

 

根据上面每一步的实现方法,可总结出以下几点:

1、查询表达式是用各种查询操作符建立的;

2、查询操作符是Enumerable中扩展方法的简写;

3、Enumerable中的许多方法都要求委托(特别是Func<>)作为参数;

4、任何要求委托的参数都可以传入一个lambda表达式;

5、lambda表达式是匿名方法的简写;

6、匿名方法是手工指定一个委托对象,然后再指定一个目标方法的简写形式。

第14章 .NET程序集入门

.NET程序集定义:一个以CLR为宿主、版本化的、自描述的.NET二进制文件。(和Windows二进制文件内部构造完全不同)

一个可执行程序集完全可以引用外部可执行文件中的类型,因此,一个被引用的*.exe文件也可以认为是代码库。

.NET程序集是语言无关的,可以在.NET平台上的多种语言之间相互调用。

 

.NET程序集特点:

1、促进代码重用;

2、确定类型边界;

3、可版本化,格式:<major>.<minor>.<build>.<version>

4、自描述的(包含程序集清单、类型元数据、CIL等)

5、可配置的

 

.NET程序集构造部分:

Windows文件首部--注:使得程序集可以被Windows系列操作系统加载和操作;

CLR文件首部--注:为了使程序集可以驻留于CLR中,使得CLR可以了解程序集的布局;

CIL代码--注:程序集中类型的微软中间语言代码;

类型元数据--注:对程序集中类型的描述;

程序集清单(程序集元数据)--注:描述程序集自身信息,以及记录程序集引用的外部程序集信息等。

可选的嵌入资源--注:如应用程序图标、图像文件、声音片段等。

 

.NET程序集可以分为单文件程序集和多文件程序集。

单文件程序集顾名思义就是只有一个*.dll文件的逻辑单元;

多文件程序集由多个二进制文件组成一个逻辑单元,包含一个主模块和一个或多个辅助模块,主模块包含程序集级别的清单和其它依赖模块的引用信息,辅助模块后缀名为.netmodule。

使用csc.exe命令编译多文件程序集:

1、编译辅助模块:

csc.exe /t:module ufo.cs

2、编译主模块生产程序集dll的同时添加对辅助模块的引用:

csc.exe /t:library /addmodule:ufo.netmodule /out:airvehicles.dll helicoper.cs

多文件程序集的辅助模块只有在其中的类型被应用到时才会被加载。

 

.NET程序集又可以分为私有程序集和共享程序集。

私有程序集:若该程序集被引用将拷贝一份副本到应用程序所在目录下(或子目录);其全标识包括友好名称和版本号,两者都被记录在程序集清单中。

使用配置文件指示CLR到指定目录加载应用程序引用的程序集:

//privatePath特性不能指定一个绝对路径或虚拟路径

image

//指定多个目录:

image

配置文件命名格式:AppName.exe.config,.config前面的友好名称必须和应用程序的名称一致。

 

共享程序集:与私有程序集的本质区别是共享程序集的一份副本可以供一台机器上的多个应用程序使用。共享程序集安装在GAC(Global Assembly Cache)中。

在部署程序集到GAC时,必须赋予程序集一个强名称,强名称用于标识给定.NET二进制文件的发行者。

强名称由一组相关数据组成,可以使用程序集级别特性定义:

image

 

如何判断一个程序集是私有还是公有的:

若该程序集清单中有.publickey标记的公钥值,则说明该程序集是公有的。

 

使用配置文件动态指示应用程序加载不同版本的共享程序集:

image

发行者策略程序集:

允许将配置文件(使得能够加载不同版本的程序集)和程序集一起写入GAC,这样就不用为该机器上的使用该程序集的不同应用程序再单独配置文件了。

System.Configuration

提供方法允许以编程方式读取配置文件信息,但是配置信息必须放在<appSetting>节里面。

image

第15章 类型反射、晚期绑定和基于特性的编程

(代码中的字符面量在程序集中用元数据描述时是不会加密的,因此,在代码中用字符面量存储密码等敏感信息是非常危险的)

使用System.Type进行类型反射

System.Type中提供了很多成员,用于检查类型元数据,一个System.Type实例可以用来表示要检查的类型元数据。

image

System.Type中成员方法大多返回System.Reflection中类型。

 

有以下三种方式获得System.Type实例引用:

1、System.Object.GetType()方法

image

该方法要求得到SportsCar的编译时信息,并且在内存中还得有一个SportsCar实例;

2、使用typeof关键字

image

因为typeof需要一个强类型,所以还是需要得到编译时信息;

3、System.Type.GetType()方法

Type type = System.Type.GetType("System.String");

(另一个重载方法)

image

获取外部程序集:

image

传入字符串格式:类型完全限定名 + 程序集名称(用逗号分隔);

该方法不需要类型的编译时信息和类型实例。

 

如何反射泛型类型?

给Type.GetType()传入字符串格式为:System.Collections.Generic.List`1(后面数字为类型参数个数)

 

同时System.Reflection中类型提供的成员可以继续反射其它元数据,如方法返回值和参数:

//获取方法返回值,m为MethodInfo类型实例引用

image

//获取方法参数,返回ParameterInfo数组

image

动态加载程序集

通过System.Reflection中的Assembly类型,我们可以动态的加载外部程序集(即程序清单中没有记录的外部程序集)。

Assembly.Load()只能加载应用程序bin/debug/目录下的二进制文件,方法参数传入程序集友好名称字符面量;

Assemb.LoadFrom()可以通过指定的绝对路径加载任何位置的程序集。

 

识别一组程序集的术语叫做显示名称(包括程序集友好名称、版本号、区域设置信息和公钥值):

image

如下使用显示名称加载程序集:

//PublicKeyToken为null表示非强名称程序集,Culture=””表示使用机器默认的区域设置

image

另外还可以通过组合一系列对象模型来表示程序集显示名称,同样可以传入Assembly.Load()方法:

image

晚期绑定

晚期绑定是一种在运行时创建类型实例并调用其成员,而在编译时并不知道该类型存在的技术。晚期绑定的类型程序集以动态加载方式引用,程序清单中没有直接列出这个程序集。

使用晚期绑定调用外部程序集类型中的成员示例:

//1、动态加载外部程序集

image

//2、获取程序集中类型元数据

image

//3、使用System.Reflection.Activator()方法创建一个MiniVan实例

//注意:不能使用点操作符调用实例成员或者使用强制转换,因为在编译时该类型是未知的

image

//4、使用反射获取MiniVan类型中方法描述元数据对象模型

image

//5、通过MethodInfo的Invoke方法调用Minivan成员方法

//注意:第一个参数为object类型的Minivan实例引用,

//第二个参数为需要传入的一组方法参数,用object[]表示

image

image

 

.NET特性的作用

.NET特性就是派生自抽象System.Attribute基类的类类型,.NET平台允许使用特性将更多地类型描述、成员描述、程序集描述、模块描述等元数据写入程序集。

image

特性级别的数据对象只有在被反射时才会被分配到内存中。

特性要点:

image

自定义特性:

和类的定义一样,注意:必须继承System.Attrabute,为了安全性,一般封装为密封类。

image

 

命名属性

在应用特性时使用命名属性,可以使得代理反射时能够获取到传入的数据(一般为字符面量)。

使用特性限制自定义特性的使用

image

在程序集级别应用特性

image

动态加载、晚期绑定和特性应用的场景

一般应用于构建可扩展的应用程序。首先在可扩展应用程序中编写一些可扩展接口,然后在可扩展应用程序中编写基于这些接口的适配器(由于适配器需要加载未知的第三方插件,所以这里的代码需要用到晚期绑定和反射),最后第三方的扩展插件只要支持这些接口(基于接口的编程)就可以对应用程序进行扩展(在加载第三方插件时需要动态加载扩展程序集)。

posted @ 2017-03-31 18:09  JDotNet  阅读(356)  评论(0编辑  收藏  举报