C#基础
C#/.NET完整视频课程
cnblogs.com/Ming8006/p/6140008.html
26.1 CLR线程池基础
如25章所述,创建和销毁线程是一个比较昂贵的操作:
- 太多的线程也会浪费内存资源。
- 由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能。
为了改善这个情况,CLR使用了代码来管理它自己的线程池。可将线程池想像成可由你的应用程序使用的一个线程集合。每个进程都有一个线程池,它在各个应用程序域(AppDomain)是共享的.
线程池是如何工作的:
- CLR初始化时,线程池是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序想执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池的队列中。线程池的代码从这个队列中提取记录项,将这个记录项派遣(dispatch)给一个线程池线程。如果线程池中没有线程,就创建新的线程。创建线程要产生一定的性能损失。然而,当线程池完成任务后,线程不会被销毁。相反,线程会返回线程池,在那里进入空闲状态,等待响应另一个请求。由于线程不销毁自身,所以不再产生额外的性能损失。
- 如果你的应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有的请求。然而,如果你的应用程序发出请求的速度超过了线程池处理它们的速度,就会创建额外的线程。最终,你的应用程序所有请求都可能有少量的线程处理,所有线程池不必创建大量的线程。
- 如果你的应用程序停止向线程池发出请求,池中含有大量空闲的线程。这是对内存资源的一种浪费。所以,当一个线程池线程空闲一段时间以后,线程会自己醒来终止自己以释放资源。
在内部,线程池将自己的线程划分为工作者(Worker)线程和I/O线程。应用程序要求线程池执行一个异步的计算限制操作时(这个操作可能发起一个I/O限制的操作),使用的就是工作者线程。I/O线程用于通知你的代码一个异步I/O限制操作已经完成,具体的说,这意味着使用"异步编程模型"发出I/O请求,比如访问文件、网络服务器、数据库等等。
26.2 执行简单的计算限制操作
将一个异步的、计算限制的操作放到一个线程池的队列中,通常可以调用ThreadPool类定义的以下方法之一:
1 static Boolean QueueUserWorkItem(WaitCallback callBack); 2 static Boolean QueueUserWorkItem(WaitCallback callBack,Object state);
这些方法向线程池的队列中添加一个"工作项"(work item)以及可选的状态数据, 如果此方法成功排队,则为 true;如果无法将该工作项排队,则引发 OutOfMemoryException。工作项其实就是由callBack参数标识的一个方法,该方法将由线程池线程调用。可通过state实参(状态数据)向方法传递一个参数。无state参数的那个版本的QueueUserWorkItem则向回调方法传递null。最终,池中的某个线程会处理工作项,造成你指定的方法被调用。你写的回调方法必须匹配System.Threading.WaitCallBack委托类型,它的定义如下:
delegate void WaitCallback(Object state);
注意:WaitCallback委托、TimerCallback委托(在本章26.8节“执行时计算限制操作”中讨论)和ParameterizedThreadStart委托(在第25章“线程基础”中讨论)是完全一致的。因此,只要定义一个和该签名匹配的方法后,使用ThreadPool.QueueUserWorkItem方法,使用System.Threading.Timer对象,或着使用System.Threading.Thread对象,都一个调用这个方法。
以下演示了如何让一个线程池线程以异步方式调用一个方法:
View Code
编译运行的结果是:
Main thread: queuing an asynchronous operation
Main thread: Doing other work here...
In ComputeBoundOp: state=5
但有时也会得到一下输出:
Main thread: queuing an asynchronous operation
In ComputeBoundOp: state=5
Main thread: Doing other work here...
之所以有两种输出结果,是因为这两个方法相互之间是异步运行的。由Windows调度器决定先调度哪一个线程。
26.3 执行上下文
每个线程都关联了一个执行上下文数据结构。执行上下文(execution context)包括的东西有:
- 安全设置:压缩栈、Thread的Principal属性[指示线程的调度优先级]和Windows身份;
- 宿主设置:参见System.Threading.HostExecutionContextManager[提供使公共语言运行时宿主可以参与执行上下文的流动(或移植)的功能];
- 逻辑调用上下文数据:参见System.Runtime.Remoting.Messaging.CallContext[提供与执行代码路径一起传送的属性集]的LogicalSetData[将一个给定对象存储在逻辑调用上下文中并将该对象与指定名称相关联]和LogicalGetData[从逻辑调用上下文中检索具有指定名称的对象]。
线程执行代码时,有的操作会受到线程的执行上下文设置(尤其是安全设置)的影响。理想情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该"流动"(复制)到辅助线程。这就确保辅助线程执行的任何操作使用的都是相同的安全设置和宿主设置。还确保了初始线程的逻辑调用上下文可以在辅助线程中使用。
默认情况下,CLR自动造成初始线程的执行上下文会"流动"(复制)到任何辅助线程。这就是将上下文信息传输到辅助线程,但这对损失性能,因为执行上下文中包含大量信息,而收集这些信息,再将这些信息复制到辅助线程,要耗费不少时间。如果辅助线程又采用更多的辅助线程,还必须创建和初始化更多的执行上下文数据结构。
System.Threading命名空间中有一个ExecutionContext类[管理当前线程的执行上下文],它允许你控制线程的执行上下文如何从一个线程"流动"(复制)到另一个线程。下面展示了这个类的样子:
View Code
可用这个类阻止一个执行上下文的流动,从而提升应用程序的性能。对于服务器应用程序,性能的提升可能非常显著。但是,客户端应用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]attribute进行了标识,所以在某些客户端应用程序(比如Silverlight)中是无法调用的。当然,只有在辅助线程不需要或者不防问上下文信息时,才应该组织执行上下文的流动。如果初始线程的执行上下文不流向辅助线程,辅助线程会使用和它关联起来的任何执行上下文。在这种情况下,辅助线程不应该执行要依赖于执行上下文状态(比如用户的Windows身份)的代码。
注意:添加到逻辑调用上下文的项必须是可序列化的。对于包含了逻辑调用上下文数据线的一个执行上下文,如果让它流动,可能严重损害性能,因为为了捕捉执行上下文,需对所有数据项进行序列化和反序列化。
下例展示了向CLR的线程池队列添加一个工作项的时候,如何通过阻止执行上下文的流动来影响线程逻辑调用上下文中的数据:
View Code
会得到一下结果:
Name=Jeffrey
Name=
虽然现在我们讨论的是调用ThreadPool.QueueUserWorkItem时阻止执行上下文的流动,但在使用Task对象(参见26.5节”任务“),以及在发起异步I/O操作(参见第27章“I/o限制的异步操作”)时,这个技术也会用到。
参考
[转] C#与Java的比较
转自:C#与Java的比较
2015-06-26
目录
C#(C-Sharp)是Microsoft的新编程语言,被誉为“C/C++家族中第一种面向组件的语言”。然而,不管它自己宣称的是什么,许多人认为C#更像是Java的一种克隆,或者是Microsoft用来替代Java的产品。事实是否是这样的呢?
本文的比较结果表明,C#不止是Java的同胞那么简单。如果你是一个Java开发者,想要学习C#或者了解更多有关C#的知识,那么本文就是你必须把最初10分钟投入于其中的所在。
一、C#、C++和Java
C#的语言规范由Microsoft的Anders Hejlsberg与Scott Wiltamuth编写。在当前Microsoft天花乱坠的宣传中,对C#和C++、Java作一番比较总是很有趣的。考虑到当前IT媒体的舆论倾向,如果你早就知道C#更接近Java而不是C++,事情也不值得大惊小怪。
对于刚刚加入这场讨论的读者,下面的表1让你自己作出判断。显然,结论应该是:Java和C#虽然不是孪生子,但C#最主要的特色却更接近Java而不是C++。
表1:比较C#、C++和Java最重要的功能
|
|||
功能
|
C#
|
C++
|
Java
|
继承
|
允许继承单个类,允许实现多个接口
|
允许从多个类
继承
|
允许继承单个类,允许实现多个接口
|
接口实现
|
通过“interface”关键词
|
通过抽象类
|
通过“interface”关键词
|
内存管理
|
由运行时环境管理,使用垃圾收集器
|
需要手工管理
|
由运行时环境管理,使用垃圾收集器
|
指针
|
支持,但只在很少使用的非安全模式下才支持。通常以引用取代指针
|
支持,一种很常用的功能。
|
完全不支持。代之以引用。
|
源代码编译后的形式
|
.NET中间语言(IL)
|
可执行代码
|
字节码
|
单一的公共基类
|
是
|
否
|
是
|
异常处理
|
异常处理
|
返回错误
|
异常处理。
|
二、语言规范的比较
2.1、简单数据类型
简单数据类型(Primitive)在C#中称为值类型,C#预定义的简单数据类型比Java多。例如,C#有unit,即无符号整数。
表2列出了所有C#的预定义数据类型:
表2:C#中的值类型
|
|
类型
|
说明
|
object
|
所有类型的最终极的基类
|
string
|
字符串类型;字符串是一个Unicode字符的序列
|
sbyte
|
8位带符号整数
|
short
|
16位带符号整数
|
int
|
32位带符号整数
|
long
|
64位带符号整数
|
byte
|
8位无符号整数
|
ushort
|
16位无符号整数
|
uint
|
32位无符号整数
|
ulong
|
64位无符号整数
|
float
|
单精度浮点数类型
|
double
|
双精度浮点数类型
|
bool
|
布尔类型;bool值或者是true,或者是false
|
char
|
字符类型;一个char值即是一个Unicode字符
|
decimal
|
有28位有效数字的高精度小数类型
|
2.2、常量
忘掉Java中的static final修饰符。在C#中,常量可以用const关键词声明。
public const int x = 55;
|
此外,C#的设计者还增加了readonly关键词。如果编译器编译时未能确定常量值,你可以使用readonly关键词。readonly域只能通过初始化器或类的构造函数设置。
2.3、公用类的入口点
在Java中,公用类的入口点是一个名为main的公用静态方法。main方法的参数是String对象数组,它没有返回值。在C#中,main方法变成了公用静态方法Main(大写的M),Main方法的参数也是一个String对象数组,而且也没有返回值,如下面的原型声明所示:
public static void Main(String[] args)
|
但是,C#的Main方法不局限于此。如果不向Main方法传递任何参数,你可以使用上述Main方法的一个重载版本,即不带参数列表的版本。也就是说,下面的Main方法也是一个合法的入口点:
public static void Main()
|
另外,如果你认为有必要的话,Main方法还可以返回一个int。例如,下面代码中的Main方法返回1:
View Code
与此相对,在Java中重载main方法是不合法的。
2.4、switch语句
在Java中,switch语句只能处理整数。但C#中的switch语句不同,它还能够处理字符变量。请考虑下面用switch语句处理字符串变量的C#代码:
View Code
与Java中的switch不同,C#的switch语句要求每一个case块或者在块的末尾提供一个break语句,或者用goto转到switch内的其他case标签。
2.5、foreach语句
foreach语句枚举集合中的各个元素,为集合中的每一个元素执行一次代码块。请参见下面的例子。
View Code
如果在运行这个执行文件的时候指定了参数,比如“Hello Peter Kevin Richard”,则程序的输出将是下面几行文字:
Peter
Kevin
Richard
2.6、C#没有>>>移位操作符
C#支持uint和ulong之类的无符号变量类型。因此,在C#中,右移操作符(即“>>”)对于无符号变量类型和带符号变量类型(比如int和long)的处理方式不同。右移uint和ulong丢弃低位并把空出的高位设置为零;但对于int和long类型的变量,“>>”操作符丢弃低位,同时,只有当变量值是正数时,“>>”才把空出的高位设置成零;如果“>>”操作的是一个负数,空出的高位被设置成为1。
Java中不存在无符号的变量类型。因此,我们用“>>>”操作符在右移时引入负号位;否则,使用“>>”操作符。
2.7、goto关键词
Java不用goto关键词。在C#中,goto允许你转到指定的标签。不过,C#以特别谨慎的态度对待goto,比如它不允许goto转入到语句块的内部。在Java中,你可以用带标签的语句加上break或continue取代C#中的goto。
2.8、声明数组
在Java中,数组的声明方法非常灵活,实际上有许多种声明方法都属于合法的方法。例如,下面的几行代码是等价的:
View Code
但在C#中,只有第一行代码合法,[]不能放到变量名字之后。
2.9、包
在C#中,包(Package)被称为名称空间。把名称空间引入C#程序的关键词是“using”。例如,“using System;”这个语句引入了System名称空间。
然而,与Java不同的是,C#允许为名称空间或者名称空间中的类指定别名:
View Code
虽然从概念上看,Java的包类似于.NET的名称空间。然而,两者的实现方式不同。在Java中,包的名字同时也是实际存在的实体,它决定了放置.java文件的目录结构。在C#中,物理的包和逻辑的名称之间是完全分离的,也就是说,名称空间的名字不会对物理的打包方式产生任何影响。在C#中,每一个源代码文件可以从属于多个名称空间,而且它可以容纳多个公共类。
.NET中包的实体称为程序集(Assembly)。每一个程序集包含一个manifest结构。manifest列举程序集所包含的文件,控制哪些类型和资源被显露到程序集之外,并把对这些类型和资源的引用映射到包含这些类型与资源的文件。程序集是自包含的,一个程序集可以放置到单一的文件之内,也可以分割成多个文件。.NET的这种封装机制解决了DLL文件所面临的问题,即臭名昭著的DLL Hell问题。
2.10、默认包
在Java中,java.lang包是默认的包,它无需显式导入就已经自动包含。例如,要把一些文本输出到控制台,你可以使用下面的代码:
System.out.println("Hello world from Java");
|
C#中不存在默认的包。如果要向控制台输出文本,你使用System名称空间Console对象的WriteLine方法。但是,你必须显式导入所有的类。代码如下:
View Code
2.11、面向对象
Java和C#都是完全面向对象的语言。在面向对象编程的三大原则方面,这两种语言接近得不能再接近。
- 继承:这两种语言都支持类的单一继承,但类可以实现多个接口。所有类都从一个公共的基类继承。
- 封装与可见性:无论是在Java还是C#中,你都可以决定类成员是否可见。除了C#的internal访问修饰符之外,两者的可见性机制非常相似。
- 多态性:Java和C#都支持某些形式的多态性机制,且两者实现方法非常类似。
2.12、可访问性
类的每个成员都有特定类型的可访问性。C#中的访问修饰符与Java中的基本对应,但多出了一个internal。简而言之,C#有5种类型的可访问性,如下所示:
- public:成员可以从任何代码访问。
- protected:成员只能从派生类访问。
- internal:成员只能从同一程序集的内部访问。
- protected internal:成员只能从同一程序集内的派生类访问。
- private:成员只能在当前类的内部访问。
2.13、派生类
在Java中,我们用关键词“extends”实现继承。C#采用了C++的类派生语法。例如,下面的代码显示了如何派生父类Control从而创建出新类Button:
public class Button: Control { . . }
|
2.14、最终类
由于C#中不存在final关键词,如果想要某个类不再被派生,你可以使用sealed关键词,如下例所示:
sealed class FinalClass { . . }
|
2.15、接口
接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母“I”开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:
1
|
interface IShape { void Draw(); }
|
扩展接口的语法与扩展类的语法一样。例如,下例的IRectangularShape接口扩展IShape接口(即,从IShape接口派生出IRectangularShape接口)。
interface IRectangularShape: IShape { int GetWidth(); }
|
如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:
interface INewInterface: IParent1, IParent2 { }
|
然而,与Java不同,C#中的接口不能包含域(Field)。
另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法声明可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。
2.16、is和as操作符
C#中的is操作符与Java中的instanceof操作符一样,两者都可以用来测试某个对象的实例是否属于特定的类型。在Java中没有与C#中的as操作符等价的操作符。as操作符与is操作符非常相似,但它更富有“进取心”:如果类型正确的话,as操作符会尝试把被测试的对象引用转换成目标类型;否则,它把变量引用设置成null。
为正确理解as操作符,首先请考虑下面这个例子中is操作符的运用。这个例子包含一个IShape接口,以及两个实现了IShape接口的类Rectangle和Circle。
View Code
编译好代码之后,用户可以输入“rectangle”或者“circle”作为Main方法的参数。如果用户输入的是“circle”,则shape被实例化成为一个Circle类型的对象;反之,如果用户输入的是“rectangle”,则shape被实例化成为Rectangle类型的对象。随后,程序用is操作符测试shape的变量类型:如果shape是一个矩形,则shape被转换成为Rectangle对象,我们调用它的GetWidth方法;如果shape是一个圆,则shape被转换成为一个Circle对象,我们调用它的GetRadius方法。
如果使用as操作符,则上述代码可以改成如下形式:
View Code
在上面代码的粗体部分中,我们在没有测试shape对象类型的情况下,就用as操作符把shape转换成Rectangle类型的对象。如果shape正好是一个Rectangle,则shape被转换成为Rectangle类型的对象并保存到rectangle变量,然后我们调用它的GetWidth方法。如果这种转换失败,则我们进行第二次尝试。这一次,shape被转换成为Circle类型的对象并保存到circle变量。如果shape确实是一个Circle对象,则circle现在引用了一个Circle对象,我们调用它的GetRadius方法。
2.17、库
C#没有自己的类库。但是,C#共享了.NET的类库。当然,.NET类库也可以用于其他.NET语言,比如VB.NET或者JScript.NET。值得一提的是StringBuilder类,它是对String类的补充。StringBuilder类与Java的StringBuffer类非常相似。
2.18、垃圾收集
C++已经让我们认识到手工管理内存是多么缺乏效率和浪费时间。当你在C++中创建了一个对象,你就必须手工地拆除这个对象。代码越复杂,这个任务也越困难。Java用垃圾收集器来解决这个问题,由垃圾收集器搜集不再使用的对象并释放内存。C#同样采用了这种方法。应该说,如果你也在开发一种新的OOP语言,追随这条道路是一种非常自然的选择。C#仍旧保留了C++的内存手工管理方法,它适合在速度极端重要的场合使用,而在Java中这是不允许的。
2.19、异常处理
如果你听说C#使用与Java相似的异常处理机制,你不会为此而惊讶,对吧?在C#中,所有的异常都从一个名为Exception的类派生(听起来很熟悉?)另外,正如在Java中一样,你还有熟悉的try和catch语句。Exception类属于.NET System名称空间的一部分。
三、Java没有的功能
C#出生在Java成熟之后,因此,C#拥有一些Java(目前)还没有的绝妙功能也就不足为奇。
3.1、枚举器
枚举器即enum类型(Enumerator,或称为计数器),它是一个相关常量的集合。精确地说,enum类型声明为一组相关的符号常量定义了一个类型名字。例如,你可以创建一个名为Fruit(水果)的枚举器,把它作为一个变量值的类型使用,从而把变量可能的取值范围限制为枚举器中出现的值。
View Code
在上例的Process方法中,虽然你可以用int作为myVar变量的类型,但是,使用枚举器Fruit之后,变量的取值范围限制到了Applet、Banana、Cherry和Durian这几个值之内。与int相比,enum的可读性更好,自我说明能力更强。
3.2、结构
结构(Struct)与类很相似。然而,类是作为一种引用类型在堆中创建,而结构是一种值类型,它存储在栈中或者是嵌入式的。因此,只要谨慎运用,结构要比类快。结构可以实现接口,可以象类一样拥有成员,但结构不支持继承。
然而,简单地用结构来取代类可能导致惨重损失。这是因为,结构是以值的方式传递,由于这种传递方式要把值复制到新的位置,所以传递一个“肥胖的”结构需要较大的开销。而对于类,传递的时候只需传递它的引用。
下面是一个结构的例子。注意它与类非常相似,只要把单词“struct”替换成“class”,你就得到了一个类。
View Code
3.3、属性
C#类除了可以拥有域(Field)之外,它还可以拥有属性(Property)。属性是一个与类或对象关联的命名的特征。属性是域的一种自然扩展——两者都是有类型、有名字的类成员。然而,和域不同的是,属性不表示存储位置;相反,属性拥有存取器(accessor),存取器定义了读取或者写入属性值时必须执行的代码。因此,属性提供了一种把动作和读取、写入对象属性值的操作关联起来的机制,而且它们允许属性值通过计算得到。
3.4、以引用方式传递简单数据类型的参数
在Java中,当你把一个简单数据类型的值作为参数传递给方法时,参数总是以值的方式传递——即,系统将为被调用的方法创建一个参数值的副本。在C#中,你可以用引用的方式传递一个简单数据类型的值。此时,被调用的方法将直接使用传递给它的那个值——也就是说,如果在被调用方法内部修改了参数的值,则原来的变量值也随之改变。
在C#中以引用方式传递值时,我们使用ref关键词。例如,如果编译并运行下面的代码,你将在控制台上看到输出结果16。注意i值被传递给ProcessNumber之后是如何被改变的。
View Code
C#中还有一个允许以引用方式传递参数的关键词out,它与ref相似。但是,使用out时,作为参数传递的变量在传递之前不必具有已知的值。在上例中,如果整数i在传递给ProcessNumber方法之前没有初始化,则代码将出错。如果用out来取代ref,你就可以传递一个未经初始化的值,如下面这个修改后的例子所示。
View Code
经过修改之后,虽然i值在传递给ProcessNumber方法之前没有初始化,但PassByReference类能够顺利通过编译。
3.5、C#保留了指针
对于那些觉得自己能够恰到好处地运用指针并乐意手工进行内存管理的开发者来说,在C#中,他们仍旧可以用既不安全也不容易使用的“古老的”指针来提高程序的性能。C#提供了支持“不安全”(unsafe)代码的能力,这种代码能够直接操作指针,能够“固定”对象以便临时地阻止垃圾收集器移动对象。无论从开发者还是用户的眼光来看,这种对“不安全”代码的支持其实是一种安全功能。“不安全”的代码必须用unsafe关键词显式地标明,因此开发者不可能在无意之中使用“不安全”的代码。同时,C#编译器又和执行引擎协作,保证了“不安全”的代码不能伪装成为安全代码。
View Code
C#中的unsafe代码适合在下列情形下使用:当速度极端重要时,或者当对象需要与现有的软件(比如COM对象或者DLL形式的C代码)交互时。
3.6、代理
代理(delegate)可以看作C++或者其他语言中的函数指针。然而,与函数指针不同的是,C#中的代理是面向对象的、类型安全的、可靠的。而且,函数指针只能用来引用静态函数,但代理既能够引用静态方法,也能够引用实例方法。代理用来封装可调用方法。你可以在类里面编写方法并在该方法上创建代理,此后这个代理就可以被传递到第二个方法。这样,第二个方法就可以调用第一个方法。
代理是从公共基类System.Delegate派生的引用类型。定义和使用代理包括三个步骤:声明,创建实例,调用。代理用delegate声明语法声明。例如,一个不需要参数且没有返回值的代理可以用如下代码声明:
delegate void TheDelegate();
|
创建代理实例的语法是:使用new关键词,并引用一个实例或类方法,该方法必须符合代理指定的特征。一旦创建了代理的实例,我们就可以用调用方法的语法调用它。
3.7、包装和解除包装
在面向对象的编程语言中,我们通常使用的是对象。但为了提高速度,C#也提供了简单数据类型。因此,C#程序既包含一大堆的对象,又有大量的值。在这种环境下,让这两者协同工作始终是一个不可回避的问题,你必须要有一种让引用和值进行通信的方法。
在C#以及.NET运行时环境中,这个“通信”问题通过包装(Boxing)和解除包装(Unboxing)解决。包装是一种让值类型看起来象引用类型的处理过程。当一个值类型(简单数据类型)被用于一个要求或者可以使用对象的场合时,包装操作自动进行。包装一个value-type值的步骤包括:分配一个对象实例,然后把value-type值复制到对象实例。
解除包装所执行的动作与包装相反,它把一个引用类型转换成值类型。解除包装操作的步骤包括:首先检查并确认对象实例确实是给定value-type的一个经过包装的值,然后从对象实例复制出值。
Java对该问题的处理方式略有不同。Java为每一种简单数据类型提供了一个对应的类封装器。例如,用Integer类封装int类型,用Byte类封装byte类型。
【结束语】本文为你比较了C#和Java。这两种语言很相似,然而,说C#是Java的克隆或许已经大大地言过其实。面向对象、中间语言这类概念并不是什么新东西。如果你准备设计一种面向对象的新语言,而且它必须在一个受管理的安全环境内运行,你难道不会搞出与C#差不多的东西吗?
作者:彭白洋 创建于:2009-01-15 出处:http://www.cnblogs.com/siceblue/archive/2009/01/15/1376430.html 收录于:2013-02-28
看如下代码:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 DriveB d = new DriveB(); 6 Console.Read(); 7 } 8 } 9 class BaseA 10 { 11 //4.基类静态成员初始化; 12 static DisplayClass a = new DisplayClass("基类静态成员初始化"); 13 //6.基类实例成员初始化; 14 DisplayClass BaseA_c = new DisplayClass("基类实例变量BaseA_c初始化"); 15 //5.基类静态构造函数调用 16 static BaseA() 17 { 18 Console.WriteLine("基类静态构造方法被调用"); 19 } 20 //7.基类构造方法调用; 21 public BaseA() 22 { 23 Console.WriteLine("基类构造方法被调用"); 24 } 25 } 26 class DriveB : BaseA 27 { 28 //1.继承类静态成员初始化; 29 static DisplayClass DriveB_b = new DisplayClass("继承类静态成员DriveB_b初始化"); 30 //3.继承类实例成员初始化; 31 DisplayClass DriveB_c = new DisplayClass("继承类实例变量DriveB_c初始化"); 32 //2.继承类静态构造函数调用 33 static DriveB() 34 { 35 Console.WriteLine("继承类静态构造方法被调用"); 36 } 37 //8.继承类构造方法调用; 38 public DriveB() 39 { 40 Console.WriteLine("继承类构造方法被调用"); 41 } 42 } 43 class DisplayClass 44 { 45 public DisplayClass(string diplayString) 46 { 47 Console.WriteLine(diplayString); 48 Console.WriteLine(); 49 } 50 }
得出初始化顺序结论:
1)继承类静态成员变量初始化
2)继承类实例变量初始化
3)基类静态静态成员变量初始化
4)基类实例变量初始化
5)基类构造方法调用
6)继承类构造方法调用。
加了静态构造函数后结论:
1)继承类静态成员变量初始化
1.1)继承类静态构造函数调用
2)继承类实例变量初始化
3)基类静态静态成员变量初始化
3.1)基类静态构造函数调用
4)基类实例变量初始化
5)基类构造方法调用
6)继承类构造方法调用。
总结:
1 静态成员优先于实例成员
2 实例成员优先于构造函数
3 在调用子类实例构造函数之前,先调用基类实例构造函数
4 继承类静态构造函数不会调用基类构造函数,因为静态实例时放在类型对象中的。
和JAVA的有点不一样啊,像JAVA是严格的从基类到派生类
了解了静态成员初始化,就引出了另外一个问题,如果两个类相互间引用,比如A类的静态初始化里引用到了B类,B类的静态初始化里又引用到了A类,请看下面这段代码:
1 class A 2 { 3 public static int X; 4 static A() 5 { 6 X=B.Y+1; 7 } 8 } 9 class B 10 { 11 public static int Y=A.X+1; 12 static B(){} 13 static void Main() 14 { 15 Console.WriteLine("X={0},Y={1}",A.X,B.Y); 16 } 17 }
结果:x=1,y=2
解析:
一般来说静态声明赋值语句先于静态构造函数执行,没有赋值的类成员(局部变量不会有默认值)声明(这里不是初始化)会被初始化成该类型的默认值,所以刚开始x,y的默认值都是0.。
因为Main函数在class B中,所以程序先执行的是上面的第二条语句 (public static int Y=A.X+1;),声明一个Y,再给Y赋值
在赋值的时候又调用到了A类中的X静态,当第一次访问A.X的时候,会先调用A类的静态构造函数,这里执行赋值X=B.Y+1,而重新去访问B类的成员,因为前面说的静态初始化只有第一次被访问的时候会执行,所以再次访问B类的时候不会重复进行静态初始化的。这时会因为前一次初始化还未完成,特别是B.Y还没有赋值完成,所以根据上面说的,B.Y现在处理只是声明完成的状态,所以现在B.Y的值就是0,相应的得到的X的值就是1了,在A类的静态构造函数执行完成的时候,程序会再回到B中Y的赋值语句上来,这时候得到的A.X的值就是1,而Y赋值完成后,此时值就变成了2了。
为了说明上述红色字体语句,看一下代码和结果:
1 class Program 2 { 3 static void Main() 4 { 5 Console.WriteLine("X={0},Y={1}", A.X, B.Y); 6 Console.Read(); 7 } 8 } 9 class A 10 { 11 public static int X; 12 static A() 13 { 14 X = B.Y + 1; 15 } 16 } 17 class B 18 { 19 public static int Y = A.X + 1; 20 static B() { } 21 22 }
结果:x=2,y=1
对于引用类型成员的初始化说了这么多还是总结一下吧.C#中初始化变量(包括实例成员变量和静态成员变量)可以采用成员声明的地方赋值的方式,也可以采用构造函数的方式.我个人在使用实例对象的时候比较推荐采用构造函数的方式,因为构造函数赋值的方式执行的顺序是从父类到子类,这种顺序避免了子类成员变量的初始化过程引用了未赋值的父类成员变量.而且在构造函数中初始化变量可以采用更多的语句块,更多的判断逻辑来初始化,甚至可以加上结构化异常处理try{}catch{}来处理异常信息,远比单单一个赋值语句来得灵活.不过对于简单的内置基本类型(如int,Enum,string等)就无所谓在哪里进行初始化了.
分类: C#
C#集合概述
2016-11-29
集合
|
顺序排列
|
连顺存储
|
直接访问方式
|
访问时间
|
操作时间
|
备注
|
Dictionary
|
|
是
|
Key
|
Key:O(1)
|
O(1)
|
访问性能最快,不支持排序
|
SortedDinctionary
|
顺序排列
|
否
|
Key
|
Key:O(log n)
|
O(log n)
|
快速访问和支持排序的折衷
|
SortedList
|
顺序排列
|
是
|
Key
|
Key:O(log n)
|
O(n)
|
和SortedDictionary相似,只是内部用数据替代树作为存储结构。
|
List
|
使用者可以精确控制元素的位置
|
是
|
Index
|
Index: O(1)
Value: O(n)
|
O(n)
|
最适合需要直接访问每一个元素的少量集合。
|
LinkedList
|
使用者可以精确控制元素的位置
|
否
|
不支持
|
Value:O(n)
|
O(1)
|
最适合不需要直接访问单个元素,但是在集合中添加/移除非常频繁的场景。
|
HashSet
|
不支持
|
是
|
Key
|
Key:O(1)
|
O(1)
|
能保持元素唯一性的集合。不支持排序
|
SortedSet
|
顺序排列
|
否
|
Key
|
Key:O(log n)
|
O(log n)
|
能保持元素唯一性并且支持排序。
|
Stack
|
LIFO
|
是
|
只能获取顶部元素
|
Top: O(1)
|
O(1)
|
|
Queue
|
FIFO
|
是
|
只能获底部元素
|
Front: O(1)
|
O(1)
|
|
【2】C#知识简单梳理
【3】C#集合类型大盘点