Contentshtml
第1章CLR的执行模型... 4git
1.1将源代码编译成托管代码模块... 4程序员
1.2 将托管模块合并成程序集... 6正则表达式
1.3加载公共语言运行时... 7redis
1.4执行程序集的代码... 8算法
1.5本地代码生成器:NGen.exe. 11数据库
1.6 Framework类库... 11编程
1.7通用数据类型... 12c#
1.8公共语言规范... 12windows
第2章 生成、打包、部署和管理应用程序及类型... 13
2.1 .Net Framework部署目标... 13
2.2将类型生成到模块中... 13
2.3元数据概述... 13
2.4将模块合并成程序集... 14
第4章 基础类型... 14
4.1 全部类型都从System.Object派生... 14
4.2类型转换... 18
4.3命名空间和程序集... 20
4.4运行时的相互关系... 21
第5章 基元类型、引用类型和值类型... 21
5.1编程语言的基元类型... 21
5.2引用类型和值类型... 22
5.3 值类型的装箱和拆箱... 26
5.4对象哈希码(之后再看)... 29
5.5 dynamic基元类型... 30
第6章 类型和成员基础... 31
6.1类型的各类成员... 31
6.2类型的可见性... 33
6.3成员的可访问性accessibility. 34
6.4静态类... 35
6.5 分部类、结构和接口... 36
6.6组件、多态和版本控制... 37
第7章 常量和字段... 37
7.1常量... 37
7.2字段... 38
第8章 方法... 40
8.1实例构造器和类(引用类型)... 40
8.2实例构造器和结构(值类型)... 43
8.3类型构造器... 43
8.4操做符重载方法... 45
8.5转换操做符方法... 47
8.6扩展方法... 47
8.7分部方法... 49
第9章 参数... 50
9.1可选参数和命名参数... 50
9.2 隐式类型的局部变量... 53
9.3以传引用的方式向方法传递参数... 54
9.4向方法传递可变数量的参数... 54
9.5参数和返回类型的指导原则... 55
9.6 常量性... 55
第10章 属性... 55
10.1无参属性... 55
10.1.1自动实现的属性... 57
10.1.2合理定义属性... 57
10.1.3对象和集合初始化器... 57
10.1.4匿名类型... 59
10.1.5System.Tuple类型... 59
10.2有参属性... 59
第11章事件... 59
11.1设计要公开事件的类型... 60
11.1.1第一步:定义类型来容纳全部须要发送给事件通知接收者的附加信息... 60
11.1.2第二步:定义事件成员... 61
11.1.3 第三步:定义负责引起事件的方法来通知事件的登记对象... 61
第12章 泛型... 61
12.1 Framework类库中的泛型... 63
12.2Wintellect的Power Collections库... 63
12.3泛型基础结构... 63
12.3.1开放类型和封闭成员... 63
第13章 接口... 63
13.1类和接口继承... 64
13.2定义接口... 64
13.3继承接口... 65
第14章 字符、字符串和文本处理... 66
14.1字符... 66
14.2 System.String类型... 67
14.2.1构造字符串... 67
14.2.2字符串是不可变的... 68
14.2.3比较字符串... 68
14.2.4字符串留用... 69
14.2.5字符串池... 70
14.2.6检查字符串的字符和文本元素... 70
14.2.7其余字符串操做... 70
14.3高效率构造字符串... 70
第17章委托... 71
17.1初识委托... 73
17.2用委托回调静态方法... 75
17.3用委托回调实例方法... 75
17.4委托揭秘... 75
本书分为五个部分:
v 第一部分,CLR基础(CLR Basics),介绍CLR的执行模型,程序集概念,以及建立、打包、部署、管理程序集等。
v 第二部分,设计类型(Designing Types),包括CLR类型基础,基础类型,方法,特性(Property),事件,泛型,接口等内容。
v 第三部分,基本类型(Essential Types),包括字符、字符串及文本的处理,枚举类型,数组,委托(Delegate),自定义属性(Attribute),可控制类型等。
v 第四部分,核心设施(Core Facilities),包括异常与状态管理,自动内存管理(垃圾收集),CLR托管与应用程序域(AppDomain),程序集加载与反射,运行时序列化等。
v 第五部分,线程(Threading),这是第三版新增长的内容,包括线程基础,计算密集的异步操做,I/O密集的异步操做,基本的线程同步构造,混合的线程同步构造等。
本章内容:
v 将源代码编译成托管代码模块
v 将托管模块合并成程序集
v 加载公共语言运行时
v 执行程序集的代码
v 本地代码生成器:NGen.exe
v Framework类库入门
v 通用类型系统
v 公共语言规范(CLS)
v 与非托管代码的互操做性
公共语言运行时(Common Language Runtime, CLR)是一个供多种编程语言使用的运行时。可用任何编程语言来开发代码,只要编译器是面向CLR的就行。
CLR也能够看做一个在执行时管理代码的代理,管理代码是CLR的基本原则。可以被管理的代码称为托管(managed)代码,反之称为非托管代码。
CLR的核心功能中的内存管理、程序集加载、安全性、异常处理和线程同步等可被面向CLR的全部语言使用。(不懂)
用支持CLR的任何一种语言建立源代码文件。用一个对应的编译器来检查语法和分析源代码。经编译器编译后生成托管模块(managed module),它是一个可移植执行体文件,它多是32位(PE32)文件,也多是64位(PE32+)文件。包括中间语言和元数据,须要CLR才能执行。
托管模块包含以下几个部分组成:
v PE32/PE32+ 头:标准的 Windows PE文件头。若是文件头使用PE32格式,则此文件只能在Windows 的32位或64位版本上运行;若是文件头使用PE32+格式,则此文件只能在Windows 的64位版本上运行。编译器在编译时,可经过编译平台/platform开关来指定该程序集包含一个PE32头或PE32+头。
v IL代码: 也是中间语言。编译器编译源代码时生成的中间代码,在执行环境中,这些IL代码将被CLR的JIT编辑器翻译成CPU能识别的指令,供CPU执行。
v 元数据
中间语言IL(Intermediate Language)代码:编译器编译源代码后生成的代码(.exe或.dll文件),但此时编译出来的程序代码并非CPU能直接执行的机器代码。在运行时,CLR将IL代码编译成本地CPU指令。
CPU:中央处理器(Central Processing Unit),是一台计算机的运算核心和控制核心。它的功能主要是解释计算机指令以及处理计算机软件中的数据。
DLL (Dynamic Link Library) 文件为动态连接库文件,是一种做为共享函数库的可执行文件。动态连接提供了一种方法,使进程能够调用不属于其可执行代码的函数。可执行代码就是将编译器处理源代码后所生成的代码 链接后造成的可执行代码,它通常由机器代码或接近于机器语言的代码组成。
在Windows中,许多应用程序并非一个完整的可执行文件,它们被分割成一些相对独立的动态连接库,即DLL文件,放置于系统中。当咱们执行某一个程序时,相应的DLL文件就会被调用。多个应用程序也能够同时访问内存中单个 DLL 副本的内容。DLL 有助于共享数据和资源。
动态连接与静态连接的不一样之处在于:动态连接容许可执行模块(.dll 文件或 .exe 文件)仅包含 在运行时定位 DLL 函数的可执行代码所需的信息。在静态连接中,连接器从静态连接库获取全部被引用的函数,并将库同代码一块儿放到可执行文件中。
使用中间语言的优势(跨平台,跨语言):
v 能够实现平台无关性,即与特定CPU无关。
v 只需把.NET框架中的某种语言编译成IL代码,就实现.NET框架中语言之间的交互操做。
本地代码编译器(native code compiler)生成的是面向特定CPU架构(好比x86,x64)的代码。相反,每一个面向CLR的编译器生成的都是IL代码。
除了生成IL,面向CLR的每一个编译器还要在每一个托管模块中生成完整的元数据(Metadata)。元数据是描述数据(类型信息) 的数据,一般被解释为data about data,是由一组数据表构成的一个二进制数据块。元数据被CLR编译器编译后保存在Windows可移植执行体(PE)文件中,即和它描述的IL嵌入在EXE/DLL文件中,使IL和元数据永远同步。
PE (Portable Execute) 文件是微软Windows操做系统上的程序文件,EXE、DLL、OCX、SYS、COM都是PE文件。
元数据主要的类型表:
v 定义表 描述当前程序集中定义的类型和成员信息。
v 引用表 描述任何一个被内部类型引用的外部的类型和成员信息。
v 清单表包含了组成程序集所须要的全部信息,同时包含了对其余程序集的引用信息。
元数据的用途:
v 编译时,元数据消除了对本地C/C++头和库文件的需求,由于在负责实现类型/成员的IL代码文件中,已包含和 引用的类型/成员有关的所有信息。编译器能够直接从托管模块读取元数据。
v CLR的代码验证过程当中使用元数据确保代码只执行“类型安全”的操做。
v VS使用元数据帮助您写代码。它的“智能感知”(IntelliSense)技术能够解析元数据,指出一个类型提供了哪些方法、属性、事件和字段
v 元数据容许将一个对象的字段序列化到一个内存中,将其发送给另外一台机器。而后反序列化,在远程机器上重建对象的状态。(内存:与CPU进行沟通的桥梁,计算机中全部程序的运行都是在内存中进行的,所以内存的性能对计算机的影响很是大)。
v 元数据容许垃圾回收器跟踪对象的生存期,垃圾回收器能判断任何对象的类型,并从元数据知道那个对象中哪些字段引用了其余对象。
程序集(assembly)是一个或多个托管模块,以及一些资源文件的逻辑组合。是重用、安全性以及版本控制的最小单元。
CLR不和托管模块一块儿工做,是和程序集一块儿工做的。CLR是经过程序集与托管模块进行沟通的。
C#编译器将生成的多个托管模块和资源文件合并成程序集。
在程序集内有一个清单,其描述了程序集内的文件列表,如托管模块、jpeg文件、XML文件等。
对于一个可重用的、可保护的、可版本控制的组件,程序集把它的逻辑表示(代码)和物理表示(资源)区分开。
你生成的每一个程序集既能够是可执行的应用程序(.EXE),也能够是DLL。最终都由CLR管理这些程序集中代码的执行。
Microsoft建立了重分发包(redistribution package),容许将.NET Framework免费分发并安装到你的用户的计算机上。
检查机器上是否安装好.Net Framework,只需在C:\Windows\System32下检查是否有MSCorEE.dll文件(Microsoft .NET Runtime Execution Engine)。mscoree.dll必定是惟一的,且老是处于系统目录的system32下。MSCorEE.dll负责选择.NET版本、调用和初始化CLR等工做。
Windows键+R \ regedit \ KEY_LOCAL_MACHINE \ SOFTWARE \ MICROSOFT NET Framework了解安装了哪些版本的.NET Framework。
若是程序集文件只包含类型安全的托管代码,那么不管在32位仍是64位版本的Windows上,所写的代码都应该能正常运行。
X86指的是一种CPU的架构,是硬件。由于intel的8086,286,386~586而得名,amd开发的大部分CPU也是基于x86架构的。x86架构的特色是CPU的寄存器是32位的,所以也叫32位CPU。基于32位CPU开发的操做系统就叫32位操做系统。
C#编译器生成的程序集要么包含一个PE32头,要么包含一个PE32+的头。
加载CLR的过程:
v 当双击一个.exe文件时,Windows会检查EXE文件的头(PE32头或PE32+头),判断应用程序须要的是32位地址空间,仍是64位地址空间。
v 会在进程的地址空间中加载MSCorEE.dll的x86,x64版本。
v 进程的主程序调用MSCorEE.dll中定义的_CorExeMain方法,这个方法初始化CLR,加载EXE程序集,而后调用其入口方法(Main)。
v 托管的应用程序将启动并运行。
初始化CLR包括:
v 分配一块内存空间,创建托管堆及其它必要的堆,由GC监控整个托管堆。
v 建立线程池。
v 建立应用程序域 (AppDomain):利用sos.dll能够查看CLR建立了哪些AppDomain。
托管程序集包含元数据和IL。IL是和CPU无关的机器语言,比大多数CPU机器语言都要高级,可将IL视为面向对象的机器语言。
IL能访问和操做对象类型,并提供了指令来建立和初始化对象、调用对象上的虚方法以及直接操做数组元素等。
就在Main方法执行以前,CLR会检测出Main的代码中引用的全部类型。这致使CLR为类型建立一个内部数据结构,它用于管理对所引用的类型的访问。
如上图中,Main方法引用了一个Console类型,这致使CLR分配一个内部数据结构。在这个结构中,Console类型定义的每一个方法都有一个对应的记录项。每一个记录项都容纳了一个地址,根据此地址便可找到方法的实现。对这个结构进行初始化时,CLR将每一个记录项都指向包含在CLR内部的一个未文档化的函数(C#中没有函数的概念,一概称为方法)。这个函数称为JITCompiler。
Main方法首先调用WriteLine方法时,JITCompiler函数会被调用。JITCompiler函数负责将一个方法的IL代码即时编译成本地CPU指令。
JITCompiler函数被调用时,它知道要调用的是哪一个方法,以及具体是什么类型定义了该方法。而后,JITCompiler会在定义该类型的程序集的元数据中查找被调用的方法的IL。接着,JITCompiler验证IL代码,并将IL代码即时编译成本地CPU指令。本地CPU指令被保存到一个动态分配的内存块中。而后,JITCompiler返回CLR为类型建立的内部数据结构,找到与被调用的方法对应的那一条记录,修改最初对JITCompiler的引用,让它如今指向内存块的地址。最后,JITCompiler函数跳转到内存块中的代码。这些代码正是WriteLine方法的具体实现。这些代码执行完毕并返还时,会返还到Main中的代码,并向往常同样继续执行。
如今,Main要第二次调用WriteLine。这一次因为已对WriteLine的代码进行了验证和编译,因此会直接执行内存块中的代码,彻底跳过JITCompiler函数。
一个方法只有在首次调用时才会形成一些性能损失。之后对该方法的全部调用都以本地代码的形式全速进行,无需从新验证IL并把它编译成本地代码。
JIT编译器将本地CPU指令存储到动态内存中,一旦应用程序终止,编译好的代码也会被丢弃。全部,若是未来再次运行应用程序,JIT编译器必须再次将IL编译成本地指令。
在Visual Studio中新建一个C#项目时,项目的debug配置指定的是/optimize-和/debug: full开关。Release 配置指定的是/optimize-和/debug: pdbonly开关。
只有在指定/debug(+/full/pdbonly)开关的前提下,编译器才会生成一个Program Debug Database (PDB)文件。PDB文件帮助调试器查找局部变量并将IL指令映射到源代码。
1.4.1 IL和验证
IL是基于栈的。这意味着它的全部指令都要将操做数压入(push)一个执行栈,并从栈弹出(pop)结果。
操做数是操做符做用于的实体,是表达式中的一个组成部分,它规定了指令中进行数字运算的量 。操做数就是你直接处理的数据,操做数地址就是操做数存放在内存的物理地址。
表达式是操做数与操做符的组合。一般一条指令均包含操做符和操做数。
堆栈是两种数据结构。堆栈都是一种数据项按序排列的数据结构,都在进程的虚拟内存中,(在32位处理器上每一个进程的虚拟内存为4GB),只能在一端(称为栈顶(top))对数据项进行插入和删除。要点:堆(heap),队列优先,先进先出(FIFO—first in first out)。栈(stack),先进后出(FILO—First-In/Last-Out)。
IL的最大优点并不在它对底层CPU的抽象,而在于应用程序的健壮性和安全性。将IL编译成本地代码CPU指令时,CLR会执行一个名为验证(verification)的过程。这个过程会检查高级IL代码,肯定代码所作的一切都是安全的。列如,验证会核实调用的每一个方法都有正确数量的参数,传给每一个方法的每一个参数都具备正确的类型,每一个方法的返回值都获得了正确的使用,每一个方法都有 一个返回语句,等等。
在windows中,每一个进程都有它本身的虚拟地址空间,这是由于不能简单的信任一个应用程序代码。经过验证托管代码,可确保代码不会不正确地访问内存,不会干扰到另外一个应用程序的代码。这样一来就能够放心的将多个托管应用程序放到一个Windows虚拟地址空间中运行。
事实上,CLR确实提供了在一个操做系统进程中执行多个托管应用程序的能力。每一个托管的应用程序都在一个APPDomain中执行。默认状况下,每一个托管的EXE文件都在它本身的独立地址空间中运行,这个空间地址只有一个APPDomain。然而,CLR的宿主进程(好比IIS,SQL Server)可决定在单个操做系统进程中运行多个APPDomain。
.Net Framework中包含了Framework类库(Framework Class Library, FCL)。FCL是一组DLL程序集的统称,其中包含了数千个类型定义,每一个类型都公开了一些功能。
能够利用这些程序集来建立其中一部分应用程序
因为FCL包含数量极多的类型,因此有必要将相关的一组类型放到一个单独的命名空间中。System命名空间包含Object基类型,其余全部类型最终都是从这个基类型派生来的。
System命名空间包含用于整数、字符、字符串、异常处理以及控制台I/O的类型。
为了使用Framework的任何一个功能,必须知道这个功能是由什么类型提供的,以及该类型包含在哪一个命名空间中。
CLR是彻底围绕类型展开的。因为类型是CLR的根本,因此Microsoft制定了一个正式的规范,即“通用数据类型”(Common Type System),它描述了类型的定义和行为。
CTS规范规定一个类型能够零个或多个成员:
CTS指定了类型可视性规则以及类型成员的访问规则:
CTS还为类型继承、虚方法、对象生存期等定义了相应的规则。
全部类型最终必须从预约义的System.Object类型继承。Object是System命名空间中定义的一个类型的名称。Object是其余全部类型的根。
System.Object类型容许作下面事情:
CLR集成了全部语言,容许在一种语言中使用由另外一种语言建立的对象,由于CLR使用了标准类型集、元数据以及公共执行环境。
Microsoft定义了一个公共语言规范(Common Language Specification),它详细定义了一个最小功能集。
本节讨论如何将包含多个类型的源代码文件生成为一个可部署的文件。
System. Console是Microsoft已经实现好的一个类型,用于实现这个类型的各个方法的IL代码存储在MSCorLib.dll文件中。
Program.exe中到底包含什么内容呢?
一个托管PE文件有4个部分组成:PE32(+)头、CLR头、元数据以及IL。
PE32(+)头是Windows要求的标准信息,
元数据是一个二进制数据块,由定义表(definition table)、引用表(reference table)和清单表(manifest table)构成。
对于这个如此小的Program.exe应用程序,PE头和元数据占据了文件至关大的一部分。固然随着应用程序规模的增大,它会重用它的大部分类型以及对其余类型
程序集的引用,形成元数据和头信息在整个文件中所占的比例逐渐减少。
“运行时”要求每一个类型最终都从System.Object 类型派生,因此能够保证每一个类型的每一个对象都有一组最基本的方法。
重写(override):继承时发生,在子类中从新定义父类中的方法,子类中的方法和父类的方法是同样的(即方法名,参数,返回值都相同),由 override 声明重写的方法称为重写基方法。
例如:基类方法中声明为virtual,派生类中使用override申明此方法的重写。
重写override通常用于接口实现和继承类的方法改写,要注意:
v 覆盖的方法的标志必需要和被覆盖的方法的名字和参数彻底匹配,才能达到覆盖的效果;
v 覆盖的方法的返回值必须和被覆盖的方法的返回一致;
v 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
v 被覆盖的方法不能为private,不然在其子类中只是新定义了一个方法,并无对其进行覆盖。
v 不能重写非虚方法或静态方法。重写的基方法必须是 virtual、abstract 或 override 的。
namespace 方法重写
{
class TestOverride
{
public class Employee
{
public string name;
// Basepay is defined as protected, so that it may be accessed only by this class and derived classes.
protected decimal basepay;
// Constructor to set the name and basepay values.
public Employee(string name, decimal basepay)
{
this.name = name;
this.basepay = basepay;
}
// Declared virtual so it can be overridden.
public virtual decimal CalculatePay()
{
return basepay;
}
}
// Derive a new class from Employee.
public class SalesEmployee : Employee
{
// New field that will affect the base pay.
private decimal salesbonus;
// The constructor calls the base-class version, and initializes the salesbonus field.
public SalesEmployee(string name, decimal basepay,
decimal salesbonus)
: base(name, basepay)
{
this.salesbonus = salesbonus;
}
// Override the CalculatePay method to take bonus into account.
public override decimal CalculatePay()
{
return basepay + salesbonus;
}
}
static void Main()
{
// Create some new employees.
SalesEmployee employee1 = new SalesEmployee("Alice",
1000, 500);
Employee employee2 = new Employee("Bob", 1200);
Console.WriteLine("Employee4 " + employee1.name +
" earned: " + employee1.CalculatePay());
Console.WriteLine("Employee4 " + employee2.name +
" earned: " + employee2.CalculatePay());
}
}
/*
Output:
Employee4 Alice earned: 1500
Employee4 Bob earned: 1200
*/
}
System.Object的公共方法:
System.Object的受保护的方法:
内存格局一般分为四个区:
线程堆栈(Thread Stack)和托管堆(Managed Heap)
每一个正在运行的程序都对应着一个进程(process),在一个进程内部,能够有一个或多个线程(thread),每一个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,好比函数中定义的局部变量、函数调用时传送的参数值等,这部份内存区域的分配与回收不须要程序员干涉,主要由操做系统管理。全部值类型的变量都是在线程堆栈中分配的。
另外一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,因此又称为“托管堆(managed heap)。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
CLR要求全部类型对象都用new操做符来建立:Employee emp = new Employee (“ConstructorParam1”);
用new 关键字建立类的对象时,分配给对象的内存单元就位于托管堆中。在程序中咱们能够随意地使用new 关键字建立多个对象,所以,托管堆中的内存资源是能够动态申请并使用的,固然用完了必须归还。
声明一个Employee的引用emp,在线程堆栈上给这个引用分配存储空间,这仅仅只是一个引用,不是实际的Employee对象。假定emp占4个字节的空间,包含了存储Employee的引用地址。接着分配托管堆上的内存来存储Employee对象的实例,假定Employee对象的实例是32字节,为了在托管堆上找到一个存储Employee对象的存储位置,.Net运行库在托管堆中搜索第一个从未使用的,32字节的连续块来存储Employee对象的实例,而后把分配给Employee对象实例的地址赋给emp变量。new执行了以上全部这些操做以后,会返回指向新建对象的一个引用。在前面的示例代码中,这个引用会保存到变量emp中。
如下是new操做符所作的事情:
class SampleClass
{
public string name;
public int id;
public SampleClass() { }
public SampleClass(int id, string name)
{
this.id = id;
this.name = name;
}
}
class ProgramClass
{
static void Main()
{
SampleClass Employee2 = new SampleClass(1234, "Cristina Potra");
}
}
没有和new操做符对应的一个delete操做符。换言之,没有办法显示释放 为一个对象分配的内存。CLR采用了垃圾回收机制,能自动检测到一个对象再也不被使用或访问,并自动释放对象的内存。
CLR最重要的特性之一就是类型安全性。在运行时,CLR老是知道一个对象是什么类型。调用GetType方法,老是知道一个对象确切的类型是什么。
CLR容许将一个对象转换为它的实际类型或者它的任何基类型。
C#不要求特殊语法便可将一个对象转换为它的任何基类型,由于向基类型转换被认为是安全的隐式转换。然而,将对象转换为它的某个派生类型时,C#要求开发人员只能进行显式转换。
隐式转换不须要在代码中指定转换类型,例如:int intNumber = 10; double doubleNumber = intNumber; intNumber会被隐式转换成double类型。
显式转换则相反,须要指定转换类型,例如:double doubleNumber = 10.1; int intNumber = (int)doubleNumber;
对于表示数值的基本数据类型来讲,数值范围小的数据类型转换成数值范围大的数据类型能够进行隐式转换,而反过来则必须进行显示转换。
就像上面的两个例子同样。 对于类类型来讲,子类转换成父类能够进行隐式转换,而反过来则必须进行显式转换,例如:string str1 = "abc";object obj = str1; //子类转换成父类,隐式转。 string str2 = (string)obj; //父类转换成子类,显式转换 若是两个类之间没有继承关系,则不能进行隐式转换或显式转换,此时必须在被转换的类中定义一个隐式转换方法或显式转换方法。
在Main方法中,会构造一个Manager对象,并将其传给PromoteEmployee。这些代码能成功编译并运行,由于Manager最终从Object派生的,而PromoteEmployee期待的正是Object。在PromoteEmployee内部,CLR核实o引用的是一个Employee对象,或者是从Employee派生的一个类型的对象。因为Manager是从Employee派生的,因此CLR执行类型转换,运行PromoteEmployee继续执行。
PromoteEmployee返回以后,Main继续构造一个DateTime对象,并将其传给PromoteEmployee。一样的,DateTime是从Object派生的,因此编译器会顺利编译调用
PromoteEmployee的代码。但在PromoteEmployee内部,CLR会检查类型转换,发现o引用的是一个DateTime对象,它既不是一个Employee,也不是从Employee派生的任何类型。因此CLR会禁止转型,并抛出一个System.InvalidCastException异常。
声明PromoteEmployee方法的正确方式是将参数类型指定Employee,而不是Object。
v 使用C#的is和as操做符来转型
is检查一个对象是否兼容于指定类型,并返还一个Boolean值:true或false。is操做符永远不会抛出异常。
若是对象引用为null,is操做符总会返还false,由于没有可检查其类型的对象。
as操做符的工做方式与强制类型转换同样,只是它永远不会抛出一个异常。
检查最终生成的引用是否为null。
命名空间用于对相关的类型进行逻辑性分组,开发人员使用命名空间来方便地定位一个类型。
密封类的修饰符,用了这个修饰符的类就不能被其余类继承了。
应该有一种简单的方式来直接引用FileStream和StringBuilder类型。C#编译器经过using指令来提供这种机制。
C#的using指令指示编译器尝试为一个类型附加不一样的前缀,直到找到一个匹配项。
using指令容许为一个类型或命名空间建立别名。
在C#中namespace指令的做用:只是告诉编译器为源代码中出现的每一个类型名称附加命名空间名称前缀,减小程序员的打字量。
命名空间和程序集不必定是相关的。
同一个命名空间的各个类型可能在不一样的程序集中实现。例如:System.IO.FileStream类型是在MSCorLib.dll程序集中实现,而System.IO.FileSystemWatcher类型是在System.dll程序集中实现的。
参考http://www.cnblogs.com/MeteorSeed/archive/2012/01/24/2325575.html
本节将解释类型、对象、线程堆栈和托管堆在运行时的相互关系,以及调用静态方法、实例方法和虚方法的区别。
进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程以内独立执行的一个单元。
当系统加载一个CLR的进程,进程里面可能有多个线程,这时候系统会给这个进程建立一个大小为1M的线程堆栈。这个线程堆栈用来存放方法调用的实参,和方法内部定义的局部变量。
编译器(Compiler)直接支持的数据类型称为基元类型(primitive type)。
我但愿编译器根本不要提供基元类型名称,强制开发人员使用FCL(Framework类库)类型名称:
许多开发人员都困惑于到底应该使用string仍是String。因为C#的string直接映射到System.String,因此二者是没有区别的。int始终映射到System.Int32,因此无论在什么操做系统上运行,表明的都是32位整数。
虽然FCL中大多数类型是引用类型,但程序员用的最多的仍是值类型。
值类型:原类型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct)。
为提高简单的经常使用的类型的性能,CLR提供了“值类型”的轻量级类型。值类型的实例通常在线程堆栈上分配。
引用类型老是从托管堆上分配的,C#的new操做符会返回对象的内存地址——也就是指向对象数据的内存地址。
引用类型共有四种:类类型、接口类型、数组类型和委托类型。全部引用类型变量所引用的对象,其内存都是在托管堆中分配的。
使用引用类型时必须考虑性能问题,首先考虑如下事实:
在表明值类型实例的一个变量中,并不包含一个指向实例的指针。相反,变量包含了实例自己的字段。因为变量已经包含了实例的字段,因此为了操做实例中的字段,再也不须要提领一个指针,值类型的实例不受垃圾回收器的控制,超出了做用范围,系统就会自动释放。所以值类型的使用缓解了托管堆中的压力,并减小了一个应用程序在其生存期内须要进行的垃圾回收次数。
在.Net Framework SDK文档中,任何称为“类”的类型都是引用类型。例如:System.Exception类、System.IO.FileStream类以及System.Random类都是引用类型。
相反,文档中将全部值类型称为结构或枚举。例如:System.Int32结构、System.TimeSpan结构、System.DayofWeek枚举。全部结构都是抽象类型System.ValueType的直接派生类。全部枚举都从System.Enum 抽象类型派生。System.Enum又是从System.ValueType派生。
全部值类型都是隐式密封的(sealed),目的是防止将一个值类型用做其余任何引用类型或值类型的基类型。
在代码中使用类型时,必须注意该类型是引用类型仍是值类型。
SomeVal v1 = new SomeVal ();
上面一行代码彷佛要在托管堆上分配一个SomeVal实例。然而,C#编译器知道SomeVal是一个值类型,因此会生成相应的IL代码,在线程堆栈上分配一个SomeVal实例。C#还会确保值类型中全部字段都初始化为零。
如下条件都知足时,才应该将一个类型声明为值类型:
声明为值类型除了知足上面3个条件外,还必须知足一下任何一个条件:
值类型和引用类型的区别:
private static void Main()
{
int i;
MyClass mc;
i = 5;
mc = new MyClass();
}
当一个局部变量声明以后,就会在栈的内存中分配一块内存给这个变量,至于这块内存多大,里面存放什么东西,就要看这个变量是值类型仍是引用类型了。
若是是值类型,为变量分配这块内存的大小就是值类型定义的大小,存放值类型自身的值(内容)。好比,对于上面的整型变量 i,这块内存的大小就是 4个字节(一个 int型定义的大小),若是执行 i = 5;这行代码,则这块内存的内容就是 5(如图 -1)。
对于任何值类型,不管是读取仍是写入操做,能够一步到位,由于值类型变量自己所占的内存就存放着值。
若是是引用类型,为变量分配的这块内存的大小,就是一个内存指针(实例引用、对象引用)的大小(在 32位系统上为 4字节,在 64位系统上为 8字节)。由于全部引用类型的实例(对象、值)都是建立在托管堆上的,而这个为变量分配的内存就存放变量对应在堆上的实例(对象、值)的内存首地址(内存指针),也叫实例(对象)的引用。
由图 -2可知,变量 mc中存放的是 MyClass实例(对象)的对象引用,若是须要访问 mc实例,系统须要首先从 mc变量中获得实例的引用(在堆中的地址),而后用这个引用(地址)找到堆中的实例,再进行访问。须要至少 2步操做才能够完成实例访问。
值类型是比引用类型更“轻型”的一种类型,由于它不做为对象在托管堆中分配,不会被垃圾回收,也不经过指针来引用。但在许多状况下,都须要获取对值类型的一个实例的引用,即将值类型转换成引用类型。
如上面代码,建立一个ArrayList对象(System.Collections命名空间中定义的一个类型)来容纳一组Point结构。
每一次循环迭代都会初始化值类型字段(x和y)。而后这个Point会存储到ArrayList中。但ArrayList中究竟存储的是什么?是Point结构,仍是其余什么东西。咱们必须研究ArrayList的Add方法,了解它的参数被定义成什么类型。
Add须要获取一个Object参数。换言之,Add须要获取对托管堆上的一个对象的引用(指针)来做为参数。但在以前的代码中,传递的是p,也就是一个Point,是一个值类型。为了将一个值类型转换成引用类型,要使用一个名为装箱(boxing)的机制。
对值类型的一个实例进行装箱操做时在内部发生的事情:
C#编译器会自动生成对一个值类型的实例进行装箱所需的IL代码。
在上述代码中,C#编译器检测到是向一个须要引用类型的方法传递一个值类型,因此会自动生成代码对对象进行装箱。在运行时,当前存在于Point值类型实例p中字段会复制到新分配的Point对象中。已装箱的Point对象(如今是一个引用类型)的地址会返回给Add方法。Point对象会一直存在于堆中,直到被垃圾回收。Point值类型变量p能够重用,由于ArrayList根本不知道关于它的任何事情。在这种状况下,已装箱的值类型的生存期超过了未装箱的值类型的生存期。
在知道装箱如何进行以后,接着谈谈拆箱。
假定须要使用如下代码获取ArrayList的第一个元素:
如今是要获取ArrayList的元素0中包含的引用(或指针),并试图将其放到一个Point值类型的实例p中。包含在已装箱Point对象中的全部字段都必须复制到值类型变量p中,后者在线程栈上。
CLR分两步完成这个复制操做(拆箱/复制)。
第一步,获取已装箱的Point对象中的各个Point字段的地址。这个过程称为拆箱(unboxing)
第二步,将这些字段包含的值从堆中复制到基于栈的值类型实例中
拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低的多。拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。因此,和装箱不一样,拆箱不要求在内存中复制任何字节,每每会紧接着拆箱操做后发生一次字段的复制操做。
一个已装箱的值类型实例在拆箱时,内部会发生下面这些事情:
以上代码从逻辑上说,彻底能够获取o所引用一个已装箱的Int32,而后将其强制转换为一个Int16。然而,在对一个对象进行拆箱的时候,只能将其转型为原先未装箱时的值类型---本例即为Int32
下面的代码是正确的写法:
overload:重载指的是同一个类中有两个或多个名字相同可是参数不一样(参数个数和参数类型)的方法。
因为未装箱的值类型没有同步块索引,因此不能使用System.Threading.Monitor类型的各类方法,让多个线程同步对这个实例的访问。
5.3.2对象相等性和同一性(之后重看)
有时须要将对象放到一个集合中,并编写代码对集合中的对象进行排序、搜索或比较。
对于Object的Equals方法的默认实现来讲,它实现的实际是同一性(identity),而非相等性(equality)。
哈希:经过将哈希算法应用到任意数量的数据所获得的固定大小的结果。若是输入数据中有变化,则哈希也会发生变化。哈希可用于许多操做,包括身份验证和数字签名。也称为“消息摘要”。
哈希表:根据设定的哈希函数和处理冲突方法将一组关键字映象到一个有限的地址区间上,并以关键字在地址区间中的象做为记录在表中的存储位置,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。做为线性数据结构与表格和队列等相比,哈希表无疑是查找速度比较快的一种。
在.NetFramework中,HashTable是System.Collections命名空间提供的容器,用来处理和表现相似keyvalue的键\值对。其中key区分大小写,一般用来快速查找。value用来储存对应于key的值。Hashtable中keyvalue键\值对均为object类型,因此Hashtable能够支持任何类型的keyvalue键\值对。
Hashtable是非泛型的集合,因此在检索和存储值类型时一般会发生装箱与拆箱的操做。
在哈希表中添加一个keyvalue键\值对:HashtableObject.Add(key,value);
在哈希表中去除某个keyvalue键\值对:HashtableObject.Remove(key);
从哈希表中移除全部元素:HashtableObject.Clear();
判断哈希表是否包含特定键key:HashtableObject.Contains(key);
哈希算法:将任意长度的二进制值映射为固定长度的较小二进制值,这个小的二进制值称为哈希值。哈希值是一段数据惟一且极其紧凑的数值表示形式。
hashcode标识对象的地址,用于区别不一样的对象。
普通的查找慢是由于要一个一个比, Hash就是让把比较的次数下降 而下降的办法就是靠计算。
哈希算法会根据你要存入的数据,先经过该算法计算出一个地址值,这个地址值就是你须要存入到集合当中的数据的位置,而不会像数组那样一个个的进行挨个存储,挨个遍历一遍后面有空位就存这种状况了。而你查找的时候也是根据这个哈希算法来的。将你的要查找的数据进行计算,得出一个地址,这个地址会映射到集合当中的位置,这样就可以直接到这个位置上去找了,而不须要像数组那样,一个个遍历,一个个对比去寻找,这样天然增长了速度,提升了效率了。
若是能将任何对象的任何实例放到一个哈希表集合中,会带来不少好处。为此,System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希码。
若是你定义的一个类型重写了Equals方法,那么还应重写GetHashCode方法,确保相等性算法和对象哈希码算法是一致的。是由于在System.Collections.Hashtable类型、System.Collections.Generic.Dictionary类型以及其余一些集合的实现中,要求两个对象为了相等,必须具备相同的哈希码。
简单地说,在一个集合中添加一个键\值对时,首先会获取键对象的一个哈希码。这个哈希码指出键\值对应该储存到哪个哈希桶(bucket)中。集合须要查找一个键时,会获取指定的键对象的哈希码。这个哈希码标识了如今要搜索的目标哈希桶,要在其中查找与指定键对象相等的一个键对象。采用这种算法来储存和查找键,意味中一旦修改了集合中的一个键对象,集合就再也找不到对象。因此,须要修改一个哈希表中的键对象时,正确的作法是移除原来的键\值对,修改键对象,再将新的键\值对添加回哈希表。
C#是一种类型安全的编程语言。这意味着全部表达式都解析成某个类型的一个实例,在编译器生成的代码中,只会执行对这个类型来讲有效的操做。
从面向对象的角度来看, 对象的实例表示的是 个体, 而static的属性和方法则表示 全体所共有的方法和属性 , 如“会员张三”、“会员李四”是“会员”的两个个体, 昵称、等级是他们各自不一样的属性,而 会员总数、注册新会员 则是 全体会员所共享的属性和方法。 双好比 “圆”这个class, 半径、面积、周长是 个体的属性,而圆周率PI则是共性。
从应用的角度来看,本质就是为了节省内存,在内存中只有一个引用。
静态类的主要功能以下:
在许多时候,程序仍需处理一些运行时才会知晓的消息。若是你写的是一个纯C#应用程序,那么只有在使用反射的时候,才会在运行时才能肯定的信息打交道。然而,许多开发者在使用C#时,都要和一些不是和C#实现的组件进行通讯。有的组件是.Net动态语言,好比Python或Ruby,有的是HTML文档对象模型(DOM)对象。
C#编译器容许将一个表达式的类型标记为dynamic。还能够将一个表达式的结果放到一个变量中,并将变量的类型标记为dynamic。而后能够用这个dynamic表达式/变量调用一个成员,好比字段、属性/索引器、方法、委托以及一元/二元/转换操做符。
代码使用dynamic表达式/变量调用一个成员时,编译器会生成一个特殊的IL代码来描述所需的操做。这种特殊的代码称为payload(有效载荷)。在运行是payload代码会根据当前由dynamic表达式/变量引用的对象的实际类型来决定具体执行的操做。
这些Payload代码使用了一个称为运行时绑定器(runtime binder)的类。C#的运行时绑定器的代码在Microsoft.CSharp.dll程序集中,构建使用dynamic关键字的项目时,必须引用该程序集。
Plus方法将参数的类型申明为dynamic,在方法内部,实参做为二元+操做符的两个操做数使用。因为arg是dynamic,因此C#编译器会生成payload代码,以便在运行时检查arg的实际类型,并决定+操做符实际要作的事情。
第一次调用Plus时,传递的是5(一个Int32),因此Plus向它的调用者返回值10。结果放到result变量(一个dynamic类型)中。而后调用M方法,将result传给它。针对对M的调用,编译器会生成payload代码,以便在运行时检查传给M的值的实际类型,以及应该调用M方法的重载版本。
第二次调用Plus时,同第一次的原理同样。
在字段类型、方法参数或方法返回类型被指定为dynamic的前提下,编译器会将这个类型转换为System.Object,并在元数据中向字段、参数或方法类型应用System.Runtime.ComplierServices.DynamicAttribute的一个实例。若是是一个局部变量被指定为dynamic,变量类型也会变成Object,但不会向局部变量应用DynamicAttribute,由于它的使用限制在方法以内。
在运行时,Microsoft.CSharp.dll程序集必须加载到AppDomain中,这会损坏应用程序的性能,并增大内存耗用。虽然能用动态功能简化语法,但也要看是否值得。
在本章及本部分后续的章节,我将解释如何在一个类型中定义不一样种类的成员,从而设计出符合本身须要的类型。
在一个类型中能够定义0个或多个如下种类的成员:
不管使用什么编程语言,它的编译器都必须能处理你的源代码,为上述列表中的每一种成员生成元数据和IL代码。不管使用的编程语言是什么,元数据的格式都是彻底一致的。正是由于这个特色,才使CLR成为公共语言运行时。
CLR使用公共元数据格式决定常量、字段、构造器、方法、属性和事件在运行时的行为。元数据是整个.Net Framework开发平台的关键,它实现了编程语言、类型和对象的无缝集成。
如下C#代码展现了一个类型定义,其中包含了全部可能的成员。
在文件范围内定义类型时,能够将类型的可见性指定为public和internal。
public类型不只对它的定义程序集中的全部代码可见,还对其余程序集中的代码可见。
internal类型仅对定义程序集中的全部代码可见,对其余程序集中的代码不可见。
定义类型时,若是不显式指定类型的可见性,C#编译器默认将类型的可见性设为internal(二者中较有限的那一个)。
友元程序集
友元程序集功能用于访问内部成员;私有类型和私有成员仍然不可访问。
若要使程序集(程序集 B)可以访问另外一个程序集(程序集 A)的内部类型和成员,应使用程序集 A 中的 InternalsVisibleToAttribute 属性
在代码中引用一个成员时,成员的可访问性指出这种引用是否合法。
固然,任何成员想要被别人访问到,都必须在一个可见的类型内定义。例如,若是程序集AssemblyA定义了一个internal类型,该类型有一个public方法,那么程序集AssemblyB中的代码不能调用AssemblyA中的public方法,由于internal类型对于AssemblyB是不可见的。
在C#中,若是没有显式声明成员的可访问性,编译器一般默认选择private(限制最大的那个)。CLR要求接口类型的全部成员都具备public可访问性。
静态类是不能实例化的,例如Console,Math,Environment和ThreadPool类。这些类只有static成员。咱们直接使用它的属性与方法,静态类最大的特色就是共享,做用是将一组相关的成员组合到一块儿。例如Math类中定义了一组执行数学运算的方法。Math 类:为三角函数、对数函数和其余通用数学函数提供常数和静态方法。
static关键字只能应用于类,不能应用于结构(值类型)。这是由于CLR老是容许值类型实例化。
静态类的主要特色以下:
C#编译器对静态类进行了以下限制:
在类或结构内部定义的类型称为嵌套类型。例如:
class Container
{
class Nested
{
Nested() { }
}
}
经过static关键字修饰,是属于类,实例成员属于对象,在这个类第一次加载的时候,这个类下面的全部静态成员会被加载。
实例构造函数:使用 new 表达式建立某个类的对象时,会使用实例构造函数建立和初始化全部实例成员变量。
只要建立基于 CoOrds 类的对象,就会调用此实例构造函数。 诸如此类不带参数的构造函数称为“默认构造函数”。 然而,提供其余构造函数一般十分有用。 例如,能够向 CoOrds 类添加构造函数,以即可觉得数据成员指定初始值:
class CoOrds
{
public int x, y;
// constructor
public CoOrds()
{
x = 0;
y = 0;
}
public CoOrds(int x, int y)
{
this.x = x;
this.y = y;
}
}
class MainClass
{
static void Main()
{
CoOrds p1 = new CoOrds();
CoOrds p2 = new CoOrds(5, 3);
}
}
partial这个关键字告诉C#编译器,一个类、结构或者接口的定义源代码可能要分散到一个或多个源代码文件中。
当使用大项目或自动生成的代码(如由 Windows 窗体设计器提供的代码)时,将一个类、结构或接口类型拆分到多个文件中的作法就颇有用。
局部类型适用于如下状况:
局部类型的注意点:
组件软件编程(Component Software Programming)
下面列举了组件的一些特色:
在.Net中,版本号为1.2.3.4的程序集,其主版本号1,次版本号2,内部版本号3,修订号为4。
6.6.1 CLR如何调用虚方法、属性和事件
方法表明在类型或者类型的实例执行某些操做的代码。
在类型上执行操做称为静态方法。在类型的实例上执行操做称为非静态方法。
任何方法都有一个名称、一个签名和一个返回值(能够是void)。
常量(constant)是一个特殊的符号,它有一个从不变化的值。常量只能在声明中初始化。定义常量符号时,它的值必须在编译时肯定。
常数表达式是在编译时可被彻底计算的表达式。所以不能从一个变量中提取的值来初始化常量。
若是 const int a = b+1;b是一个变量,显然不能再编译时就计算出结果,因此常量是不能够用变量来初始化的。
肯定以后,编译器将常量的值保存到程序集的元数据中。这意味着只能为编译器认定的基元类型定义常量。然而C#也容许定义一个非基元类型的常量变量(constant variable),前提是把它的值设为null。
class Calendar1
{
public const int months = 12;
}
代码引用一个常量符号时,编译器会在定义常量的程序集的元数据中查找该符号,提取 常量的值,并将值嵌入生成的IL代码中。因为常量的值直接嵌入代码,因此在运行时不须要为常量分配任何内存。
常量是在编译时已知并在程序的生存期内不发生改变的不可变值。常量使用const修饰符进行声明。
只有C#内置类型能够声明为const。用户定义的类型(类,结构和数组)不能为const。请用readonly修饰符建立在运行时初始化一次即不可更改的类、结构和数组。
能够使用枚举类型为整数内置类型(列如int、long等)定义命名常量。
当编译器遇到 C# 源代码中的常量修饰符时,将直接把文本值替换到它生成的中间语言 (IL) 代码中。由于在运行时没有与常量关联的变量地址,因此 const 字段不能经过引用传递。
常量可标记为 public、private、protected、internal 或 protectedinternal。
未包含在定义常量的类中的表达式必须使用类名、一个句点和常量名来访问该常量。例如:
int birthstones = Calendar.months;
因为常量的值从不变化,常量老是被视为静态成员,而不是实例成员。
常量没有很好的跨程序性版本控制特性。开发人员更改了常量的值后,应用程序要获取新的常量值,必须从新编译。若是在运行时,从一个程序集提取另外一个程序集的值,那么不该该使用常量,而应该使用readonly字段。
字段(field)是一种数据成员,其中容纳了一个值类型的实例或者对一个引用类型的引用。
“字段”是直接在类或结构中声明的任何类型的变量。
字段修饰符:
v static:是类型状态的一部分,而不是对象状态的一部分。这使得调用方法在任什么时候候都可以使用字段,即便类没有任何实例。无论包含该静态字段的类生成多少个对象或根本无对象,该字段都只有一个实例,静态字段不能被撤销。必须采用以下方法引用静态字段:类名.静态字段名。
v readonly:只读字段只能在初始化期间(字段声明中)或在定义类的构造函数中赋值(这种构造器方法只能调用一次,也就是对象首次建立时),在其它任何地方都不能改变只读字段的值。注意:但可利用反射来修改readonly字段。
CLR支持类型(静态)字段和实例(非静态)字段。对于类型字段,用于容纳字段数据的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时建立的。对于实例字段,用于容纳字段数据的动态内存则是在构造类型的实例时分配的。
AppDomain简单的说就是应用程序的边界。能够经过它对相同进程中进行再次隔离。一个程序在运行的时候,它和它所引用、反射加载的全部模块的集合构成了一个程序域。普通桌面程序,一个程序就是一个AppDomain。CLR容许在一个进程中托管多个程序(好比IIS一类程序),一个IIS是能够运行不少网站的,若是这些网站都放在一个AppDomain里,一个网站崩溃了,其余网站也不能访问了。若是每一个网站都做为独立的程序,对机器的性能要求又过高,并且无法共享一些资源。因此.net就有AppDomain的概念,一个IIS进程里,给每一个网站一个AppDomain,这个每一个网站都相互独立。
因为字段存储在动态内存中,因此他们的值在运行时才能获取。字段能够是任何数据类型,没必要像常量那样仅仅是编译器内置的基元类型。
构造器(constructor)是容许将类型的实例初始化为良好状态的一种特殊方法。
编译后,构造器方法在“方法定义元数据表”中始终叫.ctor。
建立一个引用类型的实例,首先为实例的数据字段分配内存,而后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象实例的初始状态。
实例构造器永远不能被继承,派生类会自动调用基类的构造函数,也就是说类只有类本身定义的实例构造器。不能将如下修饰符应用于实例构造器:virtual, new, override, sealed和abstract。(实例构造器永远不能被继承,由于若是带参数的构造函数写了不少个,那用哪个呢?)
若是你定义的类没有显示定义任何构造器,C#编译器将自动隐式生成一个默认(无参)构造器,同时将字段初始化为它们的默认值。看以下代码:
public class A
{
}
//能够理解为它已经存在一个以下的构造函数
public class A
{
public A()
{
}
}
派生类构造函数自动调用基类的不带参数的构造函数,看如下代码:
public class B : A
{
public B()
{
}
}
//至关于
public class B : A
{
public B()
: base()
{
}
}
基类中带参数的构造函数必须显式调用,以下:
public class A
{
public A()
{
}
public A(string str)
{
}
}
public class B : A
{
public B()
: base("aaa")
{
}
}
base关键字用于从派生类中访问基类的成员:https://msdn.microsoft.com/zh-cn/library/hfw7t1ce.aspx
v 调用基类上已被其余方法重写的方法。
v 指定建立派生类实例时应调用基类的构造方法。
什么是抽象类:
抽象类提供多个派生类共享基类的公共定义,它既能够提供抽象方法,也能够提供非抽象方法。
抽象类不能实例化,必须经过继承由派生类实现其抽象方法,所以对抽象类不能使用new关键字,也不能被密封。
若是派生类没有实现全部的抽象方法,则该派生类也必须声明为抽象类。另外,实现抽象方法由overriding方法来实现。
抽象类具备如下特性:
v 抽象方法是隐式的虚方法。
v 抽象类不能实例化。
v 不能用 sealed修饰符修饰抽象类,由于这两个修饰符的含义是相反的。
采用 sealed 修饰符的类没法继承,而 abstract 修饰符要求对类进行继承。
v 从抽象类派生的非抽象类必须包括继承的全部抽象方法和抽象访问器的实际实现。
v 抽象类能够包含抽象方法和抽象访问器。
v 只允许在抽象类中使用抽象方法声明。
v 由于抽象方法声明不提供实际的实现,因此没有方法体。
方法声明只是以一个分号结束,而且在签名后没有大括号“{}”。
v 在派生类中,经过包括使用override修饰符的属性声明,能够重写抽象的继承属性。
abstract class ShapesClass
{
abstract public int Area();
}
class Square : ShapesClass
{
int side = 0;
public Square(int n)
{
side = n;
}
// Area method is required to avoid
// a compile-time error.
public override int Area()
{
return side * side;
}
static void Main()
{
Square sq = new Square(12);
Console.WriteLine("Area of the square = {0}", sq.Area());
}
}
抽象方法和虚方法最重要的区别:
v 抽象方法不能实例化,要子类必须强制性的覆盖它的方法 。
而虚方法则是提供了选择,能够覆盖能够不覆盖,继承基类中的虚方法。
虚拟方法必须有一个实现部分,并为派生类提供了覆盖该方法的选项。
相反,抽象方法没有提供实现部分,强制派生类覆盖方法(不然 派生类不能成为具体类)。
v abstract方法只能在抽象类中声明,虚方法则不是。
v abstract方法必须在派生类中重写,而virtual则没必要。
v abstract方法不能声明方法实体,虚方法则能够。
若是类的修饰符(modifier/modify declarations)为abstract,那么编译器生成的默认构造器的可访问性就为protected。
一个类型能够定义多个实例构造器。为了使代码可验证,类的实例构造器在访问从基类继承的任何字段前,必须先调用基类的构造器。
若是派生类的构造器没有显式调用基类的构造器,那么C#编译器会自动生成对默认的基类构造器的调用。
在极少数的状况下,能够在不调用实例构造器的前提下建立一个类型的实例。一个典型的例子是Object的MemberwiseClone方法。
该MemberwiseClone方法的做用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),而后将源对象的字节数据复制到新对象中。
C#语言提供了一个简单的语法,容许在构造引用类型的一个实例时,对类型中定义的字段进行初始化。换句话说,容许之内联(inline)方法初始化实例字段。
值类型构造器的工做方式与引用类型的构造器大相径庭。
CLR老是容许建立值类型的实例,而且没有办法阻止值类型的实例化。全部,值类型其实并不须要定义构造器,C#编译器根本不会为值类型生成默认的无参构造器。
类型构造器也称为静态构造器、类构造器或者类型初始化器。
类型构造器的做用是设置类型的初始状态。实例构造器的做用是设置类型的实例的初始状态。
默认状况下,类型没有定义类型构造器。类型构造器永远没有参数。
以下,C#为引用类型和值类型定义一个类型构造器:
internal sealed class SomeRefType
{
static SomeRefType() { }
}
internal struct SomeValType
{
static SomeValType() { }
}
类型构造器的特色是:无参,static标记,并且可访问性都是private,可是不能显示指定为private。
定义类型构造器相似于定义无参实例构造器,区别在于必须将它们标记为static。但C#会自动把类型构造器标记为private。事实上若是在源代码中显示将类型构造器标记为private,C#编译器会显示如下错误消息“静态构造函数中不容许出现访问修饰符”。必须是私有的缘由是,为了阻止任何由开发人员写的代码调用它,对它的调用老是由CLR完成的。
类型构造器的调用比较麻烦。当JIT编译器编译一个方法时,它会检查代码里面是否引入了其余类型。若是引入了其余类型的类型构造器,则JIT编译器会检测是否已经在AppDomain里面执行过。若是没有执行,则发起对类型构造器的调用,不然不调用。
多个线程同时调用某个类型的静态构造器时,如何确保构造器仅仅被执行一次:
在编译以后,线程会开始执行并最终获取调用构造函数的代码。实际上有多是多个线程执行同一个方法,CLR想要确保一个类型构造器在一个AppDomain里面只执行一次。当一个类型构造器被调用时,调用的线程会获取一个互斥的线程同步锁,这时若是有其余的线程在调用,则会阻塞。第一个线程会执行静态构造器中的代码。当第一个线程执行完后离开,其余的线程被唤醒并发现构造器的代码执行过了,因此不会继续去执行了,从构造器方法返回。CLR经过这种方式来确保构造器仅仅被执行一次。
因为CLR会确保类型构造器在每个AppDomain里面只会执行一次,是线程安全的。因此若是要初始化任何单例对象(singleton object),放在类型构造器里面是再合适不过了。
类型构造器里面的代码只能访问类型的静态字段,它的常规用途是初始化这些字段。C#提供了简单的语法来初始化类型的静态字段:
internal sealed class SomeType
{
private static Int32 s_x = 5;
}
上面的代码生成时,编译器自动回SomeType建立一个类型构造器以下:
internal sealed class SomeType
{
private static Int32 s_x;
static SomeType()
{
s_x = 5;
}
}
可是,C#不容许值类型使用内联字段初始化语法来实例化字段,因此下面这种方式就是错的:
internal sealed struct SomeType
{
private Int32 s_x = 5; //这样会报错,须要加static关键字
}
若是显式的定义了类型构造器,以下:
internal sealed class SomeType
{
private static Int32 s_x = 5;
static SomeType()
{
s_x = 10;
}
}
最终s_x的结果是10。这里,C#编译器首先会生成一个类型构造器方法,这个构造器首先初始化s_x为5,而后初始化为10。换句话说,在类型构造器里面的显示定义的代码会在 使用内联字段初始化语法来实例化静态字段以后执行。
只有当AppDomain卸载时,类型才会卸载。
类型构造器的性能(不懂)
有的编程语言容许一个类型定义操做符应该如何操做类型的实例。好比System.String重载了相等(==)和不等(!=)操做符。CLR对操做符重载一无所知,它甚至不知道什么是操做符。是编程语言定义了每一个操做符的含义,以及当这些操做符出现时,应该生成什么样的代码。
例如在C#中,向基元数字应用+符合,编译器会生成将两个数加到一块儿的代码。将+操做符应用于String对象,C#编译器会生成将两个字符串链接到一块儿的代码。
编译 源代码时,编译器会生成一个标识操做符行为的方法。CLR规范要求操做符重载方法必须是public和static方法。
如下C#代码中展现了一个类中定义的操做符重载方法:
namespace HelloCSharp
{
class OperatorTest
{
public int Value { get; set; }
public static void Main()
{
OperatorTest o1 = new OperatorTest();
o1.Value = 11;
OperatorTest o2 = new OperatorTest();
o2.Value = 22;
OperatorTest o3 = o1 + o2;
Console.WriteLine(o3.Value);
Console.ReadKey();
}
public static OperatorTest operator +(OperatorTest o1, OperatorTest o2)
{
OperatorTest o = new OperatorTest();
o.Value = o1.Value + o2.Value;
return o;
}
}
}
C# 容许用户定义的类型经过使用 operator 关键字定义静态成员函数来重载运算符。注意必须用public修饰,必须是类的静态的方法。同时,重载相等运算符(==)时,还必须重载不相等运算(!=)。< 和 > 运算符以及 <= 和 >= 运算符也必须成对重载。
之后重看
因为StringBuilder是可变的(mutable),因此它是处理字符串方法的首选。如今假定你想亲自定义一些缺失的方法,以方便操做一个StringBuilder。列如,StringBuilder中没有自定义的IndexOf方法,你也许想本身定义一个IndexOf方法。
C#扩展方法所作的事情是它容许你定义一个静态方法,并用实例方法的语法来调用它。为了将Indexof方法转变成扩展方法,只需在第一个参数前添加this关键字:
public static class StringBuilderExtensions
{
public static Int32 IndexOf(this StringBuilder sb, Char value)
{
for (Int32 index = 0; index < sb.Length; index++)
{
if (sb[index] == value)
return index;
}
return -1;
}
}
当C#编译器看到如下代码:
public class TestProgram
{
public static void Main()
{
StringBuilder sb = new StringBuilder("Hello. My name is Chris.");
Int32 index = sb.IndexOf('!');
//Int32 index1 = StringBuilderExtensions.IndexOf(sb, '!');
}
}
StringBuilderExtensions.IndexOf(sb, '!')影响了咱们对代码行为的理解,StringBuilderExtensions的使用显得“小题大作”,形成程序员没法专一于当前要执行的操做:IndexOf。
编译器首先检查StringBuilder类或者它的任何基类是否提供了获取单个Char参数、名为IndexOf的一个实例方法。若是存在这样的一个实例方法,编译器会生成IL代码来调用它。若是没有发现匹配的实例方法,则继续检查是否存在任何静态类定义了一个名为IndexOf的静态方法。静态方法中的第一个参数的类型是和当前用于调用方法的那个表达式的类型匹配的一个类型,而且这个类型必须使用this关键字来标识。在本例中,表达式是sb,它是StringBuilder类型。编译器会查找一个静态IndexOf方法,它有两个参数:一个StringBuilder(用this关键字进行标记),以及一个Char。编译器发现了这个IndexOf方法,并生成IL代码来调用这个静态方法。
String(引用类型)的不变性(immutable):
v String最为显著的一个特色就是它具备恒定不变性:一旦建立了一个String对象,在managed heap 上为他分配了一块连续的内存空间,咱们将不能以任何方式对这个String进行修改使之变长、变短、改变格式(不能修改String对象的值)。全部对这个String进行各项操做(好比调用ToUpper得到大写格式的String)而返回的String,其实是另外一个从新建立的String,其自己并不会产生任何变化。每次使用 String 类中的方法之一或进行运算时(如赋值、拼接等)时,都要在内存中建立一个新的字符串对象,这就须要为该新对象分配新的空间。
v StringBuilder此类表示值为可变字符序列的相似字符串的对象。之因此说值是可变的,是由于在经过追加、移除、替换或插入字符而建立它后能够对它进行修改。大多数修改此类的实例的方法都返回对同一实例的引用。实例的 int Capacity 属性,它表示内存中为存储字符串而物理分配的字符串总数。该数字为当前实例的容量。容量可经过 Capacity 属性或 EnsureCapacity 方法来增长或减小,但它不能小于 Length 属性的值。
注: .NET Framework中可变集合类如ArrayList 的Capacity 属性也相似这种自动分配机制。
8.6.1规则和原则
v C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操做符等。
v 扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。类名没有限制。扩展方法至少要有一个参数,并且只有第一个参数能用this关键字标记。
v C#编译器查找这些静态类中定义的扩展方法时,要求这些静态类自己必须具备文件做用域。换言之,此静态类不能嵌套在另外一个类中。
v 扩展方法有潜在的版本控制问题。若是Microsoft将来为StringBuilder添加了IndexOf实例方法,那么在从新编译咱们的代码时,编译器会从新绑定Microsoft的IndexOf的实例方法,而不是咱们的静态IndexOf方法。
8.6.2用扩展方法扩展各类类型
8.6.3 ExtensionAttribute类
分部方法partial method在分部类型的一个部分中定义它的签名,并在该类型的另一个部分中定义它的实现。
//工具生成的代码,存储在某个源代码文件中
internal sealed partial class Base
{
private String m_name;
//分布方法的声明
partial void OnNameChanging(String value);
public String Name
{
get { return m_name; }
set
{
OnNameChanging(value.ToUpper());
m_name = value;
}
}
}
//开发人员生成的代码,存储在另外一个源代码文件中
internal sealed partial class Base
{
//分部方法的实现
partial void OnNameChanging(String value)
{
//Calling the base class OnNameChanging method:
//base.OnNameChanging(value);
if (String.IsNullOrEmpty(value))
{
throw new ArgumentNullException(value);
}
}
}
分部方法规则和原则:
v 它们只能在分部类中声明。
v 分部方法的返回类型始终是void,任何参数都不能用out修饰符标记(out和ref的区别就是传入的参数是否已经初始化了)。
缘由:若是不是返回null,同时没有提供实现,那么调用一个未实现的方法,返回什么才合理呢?为了不对返回值进行任何无故的猜想,c#的设计者决定只容许方法返回void。
v 分部方法的声明和实现必须具备彻底一致的方法签名。
v 若是没有对应的实现部分,便不会在代码中建立一个委托来引用这个分部方法。
v 分部方法老是被视为隐式的private方法。可是C#编译器禁止你在分部方法声明以前添加访问修饰符关键字。
v 工具生成的代码,分布方法的声明要用partial关键字标记,无主体,没有方法实现。
v 开发者本身的代码中,分布方法的声明也要用partial关键字标记,有主体,有方法实现。
分部方法容许一个方法而不须要实现。若是没有实现分部方法,编译器会自动移除方法签名,不会生成任何表明分部方法的元数据。编译器也不会生成任何调用分部方法的IL指令。并且编译器也不会生成对本该传给分部方法的实参进行求值的IL的指令。在这个例子中,编译器不会生成调用ToUpper方法的指令。结果就是更少的元数据/IL,运行时的性能也获得大幅提高。
设计一个方法的参数时,可为部分或所有参数分配默认值。而后,调用这些方法的代码能够选择不指定部分实参,接受其默认值。
除此以外,调用方法时,还可经过指定参数名称的方式为其传递实参。
如下代码演示了可选参数和命名参数的用法:
public static class Program
{
private static Int32 s_n = 0;
private static void M(Int32 x = 9, String s = "A", DateTime dt = default (DateTime), Guid guid = new Guid())
{
Console.WriteLine("x={0},s={1},dt={2},guid={3}", x, s, dt, guid);
}
public static void Main() {
//若是调用时省略了一个实参,C#编译器会自动嵌入参数的默认值
M();
M(8, "x");
//为x显式传递5,指出我想为guid和dt的参数传递一个实参
M(5, guid: Guid.NewGuid(), dt: DateTime.Now);
M(s_n++, s_n++.ToString());
//使用已命名的参数传递实参
M(s: (s_n++).ToString(), x: s_n++);
}
}
Guid(Globally Unique Identifier 全球惟一标识符)一个经过特定算法产生的二进制长度为128位的数字标识符(16 字节),用于指示产品的惟一性。
向方法传递实参时,编译器按从左到右的顺序对实参进行求值。
C#中值传递与引用传递的区别:
把实参当作实参来传递时,就产生了一个新的拷贝。
class Test
{
static void Main(String[] args)
{
int x = 8;
Fo(x);
Console.WriteLine("x={0}", x);
}
static void Fo(int p)
{
p = p + 1;
Console.WriteLine("p={0}", p);
}
}
以上程序运行结果为p=9,x=8; 即X的值不会受P影响,给P赋一个新值并不会改变X的内容,由于P和X存在于内存中不一样的位置。
同理,用传值的方式传递一个引用类型对象时,只是复制这个对象自己,即复制其地址值,而不是它指代的对象。下面代码中Fo中看到的StringBuilder对象,就是在Main方法中实例化的那一个,只是有不一样的引用指向它而已。
class Test
{
static void Fo(StringBuilder foSB)
{
foSB.Append("test");
foSB = null;
}
static void Main()
{
StringBuilder sb = new StringBuilder();
Fo(sb);
Console.WriteLine(sb.ToString());
}
}
运行结果:test.
换句话说,sb和foSB是指向同一对象的不一样引用变量。由于FoSB是引用的拷贝,把它置为null并无把sb置为 null。
值传递:传的是对象的值拷贝。
引用传递:传的是栈中对象的地址。
ref和out:
ref和out关键字都致使参数经过引用传递。
传递到 ref 形参的实参必须先通过初始化,而后才能传递。
out 形参不一样,在传递以前,不须要显式初始化该形参的实参,out形参必须在Method方法中初始化。
关键字类似。
class RefExample
{
static void Method(ref int i)
{
// Rest the mouse pointer over i to verify that it is an int.
// The following statement would cause a compiler error if i
// were boxed as an object.
i = i + 44;
}
static void Main()
{
int val = 1;
Method(ref val);
Console.WriteLine(val);
// Output: 45
}
}
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}
经过引用传递的效果是,把变量做为参数传递给方法,在方法中修改该参数,会改变这个变量的值。
不要混淆经过引用传递的概念与引用类型的概念。不管方法参数是值类型仍是引用类型,都可由 ref 修改。
当经过引用传递时,不会对值类型装箱。
若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字。
尽管 ref 和 out 关键字会致使不一样的运行时行为,它们并不被视为编译时方法签名的一部分。
9.1.1规则和原则
在你定义的方法中,若是为部分参数指定了默认值,请注意下述这些额外的规则和原则:
v 能够为方法,构造器方法和有参属性的参数指定默认值。
v 没有默认值的参数必须放在有默认值的参数以前。例如,若是删除s的默认值(“A“),就会出现编译错误。
v 默认值必须是编译时能肯定的常量值。能够设置默认值的参数的类型是:C#认定的基元类型,枚举类型,以及能设为null的任何引用类型。
v 能够用default关键字或new关键字,将值类型的参数的默认值设为值类型的一个实例。
v 不要重命名参数变量(s, x, dt, guid)。不然,任何调用者以传参数名的方式传递实参,都必须修改它们的代码。
v 若是参数用ref或out关键字进行了标识,就不能设置默认值。由于没有办法为这些参数传递有意义的默认值。
v 实参可按任意顺序传递,但命名实参只能出如今实参列表的尾部。
v C#不容许省略逗号之间的实参,好比M(5, , dt: DateTime.Now);
9.1.2 DefaultParameterValueAttribute和OptionalAttribute
在C#中,一旦为某个参数分配了一个默认值,编译器就会在内部向该参数应用一个定制attribute,即System.Runtime.InteropServices.OptionalAttribute。这个attribute会在最终生成的文件的元数据中持久性的存储下来。
针对一个方法中的局部变量,C#容许根据初始化表达式的类型来推断它的类型。
默认状况下,CLR假定全部方法参数都是传值的。传递引用类型的对象时,对一个对象的引用会传给方法。注意这个引用自己是以传值方式传给方法的。
在方法中,必须知道传递的每一个参数是引用类型仍是值类型,由于用于操纵不一样类型的代码可能有显著的差别。
CLR容许以传引用而非传值的方式传递参数。在C#中,这是用关键字out或ref来作到的。这两个关键字都告诉C#编译器生成元数据来指明该参数是传引用的。编译器将生成代码来传递参数的地址,而不是传递参数自己。
若是方法的参数用out来标记,代表不期望调用者在调用方法以前初始化好对象。被调用的方法不能读取参数的值,并且在返回前必须向这个值写入。
public sealed class Program
{
public static void Main()
{
Int32 x;
GetVal(out x);//x在调用GetVal前没必要初始化
Console.WriteLine(x);
}
private static void GetVal(out Int32 v)
{
v = 10;//返回前必须初始化v
}
}
在前面的代码中,x是存储在Main的栈帧中声明的,而后x的地址传递给GetVal。GetVal的v是一个指针,它指向Main栈中的Int32值。
栈帧:在执行线程的过程当中进行的每一个方法调用都会在调用栈中建立并压入一个StackFrame。
相反,若是方法的参数用ref来标记,调用者必须在调用方法前初始化参数的值,被调用的方法能够读取值以及或者向值写入。
有时候开发人员想定义一个方法来获取可变数量的参数。
public static class Program
{
static Int32 Add(params Int32[] values)
{
Int32 sum = 0;
if (values != null)
{
for (Int32 x = 0; x < values.Length; x++)
{
sum += values[x];
}
}
return sum;
}
public static void Main()
{
//Console.WriteLine(Add(new Int32[] { 1, 2, 3 }));
Console.WriteLine(Add(1, 2, 3));
Console.ReadKey();
}
}
除了params外,以上方法的一切对你来讲都应该是很是熟悉的。
很明显数组能用任意数量的一组元素来初始化,再传给Add方法进行处理。
params只能用于方法签名中的最后一个参数。即在方法声明中的 params 关键字以后不容许任何其余参数,而且在方法声明中只容许一个 params 关键字。
params关键字告诉编译器向参数应用System.ParamArrayAttribute的一个实例。
C#编译器检测到一个方法调用时,会先检查全部具备指定名称、同时参数没有应用ParamArrayAttribute的方法。若是找到一个匹配的方法,编译器就生成调用它所需的代码。若是编译器没有找到一个以上匹配的方法,会接着检查应用了ParamArrayAttribute的方法。若是找到一个应用了ParamArrayAttribute的方法,编译器会先生成代码来构造一个数组,填充它的元素,在生成代码来调用选定的方法。
在前一个例子中,没有定义可获取3个Int32兼容实参的Add方法。可是编译器发如今一个Add方法调用中传递了一组Int32值,并且有一个Add方法的Int32数组参数应用了ParamArrayAttribute。所以,编译器会认为这是一个匹配,因此会生成代码,将实参保存到一组Int32数组中,再调用Add方法,并传递该实参。
最终的结果就是你能够直接向Add方法传递一组实参,编译器会生成代码,像上面例子中注释的代码同样,帮你构造和初始化一个数组来容纳实参。
声明方法的参数类型时,应尽可能指定最弱的类型,最好是接口而不是基类。
(重看)
CLR没有提供对常量对象/实参的支持。
属性容许源代码用一个简化的语法来调用一个方法。
CLR支持两种属性:无参属性 (parameterless property) ,有参属性(parameterful property)
C#中将有参属性称为索引器(indexer)
通常用类型的字段成员来实现获取或改变类型的状态信息。
面向对象设计和编程的重要原则之一就是数据封装(data encapsulation),它意味着类型的字段永远不该该公开。强烈建议将全部的字段都设为private。
要容许获取类型状态信息,就公开一个针对该用途的方法。
封装了字段访问的方法一般称为访问器(accessor)方法(以下面的GetName,SetName)。访问器方法能够对数据的合理性进行检查,确保对象的状态不被破坏。
public sealed class Employee
{
private String m_Nmae;
private Int32 m_Age;
public String GetName()
{
return m_Nmae;
}
public void SetName(String value)
{
m_Nmae = value;
}
public Int32 GetAge()
{
return m_Age;
}
public void SetAge(Int32 value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value", value.ToString(), "The value must be grater than or equal to 0");
}
m_Age = value;
}
public static void Main()
{
Employee e = new Employee();
e.SetName("Jeffery Richter");
String EmployeeName = e.GetName();
e.SetAge(41);
e.SetAge(-5);
Int32 EmployeeAge = e.GetAge();
}
}
将SetXxx方法标记为protected,就能够实现只容许派生类型修改值。
以上代码中,类型的用户必须调用方法,而不能直接引用一个字段名。
编程语言和CLR还提供了一种称为属性(property)的机制,以下:
public sealed class Employee
{
private String m_Nmae;
private Int32 m_Age;
public String Name
{
get { return (m_Nmae); }
set { m_Nmae = value; }
}
public Int32 Age
{
get { return (m_Age); }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value", value.ToString(), "The value must be greater than or equal to 0");
}
m_Age = value;
}
}
public static void Main()
{
Employee e = new Employee();
e.Name = "Jeffery Richter";
String EmployeeName = e.Name;
e.Age = 41;
e.Age = -5;
Int32 EmployeeAge = e.Age;
}
}
可将属性想象成智能字段(smart field),即背后有额外逻辑的字段。
每一个属性都有一个名称(Name,Age)和一个类型(String,Int32不能为void)。属性不能重载。定义属性时,能够省略set方法来定义一个只读属性,或者省略get方法来定义一个只写属性。
经过属性的get和set方法来操做类型内私有的字段,是一种很常见的作法。
之前面的Employee类型为例。编译器编译这个类型时,会发现其中的Name和Age属性。因为两个属性都有get和set访问器方法,因此编译器在Employee类型中生成4个方法定义。
若是只是为了封装一个支持字段而建立一个属性,C#还提供了更简单的语法,称为自动实现的属性(automatically Implemented Property)。
我我的不喜欢属性
System.Collections命名空间包含可以使用的集合类和相关的接口,提供了集合的基本功能。
IEnumerable 接口
System.Collections
该枚举数支持在非泛型集合上进行简单迭代
全部继承了IEnumerable的类,要使用foreach迭代器时,就须要使用该方法。所以也只有实现了该接口的类才能够使用foreach。
名称 |
说明 |
GetEnumerator() |
返回循环访问集合的枚举数。 |
IList 接口
System.Collections
IList 是 ICollection 接口的子代,而且是全部(非???)泛型列表的基接口
IList继承自ICollection
名称 |
说明 |
Add(Object) |
将某项添加到 IList 中。 |
Clear() |
从 IList 中移除全部项。 |
Contains(Object) |
肯定 IList 是否包含特定值。 |
CopyTo(Array, Int32) |
从特定的 Array 索引处开始,将 ICollection 的元素复制到一个 Array 中。(从 ICollection 继承。) |
GetEnumerator() |
返回循环访问集合的枚举数。(从 IEnumerable 继承。) |
IndexOf(Object) |
肯定 IList 中特定项的索引。 |
Insert(Int32, Object) |
将一个项插入指定索引处的 IList。 |
Remove(Object) |
从 IList 中移除特定对象的第一个匹配项。 |
RemoveAt(Int32) |
移除指定索引处的 IList 项。 |
ICollection<T> 接口
System.Collections.Generic
定义操做泛型集合的方法。
ICollection继承自IEnumerable
名称 |
说明 |
Add(T) |
将某项添加到 ICollection<T> 中。 |
Clear() |
从 ICollection<T> 中移除全部项。 |
Contains(T) |
肯定 ICollection<T> 是否包含特定值。 |
CopyTo(T[], Int32) |
从特定的 Array 索引开始,将 ICollection<T> 的元素复制到一个 Array 中。 |
GetEnumerator() |
返回一个循环访问集合的枚举器。(从 IEnumerable<T> 继承。) |
Remove(T) |
从 ICollection<T> 中移除特定对象的第一个匹配项。 |
ICollection主要针对静态集合;IList主要针对动态集合。
若是一个方法的返回值是IEnumerable<T> ,必须在方法后面使用.ToList()方法才能获得一个集合数据。
集合的初始化被认为是相加(Additive)操做,而非替换的操做。编译器发现Student属性的类型是List<String>,并且这个类型实现了IEnumerable<String>接口。以下:
public sealed class Classroom
{
private List<String> m_students = new List<String>();
public List<String> Students { get { return m_students; } }
public Classroom() { }
public static void Main()
{
Classroom classroom = new Classroom
{
Students = { "Chris","Jeff" }
};
//Classroom classroom = new Classroom();
//classroom.Students.Add("Chris");
//classroom.Students.Add("Jeff");
foreach (var student in classroom.Students)
Console.WriteLine(student);
}
}
重看
若是类型定义了事件成员,那么类型就能够通知其余对象发生了特定的事情。
例如,Button类提供了一个名为Click的事件。应用程序中的一个或多个对象可能想接收关于这个事件的通知,以便在Button被单击后采起某些操做。事件是实现这种交互的类型成员。
具体的说,若是定义一个事件成员,意味着类型要提供如下能力:
类型之因此能提供事件通知功能,是由于类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中全部已登记的方法。
CLR的事件模型创建在委托基础上的。委托是调用回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。
为了帮助你彻底理解事件在CLR中的工做机制,先来描述一个事件颇有用的场景。假定如今要设计一个电子邮件应用程序。电子邮件到达时,用户可能但愿将该邮件转发给传真机。建构这个应用程序时,假定先设计了一个名为MailManager的类型,它负责接收传入的电子邮件。MailManager类型公开了一个名为NewMail的事件。其余类型(如Fax和Pager)的对象登记它们对这个事件的关注。MailManager收到一封新电子邮件时,会引起该事件。形成邮件分发给每个已登记的对象。每一个对象都用它们本身的方式处理该邮件。
应用程序初始化时,让咱们只实例化一个MailManager实例。而后,应用程序可实例化任意数量的Fax和Pager对象。
MailManager示例应用程序展现了MailManager类型,Fax类型和Pager类型的全部源代码。
事件引起时,引起事件的对象可能但愿向接收事件通知的对象传递一些附加的信息。这些附加的信息须要封装到它本身的类中,该类一般包含一组私有字段,以及一些用于公开这些字段的只读公共属性。根据约定,这种类应该从EventArgs类派生,而且类名必须以EventArgs结束。
定义一个没有附加信息须要传递的事件时,可直接使用EventArgs.Empty,不用构造一个新的EventArg对象。
//第一步:定义一个类型来容纳全部应该发给事件通知接收者的附加信息
internal class NewMailEventArgs : EventArgs
{
private readonly String m_from, m_to, m_subject;
public NewMailEventArgs(String from, String to, String subject)
{
m_from = from; m_to = to; m_subject = subject;
}
public String From { get { return m_from; } }
public String To { get { return m_to; } }
public String Subject { get { return m_subject; } }
}
//后续的将在MailManager类中进行
internal class MailManager { }
internal class MailManager
{
public event EventHandler<NewMailEventArgs> NewMail;
void MethodName(object sender, NewMailEventArgs e);
}
事件成员使用C#关键字event来定义。每一个事件成员都要指定如下内容:
事件成员的类型是EventHandler<NewMailEventArgs>,意味着“事件通知”的全部接收者都必须提供一个原型和EventHandler<NewMailEventArgs>委托类型匹配的回调方法。
因为泛型EventHandler委托类型的定义以下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
因此方法原型必须具备如下形式:
void MethodName(object sender, NewMailEventArgs e);
。。。
泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另外一种形式的代码重用,即算法重用。
简单的说,开发人员先定义好一个算法,好比排序、搜索、交换、比较或者转换等。可是,定义算法的开发人员并不设定该算法要操做什么数据类型。该算法能够普遍地应用于不一样类型的对象。而后,另外一个开发人员,只有指定了算法要操做的具体数据类型,就能够开始使用这个现成的算法了。例如,能够用一个排序算法来操做Int32和String等类型对象。
大多数算法都封装在一个类型中,CLR容许建立泛型引用类型和泛型值类型,但不容许建立泛型枚举类型。CLR还容许建立泛型接口和泛型委托。
先来看一个简单的例子,Framework类库中定义了一个泛型列表算法,它知道如何管理一个对象集合。泛型算法没有设定这些对象的数据类型。
封装了泛型列表算法的FCL类称为List<T>。泛型List类的设计者紧接着在这个类名后添加一个<T>,代表它操做的是一个未指定的数据类型。
定义泛型类型时,它为类型指定的任何变量(好比T)都称为类型参数(type parameter)。T是一个变量名,在源代码中可以使用一个数据类型的任何位置,都能使用T。
列如:在List类定义中,
T被用做方法参数,Add方法接收一个T类型的参数public void Add(T item);
T被用做返回值,ToArray方法返回一组T类型的一维数组public T[] ToArray();
根据Microsoft的设计原则,泛型参数变量要么称为T,要么至少以大写T开头(如TKey和TValue)。
使用泛型类型或方法时,指定的具体数据类型称为类型实参(type argument)。
例如:开发人员可指定一个DateTime类型实参来使用List算法。
public class Program{
private static void SomeMethod()
{
//构造一个List来操做DateTime对象
List<DateTime> dtList = new List<DateTime>();
//向列表添加DateTime对象,不进行装箱
dtList.Add(DateTime.Now);
dtList.Add(DateTime.MinValue);
//尝试向列表中添加一个String对象,编译时报错,Invalid arguments
dtList.Add("1/1/2004");
//从列表提取一个DateTime对象
DateTime dt = dtList[0];
}
}
从以上代码能够看出,泛型为开发人员提供了如下优点:
泛型最明显的应用就是集合类。
FCL定义的几个泛型集合类,大多数都在System.Collections.Generic和System.Collections.ObjectModel命名空间中。
泛型是在CLR2.0版本中加入的,为了在CLR中加入泛型,Microsoft作了一下工做:
多继承(multiple inheritance)是指一个类从两个或多个基类派生的能力。
CLR不支持多继承,CLR只是经过接口提供了“缩水版”的多继承。
实现接口的类或结构必须实现接口定义中指定的接口成员。
interface IEquatable<T>
{
bool Equals(T obj);
}
实现IEquatable<T>接口的任何类或结构都必须包含与该接口指定的签名匹配的Equals方法的定义。
public class Car : IEquatable<Car>
{
public string Make { get; set; }
public string Model { get; set; }
public string Year { get; set; }
public bool Equals(Car car)
{
if (this.Make == car.Make && this.Model == car.Model && this.Year == car.Year)
{
return true;
}
else
{
return false;
}
}
}
IEquatable<T>的定义不为Equals提供实现,该接口仅定义签名。
类或结构能够实现多个接口,可是类只能继承单个类(抽象或不抽象)。
接口能够包含方法、属性、事件、索引器或这四种成员类型的任意组合。
接口成员会自动成为公共成员,不能包含任何访问修饰符。成员也不能是静态成员。
若要实现接口成员,实现类的对应成员必须是公共、非静态,而且具备与接口成员相同的名称和签名。
接口具备如下属性:
从Object派生任何类实际都继承了如下内容:
接口对一组方法签名进行了统一命名。接口还能定义事件,无参属性和索引器。全部这些本质上都是方法。但接口不能定义构造器方法。接口也不能定义任何实例字段。
C#禁止接口定义任何一种这样的静态成员。
在C#中是用interface关键字定义接口的。要为接口指定一个名称和一组实例方法签名。
对CLR而言,接口定义就像是一个类型定义。也就是说,CLR会为接口类型对象定义一个内部数据结构,同时可用反射机制来查询接口类型的功能。
和类型同样,接口可用在文件范围内定义,也可嵌套在另外一个类型中定义。定义接口类型时,可指定你但愿的任何可视性/可访问性(public, protect, internal等)。
接口成员会自动成为公共成员,不能包含任何访问修饰符。成员也不能是静态成员。
根据约定,接口类型名称要以大写I开头,目的是方便在源代码中辨认接口类型。
CLR支持泛型接口和在接口中的泛型方法。
如下代码展现了如何定义一个实现该接口的类型:
public interface IComparable<in T>
{
//接口成员不能包含任何访问修饰符,不能是静态成员,它会自动成为公共成员
int CompareTo(T other);
}
public sealed class Point : IComparable<Point>
{
private Int32 m_x, m_y;
//接口不能定义构造器方法,实现接口的类能够定义构造器方法
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
//和接口对应的成员必须是公共非静态的,和接口成员相同的名称和签名
public Int32 CompareTo(Point other)
{
return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)
- Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
}
public override String ToString()
{
return String.Format("{0},{1}", m_x, m_y);
}
}
public static class Program
{
public static void Main()
{
Point[] points = new Point[] { new Point(3, 3), new Point(1, 2) };
if (points[0].CompareTo(points[1]) > 0)
{
Point tempPoint = points[0];
points[0] = points[1];
points[1] = tempPoint;
}
Console.WriteLine("Points from closest to (0,0) to farthest:");
foreach (Point p in points)
Console.WriteLine(p);
}
}
C#编译器要求将用于实现一个接口的方法标记为public。
编译器会将实现接口的方法标记为virtual和sealed。
在.Net Framework中,字符老是表示成16位Unicode代码值,这简化了国际化应用程序的开发。
每一个字符都表示成System.Char结构的一个实例。
针对Char的一个实例,能够调用静态GetUnicodeCategory方法,这个方法返回的是System.Globalization.UnicodeCategory枚举类型的一个值。
Char类型提供了几个静态方法,好比IsDigit,IsUpper等。注意,全部这些方法要么获取单个字符做为参数,要么获取一个String以及目标字符在这个String中的索引做为参数。
ToLower和ToUpper之因此须要语言文化信息,是由于字母的大小写转换是一种依赖于语言文化的操做。语言文化信息是这两个方法在内部查询System.Threading.Thread类的静态CurrentCulture属性获取的。
除了这些静态方法,Char类型还提供了几个实例方法。好比:Equals方法会在两个Char实例表明同一个16位Unicode码位的前提下返回true。CompareTo方法返回两个Char实例忽略语言文化的比较结果。GetNumericValue方法,它返回字符的数值形式,如下代码演示了这个方法。
public static class Program {
public static void Main() {
Double d;
d = Char.GetNumericValue('3');//3
Console.WriteLine(d.ToString());
d = Char.GetNumericValue('A');//-1
Console.WriteLine(d.ToString());
}
}
能够使用三种技术实现各个数值类型与Char实例的相互转换。
如下代码演示了如何使用者三种技术:
public static class Program
{
public static void Main()
{
Char c;
Int32 n;
//使用C#强制类型转换
c = (Char)65;
n = (Int32)c;
//使用Convert类型
c = Convert.ToChar(65);
try
{
//700000000000对于Char的16位来讲过大
c = Convert.ToChar(700000000000);
}
catch (OverflowException)
{
Console.WriteLine("Cannot convert 700000000000 to a Char");
}
//使用IConvertible接口
c = ((IConvertible)65).ToChar(null);
}
}
一个String表明一个不可变(immutable)的顺序字符集。String类型直接派生自Object,因此它是一个 引用类型。所以,String对象永远存在于堆上,永远不会跑到线程栈。
C#将String视为一个基元类型-也就是说,编译器容许在源代码中直接表示文本常量字符串。编译器将这些文本常量字符串放到模块的元数据中,并在运行时加载和引用它们。
在C#中,不能使用new操做符从一个文本常量字符串构造一个String对象,必须使用简化过的语法。
public static class Program
{
//错误
String s = new String("Hi");
//正确
String s1 = "Hi";
}
对于换行符、回车符和退格符这样的特殊字符,C#采用转义机制。
\r return 回车
\n newline 换行
//包含回车换行符的字符串
String s = "Hi\r\nthere";
//如下是定义上述字符串的正确方式
String s1 = "Hi" + Environment.NewLine + "there";
能够使用C#的+操做符将几个字符串链接成一个。String s2 = "Hi" + "" + "there";
在上述代码中,因为全部字符串都是文本常量字符串,因此C#编译器会在编译时链接它们,最终只会将一个字符串(即"Hi there")放到模块的元数据中。对非文本常量字符串使用+操做符,链接则会在运行时进行。若要在运行时将几个字符串链接到一块儿,请避免使用+操做符,由于它会在堆上建立多个字符串对象,而堆是须要垃圾回收的,从而影响到性能。相反,应尽可能使用String.Text.StringBuilder类型。
C#还提供了“逐字字符串(verbatim strings)”声明方式,一般用于指定文件或目录的路径,或者与正则表达式配合使用。
//不使用逐字字符串字符@来声明字符串
String file = "C:\\Windows\\System32\\Notepad.exe";
//使用逐字字符串字符@来声明字符串
String file = @"C:\Windows\System32\Notepad.exe";
在字符串以前添加@符号,是编译器知道字符串是一个逐字字符串。事实上,这告诉编译器将反斜杠字符视为文本常量,而不是转义字符,使文件路径在源代码中更易读。
String对象最重要的一个事实就是,它使不可变的。也就是字符串一经建立便不能更改,不能变长变短或修改其中的任何字符。
通常会出于两方面的缘由来比较字符串:
进行排序时,应该老是执行区分大小写的比较。
Compare方法中ignoreCase设为true,不区分大小写。
判断字符串相等性或对字符串进行排序时,强烈建议调用下面的方法之一:
public bool Equals(string value, StringComparison comparisonType);
public static bool Equals(string a, string b, StringComparison comparisonType);
public static int Compare(string strA, string strB, StringComparison comparisonType);
public static int Compare(string strA, string strB, bool ignoreCase, CultureInfo culture);
public static int Compare(string strA, string strB, CultureInfo culture, CompareOptions options);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, StringComparison comparisonType);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, CultureInfo culture, CompareOptions options);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, bool ignoreCase, CultureInfo culture);
public bool StartsWith(string value, StringComparison comparisonType);
public bool StartsWith(string value, bool ignoreCase, CultureInfo culture);
public bool EndsWith(string value, StringComparison comparisonType);
public bool EndsWith(string value, bool ignoreCase, CultureInfo culture);
许多程序都将字符串用于内部编程目的,好比路径名、文件名、URL、注册表项/值、环境变量、反射、XML等。出于编程目的而比较字符串时,应该老是使用StringComparison.Ordinal,这是执行字符串比较时最快的一种方式,由于在执行比较时,不须要考虑语言文化信息。
从如今起咱们将讨论如何执行在语言文化上正确的比较。.Net Framework使用System.Globalization.CultureInfo表示一个“语言/国家”。
如下代码演示了序号比较和依赖语言文化比较的区别:
static void Main()
{
String s1 = "Strasse";
String s2 = "Straße";
Boolean eq;
//Compare返回非零值,若是传递Ordinal标志,Compare方法会忽略指定的语言文化
eq = String.Compare(s1, s2, StringComparison.Ordinal) == 0;
Console.WriteLine("Ordinal comparison:'{0}'{2}'{1}'", s1, s2, eq ? "==" : "!=");
//面向在德国说德语的人群
CultureInfo ci = new CultureInfo("de-DE");
//Compare返回零值
eq = String.Compare(s1, s2, true, ci) == 0;
Console.WriteLine("Cultural comparison:'{0}'{2}'{1}'", s1, s2, eq ? "==" : "!=");
}
如上一节所述,检查字符串的相等性是许多应用程序的常见操做 - 这个任务可能严重损害性能。
执行序号ordinal相等性检查时,CLR快速检查两个字符串是否具备数量相同的字符。若是答案是确定的,字符串有可能相等。而后CLR必须比较每一个单独的字符才能肯定。
除此以外,若是在内存中复制同一个字符串的多个实例,会形成内存的浪费,由于字符串是不可变的。若是只在内存中保留字符串的一个实例,那么将显著提升内存的利用率。须要引用字符串的全部变量只需指向单独一个字符串对象。
若是应用程序常常对字符串进行区分大小写的、序号式的比较,或者事先知道许多字符串对象都有相同的值,就可利用CLR的字符串留用(string interning)机制来显著提升性能。
CLR初始化时会建立一个内部哈希表,在这个表中,键(key)是字符串,而值(value)是对托管堆中的String对象的引用。
String类提供了两个方法,便于你访问这个内部哈希表:
public static string Intern(string str);
public static string IsInterned(string str);
Equals和ReferenceEquals的区别:
如下代码演示了字符串留用:
static void Main()
{
String s1 = "Hello";
String s2 = "Hello";
Boolean a = Object.ReferenceEquals(s1, s2);//true
s1 = String.Intern(s1);
s2 = String.Intern(s2);
Boolean b = Object.ReferenceEquals(s1, s2);//true
}
在对ReferenceEquals方法的第一个调用中,在CLR低版本中,s1引用堆中“Hello”字符串对象,而s2引用堆中另外一个“Hello”字符串对象。在CLR的4.0版本上运行时,CLR选择忽视C#编译器生成的attribute/flag。但程序集加载到AppDomain中时,CLR会对文本常量字符串“Hello”进行默认留用。结果就为True。
在对ReferenceEquals方法的第二个调用以前,“Hello”字符串被显示留用,s1如今引用一个已留用的“Hello”。而后,经过再次调用Intern,s2被设置成s1引用的同一个“Hello”字符串。如今,当第二次调用ReferenceEquals时,就能保证得到一个True的结果,无论程序集在编译时是否设置了attribute/flag。
编译器有将单个字符串的多个实例合并成一个实例的能力。
还能够利用String类型提供的一些方法来复制一个字符串或者一部分。
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
ToString:返回对同一个对象的引用
上述方法中将字符串的部分字符复制到字符数组中。
使用全部这些方法时都请牢记一点,它们返回的都是一个新的字符串对象。
因为String类型表明的是一个不可变字符串,因此FCL提供了另外一个名为System.Text.StringBuilder的类型。可利用它高效地对字符串进行动态处理,最后基于处理结果建立一个String。
从逻辑上说,StringBuilder对象包含一个字段,该字段引用了由Char结构构成的一个数组。可利用StringBuilder的成员来操纵这个字符数组,高效地缩短字符串或更改字符串中的字符。
使用StringBuilder的方法时要记住,大多数方法返回的都是对同一个StringBuilder对象的引用。因此能够方便的将几个操做连接到一块儿完成:
static void Main()
{
StringBuilder sb = new StringBuilder();
String s = sb.AppendFormat("{0} {1}", "Jeffrey", "Richter").Replace(' ', '-').Remove(4, 3).ToString();
}
拼接字符串示例:
String[] value = { "1", "2", "3" };
String a = "";
StringBuilder str = new StringBuilder();
foreach (String text in value)
{
str.AppendFormat(",{0}", text);//将value数组中的值拼接成一个字符串,以逗号分隔
}
if (str != null && str.Length > 0)
{
str.Remove(0, 1);//移除第一个逗号
}
a = str.ToString();//要将StringBuilder转换成字符串
String和StringBuilder类提供的方法并非彻底匹配的。例如:String提供了ToLower,ToUpper,EndsWith,Trim等方法,但StringBuilder类没有提供任何与之对应的方法。另外一方面,StringBuilder类提供了一个功能更全面的Replace方法,它容许替换做为一个字符串的一部分字符。而String类中Replace方法是public string Replace(char oldChar, char newChar);
因为这两个类中的方法不彻底对应,因此有时须要在String和StringBuilder转换来完成特定的任务。
StringBuilder sb = new StringBuilder();
String s = sb.AppendFormat("{0}, {1}", "Jeffrey", "Richter").ToString();
s.ToUpper();
sb.Length = 0;
sb.Append(s).Insert(8, "Marc-");
s = sb.ToString(1, 2);
咱们常常都要获取一个对象的字符串表示。能够调用ToString方法来获取任何对象的字符串表示。
无参ToStirng方法有两个问题。
String的Format方法。。。
解析字符串来获取一个对象,偶尔会用到。
Int32 x = Int32.Parse("1A", NumberStyles.HexNumber);//26
能解析一个字符串的任何类型都提供了Parse的一些public static方法。
先来看看如何将一个字符串解析成数值类型:
public static int Parse(string s, NumberStyles style, IFormatProvider provider);
s是字符串参数,NumberStyles是字符串参数s中运行的样式
Int32 x = Int32.Parse(" 123", NumberStyles.None); //要解析的字符串包含一个前导空白字符,会报FormatExpection异常
应该设成NumberStyles.AllowLeadingWhite
Microsoft在FCL中添增了一个更安全的字符串类System.Security.SecureString
Enumeration提供了一些很是炫酷的功能,相信大多数开发人员都不熟悉。这些新功能极大的简化了应用程序开发。
枚举类型(enumerated types)定义了一组“符号名称/值”配对。
如下Color类型定义了一组符号,每一个符号都标识一种颜色:
internal enum Color
{
White,//赋值0
Red, //赋值1
Greed,//赋值2
Blue, //赋值3
Orange//赋值4
}
固然,也能够写个程序用0表明白色,1表明红色,以此类推。但不该该将这些数字硬编码到代码中,而应换用枚举类型,由于:
每一个枚举类型都直接从System.Enum派生,后者从System.ValueType派生。而System.ValueType又从System.Object派生。因此,枚举类型是值类型,可表示成未装箱和已装箱形式。有别于其余值类型,枚举类型不能定义任何方法、属性和事件。
编译枚举类型时,C#编译器会把每一个符号转换成类型的一个常量字段。例如,编译器会把前面的Color枚举类型当作如下代码:
C#编译器实际上并不编译这段代码,由于它禁止定义从System.Enum这一特殊类型派生的类型。
枚举类型定义的符号是常量值,因此当编译器一旦发现代码引用了一个枚举类型的符号,就会在编译时用数值替代符号,代码将再也不引用定义了符号的枚举类型。
简单地说,枚举类型只是一个结构,其中定义了一组常量字段和一个实例字段。常量字段会嵌入程序集的元数据中,并可经过反射来访问。这意味着在运行时得到与一个枚举类型关联的全部符号及其值。还意味着能够将一个字符串符号转换成对应的数值。这些操做是经过System.Enum基类型来提供的。下面讨论其中的一些操做:
例如,System.Enum类型有一个名为GetUnderlyingType的静态方法,而System.Type类型有一个GetEnumUnderlyingType的实例方法。
public static Type GetUnderlyingType(Type enumType);
public virtual Type GetEnumUnderlyingType();
这些方法返回用于容纳一个枚举类型的值的基础类型。每一个枚举类型都有一个基础类型,能够是byte,short,int(最经常使用,也是C#默认选择的),long。C#要求只能指定基元类型名称,若是使用FCL类型名称(好比Int32),会报错。
咱们定义的枚举类型应该与须要调用它的那个类型同级。
如下代码演示了如何声明一个基础类型为byte的枚举类型:
internal enum Color : byte
{
White,
Red,
Greed,
Blue,
Orange
}
static void Main()
{
Console.WriteLine(Enum.GetUnderlyingType(typeof(Color))); //System.Byte
}
C# typeof() 和 GetType()区是什么?
C#编译器将枚举类型视为基元类型,因此,能够用许多熟悉的操做符(==,!=,<,>,<=,>=,+,-,^,&,|,++,--)来操纵枚举类型的实例。
全部这些操做符实际做用于每一个枚举类型实例内部的value_实例字段。
给定一个枚举类型的实例,可调用从System.Enum继承的ToString方法:
public static class Program
{
static void Main()
{
//Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
Color c = Color.Blue;
Console.WriteLine(c.ToString());//"Blue" 常规格式
Console.WriteLine(c.ToString("G"));//"Blue" 常规格式
Console.WriteLine(c.ToString("D"));//"3" 十进制格式
Console.WriteLine(c.ToString("X"));//"03" 十六进制格式
}
}
internal enum Color : byte
{
White,
Red,
Greed,
Blue,
Orange
}
Format:可调用它格式化一个枚举类型的值:
public static string Format(Type enumType, object value, string format);
Console.WriteLine(Enum.Format(typeof(Color), 3, "G"));//显示"Blue"
GetValues:获取枚举类型中定义的全部符号以及对应的值。
public static Array GetValues(Type enumType);
Color[] colors = (Color[])Enum.GetValues(typeof(Color));
Console.WriteLine("Number of symbols defined:" + colors.Length);
Console.WriteLine("Value\tSymbol\n-----\t------");
foreach (Color c in colors)
{
Console.WriteLine("{0,5:D}\t{0:G}", c);
}
GetName:返回数值的字符串表示。
Enum.GetName(typeof(Color), 3);//"Blue"
GetNames:返回一个String数组,每一个符号都表明一个String。
Enum.GetNames(typeof(Color));
// {string[5]}
//[0]: "White"
//[1]: "Red"
//[2]: "Greed"
//[3]: "Blue"
//[4]: "Orange"
Parse, TryParse:将一个符号转换成枚举类型的实例。
public static object Parse(Type enumType, string value, bool ignoreCase);
Color c = (Color)Enum.Parse(typeof(Color), "orange", true); //Orange
Enum.Parse(typeof(Color), "0", true);//White
bool a=Enum.TryParse<Color>("Brown", false, out c);//false, 枚举中没有定义Brown
IsDefine:判断一个值对于一个枚举类型是否合法。
Enum.IsDefined(typeof(Color), "white");//false, 执行的是区分大小写的检查
Enum.IsDefined(typeof(Color), 5);//false, Color枚举类型没有与5对应的符号
咱们能够将位标志当作一种特殊的枚举类型。
FileAttributes类型是基本类型为Int32的枚举类型,其中每一位都反映文件的一项属性。
[Flags] //指示能够将枚举做为位域(即一组标志)处理。
public enum FileAttributes
{
ReadOnly = 1,
Hidden = 2,
System = 4,
Directory = 16,
Archive = 32,
Device = 64,
Normal = 128,
Temporary = 256,
SparseFile = 512,
ReparsePoint = 1024,
Compressed = 2048,
Offline = 4096,
NotContentIndexed = 8192,
Encrypted = 16384,
IntegrityStream = 32768,
NoScrubData = 131072
}
以上FileAttributes类型中,1的二进制为1,2的二进制为10,4的二进制为100。也就是说能够用每一个二进制位来确认惟一性,这就是位标志的原理。
public static void Main()
{
//获得可执行文件(.exe文件)的相对路径(如:"...\bin\Debug\ConsoleApplication1.exe")
String file = Assembly.GetEntryAssembly().Location;
//调用System.IO.File类型的GetAttributes方法,会返回FileAttributes类型的一个实例
FileAttributes attributes = File.GetAttributes(file);
//由于二进制1&1才为1,因此只要存在最后的数值必定不为1,判断文件是否隐藏
Console.WriteLine("IS {0} hidden?{1}", file, (attributes & FileAttributes.Hidden) != 0);
//判断文件是否隐藏,换种写法。Enum有一个HasFlag方法,肯定当前实例attributes中是否设置了一个或多个位域
Console.WriteLine("IS {0} hidden?{1}", file, attributes.HasFlag(FileAttributes.Hidden));
//将一个文件的属性改成只读和隐藏
File.SetAttributes(file, FileAttributes.ReadOnly | FileAttributes.Hidden);
}
数组是容许将多个数据项看成一个集合来处理的机制。CLR支持一维数组、多维数组和交错数组(即由数组构成的数组)。全部数组类型都隐式地从System.Array抽象类派生,意味着数组始终为引用类型,是在托管堆上进行内存分配的。在你的应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组自己的元素。
Int32[] myIntegers;//声明一个数组引用
myIntegers = new Int32[100];//建立含有100个Int32的数组
在第一行代码中,myIntegers变量能指向一个一维数组。myIntegers刚开始被设为null,由于当时尚未分配数组。第二行代码中分配了含有100个Int32值的一个数组,全部Int32都被初始化为0。因为数组是引用类型,因此会在托管堆上分配容纳100个未装箱Int32所需的内存块。除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员。该数组的内存块地址被返回并保存到myIntegers变量中。
还能够建立引用类型的数组:
本章要讨论回调函数。回调函数是一种很是有用的编程机制,它的存在已经有不少年了。
Microsoft .Net Framework经过委托(delegate)来提供了一种回调函数机制。
列如:委托确保回调方法是类型安全的。委托还容许顺序调用多个方法,并支持调用静态方法和实例方法。
C#中委托是在程序运行时能够使用它们来调用不一样的函数。
举个简单的例子,你是编程的,你如今正在写一个ASP.NET网页,而JS是你不熟悉的,因而你委托你的一位同事来帮助你完成JS部分。这就是委托,把你所不能作的事情交给其余人去作。
1.简单的委托http://www.cnblogs.com/birdshover/archive/2008/01/07/1029471.html
那么委托须要承载哪些信息呢?首先它存储了方法名,还有参数列表(方法签名),以及返回类型,好比:
delegate String/*返回类型*/ ProcessDelegate(Int32 i);
蓝色部分是声明委托的关键字,红色是返回类型,黑色部分是委托的类型名,()里的就是参数部分。你要使用这个委托来作事情,必须知足一下条件:
例如:
输出的结果是:Text1Tex2
public delegate String ProcessDelegate(String s1, String s2);
class Program
{
static void Main()
{
//使用委托ProcessDelegate来调用Process方法
ProcessDelegate pd = new ProcessDelegate(new Test().Process);
Console.WriteLine(pd("Text1", "Text2"));
}
}
public class Test
{
public String Process(String s1, String s2)
{
return s1 + s2;
}
}
2.回调函数
回调函数就是把一个方法传给另外一个方法去执行。它与委托不一样在于,它的方法参数,返回值均可以和调用者的参数,返回值能够不同。
输出结果:
Text1Text2
Text1
Text2
Text2Text1
public delegate String ProcessDelegate(String s1, String s2);
class Program
{
static void Main()
{
Test t = new Test();
//Process方法(调用者)调用了一个回调函数Process1,固然这里只执行了回调函数。
//能够看出,能够把任意一个符合这个委托的方法传递进去,意思就是说这部分代码是可变的。
//将Process1 2 3方法传递给Process方法去执行
string r1 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process1));
string r2 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process2));
string r3 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process3));
Console.WriteLine(r1);
Console.WriteLine(r2);
Console.WriteLine(r3);
}
}
public class Test
{
public String Process(String s1, String s2, ProcessDelegate process)
{
return process(s1, s2);
}
public String Process1(String s1, String s2)
{
return s1 + s2;
}
public String Process2(String s1, String s2)
{
return s1 + Environment.NewLine + s2;
}
public String Process3(String s1, String s2)
{
return s2 + s1;
}
}
如下代码演示了如何声明、建立和使用委托:
using System;
using System.Windows.Forms;
using System.IO;
namespace WindowsFormsApplication1
{
//声明一个委托类型,它的实例引用一个方法
//指定一个回调函数的签名,该方法获取一个Int32参数,返回void
internal delegate void Feedback(Int32 value);
public sealed class Program
{
public static void Main()
{
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
public static void StaticDelegateDemo()
{
Console.WriteLine("----Static Delegate Demo----");
Counter(1, 3, null);
//前缀Program可选
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
}
private static void InstanceDelegateDemo()
{
Console.WriteLine("----Instance Delegate Demo----");
Program p = new Program();
Counter(1, 3, new Feedback(p.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(Program p)
{
Console.WriteLine("----Chain Delegate Demo 1----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program p)
{
Console.WriteLine("----Chain Delegate Demo 2----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
//若是指定了任何回调函数,就调用它们
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value)
{
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value)
{
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
}
在StaticDelegateDemo方法中,第一次调用Counter方法时,为第三个参数传递的是null。因为Counter的fb参数收到的是null,因此每一个数据项在处理时,都不会调用回调函数。
接着StaticDelegateDemo方法再次调用Counter方法,为第三个参数传递一个新构造的Feedback委托对象。委托对象(new操做符新建的Feedback对象)是方法的一个包装器(wrapper),使方法能经过包装器来间接回调。
在本例中,静态方法的完整名称Program.FeedbackToConsole被传给Feedback委托类型的构造器。代表FeedbackToConsole就是要包装的方法。new操做符返回的引用做为Counter的第三个参数来传递。
在一个类型中,能够经过委托来调用另外一个类型的私有成员时,只要委托对象是有具备足够安全性和可访问性的代码建立时,便不会有问题。
这个例子中的全部操做都是类型安全的。例如,在构造Feedback委托对象时,编译器确保Program的FeedbackToConsole方法的签名,兼容于Feedback委托定义的签名。具体的说,FeedbackToConsole必须获取一个参数,并且二者都必须有相同的返回类型(void)。
将一个方法绑定到委托时,C#和CLR都容许引用类型的协变形和逆变性。
InstanceDelegateDemo中构造了一个名为p的Program对象。这个Program对象没有定义任何实例字段和属性。向Counter委托类型的构造函数传递的是p.FeedbackToFile,这致使委托包装对FeedbackToFile方法的一个引用,这个方法是实例方法,而不是静态方法。当Counter调用由其fb实参标识的回调函数时,会调用FeedbackToFile实例方法。
从表面看,委托彷佛很容易使用:用C#的delegate关键字,用熟悉的new操做符构造委托实例。
CLR和编译器作了大量的工做来隐藏委托的复杂性。
首先让咱们从新审视这一行代码:
为何会有可空类型:
当咱们设计一个数据库时,可将一个数据库字段的数据类型定义成一个32位整数,并映射到FCL的Int32数据类型对象。在数据库中的一个列可能容许值为空,但在C# 语言中是不能为null的。用.Net Framework处理数据库数据可能变得至关困难,由于在CLR中,没有办法将Int32值表示为null。为了解决这个问题,Microsoft在CLR中引入了可空值类型(nullable value type)的概念。
可空类型也是值类型,只是它是包含null的一个值类型。C#用问号表示法来声明并初始化变量。
这个”?”修饰符只是C#提供的一个语法糖 (所谓语法糖,就是C#提供的一种方便的形式,其实确定没有Int32? 这个类型。这个Int32?编译器认为的就是Nullable< Int32>类型,便可空值类型)
public struct Nullable<T> where T : struct
C#容许在可空类型上执行转换和转型:
public static void ConversionsAndCasting()
{
Int32? a = 5; //从非可空的Int32转换为Nullable<Int32>, 等同与Nullable<Int32> a = 5;
Int32? b = null; //从null隐式转换为Nullable<Int32>
Int32 c = (Int32)a; //从Nullable<Int32>显式转换为非可空Int32
//在可空基元类型之间转换
Double? d = 5;//Int32转型为Double
Double? e = b;//Int32?转型为Double
}
C#容许向可空类型实例应用操做符:
public static void Operators()
{
Int32? a = 5;
Int32? b = null;
a++;
b = -b;
a = a + 3;
b = b * 3;
if (a == null) { } else { }
if (b == null) { } else { }
if (a != b) { } else { }
if (a < b) { } else { }
}
C#提供了空接合操做符,即??操做符,它要获取两个操做数。
假如左边的操做数不为null,就返回左边的这个操做数的值。若是左边的操做数为null,就返回右边的操做数的值。
利用空接合操做符,能够方便地设置变量的默认值,避免在代码中写if / else语句,简化代码数量,从而有利于阅读。
public static void NullCoalescingOperator()
{
Int32? b = null;
Int32 x = b ?? 123;//等价于x = (b.HasValue) ? b.Value : 123;
}
其实可空类型的装箱和拆箱操做你们能够就理解为非可空值类型的装箱和拆箱的过程,只是对于非可空类型由于包含null值,因此CLR会提早对它进行检查下它是否为空,为null就不不任何处理,若是不为null,就按照非可空值类型的装箱和拆箱的过程来装箱和拆箱。
错误处理要分几个部分。首先,咱们要定义什么是错误。而后,咱们要讨论如何判断代码正在经历一个错误,以及如何从这个错误中恢复。这个时候,状态就成为一个要考虑的问题,由于错误经常在不恰当的时机发生。代码可能在状态改变的中途发生错误。在这种状况下,就可能须要将一些状态还原成改变以前的状态。固然,咱们还要讨论代码如何通知它的调用者检测到了一个错误。
本章要讨论针对未处理的异常、约束执行区域(constraind execution region, CER)、代码契约、运行时包装的异常以及未捕捉的异常。
设计类型时,首先要想好类型的各类使用状况。类型名称一般是一个名词,例如FileStream或者StringBuilder。而后,要为类型定义属性、方法、事件等。这些成员(属性的数据类型、方法的参数、返回值等)的定义方式就是类型的编程接口。这些成员表明类自己或者类型实例能够执行的行动。行动成员一般用动词表示,例如Read,Write,Flush,Append,Insert和Remove等。当行动成员不能完成任务时,就应抛出异常。
面向对象的编程大大提升了开发人员的效率,由于咱们能够这样写代码:
public bool TestFunc(string input)
{
return input.Substring(1, 1).ToUpper().EndsWith("E");
}
咱们没有作任何的参数检查,而直接调用了一长串的方法。当input参数为null或空时,上面的代码就会抛出异常。即便方法为没有返回值的void型也应该报告错误,.Net Framework提供的这种机制就叫作异常处理(excepton handling)。
本节将介绍异常处理(exception handling)的机制,以及进行异常处理所需的C#构造(construct)。
下面的C#代码展现了异常处理机制的标准用法,经过它能够对异常处理及用途有一个初步认识,后续将对try,catch和finally块作进一步讲解。
private void SomeMethod()
{
try
{
//须要执行的代码放在这里
}
catch (InvalidOperationException) { }
catch (IOException)
{
//从IOException恢复的代码放在这里
}
catch
{
//从除上面的异常外的其余异常恢复的代码放在这里
throw; //从新抛出捕捉到的任何东西
}
finally
{
//这里的代码老是执行,对始于try块的任何操做进行清理
}
// 若是try块没有异常,或异常被捕获后没有抛出,就执行这里的代码
}
try块包含的代码一般须要执行一些通用的资源清理操做,或者可能抛出异常须要从异常中恢复。清理代码应放在一个finally块中。try块还可包含也许会抛出异常的代码。异常恢复代码应该放在一个或多个catch块中。针对应用程序能从中安全恢复的每一种异常,都应该建立一个catch块。一个try块至少要有一个关联的catch块或finally块。
catch块包含的是响应一个异常须要执行的代码。若是try块中的代码没有形成异常的抛出,CLR永远不会执行它的任何catch块中的代码。线程将跳过全部catch块,直接执行finally中的代码。finally块中的代码执行完毕后,从finally块后面的代码继续执行。catch关键字后面的圆括号中的表达式称为捕捉类型(catch type)。在C#中必须将捕捉类型指定为System.Exception或者它的一个派生类型。
用VS调试catch块时,可经过在监视窗口中添加特殊的变量名称$exception来查看当前抛出的异常对象。
CLR自上而下搜索一个匹配的catch块,因此应该将较具体的异常放在顶部。也就是说,首先出现的是派生程度最大的异常类型,接着是它们的基类型,最后是System.Exception。
若是在try块中的代码抛出一个异常,CLR将搜索捕捉类型与抛出的异常相同的(或者是它的基类)catch块。没有捕捉类型的catch块将捕捉剩余的全部异常。
catch块中的代码一般执行一些对异常进行处理的操做。C#容许在捕捉异常后指定一个变量。捕捉到一个异常时,该变量将引用抛出的这个System.Exception派生对象。catch块中的代码,能够经过引用该变量来访问异常的具体信息。
finally块包含的代码是保证会执行的代码。一般finally块中的代码执行的是try块中行动所要求的资源清理操做。
private void ReadData(String pathname)
{
FileStream fs = null;
try
{
fs = new FileStream(pathname, FileMode.Open);
//处理文件中的数据...
}
catch (IOException)
{
//在此添加从IOException恢复的代码
}
finally
{
//确保文件被关闭
if (fs != null)
fs.Close();
}
}
上述代码中,将关闭文件的语句放在finally块以后是不正确的,由于倘若异物抛出但未被捕捉到,该语句就执行不到,形成文件打开状态,直到下一次垃圾回收。
try块并不是必定要关联一个finally块。有时候try中的代码并不须要任何清理工做。可是,若是有finally块,它必须出如今全部catch块以后。记住,finally块中的代码是清理代码,这些代码只需负责对try块中发起的操做进行清理。
微软定义了一个System.Exception类型,并规定全部公共语言规范(CLS)相容的编程语言都必须能抛出和捕捉派生自该类型的异常。C#只容许抛出CLS相容的异常。派生自System.Exception的异常类型被认为是CLS相容的。
最经常使用的Exception的属性是Message,StackTrace和InnerException。分别表示异常的文字消息,异常的方法堆栈信息,以及内部异常。
这里有必要讲一下System.Exception类型提供的只读属性StackTrace。catch块可读取该属性来获取一个堆栈跟踪(stack trace),它描述了异常发生以前调用的全部方法和签名,该属性对于调试很是有用。访问该属性时,实际要调用CLR中的代码,该属性并非简单地返回一个字符串。构造Exception派生类型的一个新对象时,StackTrace属性被初始化为null。若是此时读取该属性,获得的不是堆栈追踪,而是一个null。
一个异常抛出时,CLR会在内部记录throw指令的位置。一个catch块捕捉到该异常时,CLR又会记录异常的捕捉位置。在catch块内访问被抛出的异常对象的StackTrace属性时,负责实现该属性的代码会调用CLR内部的代码,后者建立一个字符串来指出从异常抛出位置到异常捕捉位置的全部方法。
字符串--at ConsoleApplication2.Program.Main(String[] args) in d:\CLR练习\ConsoleApplication2\ConsoleApplication2\Program.cs:line 20
若是CLR能找到你的程序集的调试符号,那么在System.Exception属性返回的字符串中,将包含源代码文件路径和代码行号。
相反,若是仅仅使用throw关键字自己(删除后面的e)来从新抛出一个异常对象,CLR就不会重置堆栈的起点。
实现本身的方法时,若是方法没法完成方法名所指明的任务,就应抛出一个异常。
抛出异常时,须要注意两个问题:
设计本身的异常不只繁琐,还容易出错。主要缘由是从Exception派生的全部类型都应该是可序列化的,使它们能穿越AppDomain边界边界或者写入日志/数据库。
下面是建立一个自定义异常类型的几个原则:
1,声明序列化,这样能够跨AppDomain访问。
2,添加默认构造函数。
3,添加只有一个message参数的构造函数。
4,添加包含message,内部异常参数的构造函数。
5,添加序列化信息的构造函数,访问级别设为private或protected。
定义自定义异常类型:
[Serializable]
public sealed class DiskFullException : Exception
{
public DiskFullException()
: base()
{ }
public DiskFullException(string message)
: base(message)
{ }
public DiskFullException(string message, Exception innerException)
: base(message, innerException)
{ }
public DiskFullException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
使用例:
try
{
throw new DiskFullException("disk is full");
}
catch (DiskFullException ex)
{
Console.WriteLine(ex.Message);
}
面向对象编程,编译器功能,CLR功能以及庞大的类库——使.Net Framework成为一个颇具吸引力的开发平台。但全部的这些东西,都会在你的代码中引入你没有什么控制权的“错误点”,若是 OutOfMemoryExcepton等。程序开发不可能对这些异常进行一一捕捉,让应用程序变得绝对健壮。意料意外的异常每每形成程序状态的破坏,为 了缓解对状态的破坏,能够作下面几件事:
●执行catch或finally块时,CLR不容许终止线程,因此能够向下面这样写是Transfer方法变得健壮:
private void Transfer(Account from, Account to, decimal amount)
{
try {/* 这里什么也没作*/ }
finally
{
from.Money -= amount;
//如今,这里不可能发生线程终止(因为Thread.Abort/AppDomain.Unload)
to.Money += amount;
}
}
可是,毫不建议将全部代码都放到finally块中!这个技术只适合于修改及其敏感的数据。
●能够用System.Diagnostics.Contracts.Constract类向方法应用代码契约。
●能够使用约束执行区域(Constrained Excecution Region,CER),它提供了消除CLR不肯定性的一种方式。
●可利用事务(transaction)来确保状态要么修改,要么都不修改。如TransactionScope类。
●将本身的方法设计的更明确。以下面的Monitor类实现线程同步:
public static class SomeType
{
private static readonly object s_lockObject = new object();
public static void SomeMethod()
{
Monitor.Enter(s_lockObject);//若是抛出异常,是否获取了锁?
//若是已经获取了锁,它就得不到释放
try
{
//在这里执行线程安全的操做
}
finally
{
Monitor.Exit(s_lockObject);
}
}
}
因为存在上面展现的问题,这个重载的Monitor的Enter方法已经再也不鼓励使用,建议像下面这样写:
public static class SomeType
{
private static readonly object s_lockObject = new object();
public static void SomeMethod()
{
bool lockTaken = false;//假定没有获取锁
try
{
Monitor.Enter(s_lockObject,ref lockTaken);//不管是否抛出异常,如下代码都能正常工做
//在这里执行线程安全的操做
}
finally
{
//若是以获取就释放它。
if(lockTaken == true) Monitor.Exit(s_lockObject);
}
}
}
虽然以上代码变得更明确,但在线程同步锁的状况下,如今的建议是根本不要随同异常处理使用它们。
●若是肯定状态以损坏到没法修改的程度,就应销毁全部损坏的状态,防止它形成更多的伤害。而后重启应用程序,将应用程序恢复到一个良好的状态。因为托管代码不能泄露到一个AppDomain的外部,你能够调用AppDomain的Unload方法来卸载整个AppDomain。若是以为状态过于糟糕,以致于须要终止这个进程,你能够调用Environment的FailFast方法。这个方法中能够指定异常消息,调用这个方法时,不会运行任何活动的try/finally块或者Finalize方法。而后它会将消息发送个Windows Application的日志。
咱们认为finally块很是强悍!无论线程抛出什么样的异常,finally块中的代码都保证会执行。应该用finally块清理那些已成功启动的操 做,而后再返回调用者或执行finally块以后的代码。Finally块还常常用于显示释放对象以免资源泄漏。以下例:
public static void SomeMethod()
{
FileStream fs = new FileStream(@"c:\test.txt", FileMode.Open);
try
{
//显示用100除以文件第一个字节的结果
Console.WriteLine(100 / fs.ReadByte());
}
finally
{
//清理资源,即便发生异常,文件都能关闭
fs.Close();
}
}
确保清理代码的执行时如此重要,以致于许多编程语言都提供了一些构造来简化清理代码的编写。例如:只要使用了lock,using和foreach语 句,C#编译器就会自动生成try/finally块。另外,重写类的析构器(Finalize)时,C#编译器也会自动生成try/catch块。使用 这些构造时,编译器将你写的代码放到try块内,并自动将清理代码放在finally块内,具体以下:
●使用lock语句,锁会在finally块中释放。
●使用using语句,会在finally块中调用对象的Dispose方法。
●使用foreach语句,会在finally块中调用IEnumerator对象的Dispose方法。
●定义析构方法时,会在finally块调用基类的Finalize方法。
例如,用using语句代替上面的代码,代码量更少,但编译后的结果是同样的。
public static void SomeMethod()
{
using (FileStream fs = new FileStream(@"c:\test.txt", FileMode.Open))
{
Console.WriteLine(100 / fs.ReadByte());
}
}
异常抛出时,CLR会在调用栈中向上查找与抛出的异常类型匹配的catch块。
异常抛出时,CLR会在调用栈中向上查找与抛出异常类型匹配的catch块。若是没有找到一个匹配的catch块,就发生一个未处理异常。CLR检测到进程中的任何线程有一个未处理的异常,就会终止进程。Microsoft的每种应用程序都有本身的与未处理异常打交道的方式。
●对于任何应用程序,查阅System.Domain的UnhandledException事件。
●对于WinForm应用程序,查阅System.Windows.Forms.NativeWindow的 OnThreadException虚方法,System.Windows.Forms.Application的OnThreadException虚 方法,System.Windows.Forms.Application的ThreadException事件。
●对于WPF应用程序,查阅System.Windows.Application的 DispatcherUnhandledException事件和System.Windows.Threading.Dispatcher的 UnhandledException和UnhandledExceptionFilter事件。
●对于Silverlight,查阅System.Windows.Forms.Application的ThreadException事件。
●对于ASP.NET应用程序,查阅System.Web.UI.TemplateControl的Error事件。 TemplateControl类是System.Web.UI.Page类和System.Web.UI.UserControl类的基类。另外还要查 询System.Web.HttpApplication的Error事件。
约束执行区是必须对错误有适应能力的一个代码块,说白点,就是这个代码块要保证可靠性很是高,尽可能不出异常。看看下面这段代码:
public static void Demo1()
{
try {
Console.WriteLine("In Try");
}
finally
{//Type1的静态构造器在这里隐式调用
Type1.M();
}
}
private sealed class Type1
{
static Type1()
{
//若是这里抛出异常,M就得不到调用
Console.WriteLine("Type1's static ctor called.");
}
public static void M() { }
}
运行上述代码,获得如下的结果:
In Try
Type1's static ctor called.
咱们但愿的目的是,除非保证finally块中的代码获得执行,不然try块中的代码根本就不要开始执行。为了达到这个目的,能够像下面这样修改代码:
public static void Demo1()
{
//强迫finally的代码块提早准备好
RuntimeHelpers.PrepareConstrainedRegions();
try {
Console.WriteLine("In Try");
}
finally
{//Type1的静态构造器在这里隐式调用
Type1.M();
}
}
private sealed class Type1
{
static Type1()
{
//若是这里抛出异常,M就得不到调用
Console.WriteLine("Type1's static ctor called.");
}
//应用p了1System.Runtime.ConstrainedExecution命?名?空o间的IReliabilityContract特A性á
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static void M() { }
}
获得的结果以下:
Type1's static ctor called.
In Try
PrepareConstrainedRegions是个很是特别的方法,JIT编译器遇到这个方法,就会提早编译与try关联的catch和 finally块中的代码。JIT编译器会加载任何程序集,建立任何类型,调用任何静态构造器,并对方法进行JIT编译,若是其中的任何操做发生异常,这 个异常会在try块钱抛出。
须要JIT提早准备的方法必需要应用ReliabilityContract特性,而且向这个特性传递的参数必须是 Consistency.WillNotCorruptState或Consistency.MayCorruptInstance。这是因为假如方法会 损坏AppDomain或进程的状态,CLR便没法对状态的一致性作出任何保证。请确保finally块中只有刚刚描述的应用了 ReliabilityContract特性的方法。向ReliabilityContract传递的另外一个参数Cer.Success,表示保证该方法 不会失败,不然用Cer.MayFail。Cer.None这个值代表方法不进行CER保证。换言之,方法没有CER的概念。对于没有应用 ReliabilityContract特性的方法等价于下面这样
[ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)]
迫使JIT编译器预先准备的还有几个静态方法,它们都定义在RuntimeHelper中:
public static void PrepareMethod(RuntimeMethodHandle method);
public static void PrepareMethod(RuntimeMethodHandle method, RuntimeTypeHandle[] instantiation);
public static void PrepareDelegate(Delegate d);
public static void PrepareContractedDelegate(Delegate d);
还应关注下RuntimeHelpers 的ExecuteCodeWithGuaranteedCleanup这个方法,它是在资源保证获得清理的前提下执行代码的另外一种方式:
public static void ExecuteCodeWithGuaranteedCleanup(RuntimeHelpers.TryCode code, RuntimeHelpers.CleanupCode backoutCode, object userData);
调用这个方法要将try和finally块的主体做为回调方法传递,他们的原型要分别匹配如下的两个委托:
public delegate void TryCode(object userData);
public delegate void CleanupCode(object userData, bool exceptionThrown);
最后,另外一种保证代码得以执行的方式是使用CriticalFinalizerObject类。
代码契约(code contract)提供了直接在代码中申明代码设计决策的一种方式。
●前条件 通常用于参数的验证。
●后条件 方法由于一次普通的返回或者由于抛出一个异常而终止时,对状态进行验证。
●对象不变性(object Invariant) 用于对象的整个生命期内,保持对象字段的良好性状态。
代码契约有利于代码的使用、理解、进化、测试、文档和初期错误检查。可将前条件、后条件和对象不变性想象为方法签名的一部分。因此,代码新版本的契约能够变得更宽松,可是,除非破坏向后兼容性,不然代码新版本的契约不能变得更严格。
代码契约的核心是静态类System.Diagnostics.Contracts.Contract。因为该技术较新,实际中运用机会很少,故再也不投入大量精力去研究。具体用时能够查阅MSDN相关文档。
本章将讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。简单的说,本章要解释CLR中的垃圾回收器是如何工做的,还要解释与它有关的性能问题。
在.NET Framework中,内存中的资源(即全部二进制信息的集合)分为“托管资源”和“非托管资源”。托管资源必须接受.NET Framework的CLR的管理 (如内存类型安全性检查) 。而非托管资源则没必要接受.NET Framework的CLR管理, 须要手动清理垃圾(显式释放)。注意,“垃圾回收”机制是.NET Framework的特性,而不是C#的。
每一个程序都要使用这样或那样的资源,好比文件、内存缓冲区、屏幕空间、网络链接、数据库资源等。事实上,在面向对象的环境中,每一个类型都表明可供程序使用的一种资源。要使用这些资源,必须为表明资源的类型分配内存。
如下是访问一个资源所需的具体步骤
垃圾回收(garbage collection)自动发现和回收再也不使用的内存,不须要程序员的协助。使开发人员获得了解放,如今没必要跟踪内存的使用,也没必要知道在何时释放内存。可是,垃圾回收器不能够管理内存中的全部资源,对内存中的类型所表明的资源也是一无所知的。这意味着垃圾回收器不知道怎么执行“摧毁资源的状态以进行清理”。这部分资源就须要开发人员本身写代码实现回收。在.Net framework中,开发人员一般会把清理这类资源的代码写到Dispose,Finalize和Close方法中。
在.net中提供三种模式来回收内存资源:dispose模式,finalize方法,close方法:
然而,值类型、集合类型、String、Attribute、Delegate和Exception所表明的资源无需执行特殊的清理操做。列如,只需销毁对象的内存中维护的字符数组,一个String资源就会被彻底清理。
值类型(包括引用和对象实例)和引用类型的引用实际上是不须要什么“垃圾回收器”来释放内存的,由于当它们出了做用域后会自动释放所占内存(由于它们都保存在“堆栈”中,学过数据结构可知这是一种先进后出的结构)。只有引用类型的引用所指向的对象实例才保存在“堆”中,而堆由于是一个自由存储空间,因此它并无像“堆栈”那样有生存期 (“堆栈”的元素弹出后就代 表生存期结束,也就表明释放了内存)。而且很是要注意的是,“垃圾回收器”只对“堆”这块区域起做用。
从托管堆分配资源
.Net clr把全部的引用对象都分配到托管堆上,这一点很像c-runtime堆。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆,而且这个地址空间最初并无对应的物理存储空间。除值类型外,CLR要求全部资源都从托管堆分配。
托管堆还维护着一个指针,我把它称为NextObjPtr。它指向下一个对象在堆中的分配位置。
IL指令newobj用于建立一个对象。C#提供了new操做符,它致使编译器在方法IL代码中生成一个newobj指令。newobj指令将致使CLR执行如下步骤(如何为类型分配内存?):
下图展现了3个对象(A,B和C)的一个托管堆。若是要分配新对象,它将放在NextObjPtr指针指向的位置(紧接着对象C后)。
应用程序调用new操做符建立对象时,可能没有足够的地址空间来分配该对象。托管堆将对象须要的字节数加到NextObjPtr指针中的地址上来检测这个状况。若是结果值超过了地址空间的末尾,代表托管堆已满,必须执行一次垃圾回收。
垃圾回收器检查托管堆中是否有应用程序再也不使用的对象。若是有,它们使用的内存就能够被回收。那么,垃圾回收器是怎么知道一个对象再也不被使用呢?
CPU寄存器(CPU Register)是CPU本身的“临时存储器”,比内存的存取还快。按与CPU远近来分,离得最近的是寄存器,而后缓存 (计算机1、2、三级缓存),最后内存。
每一个应用程序都包含一组根(Roots)。每一个根都是一个存储位置,他们可能指向托管堆上的某个地址,也多是null。
类中定义的任何静态字段,方法的参数,局部变量(仅限引用类型变量)等都是根,另外cpu寄存器中的对象指针也是根。
例如,全部的全局和静态对象指针是应用程序的根,另外在线程栈上的局部变量/参数也是应用程序的根。只有引用类型的变量才被认为是根,值类型的变量永远不被认为是跟。
若是一个根引用了堆中的一个对象,则该对象为“可达”,不然便是“不可达”。被根引用的堆中的对象不被视为垃圾。
当垃圾回收器开始运行,它会假设托管堆上的全部对象都是垃圾。换句话说,它假设线程栈中没有引用堆中对象的变量,没有CPU寄存器引用堆中的对象,也没有静态字段引用堆中的对象。
垃圾回收分为2个阶段:
垃圾回收器的第一阶段是所谓的标记(marking)阶段。
垃圾回收器沿着线程栈上行以检查全部的根。若是发现一个根引用了一个对象,就在对象的“同步块索引字段”上开启一位(将这个bit设为1)---对象就是这样被标记的。当全部的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象。
以下图,展现了一个堆,其中包含几个已分配的对象。应用程序的根直接引用对象ACDF,全部这些对象都被标记。标记好根和它的字段引用对象以后,垃圾回收器检查下一个根,并继续标记对象。若是垃圾回收器试图标记以前已经被标记过的对象,就会换一个路径继续遍历。这样作有两个目的:首先,垃圾回收器不会屡次遍历一组对象,提升性能。其次,若是存在对象的循环链表,能够避免无限循环。
垃圾回收器的第二个阶段是压缩(compact)阶段。
在这个阶段中,垃圾回收器线性地遍历堆,以寻找未标记的连续内存块。若是发现大的可用的连续内存块,垃圾回收器会把非垃圾(标记/可达)的对象移动到这里来进行压缩堆。堆内存压缩后,托管堆的NextObjPtr指针将指向紧接在最后一个非垃圾对象以后的位置。这时候new操做符就能够继续成功的建立对象了。这个过程有点相似于磁盘空间的碎片整理。以此,对堆进行压缩,不会形成进程虚拟地址空间的碎片化。
如上图所示,绿色框表示可达对象,黄色框为不可达对象。不可达对象清除后,移动可达对象实现内存压缩(变得更紧凑)。
压缩以后,“指向这些对象的指针”的变量和CPU寄存器如今都会失效,垃圾回收器必须从新访问全部根,并修改它们来指向对象的新内存位置。这会形成显著的性能损失。这个损失也是托管堆的主要缺点。
基于以上特色,垃圾回收引起的回收算法也是一项研究课题。由于若是真等到托管堆满才开始执行垃圾回收,那就真的太“慢”了。
垃圾回收器的好处:
垃圾回收算法 --- 分代(Generation)算法
代是CLR垃圾回收器采用的一种机制,它惟一的目的就是提高应用程序的性能。分代回收,速度显然快于回收整个堆。
CLR托管堆支持3代:第0代,第1代,第2代。第0代的空间约为256KB,第1代约为2M,第2代约为10M。新构造的对象会被分配到第0代。
如上图所示,当第0代的空间满时,垃圾回收器启动回收,不可达对象(上图C、E)会被回收,存活的对象被归为第1代。
当第0代空间已满,第1代也开始有不少不可达对象以致空间将满时,这时两代垃圾都将被回收。存活下来的对象(可达对象),第0代升为第1代,第1代升为第2代。
实际CLR的代回收机制更加“智能”,若是新建立的对象生存周期很短,第0代垃圾也会马上被垃圾回收器回收(不用等空间分配满)。另外,若是回收了第0代,发现还有不少对象“可达”,
并无释放多少内存,就会增大第0代的预算至512KB,回收效果就会转变为:垃圾回收的次数将减小,但每次都会回收大量的内存。若是尚未释放多少内存,垃圾回收器将执行彻底回收(3代),若是仍是不够,则会抛出“内存溢出”异常。
也就是说,垃圾回收器会根据回收内存的大小,动态的调整每一代的分配空间预算!达到自动优化!
.NET垃圾回收器的基本工做原理是:经过最基本的标记清除原理,清除不可达对象;再像磁盘碎片整理同样压缩、整理可用内存;最后经过分代算法实现性能最优化。
终结(Finalization)是CLR提供的一种机制,容许对象在垃圾回收器回收其内存以前执行一些得体的清理工做。任何包装了本地资源(例如文件)的类型都必须支持终结操做。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收器判断一个对象是垃圾时,会调用对象的Finalize方法。
C#团队认为,Finalize方法是编程语言中须要特殊语法的一种方法。在C#中,必须在类名前加一个~符号来定义Finalize方法。
internal sealed class SomeType
{
~SomeType()
{
//这里的代码会进入Finalize方法
}
}
编译上述代码,会发现C#编译器实际是在模块的元数据中生成一个名为Finalize的protected override方法。方法主体被放到try块中,finally块放入了一个对base.Finalize的调用。
实现Finalize方法时,通常都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。
若是包装了本地资源的类型没有定义Finalize方法,本地资源就得不到关闭,致使资源泄露,直至进程终止。进程终止时,这些本地资源才会被操做系统回收。
不要对托管资源进行终结操做,终结操做几乎专供释放本地资源。
Finalize方法在垃圾回收结束时调用,有如下5种事件会致使开始垃圾回收:
终结操做表面看起来简单:建立一个对象,当它被回收时,它的Finalize方法会获得调用。但深究下去,远没有这么简单。
应用程序建立一个新对象时,new操做符会从堆中分配内存。若是对象的类型定义了Finalize方法,那么在该类型的实例构造器调用以前,会将一个指向该对象的指针放到一个终结列表 (finalization list) 中。终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象以前,会先调用对象的Finalize方法。
下图展现了包含几个对象的一个托管堆。有的对象从应用程序的根可达,有的不可达(垃圾)。对象C,E,F,I,J被建立时,系统检测到这些对象的类型定义来了Finalize方法,全部指向这些对象的指针要添加到终结列表中。
垃圾回收开始时,对象B,E,G,H,I和J被断定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列 表中移除,并追加到freachable队列中。freachable队列(发音是“F-reachable”)是垃圾回收器的内部数据结构。 Freachable队列中的每一个指针都表明其Finalize方法已准备好调用的一个对象。
下图展现了回收完毕后托管堆的状况。从图中咱们能够看出B,E和H已经从托管堆中回收了,由于它们没有Finalize方法,而E,I,J则暂时没有被回收,由于它们的Finalize方法还未调用。
一个特殊的高优先级的CLR线程负责调用Finalize方法。使用专用的线程可避免潜在的线程同步问题。freachable队列为空时,该线程将睡眠。当队列中有记录项时,该线程就会被唤醒,将每一项从freachable队列中移除,并调用每一项的 Finalize方法。
若是一个对象在freachable队列中,那么意味这该对象是可达的,不是垃圾。
本来,当对象不可达时,垃圾回收器将把该对象当成垃圾回收了,但是当对象进入freachable队列时,有奇迹般的”复活”了。而后,垃圾回收 器压缩(内存脆片整理)可回收的内存,特殊的CLR线程将清空freachable队列,并调用其中每一个对象的Finalize方法。
垃圾回收器下一次回收时,发现已终结的对象成为真正的垃圾,由于应用程序的根再也不指向它,freachhable队列也再也不指向它。因此,这些对象的内存会直接回收。
整个过程当中,可终结对象须要执行两次垃圾回收器才能释放它们占用的内存。可在实际开发中,因为对象可能被提高到较老的一代,因此可能要求不止两次进行垃圾回收。下图展现了第二次垃圾回收后托管堆中的状况。
Finalize方法很是有用,由于它确保了当托管对象的内存被释放时,本地资源不会泄漏。可是,Finalize方法的问题在于,他的调用时间不能保证。另外,因为他不是公共方法,因此类的用户不能显式调用它。
类型为了提供显式进行资源清理的能力,提供了Dispose模式。全部定义了Finalize方法的类型都应该同时实现Dispose模式,使类型的用户对资源的生存期有更多的控制。
类型经过实现System.IDisposable接口的方式来实现Dispose模式:
public interface IDisposable
{
void Dispose();
}
任何类型只有实现了该接口,将至关于声称本身遵循Dispose模式。无参Dispose和Close方法都应该是公共和非虚的。
FileStream类实现了System.IDisposable接口。
FileStream fs = new FileStream();
//显示关闭文件 Dispose/Close
fs.Dispose();
fs.Close();
fs.Write();
显示调用一个类型的Dispose或Close方法只是为了能在一个肯定的时间强迫对象执行清理。这两个方法并不能控制托管堆中的对象所占用的内存的生存期。这意味着即便一个对象已完成了清理,仍然可在它上面调用方法,但会抛出ObjectDisposedException异常。
若是决定显式地调用Dispose和Close这两个方法之一,强烈建议把它们放到一个异常处理finally块中。这样能够保证清理代码获得执行。
C#提供了一个using语句,这是一种简化的语法来得到上述效果。
using(FileStream fs = new FileStream()){
fs.Write();
}
在using语句中,咱们初始化一个对象,并将它的引用保存到一个变量中。而后在using语句的大括号内访问该变量。编译这段代码时,编译器自动生成一个try块和一个finally块。在finally块中,编译器会生成代码将变量转型成一个IDispisable并调用Dispose方法。显然,using语句只能用于哪些实现了IDisposable接口的类型。
CLR为每个AppDomain都提供了一个GC句柄表 (GC Handle table) 。该表容许应用程序监视对象的生存期,或手动控制对象的生存期。
在一个AppDomain建立之初,该句柄表是空的。句柄表中的每一个记录项都包含如下两种信息:一个指针,它指向托管堆上的一个对象;一个标志(flag),它指出你想如何监视或控制对象。
为了在这个表中添加或删除记录项,应用程序要使用System.Runtime.InteropServices.GCHandle类型。
前面说过,须要终结的一个对象被认为死亡时,垃圾回收器会强制该对象重生,使它的Finalize方法得以调用。Finalize方法调用以后,对象才真正的死亡。
须要终结的一个对象会经历死亡、重生、再死亡的“三部曲”。一个死亡的对象重生的过程称为复活(resurrection) 。复活通常不是一件好事,应避免写代码来利用CLR这个“功能”。
前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。
在现实开发中,常常会出现多个线程同时访问托管堆的状况,或至少会有多个线程同时操做堆中的对象。一个线程引起垃圾回收时,其它线程绝对不能访问任何线程,由于垃圾回收器可能移动这些对象,更改它们的内存位置。
CLR想要进行垃圾回收时,会当即挂起执行托管代码中的全部线程,正在执行非托管代码的线程不会挂起。而后,CLR检查每一个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。
若是线程的指令指针刚好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。若是线程指令指针不在表中标记的偏移位置,则代表该线程不在安全点,CLR也就不会开始垃圾回收。在这种状况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。而后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。
然而,线程有时长时间执行当前所在方法。因此,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。若是线程已抵达一个安全点,垃圾回收就能够开始了。可是,若是线程尚未抵达一个安全点,CLR就检查是否调用了另外一个方法。若是 是,CLR再一次修改线程栈,以便从最近执行的一个方法返回以后劫持线程。而后,CLR恢复线程,进行下一次劫持尝试。
全部线程都抵达安全点或被劫持以后,垃圾回收才能使用。垃圾回收完以后,全部线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。
实际应用中,CLR大多数时候都是经过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之因此如此,缘由是JIT生成表须要大量内存,会增大工做集,进而严重影响性能。
任何85000字节或更大的对象都被自动视为大对象(large object)。
大对象从一个特殊的大对象堆中分配。这个堆中采起和前面小对象同样的方式终结和释放。可是,大对象永远不压缩(内存碎片整理),由于在堆中下移850000字节的内存块会浪费太多CPU时间。
大对象老是被认为是第2代的一部分,因此只能为须要长时间存活的资源建立大对象。若是分配短期存活的大对象,将致使第2代被更频繁地回收,进而会损害性能。
寄宿容许使任务应用程序都能利用CLR的功能。寄宿(hosting)容许使任务应用程序都能利用CLR的功能。另外,寄宿还为应用程序提供了经过编程来进行自定义和扩展能力。AppDomain容许第三方的,不受信任的代码在一个现有的进程中运行,而CLR保证数据结构、代码和安全上下文不会被滥用或破坏。
.Net Framework必须用Windows能够理解的技术来构建。首先,全部托管模块和程序集都必须使用Windows PE文件格式。
开发CLR时,Microsoft实际是将它实现成包含在一个DLL中的COM服务器。也就是说,Microsoft为CLR定义了一个标准的COM接口,并未该接口和COM服务器分配了GUID(全局通用标识符)。安装.Net Framework时,表明CLR的COM服务器和其余COM服务器同样在Windows注册表中注册。
任何Windows应用程序均可以寄宿CLR。非托管宿主应该调用MetaHost.h文件中声明的CLRCreateInstance函数。CLRCreateInstance函数是在MSCorEE.dll文件中实现的,该文件通常是在C:\Windows\System32目录中。这个DLL被称为“垫片”(shim),它的工做是决定建立哪一个版本的CLR,注意垫片DLL自己并不包含CLR COM服务器。
一台机器可安装多个版本的CLR,但只有一个版本的MSCorEE.dll文件(垫片)。
CLRCreateInstance函数能够返回一个ICLRMetaHost接口。宿主应用程序可调用这个接口的GetRuntime函数,指定宿主要建立的CLR的版本。而后,垫片将所需版本的CLR加载到宿主的进程中。
宿主应用程序可调用ICLRRuntimeHost接口定义的方法作下面的事情:
寄宿使任何应用程序都能提供CLR功能和可编程性,如下是寄宿CLR的部分好处:
CLR COM服务器初始化时,会建立一个AppDomain。AppDomain是一组程序集的逻辑容器。CLR初始化时建立的第一个AppDomain称为默认AppDomain,这个默认的AppDomain只有在Windows进程终止时才会被销毁。
除了默认AppDomain,托管类型方法的一个宿主还可指示CLR建立额外的AppDomain。AppDomain惟一的做用是进行隔离。
下面总结了AppDomain的具体功能:
一个AppDomain中的代码建立了一个对象后,该对象被该AppDomain“拥有”。换言之,它的生存期不能比建立它的代码所在的AppDomain还要长。一个AppDomain中的代码为了访问另外一个AppDomain中的对象,只能使用“按引用封送”或者“按值封送”的语义。这就增强了一个清晰的分隔和边界,由于一个AppDomian中的代码没有对另外一个AppDomain中的代码所建立的对象的直接引用。
CLR不支持从AppDomain中卸载一个程序集的能力。可是,能够告诉CLR卸载一个AppDomain,从而卸载当前包含在该AppDomain内的全部程序集。
AppDomain在建立以后,会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。
AppDomain在建立以后,会关联一组配置设置。
下图演示了一个Windows进程,其中运行着一个CLR COM服务器。该CLR当前管理着两个AppDomain。每一个AppDomain都有本身的Loader堆,每一个Loader堆都记录了自AppDomain建立以来已访问过哪些类型。Loader堆中的每一个类型对象都有一个方法表,方法表中的每一个记录项都指向JIT编译的本地代码(前提是方法至少执行过一次)。
除此以外,每一个AppDomain都加载了一些程序集。AppDomain有三个程序集:MyApp.exe,TypeLib.dll和System.dll。AppDomain#2有两个程序集:Wintellect.dll和System.dll。
如图所示,System.dll程序集被加载到两个AppDomain中。若是这两个AppDomain都使用了来自System.dll的一个类型,那么在两个AppDomain的Loader堆中,都会为同一个类型分配一个类型对象;类型对象的内存不会由两个AppDomain共享。
AppDomain的所有目的就是提供隔离性;CLR要求在卸载某个AppDomain并释放它的全部资源的同时,不会对其余AppDomain产生负面影响。经过复制CLR的数据结构,就能够保证这一点。除此以外,还能保证由多个AppDomain使用的一个类型在每一个AppDomain中都有一个静态字段。
AppDomain是CLR的功能,Windows对此一无所知。
CreateInstanceAndUnwrap方法致使调用线程从当前AppDomain到新的AppDomain。线程将制定程序集加载到新 AppDomain中,并扫描程序集类型定义元数据表,查找指定类型“MarshalByRefType”)。找到类型后,调用它的无参构造函数。而后, 线程又范围默认AppDomain,对CreateInstanceAndUnwrap返回的MarshalByRefType对象进行操做。
如何将一个对象从一个AppDomain(源AppDomain,这里指真正建立对象的地方)封送到另外一个AppDomain(目标AppDomain,这里指调用CreateInstanceAndUnwrap的地方)?
1. Marshal-by-Reference
CLR会在目标AppDomain的Loader堆中定义一个代理类型。这个代理类型是用原始类型的数据定义的。所以,它看起来和原始类型彻底同样;有彻底同样的实例成员(属性、事件和方法)。可是,实例字段不会成为(代理)类型的一部分。
2. Marshal-by-Value
CLR将对象字段序列化一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。而后,CLR在目标AppDomain中 反序列化字节数组,这会强制CLR将定义了的“被反序列化的类型”的程序集加载到目标AppDomain中。接着,CLR建立类型的一个实例,并利用字节 数组初始化对象的字段,使之与源对象中的值相同。换言之,CLR在目标AppDomain中准确的复制了源对象。
AppDomain.Unload()中执行操做:
(1)CLR挂起进程中执行中执行的托管代码的全部线程;
(2)CLR检查全部线程栈,查看哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时刻返回至要卸载的那个 AppDomain。在任何一个栈上,若是准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异 常(同时恢复线程的执行)。这将致使线程展开(unwind),在展开的过程当中执行遇到的全部finally块中的内容,以执行资源清理代码。若是没有代 码捕捉ThreadAbortException,它最终会成为一个未处理的异常,CLR会“吞噬”这个异常,线程会终止,但进程能够继续运行。这一点是 很是特别的,由于对于其余全部未处理的异常,CLR都会终止进程。
重要提示:若是一个线程当前正在finally块、catch块、类构造器、临界执行区(critical execution region)域或非托管代码中执行,那么CLR不会当即终止该线程。不然,资源清理代码、错误恢复代码、类型初始化代码、关键代码或者其余任何CLR不 了解的代码都没法完成,致使应用程序的行为变得没法预测,甚至可能形成安全漏洞。线程在终止时,会等待这些代码块执行完毕。而后当代码块结束时,CLR再 强制线程抛出一个ThreadAbortException。
临界区是指线程终止或未处理异常的影响可能不限于当前任务的区域。相反,非临界区中的终止或失败只对出现错误的任务有影响。
(3)当上一步发现的全部线程都离开AppDomain后,CLR遍历堆,为引用了“已卸载的AppDomain建立的对象”的每个代理都设置一 个标志(flag)。这些代理对象如今知道它们引用的真实对象已经不在了。若是任何代码在一个无效的代理对象上调用一个方法,该方法会抛出一个 AppDomainUnloadedException
(4)CLR强制垃圾回收,对现已卸载AppDomain建立的任何对象占用的内存进行回收。这些对象的Finalize方法被调用(若是存在Finalize方法),使对象有机会完全清理它们占用的资源
(5)CLR恢复剩余全部线程的执行。调用AppDomain.Unload方法的线程将继续执行,对AppDomain.Unload的调用是同 步进行的在前面的例子中,全部工做都用一个线程来作。所以,任什么时候候只要调用AppDomain.Unload都不可能有另外一个线程在要卸载的 AppDomain中。所以,CLR没必要抛出任何ThreadAbortException异常。
程序集加载和反射,实现了在编译时对一个类型一无所知的状况下,如何在运行时发现类型的信息,建立类型的实例以及访问类型的成员。显现的功能以及效果是十分强大的,好比使用第三方提供的程序集,以及建立动态可扩展应用程序。
JIT编译器在将方法的IL代码编译成本地代码时,会查看IL代码中引用了哪些类型。在运行时,JIT编译器查看元数据表TypeRef和AssemblyRef来肯定哪个程序集定义了所引用的类型。在AssemblyRef元数据表的记录项中,包含了构成程序集强名称的各个部分 :名称(无扩展名和路径),版本,语言文化和公钥(PublicKeyToken)。
JIT编译器利用如下这些信息,链接成字符串。例如:( StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5 )。而后尝试将一个匹配的程序集加载到AppDomain中。若是是弱命名程序集,则只包含程序集的名称。
常见的程序集加载方式有三种:
(1)Assembly.Load
在内部,CLR使用System.Reflection.Assembly类的静态方法Load来尝试加载程序集,经常使用的版本原型:
public class Assembly
{
public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(String assemblyString);
}
首先熟悉下这两个方法的使用,建立个强命名程序集,而后查看强命名信息(SN.exe, Reflector工具, Assembly.GetAssemblyName)
Assembly assemblyLoadString = Assembly.Load("StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5");
Assembly assemblyLoadRef = Assembly.Load(new AssemblyName("StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5"));
注意:
a.对于强命名程序集,在内部,Load致使CLR向程序集应用一个版本绑定重定向策略,并在GAC中查找程序集。若是没有找到,就接着去应用程序的基目录,私有路径子目录和codebase位置中查找
b.若是Load时传递的是一个弱命名程序集,Load就不会向程序集应用一个版本绑定重定向策略,CLR也不会去GAC中查找程序集
c.若是找到指定的程序集,返回的是那个程序集的一个Assembly对象的引用;若是没有找到指定的程序集,抛出System.IO.FileNotFoundException
d.对于须要加载为特定CPU架构生成的程序集,在指定程序集标识时,还可包括一个进程架构部分。
StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5 , ProcessorArchitecture=MSIL
CLR容许ProcessorArchitecture取4个值之一:MSIL, x86, IA64, AMD64
(2)Assembly.LoadFrom
使用Load时,它要求你事先掌握构成程序集标识的各个部分。在某些状况下,咱们也能够指定一个程序集文件的路径名(包括文件扩展名),获取Assembly对象。
Assembly assemblyFromPath = Assembly.LoadFrom(@"E:\StrongNameDLL.dll");
对于使用LoadFrom,传入路径名的使用方式,须要了解内部的实现机制,避免误用的状况:
a.在内部,LoadFrom首先会调用System.Reflection.AssemblyName类的静态方法GetAssemblyName。
该方法打开指定的文件,提取AssemblyRef记录项中的程序集标识信息,而后以一个System.Reflection.AssemblyName对象的形式返回这些信息(文件同时关闭)。
b.LoadFrom方法在内部调用Assembly的Load方法,将AssemblyName对象传给它。
c.CLR会为应用版本绑定重定向策略,并在各个位置查找匹配的程序集。若是Load找到了匹配的程序集,就会加载它,并返回一个Assembly对象;LoadFrom返回这个值。若是没有找到匹配项,LoadFrom就会加载路径名中的程序集。
(3)Assembly.LoadFile
加载指定路径上的程序集文件。
Assembly assemblyFromPath = Assembly.LoadFile(@"E:\StrongNameDLL.dll");
注意:
a.可将具备相同标识的一个程序集屡次加载到一个AppDomain中
b.CLR不会解析任何依赖性问题
c.必须向AppDomain的AssemblyResolve事件登记,并让事件回调方法显式地加载任何依赖的程序集
三者对比
既然已经对Load,LoadFrom,LoadFile有所了解,那么接着来看看这三者之间的区别与对比。
1.Load和LoadFrom
a.根据LoadFrom的内部实现机制,LoadFrom返回的实际是Load找到的匹配程序集的一个Assembly对象(在找到匹配的程序集的状况下)
b.LoadFrom存在屡次打开文件的现象,而且内部仍是要走一套Load的逻辑操做,还存在对比状况,因此LoadFrom比Load效率低
c.LoadFrom 要求指定路径中包含 FileIOPermissionAccess.Read 和 FileIOPermissionAccess.PathDiscovery 或 WebPermission
d. LoadFrom, and the probing path includes an assembly with the same identity but a different location, an InvalidCastException, MissingMethodException, or other unexpected behavior can occur. "> 若是用 LoadFrom 加载一个程序集,而且probing路径包括具备相同标识但位置不一样的程序集,则发生 InvalidCastException 、 MissingMethodException 或其余意外行为。
2.LoadFrom和LoadFile
a.根据MSDN的解释,能够看出功能实现机制存在区别。
LoadFrom:已知程序集的文件名或路径,加载程序集。
LoadFile:加载指定路径上的程序集文件的内容。
b.LoadFrom会加载程序集的依赖项,可是LoadFile不会。
例如,上面的StrongName.dll存在引用程序集ReferenceDemo.dll。使用LoadFrom,StrongName.dll和 ReferenceDemo.dll都会被载入,可是使用LoadFile,ReferenceDemo.dll不会被载入。
c.能够使用LoadFile加载并检查具备相同标识但位于不一样路径的程序集,可是LoadFrom不行, LoadFrom returns the loaded assembly even if a different path was specified. ">若是已加载一个具备相同标识的程序集,则即便指定了不一样的路径,仍返回已加载的程序集。 。
例如,StrongName.dll存在于两个不一样路径下,LoadFile能够正确载入两个程序集;可是LoadFrom若是已经加载了一次 StrongName.dll,再一次加载不一样路径下的Assembly时,会先检查前面是否已经载入过相同名字的 Assembly,最终返回的仍是第一次加载的Assembly对象的引用。
总结:
因为LoadFrom具备很多缺点,通常优先考虑使用Load方法加载程序集。而LoadFile则只在有限的状况下使用。
微软始终OS内核时,决定在一个进程(progress)中运行应用程序的每一个实例。进程是应用程序的一个实例要使用的资源的一个集合。每一个进程都被赋予了一个虚拟地址空间,确保一个进程使用的代码和数据没法由另外一个进程访问。除此以外,OS的内核代码和数据是进程访问不到的。因此,应用程序代码破坏不了其余应用程序和OS自身。
上面听起来不错,但CPU自己呢?若是一个应用程序进入无限循环,若是机器只有一个CPU,它会执行无限循环,不会执行其余东西。微软要修复这个问题,提出了线程。做为一个Windows几率,线程(thread)的职责是对CPU进行虚拟化,可将线程理解为一个逻辑CPU。Windows为每一个进程都提供了该进程专用的线程。若是引用程序的代码进入无限循环,与那个代码关联的进程会冻结,但其余进程不会冻结,由于它们有本身的线程。