C#学习之.NET系统架构(一)

	前言:
	C#语言不可以单独使用,必须和.NET Framework一起使用。C#编程语言以及其编译器专门用于.NET平台。这也说明C#说编写的代码总是在.NET Framework中运行。
	结论:C#的特定语言功能取决于.NET的功能,或依赖于.NET基类。

1. C#和.NET的关系

C#是一门比较新的编程语言,C#语言的特点:

  1. 专门与Microsoft的.NET Framework一起使用而设计的。
  2. 基于现代面向对象的编程语言

注意:

  • .NET Framework是一个功能非常丰富的平台,可用于开发、部署和执行分布式应用程序。
  • C#不是.NET的一部分。如.NET支持一些特性,而C#却不支持,同样,C#支持的一些特性,.NET不支持,比如运算符的重载。

2. 公共语言的运行库

.NET Framework的核心是运行库的执行环境,被称为公共运行库(CLR)或.NET运行库。在CLR上运行的代码称为托管代码(managed code)。
与其他编程语言相同,编写好C#的代码之后,CLR会编译他们。主要分为两个阶段。

  1. 把源代码编译成Microsoft中间语言(lL,即Microsoft Intermediate Language,简称MSIL或者IL)
  2. CLR把IL编程成平台专用的代码

Microsoft中间语言与Java的字节代码共享一个理念:他们都是低级语言,语法简单(使用的是数字代码,而不是文本代码),可以快速转换成内部机器码,所以Java语言和运行在.NET Framework上的语言有平台无关性、提高性能、语言的互操作性等优点。

2.1. 平台无关性

Java语言中,包含字节码指令的同一文件可以放在任一平台中(通过JVM机制实现),运行时编译过程的最后阶段可以很容易完成。与此类似,将C#编译成中间语言就可以获得.NET平台无关性。
目前为止,.NET只可以运行在Windows平台。

2.2. 提高性能

IL比Java字节码的作用要大。IL总是即时编译(称为JIT编译),而Java字节码代码常常是解释性的,这也造成了Java最大的缺点就是在运行应用程序时,把Java字节码转换为内部可执行代码的过程会导致性能的损失(但是,最近Java在某些平台上也进行了JIT编译。)

JIT编译器并不是把整个应用程序一次编译完,而只是编译它调用的那部分代码,这就解决了编译时间长而导致的启动时间慢的问题。这样,代码编译一次后,得到的内部可执行代码会被存储起来,这样的优点是退出该应用程序,下次再启动应用程序,执行这段代码时,就不需要再次编译了。

这样也说明了托管IL代码的执行几乎和内部机器代码执行速度一样快。细究其原因,可以这样解释:编译过程中的最后一部分是在运行时进行的,JIT编译器确切地知道程序运行在什么类型的处理器上,可以利用该处理器提供的任何特性或特定的机器代码来优化最后的可执行代码。

传统的编译器也可以优化代码,但是它们的优化过程是独立于代码所执行的特定处理器的。因为传统的编译器是发布软件之前编译成内部机器可执行代码。即编译器不知道代码所运行的处理器类型,而JIT编译器是在发布软件之后编译成内部机器可执行代码,可以知道所运行设备的处理器类型,并针对性进行优化。

举个例子,对于传统的处理器,处理器是x86兼容处理器还是Alpha处理器,这超出了基本操作的规范,而JIT却可以优化代码所运行的特定处理器。

2.3. 语言的互操作性

使用IL不仅支持平台无关性,还支持语言的互操作性。简而言之,可以将任何一种语言编程成中间代码,此语言编译好的代码可以和其他语言编译好的代码进行交互操作。
可以在.NET平台上和C#进行交互的语言有:Visual Basic 2008、Visual C++ 2008、COM、COM++等等

3. 中间语言

C#编译器仅编译成托管代码,且有一定意义的。

中间语言的重要特征:

  1. 面向对象和使用接口
  2. 值类型和引用类型之间的巨大差别
  3. 强数据类型
  4. 使用异常来处理错误
  5. 使用特性(attribute)

3.1. 面向对象和接口的支持

.NET的语言无关性还有一些实际上的限制。中间语言在设计时,需要实现某些特殊的编程方法,面向对象语言必须与此编程方法兼容,Microsoft为IL选择的是传统的面向对象的编程,带有类的单一继承性。

​中间语言还引入了接口的概念,这些接口显示了带有COM的Windows下的第一个实现方式。任何COM接口需要支持基础结构,而.NET却不需要。如:.NET不是派生自IUnknown,也没有对应的GUID。但是.NET与COM接口共享一个概念:提供一个契约,实现给定接口的类必须提供该接口指定的方法和属性的实现方式。

因为.NET要编译成中间语言,即需要使用传统的面向对象的方法来编程。像Java和C++即使使用相同的面向对象范型,但它们不是可互相操作的语言,那为什么.NET支持的语言有互操作性呢?

语言的互操作性。

​COM允许以不同的语言编写的组件一起工作,即可以互相调用彼此的方法。其实,COM是一个二进制标准,允许组件实例化其他组件,调用他们的方法和属性,而无需考虑编写相关组件的语言。为了实现不同语言之间的交互,必须让每个对象必须通过COM运行库来实例化,通过接口来访问。根据组件的线程模型,不同线程上内存空间和运行组件之间要编组数据,这意味着可能有很大的性能损失。在极端的情况下,组件保存为可执行文件,而不是DLL文件,还需要创建单独的进程来运行它们。组件需要和其他的组件交互,但仅能通过COM运行库进行通信。不管COM是用于不同语言之间组件交互,还是创建彼此的实例,都需要通过COM作为中间件来处理。即使如此,COM不允许使用继承,这使得它丧失了面向对象编程的许多优势。

还存在一个问题,调试组件时,需要单独调试不同语言编写的组件。这样就不能够在调试器上调试不同语言的代码了。

这就引出了语言的互操作性的真实含义,语言互操作性的真实含义是:用另一种语言编写的类应能够直接与用另一种语言编写的类通信。

要具有以下几种功能:

  1. 用一种语言编写的类能够继承另一种语言编写的类

  2. 一个类应该包含另一个类的实例,而不管他们是使用什么语言编写的

  3. 一个对象应该能够调用用另一种语言编写的另一个对象的方法

  4. 对象或者对象的引用应该在方法之间传递

  5. 在不同语言之间调用方法时,应该在调试器中调试这些方法调用,即调试不同语言编写的源代码

.NET和中间语言已经实现了这个目标。在调试器上调试方法时,Visual Studio IDE提供了这样的方法(注意不是CLR提供的)

3.2 相同值类型和引用类型

与其他编程语言一样,中间语言提供了预定义的基本数据类型。它的显著特性就是值类型和引用类型有明显的区别。对于值类型,变量直接保存其数据,对于引用类型,变量仅保存地址,对应的数据可以在该地址中找到。

​在C++中,引用类型类似于通过指针访问变量,而在Visual Basic中,与应用类型最类似的是对象,Visual Basic可通过引用来访问对象。

​中间语言也有数据存储的规范:引用类型的实例总是存储在一个名为“托管堆”的内存区域中,值类型一般存在在堆栈中(但如果值类型在引用类型中声明为字段,它们就内联存储在堆中)

3.3 强数据类型

中间语言的一个重要方面是它基于强数据类型。所有的变量都清晰地标记为属于某个特定数据类型(在中间语言中没有Visual Basic和脚本语言中的Variant数据类型)。特别是中间语言不允许对模糊的数据类型执行任何操作。

​模糊数据类型的自动转换可以大大提高性能,但是会降低类型的安全性。

​这里需要注意,一些与.NET兼容的语言,比如Visual Basic 2008,在类型化方面要求比较松,但这样是可以的,因为编译器在后台确保在生成IL上强制类型安全。

​虽然强数据类型会降低性能,但是.NET提供的、依赖于类型安全的服务中获得的好处更多。这些服务包括:

  1. 语言的可操作性
  2. 垃圾收集
  3. 安全性
  4. 应用程序域
3.3.1 语言互操作性中数据类型的重要性

如果类派生自其他类,或包含其他类的实例,它就需要知道其他类使用的所有数据类型,这就是强数据类型非常重要的原因。如果没有任何系统指定这些信息,那么会对语言继承和交互操作造成障碍。这些信息不知在一个标准的可执行文件或者DLL中出现。

​比如:在Visual Basic2008类中的一个方法定义为返回一个Integer,但C#没有该名称的数据类型。显然,我们只能从该类中派生,再使用这个方法,如果在编译器中可以把Visual Basic2008中的Integer类型映射到C#定义的某种已知的类型,那么C#代码就可以返回指定的类型,与Visual Basic中的Integer对应。

​.NET是如何解决这个问题的呢?如何将Visual Basic2008中的Integer映射到C#中的指定类型。

3.3.1.1 通用类型系统(CTS)

.NET通过通用类型系统解决上述的问题。CTS定义了可以再中间语言中使用的预定义数据类型,所有面对.NET Framework的语言都可以生成最终基于这些类型的编译代码。

​比如:在Visual Basic中,Integer其实是32位有符号的整数,他其实映射中间语言类型Int32。因为在中间语言中就指定这种数据类型。因为C#编译器可以使用这种类型,在C#中,Int32使用int表示,所以.NET是通过CTS将Visual Basic中的Integer类型映射到C#中的int类型,所以,C#的编译器会认为Visual Basic返回的是int类型。

​通用类型系统不仅制定了基本数据类型,还定义了一个内容丰富的类型层次系统。在某些位置上,代码允许定义自己的类型。通用类型系统的层次结构反映了中间语言的单一继承的面向对象方法。
在这里插入图片描述
树状结构类说明表

类型含义
Type表示任何类型的基类
Value Type表示任何值类型的基类
Reference Types通过引用访问,且存储在堆中的任何数据类型
Built-in Value Types包含大多数标准基本类型,可以表示数字、Boolean值或字符
Enumerations枚举值的集合
User-defined Value Types在源代码中定义,且保存为值类型的数据类型。在C#中,它表示结构
Interface Types接口
Pointer Types指针
Self-describing Types为垃圾回收器提供信息的数据类型
Arrays包含对象数组的类型
Class Types可自我描述的类型,但不是数组
Delegates用于把引用包含在方法中的类型
User-defined Reference Types在源代码中定义,且保存为引用类型的数据类型。在C#中,它表示类
Boxed Value Types值类型,临时打包放在一个引用中,以便于存储在堆中

注意:没有列出内置的所有的值类型。在C#中,编译器识别的每一个与定义类型都映射为一个IL内置类型。同理Visual Basic也是一样的。

3.3.1.2 公共语言规范(CLS)

公共语言规范(Common Language Specification,CLS)和通用类型系统一起确保语言的互操作性。CLS是一个最低标准集,所有面向.Net的编译器都支持它。编译器的编写人员大多数吧给定的编译器功能限制为只支持IL和CLS提供的一部分特性。只要编译器支持已在CLS中定义的内容,这还是不错的。

​如果编译非CLS兼容代码是可以接受的,但是编写这样的代码后,就不能保证编译好的IL代码完全支持语言的互操作性。

举例:IL是区分大小写的语言。使用这些语言的开发人员常常利用区分大小写说提供的灵活性选择变量名。但是Visual Basic2008是不区分大小写的。CLS就要指定CLS兼容代码不使用任何只根据大小写区分的名称。所以,Visual Basic2008可以与CLS兼容代码一起使用。

​CLS的两种工作状态:

  1. 各个编译器不必强大到支持.NET上的所有功能,这就鼓励人们面向.Net的编程语言开发编译器。
  2. 提供如下保证:如果限制类只能使用CLS兼容的特性,就要保证用其他兼容语言编写的代码可以是属于这个类。

这样做的优点是:使用CLS兼容特性的限制只适用于公共和受保护的类成员和公共类。在类的私有实现方式中,可以可以编写非CLS代码。因为其他程序集中的代码不能访问这部分的代码。

在C#中,非CLS兼容特性非常少,所以CLS堆C#代码的影响不会太大。

3.3.2 垃圾回收机制

垃圾回收器用来在.Net中进行内存管理。特别是它可以恢复正在运行中的应用程序需要的内存。现在,Windows平台已经使用了两种技术来释放进程向系统动态请求的内存。

  1. 完全以手工方式使程序代码完成这些工作。
  2. 让对象维护引用计数

第一种方式,让应用程序代码负责释放内存是低级高性能的语言使用的技术,比如C++。这种方式很有效,但是确缺点是频繁出现错误。请求内存的代码还必须显式通知程序他什么身后不再需要该内存。很容易被遗漏,所以会导致内存泄漏。

​虽然后很多帮助探测泄漏的工具,但是它们很难跟踪错误,因为直到内存已大量泄漏从而使Windows拒绝为进程提供资源时,他们才会发挥作用。到那个时候,由于对内存的需求量大,会使整个计算机变得相当慢。

第二种方式,​维护引用计数是COM对象采用的一种技术,其方法是为每个组件都保留计数,记录客户机目前对它的引用数。当这个计数下降到0时,组件就会删除自己,并释放相应的内存和资源。它带来的问题是任需要客户机通知组件他们已经完成了内存的使用。只要有一个客户机没这样做,对象就仍驻留在内存中。在某些方面,这些比C++内存泄漏更为严重。因为COM对象可能存在于自己的进程中,从来不会被系统删除(在C++内存泄漏问题上,系统至少可以在进程中释放所有的内存)

​.NET运行库采用的方法是垃圾回收器,这是一个程序,目的是清除内存,方法是所有动态请求的内存都分配到堆上(所有的语言都是这样处理的,但在.NET中,CLR维护它自己的托管堆,以供.NET应用程序的使用),但.NET监测到给定进程的托管堆已满,需要清理时,就调用垃圾回收器。垃圾回收器处理目前代码中的所有变量,检测堆存储在托管堆上的对象的引用,确定哪些对象可以从代码中访问——哪些对象有引用。没有引用的对象就不能再从代码中访问,因而被删除。Java就使用于此类似的垃圾回收系统。

之所以在.Net中使用垃圾回收器,是因为中间语言已用来处理进程。其规则要求:

  1. 不能引用已有的对象,除非复制已有的引用
  2. 中间语言是类型安全的语言。

这里,其含义是如果存在对对象的任何引用,该应用中就有足够的信息来确定对象的类型。

垃圾回收器机制不能和诸如非托管C++这样的语言一起使用,因为C++允许执政自由地周末数据类型。

垃圾回收器的一个重要方面就是它的不确定性。换言之,不能保证什么时候会调用垃圾回收器,.Net运行库决定使用它时,就可以调用它(除非明确调用垃圾回收器)。但可以重写这个过程,在代码中调用垃圾回收器。

3.3.3 安全性

​.NET很好地补足了WIndows提供的安全机制,因为它提供的安全机制时基于代码的e安全性,而Windows仅提供了基于角色的安全性。

​基于角色的安全性建立在运行进程的账户的身份基础上,换而言之,就是谁拥有和运行进程。另一方面,基于代码的安全性建立在代码实际执行的任务和代码的可信程度上。因为中间语言提供了强大的类型安全性,所以CLR可以运行代码前检查它,以确定是否有需要的安全权限。.Net还提供了一种机制,可以在运行代码前指定代码需要什么安全权限。

​基于代码的安全性非常重要,原因是它降低了运行来历不明的代码的风险(例如,代码是从Internet下载下来的)。即使代码运行在管理员账户下,也有可能使用到基于代码的安全性,来确定这段代码充实仍不能自信管理员账户一般允许自信的某些类型的操作,例如读写环境变量、读写注册表或访问.NET反射特性

3.3.4 引用程序域

​应用程序域是.NET中的一个重要技术改进,它用于减少运行应用程序的系统开销,这些应用程序需要与其他程序分离开来,但仍需要彼此通信。典型的例子是Web服务器应用程序,它需要同时相应许多浏览器请求。因为,需要有许多组件实例同时响应这些同时运行的请求。

​在.NET之前,实例可以共享一个进程,但此时一个运行的实例就有可能导致整个网站的崩溃;也可以吧这些实例单独孤立在不同的进程中,但这样做会增加相关性能的系统开销。

​到目前为止,孤立代码的唯一方式是通过进程来实现的。在运行一个新的应用程序时,它会在一个进程环境内运行。

​Windows是通过地址空间吧进程分隔开来。这样,每个进程有4GB的虚拟内存来存储器数据和可执行代码(其实,4G是相对于32位系统的,64位系统有更多的内存)。Windows利用额外的间接方式吧这些虚拟内存映射到物理空间或磁盘空间的一个特定的区域中。

​每个进程都会有不同的映射,虚拟地址空间块映射的物理内存之间不能有重叠,这种情况如图:
在这里插入图片描述
一般情况下,任何进程都只能通过指定虚拟内存中的一个地址来访问内存——即进程不能直接访问物理内存。因此,一个进程不可以访问分配给另一个进程的内存。这样就可以确保任何执行出错的代码不会损坏其地址空间以外的数据。

​进程不仅是运行代码实例相互隔离的一种方式,在比较长远的Windows版本中,他们还可以构成分配了安全权限和许可的单元。每个进程都有自己的安全标识,明确地告诉Windows允许该进程可以自信的操作。

​进程对确保安全有很大的帮助,而它们最大的缺点就是性能。许多进程常常一起工作,因此需要相互相同。一个常见的例子就是进程调用一个COM组件,而该COM组件是可执行的,因此需要在自己的进程上运行。在COM中使用代理时也会出现这样的情况。因为进程不能共享任何内存,所以必须使用一个复杂的编组过程在进程之间复制数据。这样对性能有很大的影响。如果需要使组件一起工作,但不希望有损失,唯一的方法是使用基于DLL的组件,让所有的组件在同一个地址空间运行——其风险是执行出错的组件会影响其他组件。

​如果不同的可执行文件都运行在同一个进程空间中,显然它们就能轻松地共享数据,因为理论上它们可以直接访问彼此的数据。理论上是可以实现的,但是CLR会检查每个正在运行的应用程序的代码,以确保这些代码不偏离它长成的数据区域,保证不发生直接访问其他进程的数据的情况。

​如何告诉程序要做什么工作,而又不真正运行它?这样做是可行的。因为中间语言有一个强大的类型安全机制。在大多数情况下,除非代码明确使用不安全的特性,例如指针,否则它使用的数据类型可以确保内存不会被错误地访问。例如,.Net数组类型执行边界检查,以禁止执行超过出边界的数组操作。如果运行的应用程序的确需要与运行在不同应用程序域中的代码应用程序通信或者共享数据,就必须调用.Net的远程服务。

被验证不能访问超出其应用程序的数据(而不是通过明确的远程机制)的代码就是内存类安全的代码。这种代码与运行在同一个进程中单应用程序域不同的类型安全代码一起运行时安全的。

3.4 通过异常处理错误

.NET Framework可以通过异常相同的机制处理错误情况,处理的方式和Java和C++是相同的。因为IL有强大的类型系统,所以通过C++生成的IL使用异常不会带来任何的性能问题。而且,.NET和C#支持finally,当C++中是不支持的。

​代码的某些领域被看作是异常处理程序案例,每个案例都能处理某些特殊的错误情况,例如常见的找不到文件、拒绝某些操作的许可,这些条件可以定义的很宽,也可以很窄。

​异常处理机制的异常结构能够确保发生错误情况时,执行进程立即跳到最合适的异常处理程序的案例上,处理错误情况。

​异常处理情况还提供了一种方便的方式,当对象包含错误情况的准确信息时,该对象就可以传送给错误处理例程。其中,这个对象包括给用户提供的相应信息和在代码的什么地方监测到的错误的确切信息。

​大部分的异常处理结构,包含异常发生的程序流控制,都是通过高级语言处理的,任何中间语言的命令都不支持它。

​.NET提供了一种基础结构,让面向.NET的编译器支持异常处理。他提供了一组.NET类来表示异常,语言的互操作性则允许异常处理代码处理被抛出的异常对象,无论异常处理代码使用什么语言编写,都是这样。语言的无关性在C++和Java中没有体现,但是在COM的错误处理机制中有一定限度的体现。COM的错误处理机制包括从方法中返回错误代码以及传递错误对象。

​在不同的语言中,异常的处理是一致的,这是多语言开发的重要一环。

3.5 特性的使用

特性(attribute)是C++编写COM组件的一个功能,在Microsoft的COM接口定义语言(Interface Definition Language,IDL)中使用的特性。特性最初是为了在程序中提供与某些项相关的额外信息,以供编译器使用。

​.NET支持特性,因此现在的C++、C#和Visual Basic也支持特性。在.NET中,对特性的革新建立了一个机制,通过该机制可以在源代码中定义自己的特性。这些用户定义的特性将和对应数据类型或方法的元数据放在一起,这对于文档说明书十分有用,他们和反射技术一起使用,以根据特性执行编程任务。另外,与.NET的语言无关性的基本原理一样,特性可以在一种语言的源代码中定义,而被用另一种语言编写的代码读取。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wells Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值