csharp入门经典非衣居士

.NETFramework是Microsoft为开发应用程序而创建的一个具有革命意义的平台,它有运行在其他操作系统上的版本

.NETFramework的设计方式确保它可以用于各种语言,包括本书介绍的C#语言,以及C++、VisualBasic、JScript等

.NETFramework主要包含一个庞大的代码库,可以在客户语言中通过面向对象编程技术(OOP)来使用这些代码。这个库分为多个不同的模块,这样就可以根据希望得到的结果来选择使用其中的各个部分。例如,一个模块包含Windows应用程序的构件,另一个模块包含网络编程的代码块,还有一个模块包含Web开发的代码块。一些模块还分为更具体的子模块,例如,在Web开发模块中,有用于建立Web服务的子模块其目的是,不同操作系统可以根据各自的特性,支持其中的部分或全部模块

.NETFramework还包含.NET公共语言运行库(CommonLanguageRuntime,CLR),它负责管理用.NET库开发的所有应用程序的执行

在.NETFramework下,编译代码的过程有所不同,此过程包括两个阶段

程序集编译应用程序时,所创建的CIL代码存储在一个程序集中、程序集包含可执行应用程序文件(.exe)和其他应用程序使用的库(.dll)

除CIL外,程序集还包含元数据(程序集中包含的数据的信息)和可选的资源(CIL使用的其他数据,如文件和图片)。元信息允许程序集是完全自描述的。不需要其他信息就可以使用程序集

不必把运行应用程序需要的所有信息都安装到一个地方。可以编写一些代码来执行多个应用程序所要求的任务。此时通常把这些可重用的代码放在所有应用程序都可以访问的地方。在.NETFramework中,这个地方是全局程序集缓存(GlobalAssemblyCache,GAC),把代码放在这个缓存中是很简单的,只需把包含代码的程序集放在包含该缓存的目录中即可

托管代码在将代码编译为CIL,再用JIT编译器将它编译为本机代码后,CLR的任务尚未全部完成,还需要管理正在执行的用.NETFramework编写的代码(这个执行代码的阶段称为运行时)

CLR管理着应用程序,其方式是管理内存、处理安全性以及允许进行跨语言调试等。相反,不受CLR控制运行的应用程序属于非托管类型,某些语言(如C++)可以用于编写此类应用程序,例如,访问操作系统的底层功能。但是在C#中,只能编写在托管环境下运行的代码。我们将使用CLR的托管功能,让.NET处理与操作系统的任何交互

垃圾回收托管代码最重要的一个功能是垃圾回收

创建.NET应用程序的所需步骤:

C#是类型安全的语言,在类型之间转换时,必须遵守严格的规则。执行相同的任务时,用C#编写的代码通常比用C++编写的代码长。但C#代码更健壮,调试起来也比较简单,.NET始终可以随时跟踪数据的类型

.NETFramework没有限制应用程序的类型。C#使用的是.NETFramework,所以也没有限制应用程序的类型

C#代码的外观和操作方式与cpp和Java非常类似

可以使用#region和#endregion关键字(以#开头实际上是预处理指令,并不是关键字)来定义要展开和折叠的代码区域的开头和结尾

#region/*注释*/ //代码块#endregion整数类型

//介于–128和127之间的整数sbyteSystem.SByte//介于0和255之间的整数byteSystem.Byte//介于–32768和32767之间的整数shortSystem.Int16//介于0和65535之间的整数ushortSystem.UInt16//介于–2147483648和2147483647之间的整数intSystem.Int32//介于0和4294967295之间的整数uintSystem.UInt32//介于–9223372036854775808和9223372036854775807之间的整数longSystem.Int64//介于0和18446744073709551615之间的整数ulongSystem.UInt64这些类型中的每一种都利用了.NETFramework中定义的标准类型,使用标准类型可以在语言之间交互操作,u是unsigned的缩写

浮点类型前两种可以用+/–m×2^e的形式存储浮点数,m和e的值因类型而异。decimal使用另一种形式:+/–m×10^e

//m:0~2^24,e:-149~104floatSystem.Single//m:0~2^53,e:-1075~970doubleSystem.Double//m:0~2^96,e:-28-0decimalSystem.Decimal文本和布尔类型

//1个Unicode字符,存储0~65535之间的整数charSystem.Char//字符串,字符数量没有上限stringSystem.String//布尔值boolSystem.Boolean变量命名规则

字面值转义字符

\0//null0x0000\a//警告蜂鸣0x0007\b//退格0x0008\f//换页0x000C\n//换行0x000A\r//回车0x000D\t//水平制表符0x0009\v//垂直制表符0x000B//可以使用\u后跟一个4位16进制值来使用对应的Unicode转义字符\u000D也可以一字不变地指定字符串,即两个双引号之间的所有字符都包含在字符串中,包括行末字符和原本需要转义的字符

Console.WriteLine("Verbatimstringliteral:item1");//error//开头使用@,一字不变地指定字符串,无需使用转义字符 Console.WriteLine(@"Verbatimstringliteral:item1"); 字符串是引用类型,可赋予null值,表示字符串变量不引用字符串

表达式把操作数(变量和字面值)与运算符组合起来,就可以创建表达式,它是计算的基本构件

var1=+var2//var1的值等于var2的值var1+=var2//var1的值等于var1加var2,不会把负值变为正数var1=-var2//var1的值等于var2乘以-1var1=-var2//var1的值等于var1减var2注意区分它们,前者是一元运算符,结合的是操作数

运算符优先级由高到底:

命名空间命名空间的主要目的是避免命名冲突,并提供一种组织代码的方式,使得代码更加清晰和易于管理命名空间可以嵌套在其他命名空间中,形成一个层次结构

默认情况下,C#代码包含在全局名称空间中。这意味着对于包含在这段代码中的项,全局名称空间中的其他代码只需通过名称进行引用就可以访问它们

可以使用namespace关键字为花括号中的代码块显式定义命名空间,如果在该命名空间代码的外部使用该命名空间中的名称,就必须写出该命名空间中的限定名称

如果一个命名空间中的代码需要使用在另一个命名空间中定义的名称,就必须包括对该命名空间的引用。限定名称在不同的命名空间级别之间使用点字符(.)

using语句本身不能访问另一个命名空间,除非命名空间中的代码以某种方式链接到项目上,或者代码是在该项目的源文件中定义的,或者是在链接到该项目的其他代码中定义的,否则就不能访问其中包含的名称

如果包含名称空间的代码链接到项目上,那么无论是否使用using,都可以访问其中包含的名称。using语句便于我们访问这些名称,减少代码量,以及提高可读性

[!info]C#6新增了usingstatic关键字,允许把静态成员直接包含到C#程序的作用域中把usingstaticSystem.Console添加到名称空间列表中时,访问WriteLine()方法就不再需要在前面加上静态类名

19世纪中叶的英国数学家乔治●布尔为布尔逻辑奠定了基础

布尔赋值运算符可以把布尔比较与赋值组合起来,与数学赋值运算符相同

var1&=var2//var1的值是var1&var2的结果var1|=var2//var1的值是var1|var2的结果var1^=var2//var1的值是var1^var2的结果[!quote]多数流程控制语句在cpp中已学习,无需笔记

switch语句的基本结构如下:

switch(expression){casevalue1://当expression等于value1时执行的代码break;casevalue2://当expression等于value2时执行的代码break;//可以有多个case语句default://如果expression的值与任何case都不匹配,则执行default部分的代码break;}[!caution]

switch语句在c++中执行完一个case语句可以继续运行其他case语句,直到遇到break但C#中不行,在执行完一个case块后,再执行第二个case语句是非法的

也可以使用return语句,中断当前函数的运行,不仅是中断switch结构的执行。也可以使用goto语句,因为case语句实际上是在C#代码中定义的标签:gotocase:...

这些条件也适用于default语句。default语句不一定要放在比较操作列表的最后,还可以把它和case语句放在一起。用break或return添加一个断点,可确保在任何情况下,该结构都有一条有效的执行路径

usingstaticSystem.Console;usingSystem;classTest{staticvoidMain(string[]args){conststringmyName="god";conststringniceName="pjl";conststringsillyName="xwj";stringname;WriteLine("Whatisyourname");name=ReadLine();switch(name.ToLower()){casemyName:WriteLine("Youhavethesamenameasme!");break;caseniceName:WriteLine("My,whatanicenameyouhave!");break;casesillyName:WriteLine("That'saverysillyname.");break;}WriteLine($"Hello{name}!");}}变量的更多内容[!important]隐式转换规则任何类型A,只要其取值范围完全包含在类型B的取值范围内,就可以隐式转换为类型B

如果类型A中的值在类型B的取值范围内,也可以转换该值,但必须使用显式转换

显式转换

//显式类型转换,彼此之间几乎没有什么关系的类型或根本没有关系的类型不能进行强制转换(destinationType)sourceVar当使用checked上下文时,如果整数运算的结果超出了该整数类型的表示范围,则会引发一个OverflowException异常。这通常用于确保算术运算不会导致数据丢失或错误的结果设置溢出检查上下文:

inta=281;byteb;//byte表示范围:0~255b=(byte)a;//系统无视转换造成的数据丢失或错误b=checked((byte)a);//会引发一个OverflowException异常uncecked则表示不检查,不会引发异常可以配置应用程序,让这种类型的表达式都和包含checked关键字一样,在vistualstudio2022中的SolutionExploer打开Properties,选择Build中的Advanced,勾选Checkforarithmeticoverflow此后只要不显示使用unchecked都会默认检查整数类型的算术运算结果是否溢出

使用Convert命令进行显式转换使用ToDouble()把Number字符串转换为double值,将引发异常

为成功执行此类转换,所提供的字符串必须是数值的有效表达方式,该数还必须是不会溢出的数数值的有效表达方式是:首先是一个可选符号(+/-),然后是0位或多位数字,一个可选的句点(.)后跟1位或多位数字,还有一个可选的e/E,后跟一个可选符号和1位或多位数字,除了还可能有空格(在这个序列之前或之后),不能有其他字符

利用这些可选的额外数据,可将–1.2451e–24这样复杂的字符串识别为数值

对于此类转换,总是会进行溢出检查,unchecked关键字和项目属性设置不起作用

//转换示例usingSystem;usingstaticSystem.Console;usingstaticSystem.Convert;classTest{staticvoidMain(string[]args){shortshortResult,shortVal=4;intintegerVal=67;longlongResult;floatfloatVal=10.5F;doubledoubleResult,doubleVal=99.999;stringstringResult,stringVal="17";boolboolVal=true;WriteLine("VariableConversionExamples\n");//float和short相乘,double可以容纳它们,因此隐式转换doubleResult=floatVal*shortVal;WriteLine($"Implicit,->double:{floatVal}*{shortVal}->{doubleResult}");//float显式转换为short,会截断小数部分shortResult=(short)floatVal;WriteLine($"Explicit,->short:{floatVal}->{shortResult}");//Convert.string将bool和double类型显式转换为字符串并拼接stringResult=Convert.ToString(boolVal)+Convert.ToString(doubleVal);WriteLine($"Explicit,->string:\"{boolVal}\"+\"{doubleVal}\"->"+$"{stringResult}");//string显式转换为long,与int相加,自然longlongResult=integerVal+ToInt64(stringVal);WriteLine($"Mixed,->long:{integerVal}+{stringVal}->{longResult}");ReadKey();}}复杂的变量类型

枚举enum枚举是值类型,枚举使用一个基本类型来存储,枚举类型可取的每个值都存储为该基本类型的一个值,默认情况下为int,可使用enum枚举名:类型名来指定该枚举的底层类型

enumDays{ Sunday,//0 Monday,//1 Tuesday,//2,以此类推 Wednesday, Thursday, Friday, Saturday}classTest{ staticvoidMain(){ //使用枚举 Daystoday=Days.Monday; //输出枚举的值(整数值)Console.WriteLine((int)today);//输出1//输出枚举的名称Console.WriteLine(today);//输出Monday//显式地将整数转换为枚举类型Daysday=(Days)2;Console.WriteLine(day);//输出Tuesday//枚举类型的比较if(today==Days.Monday)Console.WriteLine("TodayisMonday."); }}枚举的基本类型可以是byte、sbyte、short、ushort、int、uint、long和ulong

结构struct结构是值类型,可以组合多个数据成员到一个单一的类型中,通常用于表示小型的数据集合

structPoint{publicintX;//公共字段publicintY;//公共字段//结构可以包含方法publicvoidMove(intdeltaX,intdeltaY){X+=deltaX;Y+=deltaY;}}classProgram{staticvoidMain(){//创建结构的实例Pointpoint=newPoint();point.X=10;point.Y=20;//调用结构中的方法point.Move(5,5);//输出点的坐标Console.WriteLine($"Pointcoordinates:({point.X},{point.Y})");//由于结构是值类型,所以将它传递给方法时,会传递它的一个副本//可以使用ref或out关键字来传递它本身MovePoint(point);//输出点的坐标,它不会改变,因为MovePoint方法接收的是副本Console.WriteLine($"PointcoordinatesafterMovePoint:({point.X},{point.Y})");}staticvoidMovePoint(Pointp){p.X+=10;p.Y+=10;}}[!warning]cpp的结构体默认是public,但C#不是从C#7.2开始,结构体的成员默认是private,结构体本身是类型,可见性取决于上下文

数组

foreach(变量类型变量名in数组名)不过注意,foreach循环对数组内容只读访问,不能改变任何元素的值for循环才可以给数组元素赋值

多维数组多维数组只需要更多逗号

//零初始化double[,]four=newdouble[3,4]//字面值初始化double[,]hillHeight={ {1,2,3,4}, {2,3,4,5}, {3,4,5,6}};foreach循环可以访问多维数组中的所有元素,其方式与访问一维数组相同

交错数组(数组的数组)多维数组可称为矩形数组,因为每一行的元素个数都相同,而交错数组每行的元素个数可能不同,其中的每一个元素都是另一个数组,这些数组都必须具有相同的基本类型

交错数组的初始化比多维数组麻烦

字符串的处理string类型的变量可以看成char变量的只读数组

stringmyString="Astring";charmyChar=myString[1];//但不能采用这种方式为各个字符赋值,它是只读数组//使用数组变量的ToCharArray()可以将一个字符串转换为一个字符数组并返回,以此获得一个可写的char数组char[]myChars=myString.ToCharArray();//在foreach循环中使用字符串foreach(varcharacterinmyString){ WriteLine(character);}与数组一样,还可以使用.Length获取元素个数,这将给出字符串中的字符数

.ToLower()和.ToUpper()可以分别把字符串转换为小写或大写形式

.Trim()删除字符串前后的空格,也可以删除其他字符,只需在一个char数组中指定这些字符即可

char[]trimChars={'','e','s'};userResponse=userResponse.Trim(trimChars);//删除trimChars数组指定的字符.TrimStart()和.TrimEnd()命令可以把字符串前面或后面的空格删掉,使用这些命令时也可以指定char数组

.PadLeft()和.PadRight()可以在字符串的左边或右边添加空格,使字符串达到指定的长度

.Replace("n1","n2")用n2替换n1并返回

.Split()用于将一个字符串拆分成一个子字符串数组。这个方法根据指定的分隔符将字符串分割成多个部分,并返回这些部分作为字符串数组

这些命令和之前的其他命令一样,不会真正改变应用它的字符串。把这个命令与字符串结合使用,就会创建一个新的字符串

函数的定义包括函数名、返回类型以及一个参数列表,这个参数列表指定了该函数需要的参数数量和参数类型,函数的名称和参数共同定义了函数的签名

执行一行代码的函数可使用C#6引入的表达式体方法(expression-bodiedmethod),使用=>(Lambda箭头)来实现这一功能

staticdoubleMultiply(doublemyVal1,doublemyVal2){returnmyVal1*myVal2;}//使用表达式体方法staticdoubleMultiply(doublemyVal1,doublemyVal2)=>myVal1*myVal2;参数数组C#允许为函数指定一个(也只能指定一个)特殊参数,这个参数必须是函数定义中的最后一个参数,称为参数数组

参数数组允许使用数量不定的参数调用函数,可使用params关键字定义它们

参数数组可以简化代码,因为在调用代码中不必传递数组,而是传递同类型的几个参数,这些参数会放在可在函数中使用的一个数组中

static返回类型函数名(参数,params类型名[]数组名){ //codes}staticintSumValues(paramsint[]vals){intsum=0;foreach(intvalinvals)sum+=val;returnsum;}引用参数和值参数引用传递变量本身,值传递变量副本

//ref关键字指定参数既可引用传递staticvoidShowDouble(refintval){ val*=2; WriteLine($"valdoubled={val}");}ShowDouble(refNumber);//函数调用时也必须显式指定ref参数的变量不能是常量,且必须使用初始化过的变量,C#不允许ref参数在使用它的函数中初始化

输出参数可以使用out关键字指定所给的参数是一个输出参数,out关键字使用方式与ref关键字相同(在函数定义和函数调用中用作参数的修饰符)

它的执行方式与引用参数几乎完全一样,因为在函数执行完毕后,该参数的值将返回给函数调用中使用的变量。但是二者存在一些重要区别:

使用static或const关键字来定义全局变量。如果要修改全局变量的值,就需要使用static,因为const禁止修改变量的值

如果局部变量和全局变量同名,会屏蔽全局变量

Main()是C#应用程序的入口点,执行这个函数就是执行应用程序,Main函数可以返回void或int,有一个可选参数string[]argsMain函数可使用如下4种版本:

staticvoidMain()staticvoidMain(string[]args)staticintMain()staticintMain(string[]args)返回的int值可以表示应用程序的终止方式,通常用作一种错误提示

可选参数args是从应用程序外部接受信息的方法,这些信息在运行应用程序时以命令行参数的形式指定。在执行控制台应用程序时,指定的任何命令行参数都放在这个args数组中

结构函数结构除了数据还可以包含函数

structCustomerName{publicstringfirstName,lastName;publicstringName()=>firstName+""+lastName;}把函数添加到结构中,就可以集中处理常见任务,从而简化这个过程static关键字不是结构函数所必需的

函数重载函数的返回类型不是其签名的一部分,所以不能定义两个仅返回类型不同的函数,它们实际上有相同的签名

委托委托是一种存储函数引用的类型

有了引用函数的变量,就可以执行无法用其他方式完成的操作。例如,可以把委托变量作为参数传递给一个函数,该函数就可以使用委托调用它引用的任何函数,而且在运行之前不必知道调用的是哪个函数

if(input=="M") process=Multiply;else process=Divide;编译器会发现process变量的委托类型匹配两个函数的签名,于是自动初始化一个委托。可以自行确定使用哪种语法

已引用函数的委托变量就像函数一样使用,但比起函数可以执行更多操作,例如可以通过参数将其传递给下一个函数

staticvoidExecuteFunction(ProcessDelegateprocess)=>process(2.2,3.3);调试和错误处理输出调试信息

这两种方法包含在System.Diagnostics命名空间内

它们唯一的字符串参数用于输出消息,而不使用{X}语法插入变量值。这意味着必须使用+串联运算符等方式在字符串中插入变量值它们可以有第二个字符串参数,用于显示输出文本的类别

Debug.WriteLine(string.Format($"Duplicatemaximumfoundatelementindex{i}."));Debug.WriteLine(string.Format("Duplicatemaximumfoundatelementindex{0}.",i));//字符串差值Debug.WriteLine($"Duplicatemaximumfoundatelementindex{i}.");//传统字符串格式化Debug.WriteLine("Duplicatemaximumfoundatelementindex{0}.",i);经本人测试,这四种方法都可以正常输出,如果在旧版不支持字符串插值C#或需要更复杂的格式化选项,如自定义数字、日期或其他类型格式时还是可选用string.Format,一般情况字符串插值更间接明了

[!note]跟踪点vistualstudio自带的,可以便捷地添加额外信息和删除,和打断点一样,只是要在actions里选择在output里输出的信息

跟踪点和Trace命令并不等价,在应用程序的已编译版本中,跟踪点是不存在的,只有应用程序运行在VS调试器中时,跟踪点才起作用

中断模式除了vs自带的断点,还可以生成一条判定语句时中断

判定语句是可以用用户定义的消息中断应用程序的指令。它们常用于应用程序的开发过程,作为测试程序能否平滑运行的一种方式判定函数也有两个版本:

Trace.Assert(i>10,"Variableoutofbounds.","PleasecontactvendorwiththeerrorcodeKCW001.");错误处理预料到错误的发生,编写足够健壮的代码以处理这些错误,而不必中断程序的执行

C#包含结构化异常处理SEH(StructuredExceptionHandling)的语法。用3个关键字(try、catch、finally)可以标记出能处理异常的代码和指令,如果发生异常,就使用这些指令处理异常

可以在catch或finally块内使用async/await关键字,用于支持先进的异步编程技术,避免瓶颈,且可以提高应用程序的总体性能和响应能力C#7.0引入了throw表达式可以与catch块配合使用

可以只有try块和finally块,而没有catch块,或者有一个try块和好几个catch块。如果有一个或多个catch块,finally块就是可选的

[!info]如果存在两个处理相同异常类型的catch块,就只执行异常过滤器为true的catch块中的代码。如果还存在一个处理相同异常类型的catch块,但没有异常过滤器或异常过滤器是false,就忽略它。只执行一个catch块的代码,catch块的顺序不影响执行流

C#中的对象从类型中创建,就像变量一样,对象的类型在面向对象编程中叫类,可以使用类的定义实例化对象,类的实例==对象

对象的生命周期每个对象都有一个明确定义的生命周期,除了“正在使用”的正常状态之外,还有两个重要的阶段:

构造函数对象的初始化过程是自动完成的,不需要自己寻找适用于存储新对象的内存空间但在初始化对象的过程中有时需要执行一些额外工作,例如需要初始化对象存储的数据。构造函数就是用于初始化数据的函数

所有的类定义都至少包含一个构造函数。在这些构造函数中,可能有一个默认构造函数,该函数没有参数,与类同名

类定义还可能包含几个带有参数的构造函数,称为非默认的构造函数。代码可以使用它们以许多方式实例化对象,例如给存储在对象中的数据提供初始值

在C#中使用new关键字来调用构造函数

类名对象名=new类名()//可以使用非默认的构造函数来实例化对象类名对象名=new类名(参数列表)构造函数与字段、属性和方法一样,可以是公共或私有的。在类外部的代码不能使用私有构造函数实例化对象,而必须使用公共构造函数。通过把默认构造函数设置为私有的,就可以强制类的用户使用非默认的构造函数

一些来没有公共的构造函数,外部的代码不可能实例化它们,这些类称为不可创建的类,不可创建的类不是完全没有用的

析构函数.NETFramework使用析构函数来清理对象。一般情况下不需要提供析构函数的代码,而由默认的析构函数自动执行操作。但如果在删除对象实例前需要完成一些重要操作,就应提供具体的析构函数

例如如果变量超出范围,代码就不能访问它,但该变量仍存在于计算机内存的某个地方,只有.NET运行程序执行其垃圾回收,进行清理时,该实例才被彻底删除

静态成员和实例类成员属性、字段和方法等成员是对象实例特有的

静态成员,也称共享成员(静态方法、静态属性、静态字段)

使用静态构造函数可以执行此类初始化任务,一个类只能有一个静态构造函数,该构造函数不能有访问修饰符,也不能有任何参数

静态构造函数不能直接调用,只能在下述情况下执行:

静态类希望类只包含静态成员,且不能用于实例化对象(如Console)。为此一种简单的方法是使用静态类,而不是把类的构造函数设置为私有

静态类只能只能包含静态成员,不能包含实例构造函数。只可以有一个静态构造函数

OOP技术接口接口是把公共实例(非静态)方法和属性组合起来,以封装特定功能的一个集合。定义了接口后就可以在类中实现它,这样类就可以支持接口所指定的所有属性和成员

[!caution]

一个类可以支持多个接口,多个类也可以支持相同的接口。所以接口的概念让用户和其他开发人员更容易理解其他人的代码

可删除的对象IDisposable接口是.NET框架中一个非常重要的接口,它允许开发人员显式释放不再需要的对象所占用的资源。支持IDisposable接口的对象必须实现Dispose()方法,即它们必须提供这个方法的代码

C#允许使用一种可以优化使用这个方法的结构,using关键字可以在代码块中初始化使用重要资源的对象,在该代码块的末尾会自动调用Dispose()方法:

=new();...using(){ ...}//也可以把初始化对象作为using语句的一部分using(=new()){ ...}在这两种情况下,可在using代码块中使用变量,并在代码块的末尾自动删除(在代码块执行完毕后,调用Dispose()方法)

继承继承是OOP最重要的特性之一

任何类都可以从另一个类继承,C#中的对象只能直接派生于一个基类,基类可以有自己的基类

基类可以定义为抽象类,抽象类不能直接实例化,要使用抽象类,必须继承该类,抽象类可以有抽象成员,这些成员在基类中没有实现代码,所以派生类必须实现它们

类可以是密封的,密封类不能用作基类,所以没有派生类

在继承一个基类时派生类不能访问基类的私有成员,只能访问其公共成员,但外部代码也可以访问类的公共成员

因此C#提供了第三种可访问性:protected,只有派生类才能访问protected成员,外部代码不能访问private成员和protected成员

除定义成员的保护级别外,还可以为成员定义其继承行为。基类的成员可以是虚拟的,即成员可以在派生类中重写

派生类可以提供成员的另一种实现代码,这种实现代码不会删除原来的代码,仍可在类中访问原来的代码,但外部代码不能访问它们。如果没有提供其他实现方式,通过派生类使用成员的外部代码就自动访问基类中成员的实现代码虚拟类不能是私有成员,因为不能既要求派生类重写成员,又不让派生类访问该成员

C#中所有对象都有一个共同的基类object(在.NETFramework中,它是System.Object类的别名)

接口可以继承自其他接口。与类不同的是,接口可以继承多个基接口

多态性表示在不同的上下文中,同一个接口、函数或者类可以有不同的实现和表现形式。具体来说,多态性允许不同类型的对象对同一消息作出不同的响应多态性的主要体现:

继承的一个结果是派生于基类的类在方法和属性上有一定的重叠,因此可以使用相同的语法处理从同一个基类实例化的对象

例如,如果基类Animal有一个EatFood()方法,则在其派生类Cow和Chicken中调用这个方法的语法是类似的:

//Cow和Chicken派生于AnimalCowmyCow=newCow();ChickenmyChicken=newChicken();myCow.EatFood();myChicken.EatFood();多态性则更推进了一步,可以把某个派生类型的变量赋给基本类型的变量

AnimalmyAnimal=myCow;不需要强制转换,就可以通过该变量调用基类的方法

myAnimal.EatFood();//调用派生类中的EatFood()实现代码//注意不能以相同的方式调用派生类上定义的方法myAnimal.M();//error//可以把基本类型变量转换为派生类变量,以此调用派生类的方法CowmyNewCow=(Cow)myAnimal;myNewCow.M();//如果原始变量的类型不是Cow或派生于Cow的类型,这个强制类型转换就会引发一个异常在派生于同一个类的不同对象上执行任务时,多态性是一种极有效的技巧,其使用的代码最少

不是只有共享同一个基类的类才能利用多态性,只要派生类在继承层次结构中有一个相同的类,它们就可以使用同样的方法利用多态性

object类是继承层次结构中的根,可以把所有对象看成object类的实例。这就是在建立字符串时,WriteLine()可以处理无数多种参数组合的原因,第一个参数后面的每个参数都可以看成一个object实例,所以可以把任何对象的输出结果写到屏幕上。为此,需要调用方法ToString()

接口的多态性虽然不能像对象一样实例化接口,但可以建立接口类型的变量,然后就可以在支持该接口的对象上使用该变量来访问该接口提供的方法和属性

例如,假定不使用基类Animal提供的EatFood()方法,而是把该方法放在IConsume接口上。Cow和Chicken类也支持这个接口,唯一的区别是它们必须提供EatFood()方法的实现代码(因为接口不包含实现代码),接着就可以使用下述代码访问该方法

CowmyCow=newCow();ChickenmyChicken=newChicken();IConsumeconsumeInterface;//将Cow对象赋值给接口类型的变量consumeInterface=myCow;//通过consumeInterface调用Cow中实现的EatFood方法consumeInterface.EatFood();consumeInterface=myChicken;consumeInterface.EatFood();派生类会继承其基类支持的接口。有共同基类的类不一定有共同接口,有共同接口的类也不一定有共同基类

对象之间的关系继承是对象之间的一种简单关系,可以让派生类完整地获得基类的特性。对象之间还具有其他一些重要关系

包含关系一个类包含另一个类,类似于继承关系,但包含类可以控制对被包含类的成员的访问,甚至在使用被包含类的成员前进行其他处理

用一个成员字段包含对象实例,就可以实现包含关系。这个成员字段可以是公共字段,此时与继承关系相同,容器对象的用户就可以访问它的方法和属性,但不能像继承关系那样通过派生类访问类的内部代码

可以让被包含的成员对象变为私有成员,用户就不能直接访问任何成员,即使这些成员是公共的,但可以使用包含类的成员访问这些私有成员

可以完全控制被包含的类对外提供什么成员或不提供任何成员,还可以在访问被包含类的成员前,在包含类的成员上执行其他处理

集合关系一个类用作另一个类的多个实例的容器。这类似于对象数组,但集合具有其他功能,包括索引、排序和重新设置大小等

集合基本就是一个增加了功能的数组,集合以与其他对象相同的方式实现为类,通常以所存储的对象名称的复数形式来命名

数组与集合的主要区别是,集合通常实现额外的功能,例如Add()和Remove()方法可添加和删除集合中的项。且集合通常有一个Item属性,它根据对象的索引返回该对象。通常这个属性还允许实现更复杂的访问方式

运算符重载可以把运算符用于从类实例化而来的对象,因为类可以包含如何处理运算符的指令

只能采用这种方式重载现有的C#运算符,不能创建新的运算符

事件对象可以激活和使用事件,作为它们处理的一部分。事件是非常重要的,可以在代码的其他部分起作用,类似于异常(但功能更强大)

例如可以在把Animal对象添加到Animals集合中时,执行特定的代码,而这部分代码不是Animals类的一部分,也不是调用Add()方法的代码的一部分。为此需要给代码添加事件处理程序,这是一种特殊类型的函数,在事件发生时调用。还需要配置这个处理程序,以监听自己感兴趣的事件

引用类型和值类型

在使用C#时不必过多考虑这个问题

值类型和引用类型的一个主要区别是:值类型总是包含一个值,而引用类型可以是null,表示它们不包含值。但可以使用可空类型创建值类型,使值类型在这个方面的行为类似于引用类型(即可以为null)

string和object类型是简单的引用类型,数组也是隐式的引用类型,创建的每个类都是引用类型

C#使用class关键字来定义类,定义了一个类后,就可以在项目中能访问该定义的其他位置对该类进行实例化

[!hint]

internal类强调的是封装性和内部复用,适合于隐藏内部实现细节;而public类则允许跨程序集共享和重用,适用于对外公开的接口和组件

可以指定类是抽象的(不能实例化,只能继承,只有抽象类可以有抽象成员)或密封的(不能继承,只能实例化,密封成员不能被重写),使用两个互斥的关键字abstract或sealed

抽象类可以是公共的,也可以是内部的;密封类也可以是公共或内部的

在类定义中指定继承,要在类名的后面加上一个冒号,后跟基类名

publicclassTest:Program在C#的类定义中,只能有一个基类。如果继承了一个抽象类,就必须实现所继承的所有抽象成员(除非派生类也是抽象的)

编译器不允许派生类的可访问性高于基类,即内部类可以继承于一个公共基类,但公共类不能继承于一个内部基类如果没有使用基类,被定义的类就只继承于基类System.Object

除了在冒号之后指定基类外,还可以指定支持的接口,基类只能有一个,但可以实现任意数量的接口

publicclass类名:接口1,接口2//当有基类时,需要先紧跟基类publicclass类名:基类,接口1,接口2支持该接口的类必须实现所有接口成员,但如果不想使用给定的接口成员,可用提供一种“空”的实现方式(没有函数代码)。还可以把接口成员实现为抽象类中的抽象成员

interface接口访问修饰符关键字public和internal的使用方式是相同的,与类一样,接口默认定义为内部接口,要使接口可以公开访问,必须使用public关键字

不能在接口中使用关键字abstract和sealed,因为这两个修饰符在接口定义中是没有意义的(它们不包含实现代码,所以不能直接实例化,且必须是可以继承的)

接口的继承可以使用多个基接口

publicinterface接口:接口1,接口2接口不是类,所以没有继承System.Object,但System.Object的成员可以通过接口类型的变量来访问。不能使用实例化类的方式来实例化接口

System.Object因为所有类都继承于System.Object,所以这些类都可以访问该类中受保护的成员和公共成员

下表是该类中的方法,未列出构造/析构函数,这些方法是.NETFramework中对象类型必须支持的基本方法

例如,如果函数接受一个object类型的参数(表示可以给该函数传输任何信息),就可以在遇到某些对象时执行额外任务。结合使用GetType()和typeof(这是一个C#运算符,可以把类名转换为System.Type对象),就可以进行比较

if(myObj.GetType()==typeof(MyComplexClass)){ //myObjisaninstanceoftheclassMyComplexClass.}构造函数和析构函数构造函数名必须与包含它的类同名,没有参数则是默认构造函数。构造函数可以公共或私有,私有即不能用这个构造函数来创建这个类的对象实例

析构函数由一个波浪号~后跟类名组成,没有参数和返回类型

析构函数不能被直接调用,它由垃圾回收器(GC)在确定对象不再被引用且需要回收内存时自动调用。调用这个析构函数后,还将隐式地调用基类的析构函数,包括System.Object根类中的Finalize()调用

.NET框架中的大多数资源管理已经高度优化,使用using语句和实现了IDisposable的对象可以更有效地进行资源管理,对于非托管资源(文件、数据库连接),应优先考虑实现IDisposable接口而非依赖析构函数

构造函数的执行序列任何构造函数都可以配置为在执行自己的代码前调用其他构造函数

为了实例化派生的类,必须实例化它的基类。而要实例化这个基类,又必须实例化这个基类的基类,这样一直到实例化System.Object为止。结果是无论使用什么构造函数实例化一个类,总是首先调用System.Object.Object()

无论在派生类上使用默认/非默认构造函数,除非明确指定,否则就使用基类的默认构造函数

在C#中,构造函数初始化器允许在构造函数定义的冒号后面直接初始化类的成员变量。这样可以提高代码的可读性和减少冗余代码,特别是在需要对多个成员进行相同操作时

publicclassDerivedClass:BaseClass{ ... publicDerivedClass(inti,intj):base(i) { }}这里使用一个int参数,因此会调用BaseClass的BaseClass(inti)构造函数初始化基类的成员变量,也可以使用这个关键字指定基类构造函数的字面值

```cspublicclassDerivedClass:BaseClass{ publicDerivedClass():this(5,6) { } publicDerivedClass(inti,intj):base(i) { }}使用DerivedClass.DerivedClass()构造函数,将得到如下执行顺序:

注意在定义构造函数时,不要创建无限循环

类库项目编译为.dll程序集,在其他项目中添加对类库项目的引用,就可以访问它的内容。修改和更新类库不会影响使用它们的其他项目

派生类只能继承自一个基类,即只能直接继承自一个抽象类,但可以用一个继承链包含多个抽象类;而类可以使用任意多个接口

抽象类可以拥有抽象成员(没有代码体,且必须在派生类中实现,否则派生类本身必须也是抽象的)和非抽象成员(拥有代码体,可以是虚拟的,这样就可以在派生类中重写)

接口成员必须都在使用接口的类上实现,它们没有代码体。接口成员是公共的,但抽象类的成员可以是私有的(只要它们不是抽象的)、受保护的、内部的或受保护的内部成员(受保护的内部成员只能在应用程序的代码或派生类中访问)

此外接口不能包含字段、构造函数、析构函数、静态成员或常量

假定有一个对象系列表示火车,基类Train包含火车的核心定义,例如车轮的规格和引擎的类型。但这个类是抽象的,因为并没有一般的火车

为创建一辆实际的火车,需要给该火车添加特性。为此派生一些类,Train可以派生于一个相同的基类Vehicle,客运列车可以运送乘客,货运列车可以运送货物,假设高铁两者都可以运送,为它们设计相应的接口

在进行更详细的分解之前,把对象系统以这种方式进行分解,可以清晰地看到哪种情形适合使用抽象类,哪种情形适合使用接口

结构类型对象是引用类型,把对象赋给变量时,实际上是把带有一个指针的变量赋给了该指针所指向的对象

而结构是值类型,其变量包含结构本身,把结构赋给变量,是把一个结构的所有信息复制到另一个结构中

浅度和深度复制简单地按照成员复制对象可以通过派生于System.Object的MemberwiseClone()方法来完成,这是一个受保护的方法,但很容易在对象上定义一个调用该方法的公共方法。该方法提供的复制功能称为浅度赋值,因为它未考虑引用类型成员。因此新对象中的引用成员就会指向源对象中相同成员引用的对象

如果想要创建成员的新实例(复制值,不复制引用),此时需要使用深度复制可以实现一个ICloneable接口,以标准方式进行深度赋值,如果使用这个接口,就必须实现它包含的Clone()方法。这个方法返回一个类型为System.Object的值。可以采用各种处理方式,实现所选的任何一个方法体来得到这个对象

定义成员

后两个关键字可以结合使用,所以也有protectedinternal成员,它们只能由程序集中派生类的代码来访问

classTest{ publicintInt;}.NETFramework的公共字段使用驼峰命名法,私有字段一般全小写

字段可以使用关键字readonly,表示该字段只能在执行构造函数的过程或初始化语句赋值

classTest{ publicreadonlyintInt=16;}[!important]

如果使用了static关键字,这个方法就只能通过类来访问,不能通过对象实例来访问

可以在方法定义中使用下述关键字

定义属性属性提供对类或结构体内部私有字段的间接访问。属性允许控制对这些私有字段的读取和写入操作,从而实现数据验证、逻辑封装等目的

属性定义方式与字段,但包含的内容比较多,属性比字段复杂,因为它们在修改状态前还可以执行一些额外操作,也可能并不修改状态

属性拥有两个类似于函数的块,一个块用于获取属性的值,一个块用于设置属性的值。这两个块也称为访问器,分别使用get和set关键字来定义

访问器可以用于控制属性的访问级别。忽略其中一个块来创建只读或只写属性,这仅适用于外部代码,因为类中的其他代码可以访问这些代码块能访问的数据。可以在访问器上包含可访问修饰符

属性的基本结构包括标准的可访问修饰符,后跟类名、属性名和访问器

//Fieldusedbyproperty.privateintmyInt;//Property.publicintMyIntProp{ get{returnmyInt;} set{//Propertysetcode.}}类外部的代码不能直接访问myInt字段,因为其访问级别是私有的。外部代码必须使用属性来访问该字段。set访问器采用类似方式把一个值赋给字段。可以使用关键字value表示用户提供的属性值:

publicintMyIntProp{ get{returnmyInt;} set{myInt=value;}}value等于类型与属性相同的一个值,所以如果属性和字段使用相同的类型,就不必考虑数据类型转换这个简单属性只是用来阻止对myInt字段的直接访问。在对操作进行更多控制时,属性的真正作用才能发挥出来

set{ if(value>=0&&value<=10) myInt=value;}如果使用了无效值,通常继续执行,但记录下该事件,以备将来分析或直接抛出异常是比较好的选择,选择哪个选项取决于如何使用类以及给类的用户授予了多少控制权

set{ if(value>=0&&value<=10) myInt=value; else throw(newArgumentOutOfRangeException("MyIntProp",value,"MyIntPropmustbeassignedavaluebetween0and10."));}属性可以使用virtual、override和abstract关键字,就像方法一样,但这几个关键字不能用于字段。访问器可以有自己的可访问性

只有类或派生类中的代码才能使用set访问器

访问器可以使用的访问修饰符取决于属性的可访问性,访问器的可访问性不能高于它所属的属性,即私有属性对它的访问器不能包含任何可访问修饰符,而公共属性可以对其访问器使用所有的可访问修饰符

C#6引入了一个名为“基于表达式的属性”的功能,该功能可以把属性的定义减少为一行代码下面的属性对一个值进行数学计算,使用Lambda箭头后跟等式来定义:

//FieldusedbypropertyprivateintmyDoubledInt=5;//PropertypublicintMyDoubledIntProp=>(myDoubledInt*2);重构成员“重构”表示使用工具修改代码,而不是手工修改。为此,只需要右击类图中的某个成员,或在代码视图中右击某个成员即可

publicstringmyString;右击该字段,选择快速操作和重构,选择需要的选项

自动属性属性是访问对象状态的首选方式,因为它们禁止外部代码访问对象内部的数据存储机制的实现,还对内部数据的访问方式施加了更多控制

一般以非常标准的方式定义属性,即通过一个公共属性来直接访问一个私有成员

//会定义一个自动属性publicintMyIntProp{get;set;}按照通常的方式定义属性的可访问性、类型和名称,但没有给get和set访问器提供实现代码。这些块的实现代码和底层的字段都由编译器提供

[!tip]输入prop后按Tab键两次,就可以自动创建publicintMyProperty{get;set;}

使用自由属性时,只能通过属性访问数据,不能通过底层的私有字段来访问,因为不知道底层私有字段的名称,该名称是在编译期间定义的。但这并不是一个真正意义上的限制,因为可以直接使用属性名

自动属性的唯一限制是它们必须包含get和set访问器,无法使用这种方式定义只读或只写属性。但可以改变这些访问器的可访问性。例如,可采用如下方式创建一个外部只读属性

//只能在类定义的代码中访问该属性的值publicintMyIntProp{get;privateset;}C#6引入了只有get访问器的自动属性和自动属性的初始化器。不变数据类型的简单定义是:一旦创建,就不会改变状态。使用不变的数据类型有很多优点,比如简化了并发编程和线程的同步

//只有get访问器的自动属性publicintMyIntProp{get;}//自动属性的初始化publicintMyIntProp{get;}=9;隐藏基类方法当从基类继承一个非抽象成员时,也就继承了其实现代码,如果继承的成员是虚拟的,就可以使用override关键字重写这段实现代码。无论继承成员是否为虚拟,都可以隐藏这些实现代码。无论继承的成员是否为虚拟,都可以隐藏这些实现代码

publicclassBaseClass{ publicvoidDoSomething() { //Baseimplementation. }}publicclassDerivedClass:BaseClass{ publicvoidDoSomething() { //Derivedclassimplementation,hidesbaseimplementation. }}这段代码可以正常运行,但会生成一个警告,说明隐藏了一个基类成员,如果确实要隐藏该成员,可以使用new关键字显式地表明意图

newpublicvoidDoSomething() { //Derivedclassimplementation,hidesbaseimplementation. }其工作方式是完全相同的,但不会显示警告

注意隐藏基类成员和重写它们的区别

可使用base关键字,它表示包含在派生类中的基类的实现代码

publicclassBaseDerivedClass{publicvirtualvoidDoSomething(){//Baseimplementation.}}publicclassDerivedClass:BaseDerivedClass{publicoverridevoidDoSomething(){//Derivedclassimplementation,extendsbaseclassimplementation.base.DoSomething();//Morederivedclassimplementation.}}在DerivedClass包含的DoSomething()方法中,执行包含在BaseDerivedClass中的DoSomething()版本。base使用的是对象实例,base关键字不能用于访问非虚方法、静态方法或私有成员

也可以使用this关键字,this也可以用在类成员内部,也引用对象实例,,this引用的是当前的对象实例,因此不能在静态成员中使用this关键字,因为静态成员不是对象实例的一部分

this关键字最常用的功能是把当前对象实例的引用传递给一个方法

publicvoiddoSomething(){ TargetClassmyObj=newTargetClass(); myObj.DoSomethingWith(this); /*this的类型与包含上述方法的类兼容。这个参数类型可以是类的类型、由这个类继承的类类型,或者由这个类或System.Object实现的一个接口*/}this关键字的另一个常见用法是限定局部类型的成员

publicclassMyClass{ privateintsomeData; publicintSomeData { get { returnthis.someData; } }}许多开发人员都喜欢这个语法,它可以用于任意成员类型,因为可以一眼看出引用的是成员,而不是局部变量

嵌套的类型定义除了在命名空间中定义类型,还可以在其他类中定义它们。这样就可以在定义中使用各种访问修饰符,也可以使用new关键字来隐藏继承于基类的类型定义

publicclassMyClass{ publicclassMyNestedClass { publicintNestedClassField; }}//在MyClass的外部实例化myNestedClass,必须限定名称MyClass.MyNestedClassmyObj=newMyClass.MyNestedClass();usingSystem;usingstaticSystem.Console;namespaceTest{publicclassClassA{//私有属性privateintState=-1;//只读属性publicintOnlyReadState{get{returnState;}}publicclassClassB{//嵌套类可以访问包含它类的底层字段,即使它是一个私有字段//因此仍然可以修改私有属性的值publicvoidSetPrivateState(ClassAtarget,intnewState){target.State=newState;}}}classProgram{staticvoidMain(string[]args){ClassAmyObject=newClassA();WriteLine($"myObject.State={myObject.OnlyReadState}");ClassA.ClassBmyOtherObject=newClassA.ClassB();myOtherObject.SetPrivateState(myObject,999);WriteLine($"myObject.State={myObject.OnlyReadState}");}}}接口的实现接口成员的定义与类定义相似,但具有几个重要区别:

要隐藏基接口中继承的成员,和隐藏继承的类成员一样使用关键字new定义它们

接口中定义的属性可以定义访问器get和set中的哪一个或都用于该属性

接口没有指定应如何存储属性数据,接口不能指定字段,例如用于存储属性数据的字段,接口和类一样可以定义为类成员,但不能定义为其它接口的成员,因为接口不能包含类型定义

在类中实现接口实现接口的类必须包含该接口所有成员的实现代码,且必须匹配指定的签名,包括匹配指定的get和set,且必须是公共的

publicinterfaceIMyInterface{voidDoSomething();voidDoSomethingElse();}publicclassMyClass:IMyInterface{publicvoidDoSomething(){}publicvoidDoSomethingElse(){}}可使用关键字virtual或abstract来实现接口成员,但不能使用static或const。可以在基类上实现接口成员

publicinterfaceIMyInterface{voidDoSomething();voidDoSomethingElse();}publicclassMyBaseClass{publicvoidDoSomething(){}}publicclassMyDerivedClass:MyBaseClass,IMyInterface{ //基类实现了接口的一个成员,因此会继承过来,可以不用实现publicvoidDoSomethingElse(){}}继承一个实现给定接口的基类,就意味着派生类隐式地支持这个接口

publicinterfaceIMyInterface{voidDoSomething();voidDoSomethingElse();}publicclassMyBaseClass:IMyInterface{publicvirtualvoidDoSomething(){}publicvirtualvoidDoSomethingElse(){}}publicclassMyDerivedClass:MyBaseClass{publicoverridevoidDoSomething(){}}在基类中把实现代码定义为virtual,派生类就可以可选的使用override关键字来重写实现代码,而不是隐藏它们

类显式实现接口成员如果由类显式地实现接口成员,就只能通过接口来访问该成员,不能桶过类来访问,隐式成员可以通过类和接口来访问

classTest{interfaceIAnimal{voidSpeak();}publicclassDog:IAnimal{//隐式实现IAnimal接口的Speak方法publicvoidSpeak(){Console.WriteLine("Woof!");}}publicstaticvoidMain(){Dogdog=newDog();dog.Speak();//输出"Woof!"}}classTest{interfaceIAnimal{voidSpeak();}publicclassCat:IAnimal{//显式实现IAnimal接口的Speak方法voidIAnimal.Speak(){Console.WriteLine("Meow!");}}publicstaticvoidMain(){Catcat=newCat();((IAnimal)cat).Speak();//输出Meow!//cat.Speak()会报错}}在显式实现的情况下,Cat类自身并没有名为Speak的公共成员,只有通过类型转换为IAnimal接口后才能调用到Speak方法

其他属性访问器如果在定义属性的接口中只包含set,就可给类中的属性添加get,反之亦然。但只有隐式实现接口时才能这么做。

大多数时候,都想让所添加的访问器的可访问修饰符比接口中定义的访问器的可访问修饰符更严格。因为按照定义,接口定义的访问器是公共的,也就是说,只能添加非公共的访问器

如果将新添加的访问器定义为公共的,那么能够访问实现该接口的类的代码也可以访问该访问器。但是只能访问接口的代码就不能访问该访问器

部分类定义如果所创建的类包含一种类型或其他类型的许多成员时,就很容易引起混淆,代码文件也比较长。这时就可以使用#region和#endregion来给代码定义区域,就可以折叠和展开各个代码区,使代码更具可读性可按这种方式嵌套各个区域,这样一些区域就只能在包含它们的区域被展开后才能看到

另一种方法是使用部分类定义,把类的定义放在多个文件中,例如可将字段、属性和构造函数放在一个文件中,而把方法放在另一个文件中。在包含部分类定义的每个文件中对类使用partial关键字即可

如果使用部分类定义,partial关键字就必须出现在包含部分类定义的每个文件的与此相同的位置

对于部分类,要注意的一点是:应用于部分类的接口也会应用于整个类

publicpartialclassMyClass:IMyInterface1{...}publicpartialclassMyClass:IMyInterface2{...}//和下面是等价的publicclassMyClass:IMyInterface1,IMyInterface2{...}基类可以在多个定义文件中指定,但必须是同一个基类,因为C#中,类只能继承一个基类

部分方法定义部部分方法在一个部分类中定义,在另一个部分类中实现。在这两个部分类中,都要使用partial关键字

部分方法可以是静态,但它们总是私有的,且不能有返回值,它们只可以使用ref参数,部分方法也不能使用virtual、abstract、override、new、sealed、extern修饰符

部分方法的重要性体现在编译代码时,而不是使用代码时

usingstaticSystem.Console;classTest{publicpartialclassMyClass{partialvoidDoSomethingElse();publicvoidDoSomething(){WriteLine("DoSomething()executionstarted.");DoSomethingElse();WriteLine("DoSomething()executionfinished.");}}publicpartialclassMyClass{partialvoidDoSomethingElse()=>WriteLine("DoSomethingElse()called.");}publicstaticvoidMain(){MyClassObject=new();//简化方式Object.DoSomething();}}/*output:DoSomething()executionstarted.DoSomethingElse()called.DoSomething()executionfinished.*/删除部分类的实现代码,输出就如下所示:

DoSomething()executionstarted.DoSomething()executionfinished.编译代码时,如果代码包含一个没有实现代码的部分方法,编译器会完全删除该方法,还会删除对该方法的所有调用。执行代码时,不会检查实现代码,因为没有要检查的方法调用。这会略微提高性能

开发一个类模块,以便在后续章节中使用,该类模块包含两个类:

规划应用程序

Card类基本是由两个只读字段suit和rank的容器,字段指定为只读的原因是“空白”的牌是没有意义的,牌在创建好后也不能修改。把默认的构造函数指定为私有,并提供另一个构造函数,使用给定的suit和rank建立一副扑克牌

此外Card类要重写System.Object的ToString()方法,这样才能获得人们可以理解的字符串来表示扑克牌。为使编码简单一些,为两个字段suit和rank提供枚举

Deck类包含52个Card对象,使用简单的数组类型,这个数组不能直接访问,对Card对象的访问要通过GetCaed()方法来实现,该方法返回指定索引的Card对象,这个类有一个Shuffle()方法,用于重新排列数组中的牌

编写类库可以自己手动编写,也可以借助vs的类图来快速设计,以下为使用类图工具箱设计自动生成的代码:

//Suit.cs文件usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;namespaceCardLib{publicenumSuit{Club,Diamond,Heart,Spade}}//Rank.cs文件usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;namespaceCardLib{publicenumRank{Ace=1,Deuce,Three,Four,Five,Six,Seven,Eight,Nine,Ten,Jack,Queen,King}}添加Card类

//Card.cs文件namespaceCardLib{publicclassCard{publicreadonlySuitsuit;publicreadonlyRankrank;publicCard(SuitnewSuit,RanknewRank){suit=newSuit;rank=newRank;}privateCard(){}//重写的ToString()方法将已存储的枚举值的字符串表示写入到返回的字符串中,非默认的构造函数初始化suit和rank字段的值publicoverridestringToString(){return"The"+rank+"of"+suit+"s";}}}添加Deck类

然后新建一个控制台应用程序,对它添加一个对类库项目CardLib的引用。因为新项目是创建的第二个项目,所以还需要指定该项目是解决方法的启动项目

//新项目主文件代码usingstaticSystem.Console;usingCardLib;namespaceCardClient{internalclassProgram{privatestaticvoidMain(string[]args){DeckmyDeck=newDeck();myDeck.Shuffle();for(inti=0;i<52;i++){CardtempCard=myDeck.GetCard(i);Write(tempCard.ToString());if(i!=51)Write("\n");elseWriteLine();}}}}集合、比较和转换集合使用数组可以创建包含许多对象或值的变量类型,但数组有一定的限制,最大的限制就是一旦创建好数组,它们的大小就不可改变

C#中数组实现为System.Array类的实例,它们只是集合类中的一种类型。集合类一般用于处理对象列表,其功能比简单数组要多,功能大多是通过实现System.Collections名称空间中的接口而获得

集合的功能包括基本功能都可以通过接口来实现,所以不仅可以使用基本集合类,例如System.Array,还可以创建自己的定制集合类。

这些集合可以专用于要枚举的对象(即要从中建立集合的对象)。这么做的一个优点是定制的集合类可以是强类型化的。也就是说,从集合中提取项时,不需要把它们转换为正确类型。另一个优点是提供专用的方法,例如,可以提供获得项子集的快捷方法

System.Collections名称空间中的几个接口提供了基本的集合功能:

System.Array类实现了IList、ICollection、IEnumerable,但不支持IList的一些更高级功能,它表示大小固定的项列表

使用集合Systems.Collections名称空间中的类System.Collections.ArrayList也实现了IList、ICollection、IEnumerable接口,但实现方式比System.Array更复杂。数组的大小是固定不变的,而这个类可以用于表示大小可变的项列表

有几个处理操作可以应用到Array和ArrayList集合上,但它们的语法略有区别。也有一些操作只能使用更高级的ArrayList类型

//简单数组必须使用固定大小来初始化数组才能使用Animal[]animalArray=newAnimal[2];//而ArrayList集合不需要初始化其大小ArrayListanimalArrayList=newArrayList();数组是引用类型,所以用一个长度初始化数组并没有初始化它所包含的项,要使用一个指定的项还需初始化

CowmyCow1=newCow("Lea");animalArray[0]=myCow1;animalArray[1]=newChicken("Noa");而ArrayList集合没有现成的项,也没有null引用的项。这样就不能以相同的方式给索引赋予新实例,使用Add()方法添加新项

CowmyCow2=newCow("Rual");animalArrayList.Add(myCow2);animalArrayList.Add(newChicken("Andrea"));以这种方式添加项之后,就可以使用与数组相同的语法改写该项

nimalArrayList[0]=newCow("Alma");使用foreach结构迭代一个数组是可以的,因为System.Array类实现了IEnumerable接口,这个接口的唯一方法GetEnumerator()可以迭代集合中的各项

//它们使用foreach的语法是相同的foreach(AnimalmyAnimalinanimalArray)foreach(AnimalmyAnimalinanimalArrayList)数组使用Length属性输出数组中个数,而ArrayList集合使用Count属性,该属性是ICollection接口的一部分

//ArrayWriteLine($"Arraycollectioncontains{animalArray.Length}objects.");//ArrayListWriteLine($"ArrayListcollectioncontains{animalArrayList.Count}objects.");如果不能访问集合(无论是简单数组还是较复杂的集合中的项),它们就没有什么用途。简单数组是强类型化的,可以直接访问它们所包含的项类型,所以可以直接调用项的方法:

animalArray[0].Feed();数组类型是抽象类型Animal,因此不能直接调用由派生类提供的方法,而必须使用数据类型转换

((Chicken)animalArray[1]).LayEgg();ArrayList集合是System.Object对象的集合(通过多态性赋给Animal对象),所以必须对所有的项进行数据类型转换

((Animal)animalArrayList[0]).Feed();((Chicken)animalArrayList[1]).LayEgg();ArrayList集合比Array集合多出一些功能,可以使用Remove()和RemoveAt()方法删除项,它们分别根据项的引用或索引从数组中删除项

animalArrayList.Remove(myCow2);animalArrayList.RemoveAt(0);ArrayList集合可以用AddRange()方法一次添加好几项。这个方法接受带有ICollection接口的任意对象,包括前面的代码所创建的animalArray数组

animalArrayList.AddRange(animalArray);AddRange()方法不是ArrayList提供的任何接口的一部分。这个方法专用于ArrayList类,证实了可以在集合类中执行定制操作。该类还提供了其他方法,如InsertRange(),它可以把数组对象插入到列表中的任何位置,还有用于排序和重新排序数组的方法

CollectionBase类有接口IEnumerable、ICollection、IList,但只提供了一些必要的实现代码,主要是IList的Clear()和RemoveAt()方法,以及ICollection的Count属性。如果要使用提供的功能,就需要自己实现其他代码

CollectionBase提供了两个受保护的属性,它们可以访问存储的对象本身。可以使用List、InnerList,List可以通过IList接口访问项,InnerList则是用于存储项的ArrayList对象

例如,存储Animal对象的集合类定义可以如下:

publicclassAnimals:CollectionBase{ publicvoidAdd(AnimalnewAnimal) { List.Add(newAnimal); } publicvoidRemove(AnimaloldAnimal) { List.Remove(oldAnimal); } publicAnimals(){}}Add()和Remove()方法实现为强类型化的方法,使用IList接口中用于访问项的标准Add()方法。这些方法现在只用于处理Animal类或派生于Animal的类,而前面的ArrayList实现代码可处理任何对象

CollectionBase类可以对派生的集合使用foreach语法

WriteLine("UsingcustomcollectionclassAnimals:");AnimalsanimalCollection=newAnimals();animalCollection.Add(newCow("Lea"));foreach(AnimalmyAnimalinanimalCollection){ WriteLine($"New{myAnimal.ToString()}objectaddedtocustom"+$"collection,Name={myAnimal.Name}");}但不能使用下面的代码:

animalCollection[0].Feed();要以这种方式通过索引来访问项,就需要使用索引符

索引符索引符indexer是一种特殊类型的属性,可以把它添加到一个类中,以提供类似于数组的访问。可通过索引符提供更复杂的访问,因为可以用方括号语法和使用复杂的参数类型,它最常见的一个用法是对项实现简单的数字索引

在Animal对象的Animals集合中添加一个索引符

publicclassAnimals:CollectionBase{ ...publicAnimalthis[intanimalIndex] { get{return(Animal)List[animalIndex];} set{List[animalIndex]=value;} }}this关键字需要和方括号中的参数一起使用,除此之外,索引符与其他属性十分类似。在访问索引符时,将使用对象名,后跟放在方括号中的索引参数

return(Animal)List[animalIndex];对List属性使用一个索引符,即在IList接口上,可以访问CollectionBase中的ArrayList。ArrayList存储了项。这里需要进行显式数据类型转换,因为IList.List属性返回一个System.Object对象为索引符定义了一个类型,使用该索引符定义了一个类型,使用该索引符访问某项时,就可以得到这个类型,这种强类型化功能就可以编写下述代码

与索引的集合一样,可以使用一个基类简化IDictionary接口的实现,这个基类就是DictionaryBase,它也实现IEnumerable和ICollection,提供了对任何集合都相同的基本集合处理功能

DictionaryBase与CollectionBase一样,实现通过其支持的接口获得一些成员(不是全部成员)。DictionaryBase也实现Clear和Count成员,但不实现RemoveAt()。因为RemoveAt()是IList接口中的一个方法,不是IDictionary接口中的一个方法,但IDictionary有一个Remove()方法,这是一个应在基于DictionaryBase的定制集合类上实现的方法

下面的代码是Animals类的另一个版本,该类派生于DictionaryBase。下面代码包括Add()、Remove()和一个通过键访问的索引符的实现代码

publicclassAnimals:DictionaryBase{ //参数是键值 //继承于IDictionary接口,有自己的Add()方法,该方法带有两个object参数publicvoidAdd(stringnewID,AnimalnewAnimal){ Dictionary.Add(newID,newAnimal);}//以一个键作为参数,与指定键值对应的项被删除publicvoidRemove(stringanimalID){Dictionary.Remove(animalID);}publicAnimals(){} //索引符使用一个字符串键值,而不是索引,用于通过Dictionary的继承成员来访问存储的项,仍需进行数据类型转换publicAnimalthis[stringanimalID]{get{return(Animal)Dictionary[animalID];}set{Dictionary[animalID]=value;}}}基于DictionaryBase的集合和基于CollectionBase的集合之间的另一个区别是foreach的工作方式稍有区别

foreach(AnimalmyAnimalinanimalCollection){WriteLine($"New{myAnimal.ToString()}objectaddedtocustomcollection,Name={myAnimal.Name}");}//等价基于CollectionBase的集合的代码:foreach(DictionaryEntrymyEntryinanimalCollection){WriteLine($"New{myEntry.Value.ToString()}objectaddedtocustomcollection,Name={((Animal)myEntry.Value).Name}");}有许多方式可以重写这段代码,以便直接通过foreach访问Animal对象,最简单的方式是实现一个迭代器迭代器IEnumerable接口允许使用foreach循环。在foreach循环中并不是只能使用集合类,在foreach循环中使用定制类通常有很多优点

但是重写使用foreach循环的方式或者提供定制的实现方式并不简单。一个较简单的替代方法是使用迭代器,使用迭代器将有效地自动生成许多代码,正确地完成所有任务

迭代器的定义:它是一个代码块,按顺序提供要在foreach块中使用的所有值。一般情况下,该代码块是一个方法,但也可以使用属性访问器和其他代码块作为迭代器

无论代码块是什么,其返回类型都是有限制的,这个返回类型与所枚举的对象类型不同,例如在表示Animal对象集合的类中,迭代器返回类型不可能是Animal,两种可能的返回类型是前面提到的接口类型IEnumerable和IEnumerator

使用这两种类型的场合:

在迭代器块中,使用yield关键字选择要在foreach循环中使用的值

yieldreturn;使用迭代器:

usingstaticSystem.Console;usingSystem.Collections;classTest{publicstaticIEnumerableSimpleList(){yieldreturn"string1";yieldreturn"string2";yieldreturn"string3";}staticvoidMain(string[]args){foreach(stringiteminSimpleList())WriteLine(item);}}此处,静态方法SimpleList就是迭代器块,因为是方法,所以使用IEnumberable返回类型,使用yield关键字为使用它的foreach快提供了3个值,依次输出到屏幕上

实际上并没有返回string类型的项,而是返回object类型的值,因为object是所有类型的基类,所以可以从yield语句中返回任意类型

但编译器的智能程度很高,所以可以把返回值解释为foreach循环需要的任何类型。这里代码需要string类型的值,如果修改一行yield代码使之返回整数,就会出现一个类型装换异常

可以使用yieldbreak将信息返回给foreach循环的过程,遇到该语句时,迭代器的处理会立即中断,使用该迭代器的foreach循环也一样

实现一个迭代器:

publicclassCloner{publicintVal;publicCloner(intnewVal){Val=newVal;}//MemberwiseClone创建当前对象的一个浅复制//对于引用类型成员复制引用而不是实际的对象内容//对于值类型成员则直接复制其值publicobjectGetCopy()=>MemberwiseClone();}深度复制:在创建对象的一个副本时,不仅复制了原始对象的所有基本数据类型的成员变量值,同时也复制了引用类型成员变量指向的对象,并且递归地对该对象所包含的引用类型成员也进行同样的复制操作。换句话说,深度复制会生成一个与原对象完全独立的新对象树

深度复制:

//简单类用于存储整数值publicclassContent{publicintVal;}//实现ICloneable接口以支持克隆功能publicclassCloner:ICloneable{ //定义一个Content类型的成员变量publicContentMyContent=newContent();//构造函数publicCloner(intnewVal){MyContent.Val=newVal;}//实现ICloneable接口的Clone方法,用于创建当前对象的浅度复制//在该实现中仅对Clone类本身进行复制,没有递归地复制引用类型成员publicobjectClone(){ //创建一个新的Cloner实例,将原Cloner实例中MyContent的Val属性值传递给实例ClonerclonedCloner=newCloner(MyContent.Val);//返回克隆后的Cloner对象,返回object类型returnclonedCloner;}}使用包含在源Cloner对象中的Content对象(MyContent)的Val字段,创建一个新Cloner对象。这个字段是一个值类型,所以不需要深度复制如果Cloner类的MyContent字段也需要深度复制,就需要使用下面的代码:

publicclassCloner:ICloneable{publicContentMyContent=newContent();...publicobjectClone(){ //创建一个新的Cloner实例ClonerclonedCloner=newCloner();//调用Clone方法进行深度复制,确保内容也被复制一份新的副本clonedCloner.MyContent=MyContent.Clone();returnclonedCloner;}}为使这段代码能正常工作,还需要在Content类上实现ICloneable接口

对象之间比较有两类:

类型比较所有的类都从System.Object中继承GetType()方法,该方法和typeof()运算符一起使用就可以确定对象的类型

if(myObj.GetType()==typeof(MyComplexClass))//myObjisaninstanceoftheclassMyComplexClass.封箱和拆箱封箱boxing是把值类型转换为System.Object类型或转换为由值类型实现的接口类型,拆箱unboxing是相反的转换过程

structMyStruct{ publicintVal;}//可以把这种类型的结构放在object类型的变量中对其封箱//创建新变量后赋值MyStructvalType1=newMyStruct();valType1.Val=5;//然后把它封箱在object类型的变量中objectrefType=valType1;当一个值类型变量被封箱时,实际上会创建一个新的对象实例,并将该值类型变量的值复制到这个新对象中。因此封箱后得到的对象包含的是原值类型的值的一个副本,而不是源值类型变量的引用

封箱后是创建了一个新的对象并存储了源值的副本,它们的内存空间并不相同,修改不会影响对方

[!important]

但要注意,当把一个引用类型赋予对象时,实际上复制的是对同一内存位置的引用,而不是复制整个对象的内容。这意味着新变量和原变量都指向同一个对象实例,修改会互相影响

classMyStruct//一旦改成类,在封箱后修改就会改变源值{ publicintVal;}valType1.Val=6;MyStructvalType2=(MyStruct)refType;WriteLine($"valType2.Val={valType2.Val}");//6//如果是struct,那么拆箱后值还是初始值5也可以把值类型封装到接口类型中,只要它们实现这个接口即可。例如假定MyStruct类型实现IMyInterface接口

interfaceIMyInterface{}structMyStruct:IMyInterface{ publicintVal;}接着把结构封箱到一个IMyInterface类型中,然后拆箱:

MyStructvalType1=newMyStruct();IMyInterfacerefType=valType1;MyStructValType2=(MyStruct)refType;封箱是隐式执行的,但拆箱一个值需要进行显式数据类型转换

封箱非常有用,有两个重要的原因:它允许在项的类型是object的集合中使用值类型,其次,有一个内部机制允许在值类型上调用object方法

在访问值类型内容前必须进行拆箱

is运算符is运算符用于检查对象是否为给定类型或是否可转换为给定类型,如果是返回true

值比较运算符重载通过运算符重载可以对设计的类使用标准运算符,因为在使用特定的参数类型时,为这些运算符提供了自己的实现代码,其方式与重载方法相同,也是为同名方法通过不同的参数

可以在运算符重载的实现中执行任何需要的操作要重载运算符,可给类添加运算符类型成员,它们必须是static。一些运算符有多种用途,因此要指定要处理多少个操作数,以及这些操作数的类型。

一般情况下,操作数类型和定义运算符的类相同。但也可以定义处理混合类型的运算符

重载+运算符,可使用下述代码:

AddClass1op3=op1+op2;重载所有的二元运算符都是一样的,一元运算符看起来也是类似的,但只有一个参数:

publicstaticAddClass1operator-(AddClass1op1){AddClass1returnVal=newAddClass1();//返回其相反数returnVal.val=-op1.val;returnreturnVal;}这两个运算符处理的操作数类型与类相同,返回值也是该类型

classTest{publicclassAddClass1{publicintval;publicstaticAddClass3operator+(AddClass1op1,AddClass2op2){AddClass3returnVal=newAddClass3();returnVal.val=op1.val+op2.val;returnreturnVal;}}publicclassAddClass2{publicintval;}publicclassAddClass3{publicintval;}publicstaticvoidMain(){AddClass1op1=newAddClass1();op1.val=5;AddClass2op2=newAddClass2();op2.val=5;AddClass3op3=op1+op2;WriteLine(op3.val);}}如果把相同的运算符添加到AddClass2,代码就会出错,因为它弄不清要使用哪个运算符。因此要注意不要把签名相同的运算符添加到多个存在继承或包含关系的类中

还要注意,如果混合了类型,操作数的顺序必须与运算符重载的参数顺序相同。如果使用了重载运算符和顺序错误的操作数,操作就会失败

AddClass3op3=op2+op1;//error当然,可以提供另一个重载运算符和倒序的参数:

publicstaticAddClass3operator+(AddClass2op1,AddClass1op2){AddClass3returnVal=newAddClass3();returnVal.val=op1.val+op2.val;returnreturnVal;}可以重载下列运算符:

如果重载true和false运算符,就可以在布尔表达式中使用类

不能重载赋值运算符,例如+=,但这些运算符使用与它们对应的简单运算符,所以不必担心它们。重载+意味着+=如期执行

一些运算符必须成对重载,如果重载>,就必须重载<。许多情况下,可以在这些运算符中调用其他运算符,以减少需要的代码数量和可能发生的错误

publicstaticbooloperator>=(AddClass1op1,AddClass1op2)=>(op1.val>=op2.val);publicstaticbooloperator<(AddClass1op1,AddClass1op2)=>!(op1>=op2);//这里使用取反,也可以直接比较//Alsoneedimplementationsfor<=and>operators.这同样适用于==和!=,但对于这些运算符,通常需要重写Object.Equals()和Object.GetHashCode(),因为这两个函数也可以用于比较对象。重写这些方法,可以确保无论类的用户使用什么技术,都能得到相同的结果。这不太重要,但应增加进来,以保证其完整性

//重写Equals方法以比较两个AddClass1实例的val属性是否相等publicoverrideboolEquals(objectop1)=>this.val==((AddClass1)op1).val;//重写GetHashCode方法,基于val属性生成哈希码publicoverrideintGetHashCode()=>val;GetHashCode()可根据其状态,获取对象实例的一个唯一int值

注意Equals()使用object类型参数,我们需要使用这个签名,否则就将重载这个方式,而不是重写。类的用户仍可以访问默认的实现代码。这样就必须使用数据类型转换得到所需的结果,这常需要使用本章前面讨论的is运算符检查对象类型

if(op1isAddClass1) { returnval==((AddClass1)op1).val; }else { thrownewArgumentException($"CannotcompareAddClass1objectswithobjectsoftype{op1.GetType().ToString()}"); }如果传给Equals的操作数类型有误或不能转换为正确类型,就会抛出一个异常,如果只允许对类型完全相同的两个对象进行比较,就需要对if语句进行修改

if(op1.GetType()==typeof(AddClass1))IComparable和IComparer接口这两个接口是.NETFramework中比较对象的标准方式。这两个接口之间的差别如下:

一般使用IComparable给出类的默认比较代码,使用其他类给出非默认的比较代码

IComparable提供了一个方法CompareTo(),该方法接受一个对象,当前对象小于比较对象则返回负数,大于比较对象则返回正数

例如,在实现该方法时,使其可以接受一个Person对象,以便确定这个人比当前的人更年老还是更年轻。实际上,这个方法返回一个int,所以也可以确定第二个人与当前的人的年龄差:

if(person1.CompareTo(person2)==0){ WriteLine("Sameage");}elseif(person1.CompareTo(person2)>0){ WriteLine("person1isOlder");}else{ WriteLine("person1isYounger");}IComparer也提供一个方法Compare()。这个方法接受两个对象,返回一个整型结果,和ComparerTo()相同

if(personComparer.Compare(person1,person2)==0){WriteLine("Sameage");}elseif(personComparer.Compare(person1,person2)>0){WriteLine("person1isOlder");}else{WriteLine("person1isYounger");}提供给这两种方法的参数是System.Object类型。这意味着可以比较一个对象与其他任意类型的另一个对象。所以在返回结果之前,通常需要进行某种类型比较,如果使用了错误类型会抛出异常

.NETFramework在Comparer类上提供了IComparer接口的默认实现方式,Comparer位于System.Collections名称空间中,可以对简单类型以及支持IComparable接口的任意类型进行特定文化的比较

可通过下面的代码使用它:

//这里使用Comparer.Default静态成员获取Comparer类的一个实例,接着使用Compare()方法比较前两个字符串stringfirstString="FirstString";stringsecondString="SecondString";WriteLine($"Comparing'{firstString}'and'{secondString}',"+$"result:{Comparer.Default.Compare(firstString,secondString)}");intfirstNumber=35;intsecondNumber=23;WriteLine($"Comparing'{firstNumber}'and'{secondNumber}',"+$"result:{Comparer.Default.Compare(firstNumber,secondNumber)}");Compare类注意事项:

对集合排序许多集合类可以用对象的默认比较方式进行排序,或者用定制方法来排序ArrayList包含方法Sort(),该方法使用时可不带参数,此时使用默认的比较方式,也可给它传IComparer接口,以比较对象对

给ArrayList填充了简单类型时,例如整数或字符串,就会进行默认比较。对于自己的类,必须在类定义中实现IComparable或创建一个支持IComparer的类来进行比较

System.Collections命名空间中的一些类(包括CollectionBase)都没有提供排序方法。如果要对派生于这个类的集合排序,就必须多做一些工作,自己给内部的List集合排序

下面的实例说明如何使用默认和非默认的比较方式给列表排序:

checked关键字用于显式启用整数算术运算和转换时的溢出检查

as运算符expressionastype只适用于下列情况

如果不能从expression转换到type,表达式结果就是null

CollectionClassitems=newCollectionClass();items.Add(newItemClass());//使用以下代码:CollectionClassitems=newCollectionClass();items.Add(newItemClass());尖括号是把类型参数传给泛型类型的方式,定义了一个名为CollectionClass的泛型类,它允许存储任何与ItemClass类型兼容的对象

[!note]C++模板和C#泛型类的一个区别C++中,编译器会检测出在哪里使用了模板的某个特定类型,然后编译需要的代码来创建这个类型C#中,所有操作都在运行期间进行

可空类型泛型使用System.Nullable类型提供了使值类型为空的一种方式

intnullableInt;int是System.Nullable的缩写

运算符和可空类型

intop1=5;//不能直接将一个可空类型与非可空类型进行算术运算intresult=op1*2;//需要进行显式转换intresult=(int)op1*2;//或通过Value属性访问其值intresult=op1.Value*2;运算符称为空结合运算符,是一个二元运算符,允许给可能等于null的表达式提供另一个值。如果第一个值不是null,该运算符就等于第一个操作数,否则就等于第二个操作数

//这两个表达式作用等价op1op2op1==nullop2:op1op1可以是任意可空表达式,包括引用类型和可空类型

intop1=null;intresult=op1*25;//在结果中放入int类型的变量不需要显式转换,运算符会自动处理这个转换,还可以把表达式的结果放在int中.运算符称为条件成员访问运算符或空条件运算符,有助于避免繁杂的空值检查造成的代码歧义

classPerson{ publicstringName{get;set;}}Personperson=null;stringname=person.Name;//如果person为null,则name也会被赋值为null如果没有使用.运算符,尝试访问person.Name将会导致NullReferenceException异常。但使用了.后,当erson为null时,name会被赋予null值,并且代码能够安全执行下去

空条件运算符的另一个用途是触发事件

//触发事件常见方法:varonChanged=OnChanged;if(onChanged!=null){ onChanged(this,args);}但这种模式不是线程安全的,因为有人会在null检查已经完成后退订最后一个事件处理程序,此时会抛出异常,使用空条件符可以避免这种情况

//如果OnChanged不为null,则会调用它的Invoke方法来触发事件;若OnChanged为null,整个表达式会被评估为nullOnChanged.Invoke(this,args);使用可空类型

//创建了一个T类型对象的集合ListmyCollection=newList();不需要定义类、实现方法或执行其他操作,可以把List传给构造函数,在集合中设置项的起始列表。List还有一个Item属性,允许进行类似于数组的访问

TitemAtIndex2=myCollectionOfT[2];使用List

staticvoidMain(string[]args){ /*AnimalsanimalCollection=newAnimals();替换为下列代码*/ListanimalCollection=newList();animalCollection.Add(newCow("Rual"));animalCollection.Add(newChicken("Donna"));foreach(AnimalmyAnimalinanimalCollection){myAnimal.Feed();}}对泛型列表进行排序和搜索和普通的接口有些区别,使用泛型接口IComparer和IComparable,它们提供了略有区别的、针对特定类型的方法

Comparison:这个委托类型用于排序方法,其返回类型和参数如下:intmethod(TobjectA,TobjectB)

Predicate:这个委托类型用于搜索方法,其返回类型和参数如下:boolmethod(TtargetObject)

可以定义任意多个这样的方法,使用它们实现List的搜索和排序方法

Dictionary这个类型可定义键/值对的集合,需要实例化两个类型,分别用于键和值,以表示集合中的各项

使用强类型化的Add()方法添加键/值对:

//初始化一个键为字符串类型、值为整数类型的新字典Dictionarythings=newDictionary();things.Add("GreenThings",29);things.Add("BlueThings",94);things.Add("YellowThings",34);things.Add("RedThings",52);things.Add("BrownThings",27);可以使用Key和Values属性迭代集合中的键和值:

foreach(stringkeyinthings.Keys){WriteLine(key);}foreach(intvalueinthings.Values){WriteLine(value);}还可以迭代集合中的各个项,把每项作为一个KeyValuePair实例来获取:

foreach(KeyValuePairthinginthings){ WriteLine($"{thing.Key}={thing.Value}");}对于Dictionary要注意的一点是,每个项的键都必须是唯一的。如果要添加的项的键与已有项的键相同,就会抛出ArgumentException异常所以,Dictionary允许把IComparer接口传递给其构造函数。如果要把自己的类用作键,且它们不支持IComparable或IComparable接口,或者要使用非默认的过程比较对象,就必须把IComparer接口传递给其构造函数

C#6引入了一个新特性:索引初始化器,它支持在对象初始化器内部初始化索引:

varzahlen=newDictionary(){ [1]="eins", [2]="zwei"};可以使用表达式体方法

publicZObjectToGerman()=>newZObject(){[1]="eins",[2]="zwei"};定义泛型类型定义泛型类只需在类定义中包含尖括号语法:

classGenericClassT可以是任意标识符,只需遵循通常的C#命名规则即可。泛型类可在其定义中包含任意多个类型参数,参数之间用逗号分隔:

classMyGenericClass{ privateT1innerT1Object; publicMyGenericClass(T1item) { //innerT1Object=newT1(); //不能假定为类提供了什么类型,这样无法编译 innerT1Object=item; } publicT1InnerT1Object { get{returninnerT1Object;} }}类型T1的对象可以传递给构造函数,只能通过InnerT1Object属性对这个对象进行只读访问

//使用typeof运算符获取类型参数的实际类型,并将其转换为字符串publicstringGetAllTypesAsString(){ return"T1="+typeof(T1).ToString()+ ",T2="+typeof(T2).ToString()+ ",T3="+typeof(T3).ToString();}可以做一些其他工作,尤其是对集合进行操作,因为处理对象组是非常简单的,不需要对对象类型进行任何假设

[!caution]在比较为泛型类型提供的类型值和null时,只能使用运算符==和!=

default关键字要确定用于创建泛型类实例的类型,需要知道它们是引用还是值类型如果是值类型不能取null值

publicMyGenericClass(){ innerT1Object=default(T1);}如果是引用类型赋予null,值类型赋予默认值,default关键字允许对必须使用的类型执行更多操作

约束类型用于泛型类的类型称为无绑定类型,因为没有对它们进行任何约束。通过约束类型,可以限制可用于实例化泛型类的类型

//在类定义中,可以使用where关键字实现,可以提供多个约束,逗号隔开classMyGenericClasswhereT:constraint1,constraint2可以使用多个where语句,定义泛型类需要的任意类型或所有类型上的约束:

classMyGenericClasswhereT1:constraint1whereT2:constraint2约束必须出现在继承说明符的后面:

classMyGenericClass:MyBaseClass,IMyInterfacewhereT1:constraint1whereT2:constraint2泛型类型约束

struct//必须是值类型class//必须是引用类型base-class//必须是基类或继承自基类,该结束可以是任意类名interface//必须是接口或实现了接口new()//必须有一个公共无参数构造函数如果使用new()作为约束,它必须是为类型指定的最后一个约束

可通过base-class约束,把一个类型参数用作另一个类型参数的约束

classMyGenericClasswhereT2:T1T2必须与T1的类型相同或继承自T1,这称为裸类型约束,表示一个泛型类型参数用作另一个类型参数的约束

classMyGenericClasswhereT2:T1whereT1:T2//类型参数不能循环,无法编译从泛型类中继承如果某个类型所继承的基类型中受到了约束,该类型就不能解除约束。也就是说,类型T在所继承的基类型中使用时,该类型必须受到至少与基类型相同的约束

//因为T在Farm中被约束为Animal,把它约束为SuperCow就是把T约束为这些值的一个子集classSuperFarm:FarmwhereT:SuperCow{}//以下代码是错误的classSuperFarm:FarmwhereT:struct{}泛型运算符在C#中,可以像其他方法一样进行运算符的重写,这也可以在泛型类中实现此类重写

//定义一个静态运算符重载方法,用于将一个Farm对象与一个List对象中的动物合并到一个新的Farm中publicstaticFarmoperator+(Farmfarm1,Listfarm2){ //创建一个新的Farm实例,用于存储合并后的动物集合 Farmresult=newFarm(); //遍历第一个Farm类型中的所有动物并将其添加到新农场中 foreach(Tanimalinfarm1.Animals) { result.Animals.Add(animal); } //遍历第二个List类型,仅将其中不存在于新农场的动物添加进去 foreach(Tanimalinfarm2) { if(!result.Animals.Contains(animal)) { result.Animals.Add(animal); } } //返回合并后的新农场对象 returnresult;} //另一个重载版本,允许将List对象放在前面进行合并操作。这里采用右结合律,实际调用的是上面定义的方法 publicstaticFarmoperator+(Listfarm1,Farmfarm2)=>farm2+farm1;泛型结构可以用与泛型类相同的方式创建泛型结构

publicstructMyStruct{ publicT1item1; publicT2item2;}定义泛型方法泛型方法中,返回类型或参数类型由泛型类型参数来确定

publicTGetDefault()=>default(T)可以通过非泛型类来实现泛型方法:

publicclassDefaulter{ publicTGetDefault()=>default(T);}但如果类是泛型的,就必须为泛型方法使用不同的标识符

//该代码无法编译,必须重命名方法或类使用的类型TpublicclassDefaulter{ publicTGetDefault()=>default(T);}泛型方法参数可以采用与类相同的方式使用约束,可以使用任意的类类型参数

publicclassDefaulter{ publicT2GetDefault() whereT2:T1 { returndefault(T2); }}为方法提供的类型T2必须与给类提供的T1相同或者继承自T1。这是约束泛型方法的常用方式

定义泛型委托

定义委托

publicdelegateT1MyDelegate(T2op1,T2op2)whereT1:T2;这里也可以使用约束

变体是协变和抗变的统称

多态性允许把派生类型的对象放在基类型的变量中,但这不适用于接口

//以下代码无法工作IMethaneProducercowMethaneProducer=myCow;IMethaneProduceranimalMethaneProducer=cowMethaneProducer;Cow支持IMethaneProducer接口,第一行代码没有问题,但第二行代码预先假定两个接口类型有某种关系,但实际上这种关系并不存在,所以无法把一种类型转换为另一种类型

因为泛型类型的所有类型参数都是不变的,但可以在泛型接口和泛型委托上定义变体类型参数

为使上面的代码工作,IMethaneProducer接口的类型参数T必须是协变的,有了协变的类型参数,就可以在MethaneProducer和IMethaneProducer之间建立继承关系。这样一种类型的变量就可以包含另一种类型的值,这与多态性类似,但更复杂些

抗变和协变是类似的,但方向相反。抗变不能像协变那样把泛型接口值放在使用基类型的变量中,但可以把该接口放在使用派生类型的变量中

IGrassMunchercowGrassMuncher=myCow;IGrassMunchersuperCowGrassMuncher=cowGrassMuncher;协变要把泛型类型参数定义为协变,可在类型定义中使用out关键字

publicinterfaceIMethaneProducer对于接口定义,协变类型参数只能用作方法的返回值或属性get访问器

协变意味着子类类型的集合可以被看作是父类类型的集合。在泛型上下文中,如果一个类型参数用out关键字标记为协变,则该类型参数可以在派生类上进行隐式转换

抗变要把泛型类型参数定义为抗变,可在类型定义中使用in关键字

publicinterfaceIGrassMuncher对于接口定义,抗变类型参数只能用作方法参数,不能用作返回类型

抗变允许父类类型的集合被视为子类类型的集合。在泛型上下文中,如果一个类型参数用in关键字标记为抗变,则该类型参数可以在基类上进行隐式转换

::运算符和全局命名空间限定符

::运算符提供了另一种访问命名空间中类型的方式。如果要使用一个命名空间的别名,但该别名与实际命名空间层次结构之间的界限不清晰,就必须使用::运算符

usingMyNamespaceAlias=MyRootNamespace.MyNestedNamespace;namespaceMyRootNamespace{namespaceMyNamespaceAlias{publicclassMyClass{}}namespaceMyNestedNamespace{publicclassMyClass{}}}MyRootNamespace中的代码使用以下代码引用一个类:

MyNamespaceAlias.MyClass这行代码表示的类是MyRootNamespace.MyNamespaceAlias.MyClass,而不是MyRootNamespace.MyNestedNamespace.MyClass也就是说,MyRootNamespace.MyNamespaceAlias名称空间隐藏了由using语句定义的别名,该别名指向MyRootNamespace.MyNestedNamespace名称空间。仍然可以访问这个名称空间以及其中包含的类,但需要使用不同的语法:

MyNestedNamespace.MyClass//还可以使用::运算符MyNamespaceAlias::MyClass使用这个运算符会迫使编译器使用由using语句定义的别名,因此代码指向MyRootNamespace.MyNestedNamespace.MyClass

::运算符还可以与global关键字一起使用,它实际上是顶级根名称空间的别名。这有助于更清晰地说明要指向哪个名称空间

//明确指定使用全局范围内的System命名空间global::System.Collections.Generic.List定制异常有时可以从包括异常的System.Exception基类中派生自己的异常类,并使用它们,而不是使用标准的异常。这样就可以把更具体的信息发送给捕获该异常的代码,让处理异常的捕获代码更有针对性

例如,可以给异常类添加一个新属性,以便访问某些底层信息,这样异常的接收代码就可以做出必要的改变,或者仅给出异常起因的更多信息

usingSystem;//自定义异常类publicclassCustomException:Exception{ publicCustomException():base(){} publicCustomException(stringmessage):base(message){} publicCustomException(stringmessage,Exceptioninner):base(message,inner){}}classProgram{ staticvoidMain() { try { //模拟一个可能引发异常的操作 TriggerCustomException(); }catch(CustomExceptionex){ Console.WriteLine("Caughtacustomexception:"+ex.Message); }catch(Exceptionex){ Console.WriteLine("Caughtanunexpectedexception:"+ex.Message); } } staticvoidTriggerCustomException(){ //抛出自定义异常 thrownewCustomException("Thisisacustomexception."); }}事件事件类似于异常,因为它们都由对象抛出,并且都可以通过我们提供的代码来处理但它们也有几个重要区别,最重要的区别是没有try...catch类似的结构来处理事件,必须订阅它们,订阅一个事件的含义是提供代码,在事件发生时执行这些代码,它们称为事件处理程序

单个事件可供多个处理程序订阅,在该事件发生时,这些处理程序都会被调用,其中包含引发该事件的对象所在的类中的事件处理程序,事件处理程序也可能在其他类中

事件处理程序本身都是简单方法。对事件处理方法的唯一限制是它必须匹配事件所要求的返回类型和参数。这个限制是事件定义的一部分,由一个委托指定在事件中使用委托是非常有用的

基本处理过程如下所示:

处理事件要处理事件,需要提供一个事件处理方法来订阅事件,该方法的返回类型和参数应该匹配事件指定的委托

把处理程序与事件关联起来,即订阅它。为此可以使用+=运算符,给事件添加一个处理程序,其形式是使用事件处理方法初始化的一个新委托实例

myTimer.Elapsed+=newElapsedEventHandler(WriteChar);这行代码在列表中添加一个处理程序,当引发Elapsed事件时,就会调用该处理程序。可给列表添加多个处理程序,只要它们满足指定的条件即可。当引发事件时会依次调用每个处理程序

可以使用方法组概念来简化添加事件处理程序的语法:

myTimer.Elapsed+=WriteChar;最终结果是完全相同的,但不必显式指定委托类型,编译器会根据使用事件的上下文来指定它。但它降低了可读性,不再能一眼看出使用了什么委托类型

定义事件

delegate(parameters){ //Anonymousmethodcode.};parameters是一个参数列表,这些参数匹配正在实例化的委托类型,由匿名方法的代码使用

特性可以为代码段标记一些信息,而这样的信息又可以从外部读取,并通过各种方式来影响所定义类型的使用方式。这种手段通常被称为对代码进行装饰

例如,要创建的某个类包含一个极简单的方法,但即便简单,调试期间还是会对这一代码进行检查。这种情况下就可以对该方法添加一个特性,告诉VS在调试时不要进入该方法进行逐句调试,而是跳过该方法,直接调试下一条语句

[DebuggerStepThrough]publicvoidDullMethod()[DebuggerStepThrough]就是该特性,所有特性的添加都是将特性名称用方括号括起来,并写在应用的目标代码前即可,可以为一段目标代码添加多个特性

上述特性是通过DebuggerStepThroughAttribute这个类来实现的,而这个类位于System.Diagnostics命名空间中,因此使用该特性必须使用using语句来引用这一命名空间,可以使用完整名称,也可以去掉Attribute后缀

通过上述方式添加特性后,编译器就会创建该特性类的一个实例,然后将其与类方法关联起来。某些特性可以通过构造函数的参数或属性进行自定义,并在添加特性的时候进行指定

[DoesInterestingThings(1000,WhatDoesItDo="voodoo")]publicclassDecoratedClass{}将值1000传递给了DoesInterestingThingsAttribute的构造函数,并将WhatDoesItDo属性的值设置为字符串"voodoo"

读取特性读取特性值使用一种称为反射的技术,反射可以在运行时动态检查类型信息,甚至是在创建对象的位置或不必知道具体对象的情况下直接调用某个方法

反射可以取得保存在Type对象中的使用信息,以及通过System.Reflection名称空间中的各种类型来获取不同的类型信息

最简单的方法是通过Type.GetCustomAttributes()方法来实现。这个方法最多使用两个参数,然后返回一个包含一系列object实例的数组,每个实例都是一个特性实例。第一个参数是可选的,即传递我们感兴趣的类型或若干特性的类型(其他所有特性均会被忽略)。如果不使用这一参数,将返回所有特性。第二个参数是必需的,即通过一个布尔值来指示,只想了解类本身的信息,还是除了该类之外还希望了解派生自该类的所有类下面的代码列出DecoratedClass类的特性

//获取指定类型的Type对象TypeclassType=typeof(DecoratedClass);//获取该类型上应用的所有自定义特性,包括从父类继承的特性object[]customAttributes=classType.GetCustomAttributes(true);foreach(objectcustomAttributeincustomAttributes){ WriteLine($"Attributeoftype{customAttribute}found.");}创建特性通过System.Attribute类进行派生,就可以自定义特性。一般来说,如果除了包含和不包含特定的特性外,我们的代码不需要获得更多信息就可以完成需要的工作,不必完成这些额外的工作。如果希望某些特性可以被自定义,则可以提供非默认的构造函数和可写属性

还需要为自定义特性做两个选择:要将其应用到什么类型的目标(类、属性或其他),以及是否可以对同一个目标进行多次应用

要指定上述信息,需要对特性应用AttributeUsageAttribute特性,该特性带有一个类型为AttributeTargets的构造函数参数值,通过|运算符即可通过相应的枚举值组合出需要的值。该特性还有一个布尔值类型的属性AllowMultiple,用于指定是否可以多次应用特性下面的代码指定了一个特性可以应用到类或属性中

//一个预定义特性,用于指定自定义特性的使用规则和范围[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple=false)]//自定义特性类classDoesInterestingThingsAttribute:Attribute{ //构造函数 publicDoesInterestingThingsAttribute(inthowManyTimes) { HowManyTimes=howManyTimes; } //公共属性,用于储存或获取该特性所描述行为的具体内容 publicstringWhatDoesItDo{get;set;} //只读公共属性,表示该特性执行有趣行为的次数 publicintHowManyTimes{get;privateset;}}初始化器对象初始化器提供了一种简化代码的方式,可以合并对象的实例化和初始化。集合初始化器提供了一种简洁的语法,使用一个步骤就可以创建和填充集合

对象初始化器

publicclassCurry{ publicstringMainIngredient{get;set;} publicstringStyle{get;set;} publicintSpiciness{get;set;}}该类有3个属性,使用自动属性语法定义,如果希望实例化和初始化该类的一个对象实例,就必须执行如下语句

CurrytastyCurry=newCurry();tastyCurry.MainIngredient="panirtikka";tastyCurry.Style="jalfrezi";tastyCurry.Spiciness=8;如果类定义中未包含构造函数,这段代码就使用C#编译器提供的默认无参数构造函数,为简化该初始化过程,可提供一个合适的非默认构造函数

publicCurry(stringmainIngredient,stringstyle,intspiciness){ MainIngredient=mainIngredient; Style=style; Spiciness=spiciness; }这样就可以把实例化和初始化合并起来

CurrytastyCurry=newCurry("panirtikka","jalfrezi",8);代码可以工作,但它强制使用Carry类的代码使用该构造函数,这将阻止使用无参数构造函数代码的运行,因此和C++一样都需要提供无参构造函数

publicCurry(){}对象初始化器是不必在类中添加额外代码就可以实例化和初始化对象的方式。实例化对象时,要为每个需要初始化、可公开访问的属性或字段使用名称-值对,来提供其值

=new{ =, =, ... =};重写前面的代码,实例化和初始化一个Curry类型的对象

CurrytastyCurry=newCurry{ MainIngredient="panirtikka", Style="jalfrezi", Spiciness=8};常常可以把这样的代码放在一行上,而不会严重影响可读性

使用对象初始化器时,不必显式调用类的构造函数。如果像上述代码一样省略构造函数的括号,就自动调用默认的无参构造函数。这是在初始化器设置参数值前调用的,以便在需要时为默认构造函数中的参数提供默认值

另外可以调用特定的构造函数。同样,先调用这个构造函数,所以在构造函数中对公共属性进行的初始化可能会被初始化器中提供的值覆盖。只有能够访问所使用的构造函数(如果没有显式指出,就是默认的构造函数),对象初始化器才能正常工作

可以使用嵌套的对象初始化器

CurrytastyCurry=newCurry{ MainIngredient="panirtikka", Style="jalfrezi", Spiciness=8, Origin=newRestaurant { Name="King'sBalti", Location="YorkRoad", Rating=5 }};初始化了Restaurant类型的Origin属性

对象初始化器没有替代非默认的构造函数。在实例化对象时,可以使用对象初始化器来设置属性和字段值,但这并不意味着总是知道需要初始化什么状态。通过构造函数,可以准确地指定对象需要什么值才能起作用,再执行代码,以便立即响应这些值

使用嵌套的初始化器时,首先创建顶级对象,然后创建嵌套对象。如果使用构造函数,对象的创建顺序就反了过来

集合初始化器使用值初始化数组

int[]myIntArray=newint[5]{5,9,10,2,99};这是一种合并实例化和初始化数组的简洁方式,集合初始化器只是把该语法扩展到集合上

ListmyIntCollection=newList{5,9,10,2,99};通过合并对象和集合初始化器,就可以使用简洁的代码(只能说可能增加了可读性)来配置集合

Listcurries=newList();curries.Add(newCurry("Chicken","Pathia",6));curries.Add(newCurry("Vegetable","Korma",3));curries.Add(newCurry("Prawn","Vindaloo",9));可以使用如下代码替换

ListmoreCurries=newList{ newCurry { MainIngredient="Chicken", Style="Pathia", Spiciness=6 }, newCurry { MainIngredient="Vegetable", Style="Korma", Spiciness=3 }, newCurry { MainIngredient="Prawn", Style="Vindaloo", Spiciness=9 }};类型推理强类型化语言表示每个变量都有固定类型,只能用于接收该类型的代码中

var关键字还可以通过数组初始化器来推断数组的类型

varmyArray=new[]{4,5,2};在采用这种方式隐式指定数组类型时,初始化器中使用的数组元素必须是以下情况中的一种:

如果应用最后一条规则,元素可以转换的类型就称为数组元素的最佳类型。如果这个最佳类型有任何含糊的地方,即所有元素的类型都可以隐式转换为两种或更多的类型,代码就不会编译

要注意数字值不会解释为可空类型

常常有一系列类只提供属性,什么也不做,只是存储结构化数据,在数据库或电子表格中,可以把这个类看成表中的一行。可以保存这个类的实例的集合类应表示表或电子表格中的多个行

匿名类型是简化这个编程模型的一种方式,其理念是使用C#编译器根据要存储的数据自动创建类型,而不是定义简单的数据存储类型

//对象初始化器Currycurry=newCurry{ MainIngredient="Lamb", Style="Dhansak", Spiciness=5};//使用匿名类型varcurry=new{ MainIngredient="Lamb", Style="Dhansak", Spiciness=5};匿名类型使用var关键字,这是因为匿名类型没有可以使用的标识符,且在new关键字的后面没有指定类型名,这是编译器确定我们要使用匿名类型的方式

创建匿名类型对象的数组

usingstaticSystem.Console;classTest{staticvoidMain(){varcurries=new[]{new{MainIngredient="Lamb",Style="Dhansak",Spiciness=5},new{MainIngredient="Lamb",Style="Dhansak",Spiciness=5},new{MainIngredient="Chicken",Style="Dhansak",Spiciness=5}}; //输出为该类型定义的每个属性的值WriteLine(curries[0].ToString());/*根据对象的状态为对象返回一个唯一的整数数组中的前两个对象有相同的属性值,所以其状态是相同的*/WriteLine(curries[0].GetHashCode());WriteLine(curries[1].GetHashCode());WriteLine(curries[2].GetHashCode());//由于匿名类型没有重写Equals,默认基于引用比较,这里返回false//即使属性完全相同,因为它们是不同的对象实例//==操作符也是基于引用比较,因此即使属性值相同也会返回falseWriteLine(curries[0].Equals(curries[1]));WriteLine(curries[0].Equals(curries[2]));WriteLine(curries[0]==curries[1]);WriteLine(curries[0]==curries[2]);}}动态查找var关键字本身不是类型,只是根据表达式推导类型,C#虽然是强类型化语言,但从C#4开始就引入了动态变量的概念,即类型可变的变量

引入的目的是为了在许多情况下,希望使用C#处理另一种语言创建的对象,这包括对旧技术的交互操作。另一个使用动态查找的情况是处理未知类型的C#对象

在后台,动态查找功能由DynamicLanguageRuntime(动态语言运行库,DLR)支持。与CLR一样,DLR是.NET4.5的一部分

动态类型仅在编译期间存在,在运行期间会被System.Object类型替代

一些方法需要大量参数,但许多参数并不是每次调用都需要

可选参数调用参数时,常常给某个参数传输相同的值,例如可能是一个布尔值,以控制方法操作中不重要部分

publicListGetWords(stringsentence,boolcapitalizeWords=false)为参数提供一个默认值,就使其成为可选参数,如果调用此方法时没有为该参数提供值,就使用默认值

默认值必须是字面量、常量值或该值类型的默认初始值

使用可选参数时,它们必须位于方法参数列表的末尾,没有默认值的参数不能放在默认值的参数后

//非法代码publicListGetWords(boolcapitalizeWords=false,stringsentence)命名参数使用可选参数时,可能发现某个方法有几个可选参数,但可能只想给第三个可选参数传输值

命名参数允许指定要使用哪个参数,这不需要在方法定义中进行任何特殊处理,它是一个在调用方法时使用的技术

method(参数名:值,参数名:值)参数名是方法定义时使用的变量名,参数的顺序是任意的,命名参数也可以是可选的

可以仅给方法调用中的某些参数使用命名参数。当方法签名中有多个可选参数和一些必选参数时,这是非常有用的。可以首先指定必选参数,再指定命名的可选参数

如果混合使用命名参数和位置参数,就必须先包含所有的位置参数,其后是命名参数

复习匿名方法给事件添加处理程序:

实际过程会简单一些,因为一般不使用变量来存储委托,只在订阅事件时使用委托的一个实例

TimermyTimer=newTimer(100);myTimer.Elapsed+=newElapsedEventHandler(WriteChar);订阅了Timer对象的Elapsed事件。这个事件使用委托类型ElapsedEventHandler,使用方法标识符WriteChar实例化该委托类型。结果是Timer对象引发Elapsed事件时,就调用方法WriteChar()。传给WriteChar()的参数取决于由ElapsedEventHandler委托定义的参数类型和Timer中引发事件的代码传送的值

可以通过方法组语法用更简洁的代码获得相同的效果

方法组语法是指不直接实例化委托对象,而是通过指定一个方法名来隐式转换为委托类型。当某个方法的签名与委托类型的签名匹配时,可以直接将方法名用作该委托类型的实例

myTimer.Elapsed+=WriteChar;C#编译器知道Elapsed事件需要的委托类型,所以可以填充该类型。但大多数情况下,最好不要这么做,因为这会使代码更难理解,也不清楚会发生什么

使用匿名方法时,该过程会减少为一步:

//Elapsed事件添加一个匿名方法作为事件处理器myTimer.Elapsed+=delegate(objectsource,ElapsedEventArgse){ WriteLine("Eventhandlercalledafter{0}milliseconds.", //获取当前计时器周期间隔的毫秒数 (sourceasTimer).Interval);};这段代码像单独使用事件处理程序一样正常工作。主要区别是这里使用的匿名方法对于其余代码而言实际上是隐藏的。例如,不能在应用程序的其他地方重用这个事件处理程序。另外,为更好地加以描述,这里使用的语法有点沉闷。delegate关键字会带来混淆,因为它具有双重含义,匿名方法和定义委托类型都要使用它

Lambda表达式用于匿名方法Lambda表达式是简化匿名方法语法的一种方式,Lambda表达式还有其他用途

//使用Lambda表达式重写上面的代码myTimer.Elapsed+=(source,e)=>WriteLine("Eventhandlercalledafter"+$"{(sourceasTimer).Interval}milliseconds.");Lambda表达式会根据上下文和委托签名自动推导出参数类型,所以在Lambda表达式中不需要明确指定类型名

usingstaticSystem.Console;//委托类型,接受两个int参数返回一个intdelegateintTwoIntegerOperationDelegate(intparamA,intparamB);classProgram{//静态方法,接受一个委托作为参数staticvoidPerformOperations(TwoIntegerOperationDelegatedel){//两层循环遍历1到5之间的整数对for(intparamAVal=1;paramAVal<=5;paramAVal++){for(intparamBVal=1;paramBVal<=5;paramBVal++){//调用传入的委托并获取运算结果intdelegateCallResult=del(paramAVal,paramBVal);//输出当前表达式的值Write($"f({paramAVal},"+$"{paramBVal})={delegateCallResult}");//如果不是最后一列,则添加逗号和空格分隔各个运算结果if(paramBVal!=5){Write(",");}}//每一次内层循环后换行WriteLine();}}staticvoidMain(string[]args){//使用Lambda表达式创建了三种运算的委托实例WriteLine("f(a,b)=a+b:");PerformOperations((paramA,paramB)=>paramA+paramB);WriteLine();WriteLine("f(a,b)=a*b:");PerformOperations((paramA,paramB)=>paramA*paramB);WriteLine();WriteLine("f(a,b)=(a-b)%b:");PerformOperations((paramA,paramB)=>(paramA-paramB)%paramB);}}上面的Lambda表达式分为3部分:

Lambda表达式的参数Lambda表达式使用类型推理功能来确定所传递的参数类型,但也可以定义类型

(intparamA,intparamB)=>paramA+paramB优点是代码便于理解,缺点是不够简洁(我觉得还是可读性更重要)

不能在同一个Lambda表达式同时使用隐式和显式的参数类型

//错误的(intparamA,paramB)=>paramA+paramB可以定义没有参数的Lambda表达式,使用空括号表示

()=>Math.PI当委托不需要参数,但需要一个double值时,就可以使用该Lambda表达式

Lambda表达式的语句体可将Lambda表达式看成匿名方法语法的扩展,所以还可以在Lambda表达式的语句体中包含多个语句。只需要把代码块放在花括号中

如果使用Lambda表达式和返回类型不是void的委托类型,就必须用return关键字返回一个值,这与其他方法一样

(param1,param2)=>{ //Multiplestatementsahoy! returnreturnValue;}PerformOperations((paramA,paramB)=>paramA+paramB);//可以改写为PerformOperations(delegate(intparamA,intparamB){ returnparamA+paramB;});[!hint]在使用单一表达式时,Lambda表达式最有用也最简洁如果需要多个语句,则定义一个单独的非匿名方法更好,也使代码更便于复用

Lambda表达式用作委托和表达式树可采用两种方式来解释Lambda表达式

第一,Lambda表达式是一个委托。即可以把Lambda表达式赋予一个委托类型的变量

第二,可以把Lambda表达式解释为表达式树。表达式树是Lambda表达式的抽象表示,因此不能直接执行。可使用表达式树以编程方式分析Lambda表达式,执行操作,以响应Lambda表达式

THE END
1.签名设计,个性艺术签名!其中这件短袖的设计也是非常经典了,配上文字,有那feel了~ 睡醒了8 今天有点?上衣:m5is 黑色人像印花短袖T恤?裤子:guuka 撞色拼接抽绳束脚休闲裤?球鞋:椰子350黑天使 keshiw 得物er-U8P9H9L2 放学别走c 关注 签名设计,个性艺术签名! 让你名字呈现出不一样的美是我们的https://m.dewu.com/note/trend/details?id=254005218
2.一字签名设计图片图片字体分类发现字体发现“一字签名设计图片”分类,求字体网(www.qiuziti.com)是一个专注于字体识别、发现与下载的字体网站。http://www.qiuziti.com/fontlist2?id=781896
3.一笔签名设计在线艺术字一笔签名设计免费版在线,艺术字网出品,极品连笔艺术字签名转换器。 一笔签名设计转换器:一笔签,即签署姓名时飘逸潇洒,一气呵成,给人以酣畅淋漓的视觉冲击。艺术字网的一笔签注重整体签名的唯美协调,免费在线签名线条自然连贯,浑然一体;运笔跌宕起伏,签名字体迂回婉转之间体现一种自然美! http://www.yishuzi.com/b/m13.htm?1490490457
4.艺术一字签名设计专题模板艺术一字签名设计图片素材下载我图网艺术一字签名设计专题为您整理了219个原创高质量艺术一字签名设计图片素材供您在线下载,PSD/JPG/PNG格式艺术一字签名设计模板下载、高清艺术一字签名设计图片大全等,下载图片素材就上我图网。https://m.ooopic.com/sousuo/21378510/
5.一笔签名设计一笔签名设计。 一笔签是运用草书和行书的方式结合起来。 大胆夸张,字与字之间巧妙连贯,一气呵成,一挥而就。 穿插借用。左右环绕。浑然一体笔与笔之间连贯,字与字之间连接。 气势如闪电流星。颇有美感,气死磅礴大气。独具线条,艺术的美。 一笔签名不是人们认为的一笔画写完的。 https://xizhiqm.cn/article/U2-N5MbA6iH5dtvMjTWI
6.三十二个签名设计技巧签名设计就是设计签名,一般人写名字只是通过一般的汉字规则,这样写出来的名字往往很一般,没有什么艺术性,而签名设计就是由签名设计师根据中国传统书法、美术构图及字与字之间的联系来进行设计的。经过设计以后是签名书写方便、造型奇特、潇洒大方。 ?出现背景 https://www.meipian.cn/56gg6y8s
7.2024一年级语文全册教案(通用16篇)第三部分是音形提示图区。共有四幅图:第一幅图中“打鼓”的“打”帮助学生识记字母d的音,鼓槌与鼓的形状帮助识记字母d的形;第二幅图中“模特”的“特”帮助学生识记字母t的音,小姑娘的模特表演姿势帮助识记字母t的形;第三幅图中拱形门的形状帮助识记字母n的形;第四幅图中小指挥手中的指挥棒帮助学生识记字https://m.jy135.com/jiaoan/898348.html
8.中秋节主题活动方案反思(20篇范文精选)形式:映有“月映水木、情满花都”背景的祝福墙,设计成月饼形状的双层即时贴,上层和下层印有同号码,业主入场前领取即时贴,在上层签名写上祝福贴在祝福墙上,下层投入抽奖箱。 签名祝福墙形式新颖,抽奖活动可以吸引社区业主积极参与,写下给予亲人的祝福,烘托出中秋节的节日气氛。 http://www.plansum.cn/gongzuofangan/97606.html
9.有时候原谅一个人并不是真的原谅伤感意境的女生个性qq签名设计他一字一句说的很平静,仿佛事不关己却又小心翼翼。 心都为她跳动,何必管我死活。 以傲骨为路走向你,怎料中途你将我踢出局。 残忍也别过了头,想她是否能承受。 你不信我能承受所有,那么焚烧成灰你不也亲眼所见。 我胜过无数场战争,唯独死在你的刀枪下。 https://m.qqtn.com/mipc/62008.html
10.国庆节活动策划方案(通用14篇)2、在条幅上签名及国庆感言; 3、录制国庆感言语录; 4、以宿舍为单位进行彩旗设计,评比。 五、活动安排: 1、10月xx日,由校志协宣传部设计海报和横幅;广播站宣传本次活动及整个活动安排;对要进行国庆感言录制的同学进行报名;开始利用校内网、校bbs进行网上“迎国庆,文稿征集”。 https://www.yuwenmi.com/fanwen/huodongcehua/3341980.html
11.学校中秋活动方案(通用16篇)饼中有馅,表面有花纹,花纹主要有月亮、桂树、玉兔等在圆中表达美好的愿望。现代的花纹设计更是各异,别致。 辅导员:展示月饼事物,并简单介绍圆形设计的骨式。 ② 动动手,设计一个别致、精美的月饼图案。 ③队员作品欣赏。由队员自己讲解自己的设计意图。 https://www.unjs.com/huodongfangan/202108/3999029.html
12.幼儿园秋游活动方案15篇游戏准备:带子或一字形的气球 游戏玩法:一位家长和孩子各自将带子的三分之一塞进摔腰里,其余部分拖在六、设计思路:为了增进幼儿教师之间的互动关系,为了开阔幼儿的眼界,丰富幼儿的知识,我们幼儿园组织了这次5、活动结束后和教师一齐收拾,将垃圾分类扔进垃圾桶。在教师签到单上签名方可离开。 https://www.oh100.com/a/202306/7012611.html
13.如何设计签名?废话不多说!上图 签名:赵练 如何设计签名 1.了解狂草,看变型与结构连接(前期需要大量的学习与观摹) 2.利用书法字典找到合适的字型(搜集字形) 3.尝试不同的的组合,并尽可能简化(实践) 4.打破尝规的书写方式,创造独一无二的签名(创新)。 1. 对狂草要有一定的了解,有利于字的变型与结构连接。狂草是在今草https://www.jianshu.com/p/a565193000ee
14.太喜欢劳伦斯布洛克的雅贼系列这套书了!(含《图书管里的贼》精彩布洛克的雅贼系列之所以这么成功,最重要的就是正是行文风格和情趣性,前者给读者良好的阅读体验,后者让人放下书后念念不忘,给读者很大的拓展空间,去反复查找和挖掘周边信息,反复拿起来重读。太喜欢这套书了!封面设计新星出版社这套平装版的封面设计非常漂亮(第11本除外,风格不统一且毫无品味)与小说的年代感相吻合,简https://book.douban.com/review/12385857/
15.20152016年师徒结对活动计划及记录表5.教学环节中过渡句的设计还要思考,在实际课堂中,一字一句都要背出。 备注:1、此表由乙方记录,乙方填表并签名后,交指导教师签名。 2、每月一表,若一个月有多次活动,按时间顺序分别填写。 青蓝携手,结伴而行 ——育鹰学校师徒结对工作活动记录表 月份:6填表时间:6.6 http://www.yuying.edu.sh.cn/info/1166/6433.htm
16.巴彦淖尔市临河区大型排涝改造工程北边分干沟综合整治项目勘察设计4.本公告招标内容为勘察设计,具体标段划分及招标内容见下表: 序号 标段名称 招标范围 工期 巴彦淖尔市临 按照国家及行业相关标准完成项目勘察测 河区大型排涝 量,实施方案编制,施工图设计,设计概算, 改造工程北边 1 现场服务,设计变更,设计交底,解决施工 分干沟综合整 20 日http://ggzyjy.bynr.gov.cn/EpointWebBuilder/WebbuilderMIS/attach/downloadZtbAttach.jspx?attachGuid=d330ed13-4be3-40b5-bc91-158530e911f5&appUrlFlag=ztb001
17.校园捡垃圾活动策划方案校园捡垃圾活动策划书活动意义(18篇)2、“创建绿色校园有你有我”签名活动 制作大型横幅,在活动场地摆好桌子,提供签字笔,号召同学们积极响应本次活动策划贴近我们的校园生活,以本校环保现状为背景设计策划,既可以提高同学们的参与积极性,又可以一行行优美的语句,一字字情真意切的文字,让女生们感受到了阳光般的温暖。 一、活动目的: 为活跃https://www.kaoyanmiji.com/wendang/11677327.html
18.鲜花阅读,流光故事。流光海岸望青鸟,故事池塘唱绿蛙。第5页随着企业业务数据的几何级数增长,在一定程度时,SQL Server在性能与稳定性方面将不能提供良好的支持。而Oracle从设计之初便是针对海量数据系统的,能完全解决因数据量上升而带来的各种问题。其优化的数据操作算法及对数据严格的保护机制保证了系统性能及安全稳定。https://flowerread.wordpress.com/page/5/
19.签名设计签名设计图片签名设计模板觅知网为您找到214个原创签名设计图片,包括签名设计图片,签名设计素材,签名设计海报,签名设计背景,签名设计模板源文件下载服务,包含PSD、PNG、JPG、AI、CDR等格式素材,更多关于签名设计素材、图片、海报、背景、插画、配图、矢量、UI、PS、免抠,模板、艺术字、PPT、https://www.51miz.com/so-sucai/1999749.html
20.新店开业活动方案15篇3、签名幅: 为了使我们酒店和广大消费者的鱼水关系更进一步的加深。我公司设计一条90x588cm的签名幅。签名主题为“祝贺金茂大酒店开业大吉。”凡当天来店消费或参加庆典的朋友都可以签名。使酒店和支持酒店的所有群体的距离拉得更近,再次掀起开业的另一个高潮。 https://www.ruiwen.com/fangan/7232475.html
21.艺术签名图片艺术签名设计素材红动中国素材网提供5个艺术签名图片、艺术签名素材、艺术签名背景、艺术签名模板、艺术签名海报等PS素材下载,包含PSD、AI、PNG、JPG、CDR等格式源文件素材,更多精品艺术签名设计素材下载,就来红动中国,最后更新于2023-02-10 10:58:37。https://so.redocn.com/yishu/d2d5caf5c7a9c3fb.htm
22.历史上有哪些只存在于图纸上或设想中,最终无法建造出来的巨大工程随后的一个月,《纽约时报》进行了跟踪报道,不仅对项目进程和细节进行了披露,更完整呈现了设计图纸,https://www.zhihu.com/question/67419638/answer/2647244933