导图社区 csharp复习思维导图
C#复习思维导图,非常全,花了一个月整理出来的。 汇总了C#与。NET、数据类型、语句、表达式和运算符、面向对象、编程概念、版本迭代的知识。
编辑于2023-08-09 16:08:33 广东csharp
C#与.NET
1. 编程工具:IDE、.NET兼容的编译器、调试器、网站开发服务器端技术(ASP.NET或WCF); 2. BCL(基类库):.NET框架使用的一个大的类库,也可以在你的程序中使用; 3. CLR(公共语言运行库): - 内存管理和垃圾收集; - 代码安全验证; - 代码执行线程管理和异常处理;  编译时:编译成CIL 运行时:编译成本机代码并执行 1. 编译时:编译成CIL,.NET语言的编译器接受源代码文件,并生成名为程序集的输出文件。 - 程序集要么是可执行的exe,要么是DLL。 - 程序集里的代码并不是本机代码,而是名为CIL的中间语言,俗称IL代码。 - 程序集包含的信息中,包括下列项目: 1. 程序的CIL(IL代码) 2. 程序中使用的类型的元数据 3. 对其他程序集引用的元数据 2. 运行时:编译成本机代码并执行,程序的CIL直到它被调用运行时才会被编译成本机代码,步骤如下: 1. 检查程序集的安全特性; 2. 在内存中分配控件; 3. 把程序集中的可执行代码发送给实时(JIT)编译器,把其中的一部分编译成本机代码。 程序集中的可执行代码只在需要的时候由JIT编译器编译,然后它就被缓存起来以备后续程序中执行,使用这个方法意味着不被调用的代码不会被编译成本机代码,而且被调用到的只被编译一次。 一旦CIL被编译成本机代码,CLR就在它运行时管理它,执行像释放无主内存、检查数组边界、检查参数类型和管理异常之类的任务,有两个重要的术语由此产生。 - 托管代码:为.NET框架编写的代码成为托管代码,需要CLR。 - 非托管代码:不在CLR控制之下运行的代码,如Win32 C/C++ DLL。 
编程工具:IDE
BCL(基类库)
编译时:.NET语言的编译器接受源代码文件,并生成名为程序集的输出文件,程序集里的代码并不是本机代码,而是名为CIL的中间语言,俗称IL代码
CLR(公共语言运行库)
运行时:编译成本机代码并执行,程序的CIL直到它被调用运行时才会被编译成本机代码
数据类型
值类型
11个数值类型
char
bool
预定义数据类型
struct
结构是程序员定义的数据类型,与类非常相似,他们有数据成员和函数成员。虽然与类相似,但还是有许多重要的区别,最重要的区别是: 类VS结构 1. 类是引用类型,结构值类型; 2. 结构不支持继承(结构是隐式密封的); 3. 结构不能声明默认的构造函数; 结构的特征 1. 结构可带有方法、字段、索引、属性、运算符方法和事件。 2.结构可以定义实例构造函数和静态构造函数,但不能定义析构函数。但是,您不能为结构定义无参构造函数,无参构造函数(默认)是自动定义,且不能改动。 3. 与类不同,结构不能继承其他的结构或类。 4. 结构不能作为其他结构或类的基础结构。 5. 结构可以实现一个或多个接口。 6. 结构成员不能指定为abstract、virtual或protected. 7.当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。 8. 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。 字段初始化语句是不允许的 struct Simple{public int x; public int y;} Simple s1 = new Simple();
enum
枚举是由程序员定义的类型,与类或结构一样。 与结构一样,枚举是值类型,因为直接储存它们的数据,而不是分开储存成引用和数据。 枚举只有一种类型的成员:命名的整数值常量。 每每个枚举类型都有一个底层整数类型,默认为int 每个枚举成员都被赋予一个底层类型的常量值。 在默认情况下,编译器把第一个成员赋值为0,并对每一个后续成员赋的值比前一个多1 a enum TrafficLight{Green,Yellow,Red} //逗号分隔,没有分号 }}} 设置底层类型和显示值 enum TrafficLighe:int{Green = 0, Yellow = 1,Red = 2} //设置底层类型,显示的设置值 位标志 Flags特性
用户定义类型
引用类型
object
string
dynamic
预定义数据类型
class
声明类: class MyClass{ } 类成员:数据成员 函数成员 创建变量和类的实例,为数据分配内存 MyClass myClass; //声明引用变量 myClass = new MyClass(); //为类对象分配内存并赋值给变量 实例成员(默认类型,和类的实例有关的数据成员)、静态成员(与类有关,和实例无关) 访问修饰符: 私有的(private)公有的(public)受保护的(protected)内部的(internal)受保护内部的(protected internal) 类的内部访问成员,仅使用成员的名称即可访问。 类的外部访问成员,使用 . 运算符。 可以声明为static 的类成员类型 数据成员: 字段 类型 函数成员: 方法 属性 构造函数 运算符 事件 ------------简单实例,初步认识类----------------------
数据成员
字段
字段是隶属于类的变量,可以是任何类型,无论是预定义类型还是用户定义类型,和所有变量一样,字段用来保存数据。 显示和隐式字段初始化,如果没有初始化语句,字段的值会被编译器设为默认值,默认值由字段的类型决定。 int F1 = 17; 显示 int F2; 隐式 int F3,F4 = 25;声明多个字段 static int Mem2; 静态字段 int Mme1; 实例字段 静态字段被类的所有实例共享,所有实例都访问同一个内存位置。 readonly 修饰符 字段可以用readonly修饰符声明,其作用类似将字段声明为const,一旦值被设定就不能改变。 - const字段只能在字段的声明语句中初始化,而readonly字段可以在下列任意位置设置它的值。 - 字段声明语句,类似const - 类的任何构造函数,如果是static字段,初始化必须在静态构造函数中完成。 - const字段的值必须在编译时决定,而readonly字段的值可以在运行时决定,这种增加的自由行允许你在不同的环境或不同的构造函数中设置不同的值 - 和const不同,const的行为总是静态的,而对于readonl字段一下两点是正确的 - 它可以是实例字段,也可以是静态字段 - 它在内存中有储存位置。
常量
const int IntVal =100 ; //定义int类型常量 值为100 成员常量与静态: 成员常量比本地常量更有趣,因为它们表现得像静态值,他们对类得每个实例都是“可见的”,而且即使没有类的实例也可以使用,与真正的静态量不同,常量没有自己的储存位置,而是在编译时被编译器替代。这种方式类似C和C++中的#define值。
函数成员
方法
方法的简介语法包括:返回类型、名称、参数列表、方法体 方法包含了大部分组成程序行为的代码,剩余部分在其他的函数成员中,如属性和运算符。 方法是类的函数成员,具有两个部分: 1. 方法头指定方法的特征,包括: - 方法是否返回数据,如果返回,返回什么类型 - 方法的名称 - 那种类型的数据可以传递给方法或从方法返回,以及应如何处理这些数据 2. 方法体包含可执行代码的语句序列 本地变量没有隐式初始化,字段储存和对象状态有关的数据,本地变量经常是用于保存本地的或临时的计算数据。
方法头
返回类型
参数
话说古时候,在一个名字叫C#的繁华的大城市里面,有两家珠宝加工店,一家叫ref,另外一家叫out。 有一天,有名字叫a和b的两个人每人都各带了一公斤黄金要加工首饰。 a去了ref店,ref的掌柜告诉a说,请客官稍等,我们加工的速度是很快的,大概就一个小时左右吧。a说,我敢时间呢,能不能用我的黄金换现成的首饰。ref老板说,客官,实在是对不起,本店只为客人加工,我们没有这样的服务。如果您敢时间的话,我推荐您去out店,他们专门做这样的业务。 b去了out店,说,掌柜的,你必须要用我的这一公斤黄金给我加工首饰。out店的掌柜不好意思的笑了笑,客官实在是对不起我们这里只用黄金交换加工好了的首饰。我们不提供加工的服务,你可以去ref店,他们专门做这样的业务。 就这样,两家店各做各的,都说同行是冤家,两个店却关系很好。业务都蒸蒸日上。 两家店都相安无事的过了很多年。突然城东开了一家名叫params的店。这家什么都加工,不管是黄金珠宝还是黑土白云。不过由于不太专业,光顾的客人不怎么多。
值参数
形参是本地变量,声明在方法的参数列表,而不是方法体中。 值参数是把实参的值赋值给形参。
ref 引用参数
引用参数:有进有出,用在需要被调用的方法修改调用者的引用的时候。传入的参数必须先被初始化。 1. 使用引用参数时,必须在方法的声明和调用中都是用ref修饰符 2. 实参必须是变量,在用作实参前必须被赋值,如果是引用类型变量,可以赋值一个引用或者null 和值参数的区别: 1. 不会为形参在栈上分配内存 2. 形参的参数名将作为实参变量的别名,指向相同的内存位置。 引用类型作为值参数和引用参数 1. 将引用类型对象作为值参数传递:如果方法内创建一个新的对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束后,新对象也将不复存在。 2. 将引用类型对象作为引用参数传递:如果方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
out 输出参数
无进有出,out适合用在需要retrun多个返回值的地方,必须在方法中对其完成初始化。
params 参数数组
参数数组: 一个参数列表中只能由一个参数数组; 如果有,它必须是最后一个 由参数数组表示的所有参数都必须具有相同的类型 声明: 在数据类型前使用params修饰符 在数据类型后放置一组[]
方法体
本地变量
本地变量经常用于保存本地的或临时的计算数据,字段通常保存和对象状态有关的数据。 对比实例字段和本地变量 生存期: 实例字段:从实例被创建开始直到实例不再被访问时结束 本地变量:从它在块中被声明的那点开始,在块执行时结束 隐式初始化: 实例字段:初始化成该类型的默认值 本地变量:没有隐式初始化,如果变量在使用之没有被赋值,编译器就会产生一条错误信息 储存区域: 实例字段:由于是类的成员,所以所有字段都储存在堆里 本地变量:值类型,储存在栈里;引用类型,引用储存在栈里,数据储存在堆里 类型推断和var关键字 嵌套块中的本地变量
本地常量
本地常量一旦初始化,值就不能改变,如果本地变量,本地常量必须声明在块的内部。 常量的两个重要特征:常量在声名时必须初始化,常量在声明后不能改变。 常量的核心声明如下: 1. 在类型之前添加关键字const 2. 必须有初始化语句,初始化值必须在编译期决定,通常是一个预定义简单类型或由其组成的表达式,它还可以是null引用,但它不能是某个对象的引用,因为对象的引用是在运行时决定的。
控制流
方法包含了大部分组成程序行为的代码,剩余部分在其他的函数成员中,如属性和运算符。 选择语句:if if else switch 迭代语句:for while do foreach 跳转语句:break continue goto return
使用
方法重载
一个类中可以有一个以上的方法具有相同的名称,这叫做方法重载。使用相同名称的每个方法必须有一个和其他方法不同的签名。 1. 方法的签名由下列信息组成,它们在方法声明的方法头中: - 方法的名称 - 参数的数目 - 参数的数据类型和顺序 - 参数修饰符 2. 返回类型不是签名的一部分 3. 形参也不是签名的一部分
命名参数
public int Calc(int a, int b); Calc(c:2,b:5)
可选参数
public int Calc(int a ,int b = 3) 可选参数重要事项: 1. 不是所有的参数类型都可以作为可选参数 - 只要值类型的默认值在编译的时候确定,就可以使用值类型作为可选参数 - 只有在默认值是null的时候,引用类型才可以作为可选参数来使用 2. 所有必填参数必须在可选参数声明致歉声明,如果由params参数必须在所有可选参数之后声明。
栈帧
在调用方法的时候,内存从栈的顶部开始分配,保存和方法关联的一些数据项,这块内存叫做方法的栈帧。
递归
属性
语法糖:prop 静态属性: 属性也可以声明为static,静态属性的访问器和所有静态成员一样,具有以下特点: 1. 不能访问类的实例成员——虽然它们能被实例成员访问 2. 不管类是否由实例,它们都是存在的 3. 当从类的外部访问时,必须使用类名引用,而不是实例名 class MyClass{ public static int MyValue{get;set;} public void PrintValue(){ cw(MyValue); //类的内部访问 } } MyClass.MyValue //类的外部访问
事件
事件模型的五个组成部分 事件的发布者(event source、对象) 事件成员(event 、成员) 事件的订阅者(event subscriber,对象) 事件处理器(event handler,成员)——本质上是一个回调方法 事件订阅——把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的“约定”  delegate void Handler() //声明委托 发布者 class Incrementer { public event Handler CountedAozen; //创建事件并发布 public void DoCount() { for(int i =1;i<100;i++) if(i%12 == 0 && CountedADozen != null) CounedADozen(); //每增加12个计数触发一次事件 } } 订阅者 class Dozens { public int DozensCOunt{get;private set;} public Dozens(Incrementer incrementer) { DozensCount = 0; incrementer.CountedADozen += IncrementDozensCount; //订阅事件 } void IncrementDozensCount() { DozensCount++; //声明事件处理程序 } } class Program { static void Main() { Incrementer incrementer = new Incrementer(); Dozens dozensCounter = new Dozens(incrementer); incrementer.DoCount(); } }
索引器
索引器和属性在很多方面是相似的 1. 和属性一样,索引器不用分配内存来储存 2. 索引器和属性都主要用来访问其他数据成员,它们与这些成员关联,并未它们提供获取和设置访问。 属性通常表示单独的数据成员。 索引器通常表示多个数据成员。 索引器注意项: 1. 和属性一样,索引器可以只有一个访问器,也可以两个都有。 2. 索引器总是实例成员,因此不能声明为static。 3. 和属性一样,实现get和set访问器的代码不必一定要关联到某个字段或属性,这段代码可以做任何事情或什么也不做,只要get访问器返回某个指定类型的值即可。 声明索引器: 1. 索引器没有名称,在名称的位置是关键字this。 2. 参数列表再方括号中间。 3. 参数列表中必须至少声明一个参数。 索引器重载,只要索引器的参数列表不同,类就可以有任意多个索引器。 string this [int index] { set{}; get{}; }
运算符
构造函数
实例构造函数时一个特殊的方法,它在创建类的每个新实例时执行: - 构造函数用于初始化类实例的状态 - 如果希望能从类的外部创建类的实例,需要将构造函数声明为public 重要项: - 构造函数的名称和类名相同 - 构造函数不能有返回值 带参数的构造函数 构造函数在下列方面和其他方法类似: - 构造函数可以带参数,参数的语法和其他方法完全相同 - 构造函数可以被重载 class Class{ string name; public class Class{ id = 28; name = "name"} public class Class{id = val ; name = "aa"} public class Class{name = name} } class1 a = new Class(), b = new Class(7), v = new Class("bll") 构造函数也可以声明为static,实例构造函数初始化类的每一个新实例,而static构造函数初始化类级别的项,通常静态构造函数初始化类的静态字段。 - 初始化类级别的项 - 在引用任何静态成员之前 - 在创建类的任何实例之前 静态构造函数在以下方面和实例构造函数相同 - 静态构造函数的名称必须和类名相同 - 构造函数不能返回值 静态构造函数在一下方面和实例构造函数不同 - 静态构造函数声明中使用static关键字 - 类只能有一个静态构造函数,而且不能带参数 - 静态构造函数不能有访问修饰符
析构函数
析构函数执行在类的实例被销毁之前需要的清理或释放非托管资源的行为。
interface
接口是指定一组函数成员而不是先它们的引用类型。所以只能类和结构来实现接口。 接口只能包含如下类型的非静态成员函数的声明: 方法 属性 事件 索引器 public interface IComparable { int CompareTo(object obj); }
非静态函数成员
方法
属性
事件
索引器
delegate
理解委托最快的方法是把它看成一个类型安全的、面向对象的C++函数指针。 委托和类一样,是一种用户自定义的类型。但类表示的是数据和方法的集合,而委托则持有一个或多个方法,以及一系列预定义操作。 1. 方法的列表成为调用列表; 2.委托保存的方法可以来自任何类或结构,只要它们再下面两点匹配; - 委托的返回类型; - 委托的其那名(包括reg和out修饰符) delegate void MyDel(int x); delVar = new MyDel(myInstObj.MyM1); //实例方法 创建委托并保存引用 dVar = new MyDel(SClass.OtherM2); //静态方法 创建委托并保存引用 给委托赋值 MyDel delVar; delVar = myInstObj.MyM1; //创建委托对象并赋值 组合委托 MyDel dela = myInstObj.MyM1; //实例方法 MyDel delb = SClsaa.OtherM2; //静态方法 MyDel delc = dela + delb; //组合调用列表 为委托添加方法 MyDel dela = inst.MyM1 //创建并初始化 delVar += SC2.m3 //添加方法 从委托移除方法 delVar -= SC1.m3 调用委托 delVar(55); 匿名方法 MyDel del = delegate(int x) {return x+1;}; Lambda表达式 MyDel del = (int x )=>{return x+1;};
array
数组实际上是由一个变量名称表示的一组同类型的数据元素。每个元素通过变量名称和一个或多个方括号中的索引来访问。 1. 元素 数组的独立数据项被称作元素,数组的所有元素必须是相同类型的,或继承自相同的类型 2. 秩/维度 数组可以有任何为正数的维度数。数组的维度数称作秩 3. 维度长度 数组的每一个维度有一个长度,就是这个方向的位置长度 4. 数组长度 数组的所有维度中的元素的总和成为数组的长度 C#不支持动态数组,索引从0开始。 数组协变 在某些情况下,即使某个对象不是数组的基类型,我们也可以把它赋值给数组元素,这种属性叫做数组协变。 int[] arr = {1,2,3}; 一维数组 int[,] arr = {{1,2,3,4},{3,4,5,6}}; 二维数组 int[,,] arr; 三维数组 int[][] arr; 交错数组
用户定义类型
类型转换
预定义类型转换
数值类型
隐式转换 小转大不丢失精度
隐式类型转换是系统默认的,不必加说明就可以进行转换。 有些类型的转换不会丢数据或精度,例如,将8为的值转换为16位是非常容易的,而且不会丢失数据。 语言会自动做这些转换,这叫隐式转换 从位数更少的源转换为位数更多的目标类型时,目标中多出来的位需要用0或1填充。 当从更小的无符号类型转换为更大的无符号类型时,目标类型多出的最高位都以0进行填充,这叫零扩展。
显示转换和强制转换 大转小丢失精度(溢出、丢失精度)
如果把短类型转换为长类型,对于长类型来说,保存所有短类型的字符很简单,然而, 在其他情况下,目标类型也许无法在不损失数据的情况下提供源值。 强制类型转换 (sbyre)vaar1 ;//强制类型转换
溢出检测上下文 checked unchecked
显示转换可能会丢失数据并且不能在目标类型中同等地表示源值,对于整数类型,C# 提供了选择运行时是否应该在进行类型转换时检测结果溢出的能力,这将通过checked运算符和checked语句来实现。 代码片段是否被检测称为溢出检测上下文 如果我们指定一个表达式或一段代码为checked,CLR会在转换产生溢出时抛出一个OverflowException异常 如果代码不是checked,转换会继续不管是否溢出 checked(表达式);//溢出,OverflowException异常抛出异常 unchecked(表达式);//忽略溢出
装箱(隐式,值类型到引用类型)|拆箱(显式,装箱后的对象转回值类型)
装箱是一种隐式类型转换,他接受值类型的值,根据这个值在对上创建一个完整的引用类型对象并返回对象引用。 装箱时创建副本,一个有关装箱的普遍误解时在被装箱的项上发生了一些操作。其实不是,它返回的值时值的引用类型副本,在装箱产生后,该值有两个副本——原始值类型和引用类型副本,每一个都可以独立操作。 int i = 12; object oi= i ; oi = i; 拆箱是把装箱后的对象转换回值类型的过程。 拆箱是显示转换 系统会把值拆箱成ValueTypeT时执行了如下的步骤: 1. 它检测到要拆箱的对象实际时ValueTypeT的装箱值; 2. 它把对象的值复制到变量 static void Main() { int i = 10; object oi = i; //对i装箱并把引用赋值给oi int j = (int) oi; //对oi拆箱把值赋值给j }
引用类型
我们已经知道引用类型对象由内存中的两部分组成:引用和数据 由引用保存的那部分信息是指它指向的数据类型 引用转换接受源引用并返回一个指向堆中同一位置的引用,但是把引用“标记”为其他类型
隐式
所有引用类型可以被隐式转换为object; 任何类型可以隐式转换到它继承的接口; 类可以隐式转换到: 它继承中的任何类; 它实现的任何几口 委托可以隐式转换成DelegateS、System.Delegate、System.MuiticastDelegate,System.ICloneable、System.Runtime.Serialization.ISerializablle ArrayS数组,其中的元素时Ts类型,可以隐式转为成: ArrayS 、ArrayT、System.Array,System.IConeable,System.IList,System.Icollection,Ststem.IEnumerable 另一个数组ArrayT,其中的元素时Tt类型(如果满足下面的所有条件) 1. 两个数组有一杨的维度 2. 元素类型Ts和Tt都是引用类型,不是值类型 3. 在类型Ts和Tt中存在隐式转换
显示
显示引用转换是从一个普通类型到一个更精确类型的引用转换。 显示类型转换包括 1. 从object到任何引用类型的转换 2. 从基类到从它继承的类的转换
用户定义类型转换
用户自定义转换有一些很重要的约束,最重要的如下所示 只可以为类和结构定义用户自定义转换 不能重定义标准隐式转换或显示转换 对于源类型S和目标类T,如下是命题为真: 1. S和T必须是不同类型; 2. S和T不能通过继承关联,也就是说,S不能继承自T,而T也不能从S继承。 3. S和T都布恩那个是接口类型或object类型; 4. 转换运算符必须是S或T的成员 4.对相同的源和目标类型,我们不能声明隐式转换和显示转换
隐式转换 implicit关键字
public static implicit operator int (Person p ) { return p.Age; } operator 只要是运算符都能重载 operator 关键字的主要作用是用来重载运算符的,还可以用于类或结构中类型的自定义转换。
显示转换 explicit关键字
is(转换是否成功抛出异常) as()
is 转换失败抛出异常,引用转换、装箱拆箱、不能用于用户定义转换。 as 转换失败返回null,只能用于引用转换和装箱转换,不能用于用户自定义转换或到值类型的转换。
语句、表达式和运算符
表达式是运算符和操作数的字符串。
语句
程序执行的操作采用语句表达。 常见操作包括声明变量、赋值、调用方法、循环访问集合,以及根据给定条件分支到一个或另一个代码块。 语句在程序中的执行顺序称为“控制流”或“执行流”。 根据程序对运行时所收到的输入的响应,在程序每次运行时控制流可能有所不同。 语句可以是以分号结尾的单行代码,也可以是语句块中的一系列单行语句。 语句块括在括号 {} 中,并且可以包含嵌套块。 以下代码演示了两个单行语句示例和一个多行语句块:
声明语句
声明语句引入新的变量或常量。 变量声明可以选择为变量赋值。 在常量声明中必须赋值。
表达式语句
用于计算值的表达式语句必须在变量中存储该值。 有关详细信息,请参阅表达式语句。
选择语句
选择语句用于根据一个或多个指定条件分支到不同的代码段。 有关详细信息,请参阅下列主题: if else switch case
迭代语句
迭代语句用于遍历集合(如数组),或重复执行同一组语句直到满足指定的条件。 有关详细信息,请参阅下列主题: do for foreach in while
跳转语句
跳转语句将控制转移给另一代码段。 有关详细信息,请参阅下列主题: break continue default goto return yield
异常处理语句
异常处理语句用于从运行时发生的异常情况正常恢复。 有关详细信息,请参阅下列主题: throw try-catch try-finally try-catch-finally
Checked 和 unchecked语句
Checked 和 unchecked 语句用于指定将结果存储在变量中、但该变量过小而不能容纳结果值时,是否允许数值运算导致溢出。 有关详细信息,请参阅 checked 和 unchecked。
await 语句
如果用 async 修饰符标记方法,则可以使用该方法中的 await 运算符。 在控制到达异步方法的 await 表达式时,控制将返回到调用方,该方法中的进程将挂起,直到等待的任务完成为止。 任务完成后,可以在方法中恢复执行。 有关简单示例,请参阅方法的“异步方法”一节。 有关详细信息,请参阅 async 和 await 的异步编程。
yield return 语句
迭代器对集合执行自定义迭代,如列表或数组。 迭代器使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器时,将从该位置重新开始执行。 有关更多信息,请参见 迭代器。
fixed 语句
fixed 语句禁止垃圾回收器重定位可移动的变量。 有关详细信息,请参阅 fixed。
lock 语句
lock 语句用于限制一次仅允许一个线程访问代码块。 有关详细信息,请参阅 lock。
带标签的语句
可以为语句指定一个标签,然后使用 goto 关键字跳转到该带标签的语句。
空语句
空语句只含一个分号。 不执行任何操作,可以在需要语句但不需要执行任何操作的地方使用。
表达式
表达式是由一个或多个操作数以及零个或多个运算符组成的序列,其计算结果为一个值、对象、方法或命名空间。 表达式可以包含文本值、方法调用、运算符及其操作数,或简单名称 。 简单名称可以是变量名、类型成员名、方法参数名、命名空间名或类型名。 表达式可以使用运算符(运算符又可使用其他表达式作为参数)或方法调用(方法调用的参数又可以是其他方法调用),因此表达式可以非常简单,也可以极其复杂。 下面是表达式的两个示例: C# 复制 ((x < 10) && ( x > 5)) || ((x > 20) && (x < 25)); System.Convert.ToInt32("35");
表达式值
在大部分使用表达式的上下文中(例如在语句或方法参数中),表达式的计算结果应为某个值。 如果 x 和 y 是整数,表达式 x + y 的计算结果为一个数值。 表达式 new MyClass() 的计算结果为对 MyClass 类的新实例的引用。 表达式 myClass.ToString() 的计算结果为一个字符串,因为字符串是该方法的返回类型。 然而,虽然命名空间名称归类为表达式,但它的计算结果不是一个值,因此绝不会作为任何表达式的最终结果。 命名空间名称不得传递给方法参数,不能用在新表达式中,也不能赋给变量。 命名空间名称只能用作较大表达式的子表达式。 同样如此的还有类型(与 System.Type 对象不同)、方法组名称(与特定方法不同)以及事件 add 和 remove 访问器。 每个值都有关联的类型。 例如,如果 x 和 y 都是 int 类型的变量,则表达式 x + y 的值也属于 int 类型。 如果将该值赋给不同类型的变量,或者如果 x 和 y 是不同的类型,则应用类型转换规则。 若要详细了解如何进行这种转换,请参阅强制转换和类型转换。
溢出
如果值大于值类型的最大值,数值表达式可能导致溢出。 有关详细信息,请参阅内置数值转换一文的 Checked 和 Unchecked 和显式数值转换部分。
运算符优先级和关联性
文本和简单名称
调用表达式
查询表达式LINQ
Lambda 表达式
Lambda 表达式表示没有名称但可以具有输入参数和多个语句的“内联方法”。 它们在 LINQ 中广泛用于向方法传递参数。 Lambda 表达式会编译为委托或表达式树,具体取决于使用它们的上下文。 有关详细信息,请参阅 Lambda 表达式。 “Lambda 表达式”是采用以下任意一种形式的表达式: 表达式 lambda,表达式为其主体: (input-parameters) => expression 语句 lambda,语句块作为其主体: (input-parameters) => { <sequence-of-statements> }
表达式 lambda
表达式位于 => 运算符右侧的 lambda 表达式称为“表达式 lambda”。 表达式 lambda 广泛用于表达式树的构造。 表达式 lambda 会返回表达式的结果,并采用以下基本形式: (input-parameters) => expression 仅当 lambda 只有一个输入参数时,括号才是可选的;否则括号是必需的。 使用空括号指定零个输入参数: Action line = () => Console.WriteLine(); 括号内的两个或更多输入参数使用逗号加以分隔: Func<int, int, bool> testForEquality = (x, y) => x == y; 有时,编译器无法推断输入类型。 可以显式指定类型,如下面的示例所示: Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
语句 lambda
语句 lambda 与表达式 lambda 表达式类似,只是语句括在大括号中: (input-parameters) => { <sequence-of-statements> } 语句 lambda 的主体可以包含任意数量的语句;但是,实际上通常不会多于两个或三个。 Action<string> greet = name => { string greeting = $"Hello {name}!"; Console.WriteLine(greeting); }; greet("World"); // Output: // Hello World! 语句 lambda 也不能用于创建表达式目录树。
异步 lambda
通过使用 async 和 await 关键字,你可以轻松创建包含异步处理的 lambda 表达式和语句。 例如,下面的 Windows 窗体示例包含一个调用和等待异步方法 ExampleMethodAsync的事件处理程序。 public partial class Form1 : Form { public Form1() { InitializeComponent(); button1.Click += button1_Click; } private async void button1_Click(object sender, EventArgs e) { await ExampleMethodAsync(); textBox1.Text += "\r\nControl returned to Click event handler.\n"; } private async Task ExampleMethodAsync() { // The following line simulates a task-returning asynchronous process. await Task.Delay(1000); } } 你可以使用异步 lambda 添加同一事件处理程序。 若要添加此处理程序,请在 lambda 参数列表前添加 async 修饰符,如下面的示例所示: public partial class Form1 : Form { public Form1() { InitializeComponent(); button1.Click += async (sender, e) => { await ExampleMethodAsync(); textBox1.Text += "\r\nControl returned to Click event handler.\n"; }; } private async Task ExampleMethodAsync() { // The following line simulates a task-returning asynchronous process. await Task.Delay(1000); } }
lambda 表达式和元组
自 C# 7.0 起,C# 语言提供对元组的内置支持。 可以提供一个元组作为 Lambda 表达式的参数,同时 Lambda 表达式也可以返回元组。 在某些情况下,C# 编译器使用类型推理来确定元组组件的类型。 可通过用括号括住用逗号分隔的组件列表来定义元组。 下面的示例使用包含三个组件的元组,将一系列数字传递给 lambda 表达式,此表达式将每个值翻倍,然后返回包含乘法运算结果的元组(内含三个组件)。 Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3); var numbers = (2, 3, 4); var doubledNumbers = doubleThem(numbers); Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}"); // Output: // The set (2, 3, 4) doubled: (4, 6, 8) 通常,元组字段命名为 Item1、Item2 等等。但是,可以使用命名组件定义元组,如以下示例所示。 Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3); var numbers = (2, 3, 4); var doubledNumbers = doubleThem(numbers); Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
含标准查询运算符的 lambda
可以将委托实例化为 Func<int, bool> 实例,其中 int 是输入参数,bool 是返回值。 返回值始终在最后一个类型参数中指定。 例如,Func<int, string, bool> 定义包含两个输入参数(int 和 string)且返回类型为 bool的委托。 下面的 Func 委托在调用后返回布尔值,以指明输入参数是否等于 5: Func<int, bool> equalsFive = x => x == 5; bool result = equalsFive(4); Console.WriteLine(result);
Lambda 表达式中的类型推理
编写 lambda 时,通常不必为输入参数指定类型,因为编译器可以根据 lambda 主体、参数类型以及 C# 语言规范中描述的其他因素来推断类型。 对于大多数标准查询运算符,第一个输入是源序列中的元素类型。 如果要查询 IEnumerable<Customer>,则输入变量将被推断为 Customer 对象,这意味着你可以访问其方法和属性: customers.Where(c => c.City == "London"); lambda 类型推理的一般规则如下: Lambda 包含的参数数量必须与委托类型包含的参数数量相同。 Lambda 中的每个输入参数必须都能够隐式转换为其对应的委托参数。 Lambda 的返回值(如果有)必须能够隐式转换为委托的返回类型。 请注意,lambda 表达式本身没有类型,因为通用类型系统没有“lambda 表达式”这一固有概念。 不过,有时以一种非正式的方式谈论 lambda 表达式的“类型”会很方便。 在这些情况下,类型是指委托类型或 lambda 表达式所转换到的 Expression 类型。
捕获 lambda 表达式中的外部变量和变量范围
表达式树
使用表达式树可将表达式表示为数据结构。 表达式树由 LINQ 提供程序广泛使用,用来将查询表达式转换为在其他某些上下文(如 SQL 数据库)中有意义的代码。 表达式树以树形数据结构表示代码,其中每一个节点都是一种表达式,比如方法调用和 x < y 这样的二元运算等。 你可以对表达式树中的代码进行编辑和运算。 这样能够动态修改可执行代码、在不同数据库中执行 LINQ 查询以及创建动态查询。 有关 LINQ 中表达式树的详细信息,请参阅如何使用表达式树生成动态查询 (C#)。 表达式树还能用于动态语言运行时 (DLR) 以提供动态语言和 .NET 之间的互操作性,同时保证编译器编写员能够发射表达式树而非 Microsoft 中间语言 (MSIL)。 有关 DLR 的详细信息,请参阅动态语言运行时概述。 你可以基于匿名 lambda 表达式通过 C# 或者 Visual Basic 编译器创建表达式树,或者通过 System.Linq.Expressions 名称空间手动创建。
根据 Lambda 表达式创建表达式树
通过 API 创建表达式树
解析表达式树
表达式树永久性
编译表达式树
表达式主体定义
C# 支持“Expression-Bodied 成员” ,这允许为方法、构造函数、终结器、属性和索引器提供简洁的表达式主体定义。 有关详细信息,请参阅 “Expression-bodied 成员”。
Expression-bodied 成员
通过表达式主体定义,可采用非常简洁的可读形式提供成员的实现。 只要任何支持的成员(如方法或属性)的逻辑包含单个表达式,就可以使用表达式主体定义。 表达式主体定义具有下列常规语法: C# 复制 member => expression; 其中“expression” 是有效的表达式。
运算符
运算符基础
运算符优先级
在包含多个运算符的表达式中,先按优先级较高的运算符计算,再按优先级较低的运算符计算 基本 > 一元 > 范围 > switch 表达式 > 乘法 > 加法 > 移位 > 关系和类型测试 > 相等 > 布尔逻辑 AND 或按位逻辑 AND > 布尔逻辑 XOR 或按位逻辑 XOR > 布尔逻辑 OR 或按位逻辑 OR > 条件“与” > 条件“或”> Null 合并运算符 > 条件运算符 > 赋值和 lambda 声明
运算符结合性
当运算符的优先级相同,运算符的结合性决定了运算的执行顺序: 左结合运算符按从左到右的顺序计算。 除赋值运算符和 null 合并运算符外,所有二元运算符都是左结合运算符。 例如,a + b - c 将计算为 (a + b) - c。 右结合运算符按从右到左的顺序计算。 赋值运算符、null 合并运算符和条件运算符?:是右结合运算符。 例如,x = y = z 将计算为 x = (y = z)。
操作数计算
与运算符的优先级和结合性无关,从左到右计算表达式中的操作数。 以下示例展示了运算符和操作数的计算顺序: 通常,会计算所有运算符操作数。 但是,某些运算符有条件地计算操作数。 也就是说,此类运算符的最左侧操作数的值定义了是否应计算其他操作数,或计算其他哪些操作数。 这些运算符有条件逻辑 AND (&&) 和 OR (||) 运算符、null 合并运算符 ?? 和 ??=、null 条件运算符 ?. 和 ?[] 以及条件运算符?:。 有关详细信息,请参阅每个运算符的说明。
基本
x.y 成员访问表达式 .
可以使用 . 标记来访问命名空间或类型的成员
f(x) 调用表达式 ()
使用括号 () 调用方法或调用委托。 此外可以使用括号来调整表达式中计算操作的顺序。 强制转换表达式,其执行显式类型转换,也可以使用括号。
a[i] 索引器运算符 []
方括号 [] 通常用于数组、索引器或指针元素访问。 int[] fib = new int[10]; //数组访问 索引器访问 下面的示例使用 .NET Dictionary<TKey,TValue> 类型来演示索引器访问: var dict = new Dictionary<string, double>(); dict["one"] = 1; dict["pi"] = Math.PI; Console.WriteLine(dict["one"] + dict["pi"]); // output: 4.14159265358979 使用索引器,可通过类似于编制数组索引的方式对用户定义类型的实例编制索引。 与必须是整数的数组索引不同,可以将索引器参数声明为任何类型。 [] 的其他用法 方括号还用于指定属性: [System.Diagnostics.Conditional("DEBUG")] void TraceMethod() {}
Null 条件运算符 ?. 和 ?[]
如果 a 的计算结果为 null,则 a?.x 或 a?[x] 的结果为 null。 如果 a 的计算结果为非 null,则 a?.x 或 a?[x] 的结果将分别与 a.x 或 a[x] 的结果相同。 如果 a.x 或 a[x] 引发异常,则 a?.x 或 a?[x] 将对非 null a 引发相同的异常。 例如,如果 a 为非 null 数组实例且 x 在 a的边界之外,则 a?[x] 将引发 IndexOutOfRangeException。
增量运算符 ++
x++ 的结果是此操作前的 x 的值, ++x 的结果是此操作后的 x 的值,
减量运算符 --
x-- 的结果是此操作前的 x 的值 --x 的结果是此操作后的 x 的值
! (null 包容)运算符 x!
一元后缀 ! 运算符是 null 包容运算符。 在已启用的可为空的注释上下文中,可以使用 null 包容运算符来声明可为空的引用类型的表达式 x 不为 null:x!。 null 包容运算符在运行时不起作用。 它仅通过更改表达式的 null 状态来影响编译器的静态流分析。 在运行时,表达式 x! 的计算结果为基础表达式 x 的结果。 一元前缀 ! 运算符是逻辑非运算符。 null 包容运算符的一个用例是测试参数验证逻辑 #nullable enable public class Person { public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name)); public string Name { get; } }
new 运算符
new 运算符创建类型的新实例。 new 关键字还可用作成员声明修饰符或泛型类型约束。 构造函数调用 要创建类型的新实例,通常使用 new 运算符调用该类型的某个构造函数 var dict = new Dictionary<string, int>(); 数组创建 还可以使用 new 运算符创建数组实例 var numbers = new int[3]; numbers[0] = 10; numbers[1] = 20; numbers[2] = 30; 匿名类型的实例化 要创建匿名类型的实例,请使用 new 运算符和对象初始值设定项语法: var example = new { Greeting = "Hello", Name = "World" }; Console.WriteLine($"{example.Greeting}, {example.Name}!"); // Output: // Hello, World! 类型实例的析构 无需销毁此前创建的类型实例。 引用和值类型的实例将自动销毁。 包含值类型的上下文销毁后,值类型的实例随之销毁。 在引用类型的最后一次引用被删除后,垃圾回收器会在某个非指定的时间销毁其实例。 运算符可重载性 用户定义的类型不能重载 new 运算符。
typeof 运算符
typeof 运算符用于获取某个类型的 System.Type 实例。 typeof 运算符的实参必须是类型或类型形参的名称 void PrintType<T>() => Console.WriteLine(typeof(T)); Console.WriteLine(typeof(List<string>)); PrintType<int>(); PrintType<System.Int32>(); PrintType<Dictionary<int, char>>(); 使用 typeof 运算符进行类型测试 使用 typeof 运算符来检查表达式结果的运行时类型是否与给定的类型完全匹配
checked
checked 关键字用于对整型类型算术运算和转换显式启用溢出检查
unchecked
unchecked 关键字用于取消整型类型的算术运算和转换的溢出检查。 在未经检查的上下文中,如果表达式生成的值超出目标类型的范围,则不会标记溢出。 例如,由于以下示例中的计算在 unchecked 块或表达式中执行,因此将忽略计算结果对于整数而言过大的事实,并且向 int1 赋予值 -2,147,483,639。 unchecked { int1 = 2147483647 + 10; } int1 = unchecked(ConstantMax + 10);
default value 表达式
default value 表达式生成类型的默认值。 有两种类型的 default value 表达式:default 运算符调用和 default 文本。 你还可以将 default 关键字用作 switch 语句中的默认用例标签。 var val = default(T); default 文本 从 C# 7.1 开始,当编译器可以推断表达式类型时,可以使用 default 文本生成类型的默认值。 default 文本表达式生成与 default(T) 表达式(其中,T 是推断的类型)相同的值。 可以在以下任一情况下使用 default 文本: 对变量进行赋值或初始化时。 在声明可选方法参数的默认值时。 在方法调用中提供参数值时。 在 return 语句中或作为表达式主体成员中的表达式时。 default 运算符 default 运算符的实参必须是类型或类型形参的名称
nameof 表达式
nameof 表达式可生成变量、类型或成员的名称作为字符串常量: Console.WriteLine(nameof(System.Collections.Generic)); // output: Generic Console.WriteLine(nameof(List<int>)); // output: List 在逐字标识符的情况下,@ 字符不是名称的一部分,如以下示例所示: var @new = 5; Console.WriteLine(nameof(@new)); // output: new
delegate 运算符
delegate 运算符创建一个可以转换为委托类型的匿名方法: Func<int, int, int> sum = delegate (int a, int b) { return a + b; }; Console.WriteLine(sum(3, 4)); // output: 7 从 C# 3 开始,lambda 表达式提供了一种更简洁和富有表现力的方式来创建匿名函数。 使用 => 运算符构造 lambda 表达式: Func<int, int, int> sum = (a, b) => a + b; Console.WriteLine(sum(3, 4)); // output: 7 有关 lambda 表达式功能的更多信息(例如,如何捕获外部变量),请参阅 lambda 表达式。
sizeof 运算符
sizeof 运算符返回给定类型的变量所占用的字节数。 sizeof 运算符的参数必须是一个非托管类型的名称,或是一个限定为非托管类型的类型参数。 sizeof 运算符需要不安全上下文。
stackalloc 表达式
stackalloc 表达式在堆栈上分配内存块。 该方法返回时,将自动丢弃在方法执行期间创建的堆栈中分配的内存块。 不能显式释放使用 stackalloc 分配的内存。 堆栈中分配的内存块不受垃圾回收的影响,也不必通过 fixed 语句固定。
指针成员访问运算符 ->
-> 运算符将指针间接和成员访问合并。 也就是说,如果 x 是类型为 T* 的指针且 y 是类型 T 的可访问成员,则形式的表达式 x->y 等效于 (*x).y
一元
+、-
逻辑非运算符 !
一元前缀 ! 运算符计算操作数的逻辑非。 也就是说,如果操作数的计算结果为 false,它生成 true;如果操作数的计算结果为 true,它生成 false:
按位求补运算符 ~
~ 运算符通过反转每个位产生其操作数的按位求补:
增量运算符 ++
x++ 的结果是此操作前的 x 的值 ++x 的结果是此操作后的 x 的值
减量运算符 --
x-- 的结果是此操作前的 x 的值 --x 的结果是此操作后的 x 的值
从末尾运算符 ^ 开始索引
^ 运算符在 C# 8.0 和更高版本中提供,指示序列末尾的元素位置。 对于长度为 length 的序列,^n 指向与序列开头偏移 length - n 的元素。 例如,^1 指向序列的最后一个元素,^length 指向序列的第一个元素。
强制转换表达式(T)E
形式为 (T)E 的强制转换表达式将表达式 E 的结果显式转换为类型 T。 如果不存在从类型 E 到类型 T 的显式转换,则发生编译时错误。 在运行时,显式转换可能不会成功,强制转换表达式可能会引发异常。
await 运算符
await 运算符暂停对其所属的 async 方法的求值,直到其操作数表示的异步操作完成。 异步操作完成后,await 运算符将返回操作的结果(如果有)。 当 await 运算符应用到表示已完成操作的操作数时,它将立即返回操作的结果,而不会暂停其所属的方法。 await 运算符不会阻止计算异步方法的线程。 当 await 运算符暂停其所属的异步方法时,控件将返回到方法的调用方。
Address-of 运算符 &
一元 & 运算符返回其操作数的地址: unsafe { int number = 27; int* pointerToNumber = &number; Console.WriteLine($"Value of the variable: {number}"); Console.WriteLine($"Address of the variable: {(long)pointerToNumber:X}"); } // Output is similar to: // Value of the variable: 27 // Address of the variable: 6C1457DBD4
指针间接运算符 *
一元指针间接运算符 * 获取其操作数指向的变量。 它也称为取消引用运算符。 * 运算符的操作数必须是指针类型。
true 和 false 运算符
true 运算符返回 bool 值 true,以指明其操作数一定为 true。 false 运算符返回 bool 值 true,以指明其操作数一定为 false。
范围
范围运算符 . .
.. 运算符在 C# 8.0 和更高版本中提供,指定索引范围的开头和末尾作为其操作数。 左侧操作数是范围的包含性开头。 右侧操作数是范围的包含性末尾。 任一操作数都可以是序列开头或末尾的索引,如以下示例所示: C# 复制 int[] numbers = new[] { 0, 10, 20, 30, 40, 50 }; int start = 1; int amountToTake = 3; int[] subset = numbers[start..(start + amountToTake)]; Display(subset); // output: 10 20 30 int margin = 1; int[] inner = numbers[margin..^margin]; Display(inner); // output: 10 20 30 40 string line = "one two three"; int amountToTakeFromEnd = 5; Range endIndices = ^amountToTakeFromEnd..^0; string end = line[endIndices]; Console.WriteLine(end); // output: three void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs)); 如前面的示例所示,表达式 a..b 属于 System.Range 类型。 在表达式 a..b 中,a 和 b 的结果必须隐式转换为 int 或 Index。 可以省略 .. 运算符的任何操作数来获取无限制范围: a.. 等效于 a..^0 ..b 等效于 0..b .. 等效于 0..^0
switch 表达式
通常情况下,switch 语句在其每个 case 块中生成一个值。 借助 Switch 表达式,可以使用更简洁的表达式语法。 只有些许重复的 case 和 break 关键字和大括号。 以下面列出彩虹颜色的枚举为例: C# 复制 public enum Rainbow { Red, Orange, Yellow, Green, Blue, Indigo, Violet } 如果应用定义了通过 R、G 和 B 组件构造而成的 RGBColor 类型,可使用以下包含 switch 表达式的方法,将 Rainbow 转换为 RGB 值: C# 复制 public static RGBColor FromRainbow(Rainbow colorBand) => colorBand switch { Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00), Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00), Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00), Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00), Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF), Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82), Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3), _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)), };
乘除加减
乘、除、求余
加、减
移位
左移位运算符 <<
<< 运算符将其左侧操作数向左移动右侧操作数定义的位数。
右移位运算符 >>
>> 运算符将其左侧操作数向右移动右侧操作数定义的位数。
关系和类型测试
> < <= >=
is
is 运算符检查表达式结果的运行时类型是否与给定类型兼容。 从 C# 7.0 开始,is 运算符还会对照某个模式测试表达式结果。 具有类型测试 is 运算符的表达式具有以下形式 E is T 其中 E 是返回一个值的表达式,T 是类型或类型参数的名称。 E 不得为匿名方法或 Lambda 表达式。 如果 E 的结果为非 null 且可以通过引用转换、装箱转换或取消装箱转换来转换为类型 T,则 E is T 表达式将返回 true;否则,它将返回 false。 is 运算符不会考虑用户定义的转换。
as
as 运算符将表达式结果显式转换为给定的引用或可以为 null 值的类型。 如果无法进行转换,则 as 运算符返回 null。 与强制转换表达式 不同,as 运算符永远不会引发异常。 形式如下的表达式 E as T 其中,E 为返回值的表达式,T 为类型或类型参数的名称,生成相同的结果, E is T ? (T)(E) : (T)null 不同的是 E 只计算一次。 as 运算符仅考虑引用、可以为 null、装箱和取消装箱转换。 不能使用 as 运算符执行用户定义的转换。 为此,请使用强制转换表达式。
等于不等于
== 如果操作数相等,等于运算符 == 返回 true,否则返回 false。 != 如果操作数不相等,不等于运算符 != 返回 true,否则返回 false。
& ^ |
逻辑 AND 运算符 &
& 运算符计算操作数的逻辑与。 如果 x 和 y 的计算结果都为 true,则 x & y 的结果为 true。 否则,结果为 false。
逻辑异或运算符 ^
运算符计算操作数的逻辑异或(亦称为“逻辑 XOR”)。 如果 x 计算结果为 true 且 y 计算结果为 false,或者 x 计算结果为 false 且 y 计算结果为 true,那么 x ^ y 的结果为 true。 否则,结果为 false。 也就是说,对于 bool 操作数,^ 运算符的计算结果与不等运算符 != 相同。
逻辑或运算符 |
| 运算符计算操作数的逻辑或。 如果 x 或 y 的计算结果为 true,则 x | y 的结果为 true。 否则,结果为 false。
&& || &&
条件逻辑 AND 运算符 &&
条件逻辑与运算符 &&(亦称为“短路”逻辑与运算符)计算操作数的逻辑与。 如果 x 和 y 的计算结果都为 true,则 x && y 的结果为 true。 否则,结果为 false。 如果 x 的计算结果为 false,则不计算 y。
条件逻辑或运算符 ||
条件逻辑与运算符 &&(亦称为“短路”逻辑与运算符)计算操作数的逻辑与。 如果 x 和 y 的计算结果都为 true,则 x && y 的结果为 true。 否则,结果为 false。 如果 x 的计算结果为 false,则不计算 y。
条件逻辑 AND 运算符 &&
条件逻辑与运算符 &&(亦称为“短路”逻辑与运算符)计算操作数的逻辑与。 如果 x 和 y 的计算结果都为 true,则 x && y 的结果为 true。 否则,结果为 false。 如果 x 的计算结果为 false,则不计算 y。
Null 合并运算符
?? 和 ??= 运算符
如果左操作数的值不为 null,则 null 合并运算符 ?? 返回该值;否则,它会计算右操作数并返回其结果。 如果左操作数的计算结果为非 null,则 ?? 运算符不会计算其右操作数。 C# 8.0 及更高版本中可使用空合并赋值运算符 ??=,该运算符仅在左侧操作数的求值结果为 null 时,才将其右侧操作数的值赋值给左操作数。 如果左操作数的计算结果为非 null,则 ??= 运算符不会计算其右操作数
条件运算符 ?:
条件运算符 (?:) 也被称为三元条件运算符,用于计算布尔表达式,并根据布尔表达式的计算结果为 true 还是 false 来返回两个表达式中的一个结果。 条件运算符的语法如下所示: condition ? consequent : alternative condition 表达式的计算结果必须为 true 或 false。 若 condition 的计算结果为 true,将计算 consequent,其结果成为运算结果。 若 condition 的计算结果为 false,将计算 alternative,其结果成为运算结果。 只会计算 consequent 或 alternative。 consequent 和 alternative 的类型必须相同,或者必须存在从一种类型到另一种类型的隐式转换。
赋值和 lambda 声明
x = y、x += y、x -= y、x *= y、x /= y、x %= y、x &= y、x |= y、x ^= y、x <<= y、x >>= y、x ??= y、=>
面向对象
三大特性
封装
访问修饰符是C#实现类的封装的最核心手段。 封装性就是把对象的属性和服务结合成一个独立的单位,并尽可能隐蔽对象的内部细节(自动提款机)
访问修饰符
public
公有的,所有类都可以访问,是最松散的控制访问。
protected internal
所有继承该类或在该程序集内声明的类可访问
protected
受保护的,也就是除了本类之外,只有自己的子类可以访问。
internal
本程序集的,也就是说一个程序集内部都可以访问,外部不行。
private
私有的,只能本类访问,包括子类也不能访问,是最严格的控制访问。
构造函数
与类名同名。 自动运行。 不能返回任何数值,也不能加VOID. 每个类都必须有一个构造函数,用户不提供则系统会提供自己默认的构造函数。 构造函数的默认访问是private,但这样就不能在类的外部创建实例。 创建了带参数的构造函数,系统不会再创建默认构造函数。
方法的定义
方法的名称。 方法的参数列表 方法的返回类型,如果没有返回数值,要加“void” 关键字。 方法体(方法的内容)
static关键字
用static 修饰的方法,叫静态方法。 在实例方法中,调用静态方法,需要使用类名称调用。 在静态方法中,调用静态方法,可以直接调用。 用途: 是简化编写,为了更容易被访问。
继承
特殊类拥有其一般类的的全部属性和服务,成为特殊类对一般类的继承。(派生类、基类) 允许重用现有类去创建新类的特性,增加代码的可重用性 继承是向对象程序设计的一个重要特征,它允许在现有类的基础上创建新类,新类从现有类中继承类成员,从而形成类的层次或等级。一般称被继承的类为基类或父类,而继承后产生的类为派生类或子类。 (1) 在C#中只支持单一继承,object类型足所有类的基类。 (2) 继承是可传递的,如果C从B派生,而B从A派生,那么C就会既继承在B中声明的成员,又继承在A中声明的成员。 (3) 派生类扩展它的直接基类,派生类可以向它继承的成员添加新成员,但足它不能移除继承成员的定义。 (4) 实例构造函数、静态构造函数和析构函数足不可继承的,但所有其他成员足可继承的, 无论它们所声明的可访问性如何,根据它们所声明的可访问性,有些继承成员在派生类中可能是无法访问的。 (5) 派生类可以通过声明具有相冋名称或签名的新成员来隐藏那个被继承的成员,但是,请注意隐藏成员并不移除新成员,它只是使被隐藏的成员在派生类中不可直接访问。 派生类的声明 类修饰符class派生类类名:基类类名{类体}
基类、派生类
生活中有"龙生龙,凤生凤,老鼠天生会打洞",这说的是日常生活中的“继承"现象。类似情形“国民老公--"王思聪"生来就是超级富二代,而一般农民工的子女则相对穷好几个数量级。 C#语言中的继承,通过":"(冒号)来定义继承关系。(类似Java语言的extend关键字)
base,this关键字
Base: 代表父类对象。 This: 代表本类对象。 适用范围: 在子类与父类发生“方法覆盖”时候,为了能够调用被覆盖(或者“隐藏”)的方法,必须使用base .
方法覆盖
使用 new 关键字显示声明发生方法覆盖。
继承关系中构造函数的应用
1>先执行父类的构造方法,再执行子类构造方法。 2>作为良好的编程习惯: 派生类的构造函数在执行初始化时,最好调用基类的构造函数。如果不在派生类显示调用一个基类构造函数,编译器会自动插入对基类的默认构造函数的调用,然后才调用执行派生类构造函数的代码。
类的赋值
子类对象可以赋值给父类对象,但使用中只能使用父类定义的方法
多态
多态是指在基类中定义的属性或服务被派生类继承后,可以具有不同的数据类型或表现出不同的行为。 是指不同的对象收到相同的消总时,会产生不同的动作,而产生一个接口多个方法。它允许以相识的方式对待所有的派生类,尽管这些派生类是各不相同的。 C#多态性包括: 编译时的多态(静态多态):是通过重载类实现的,系统在编译时,根据传递的参数、类型信息决定实现何种操作。包括: 1.方法的重载(方法的名字相同,参数类型不同或者参数的个数不同) 2.运算符的重载 运行时的多态(动态多态):根据实际情况决定实现何种操作,通过虚函数实现。虚方法重载要求方法名称。返回值类型,参数表中的参数个数,类型顺序都必须与基类中的虚函数完全一致 在派生的类中声明对虚方法的重载要求在声明中加上.override关键字,时不能有new,static,virtual
静态多态(编译时)
方法重载
1.1> 方法(函数)签名 参数的数量 参数的类型 参数的顺序 1.2>构造函数重载
运算符重载
运算符是c#中的重要成员。系统对大部分成员都给出了常规定义,这些定义大部分和现实生活中的运算符的意义相冋,可以根据需要给这些运算符赋予一个新的含义,这就足运算符的重钱。 格式如下: 返回值类型 operator 运算符(运算对象列表) { 重载的实现部分; }
动态多态(运行时)
虚函数(方法的重写)
方法重写的目的: 就是可以不断优化代码的一种机制。 2.1> 使用 virtual 与override 关键字. A) 基类(即:父类)不为virtual 的方法,派生类不能使用override 进行重写。 B) 假如派生类(即:子类)不用override 关键字来声明方法,就不会覆盖基类方法, 它只会成为和基类的方法完全不相关的另一个方法的实现。 2.2>使用 sealed/partial 关键字. 密封类表示不能继承该类。 密封方法表示不能重写该方法。可以覆盖父类的密封方法,与父类中的方法是否为密封的没有关系。 2.3> 关于方法重写的部分规则总结 A): 不能重写父类中的sealed方法. B): 不能重写父类中的static 方法. C): 子类必须重写父类中的抽象方法 密封类:在类名前加上sealed,此类为不能继承的类 抽象类:创建一个类,该类中包含不提供具体实现的方法,但在派生类中必须实现的方法叫做抽象方法,含有抽象方法的类叫做抽象类,抽象类包含非抽象方法。抽像类是虚拟可用的。 抽象方法表示方法:[访问问修饰符]abstract 返回类型 方法名{} 抽象类表示方法:[访问修饰符] abstract 类名{} 抽象的方法与虚方法的区别: 1) 虚方法在基类中可以有实现,但抽象方法不能。 2) 虚方法可以在子类中不实现,但抽象方法必须在子类中全部实现。 3) 如果类中有一个柚象方法,这个类就足抽象类,不可以实例化对象。 接口 接口是把所需成员组合起来,以封装一定功能的集合,它是纯抽象类,只能包含抽象方法,而且不包含己实现的方法。 接口声明:[访问修饰符]interface 接口名[:基接口] 抽象类和接口的不同点 1) 一个抽象类必须至少包含一个抽象方法,可以包含具体的方法,接口的方法必须都是抽象的,即不能实现: 2) —个类继承一个抽象类,必须实现抽象方法,一个类要实现接口,她必须能够具体实现该接口包含的所有方法定义 3) 抽象类只能实现单继承,利用接口实现多继承 •
七大原则
开闭原则
定义:软件实体应当对扩展开放,对修改关闭。这句话说得有点专业,更通俗一点讲,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,去扩展新功能。开闭原则中原有“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于代码的修改是封闭的,即不应该修改原有的代码。 问题由来:凡事的产生都有缘由。我们来看看,开闭原则的产生缘由。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。这就对我们的整个系统的影响特别大,这也充分展现出了系统的耦合性如果太高,会大大的增加后期的扩展,维护。为了解决这个问题,故人们总结出了开闭原则。解决开闭原则的根本其实还是在解耦合。所以,我们面向对象的开发,我们最根本的任务就是解耦合。 解决方法:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。 小结:开闭原则具有理想主义的色彩,说的很抽象,它是面向对象设计的终极目标。其他几条原则,则可以看做是开闭原则的实现。我们要用抽象构建框架,用实现扩展细节。
单一职责原则
定义:一个类,只有一个引起它变化的原因。即:应该只有一个职责。 每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则。 问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方法:分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
里氏替换原则
定义:子类型必须能够替换掉它们的父类型。注意这里的能够两字。有人也戏称老鼠的儿子会打洞原则。 问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。 解决方法:类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法 小结:所有引用父类的地方必须能透明地使用其子类的对象。子类可以扩展父类的功能,但不能改变父类原有的功能,即:子类可以实现父类的抽象方法,子类也中可以增加自己特有的方法,但不能覆盖父类的非抽象方法。当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
迪米特原则
定义:迪米特法则又叫最少知道原则,即:一个对象应该对其他对象保持最少的了解。如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。简单定义为只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。 问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。 最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。 解决方法:尽量降低类与类之间的耦合。 自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。 迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系。故过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。中心思想是面向接口编程 问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。 解决方法:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。 在实际编程中,我们一般需要做到如下3点: 1. 低层模块尽量都要有抽象类或接口,或者两者都有。 2. 变量的声明类型尽量是抽象类或接口。 3. 使用继承时遵循里氏替换原则。 采用依赖倒置原则尤其给多人合作开发带来了极大的便利,参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。 小结:依赖倒置原则就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法 解决方法:1、 使用委托分离接口。2、 使用多重继承分离接口。3.将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。 举例说明: 下面我们来看张图,一切就一目了然了。 这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法 修改后: 如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口 小结:我们在代码编写过程中,运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。设计接口的时候,只有多花些时间去思考和筹划,就能准确地实践这一原则。
合成/聚合原则
定义:也有人叫做合成复用原则,及尽量使用合成/聚合,尽量不要使用类继承。换句话说,就是能用合成/聚合的地方,绝不用继承。 为什么要尽量使用合成/聚合而不使用类继承? 1. 对象的继承关系在编译时就定义好了,所以无法在运行时改变从父类继承的子类的实现 2. 子类的实现和它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化 3. 当你复用子类的时候,如果继承下来的实现不适合解决新的问题,则父类必须重写或者被其它更适合的类所替换,这种依赖关系限制了灵活性,并最终限制了复用性。 总结:这些原则在设计模式中体现的淋淋尽致,设计模式就是实现了这些原则,从而达到了代码复用、增强了系统的扩展性。所以设计模式被很多人奉为经典。我们可以通过好好的研究设计模式,来慢慢的体会这些设计原则。
编程概念
反射
反射提供描述程序集、模块和类型的对象(Type 类型)。 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问器字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。 有关更多信息,请参阅特性。 下面一个简单的反射示例,使用方法 GetType()(被 Object 基类的所有类型继承)以获取变量类型: 备注: 请确保在 .cs 文件顶部添加 using System; 和 using System.Reflection; 。 // Using GetType to obtain type information: int i = 42; Type type = i.GetType(); Console.WriteLine(type); 输出为:System.Int32。 下面的示例使用反射获取已加载的程序集的完整名称。 // Using Reflection to get information of an Assembly: Assembly info = typeof(int).Assembly; Console.WriteLine(info); 输出为:System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e。
特性
C# 程序中的类型、成员和其他实体支持使用修饰符来控制其行为的某些方面。 例如,方法的可访问性是由 public、protected、internal 和 private 修饰符控制。 C# 整合了这种能力,以便可以将用户定义类型的声明性信息附加到程序实体,并在运行时检索此类信息。 程序通过定义和使用特性来指定此类额外的声明性信息。 所有特性类都派生自标准库提供的 Attribute 基类。 特性的应用方式为,在相关声明前的方括号内指定特性的名称以及任意自变量。 如果特性的名称以 Attribute 结尾,那么可以在引用特性时省略这部分名称。 例如,可按如下方法使用 HelpAttribute。 [Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/attributes")] public class Widget { [Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/attributes", Topic = "Display")] public void Display(string text) {} }
版本迭代
C#1.0
表达式
语句
类
属性
事件
结构
接口
委托
特性
C#2.0解决C#1.0的问题 泛型
泛型
C#2中最重要的一个特性应该就是泛型。泛型的用处就是在一些场景下可以减少强制转换来提高性能。在C#1中就有很多的强制转换,特别是对一些集合进行遍历时,如ArrayList、HashTable,因为他们是为不同数据类型设计的集合,所以他们中键和值的类型都是object,这就意味着会平凡发生装箱拆箱的操作。C#2中有了泛型,所以我们可以使用List 、Dictionary 。泛型能够带来很好的编译时类型检查,也不会有装箱拆箱的操作,因为类型是在使用泛型的时候就已经指定了。 .NET已经通过了很多的泛型类型供我们使用,如上面提到的List ,Dictionary ,我们也可以自己来创建泛型类型(类、接口、委托、结构)或是方法。在定义泛型类型或时可以通过定义泛型约束来对泛型参数进行限制,更好的使用编译时检查。泛型约束是通过关键字where来实现的,C#2中的泛型约束有4种: 引用类型约束:确保类型实参是引用类型,使用where T:class来表示; 值类型约束:确保类型实参是值类型,使用where T:truct来表示; 构造函数类型约束,使用where T:new()来表示; 转换类型约束:约束类型实参是另外的一种类型,例如:where T:IDisposable 。
部分类型(Partil)
分部类可以允许我们在多个文件中为一个类型(class、struct、interface)编写代码,在Asp.Net2.0中用的极为广泛。新建一个Aspx页面,页面的CodeBehind和页面中的控件的定义就是通过分部类来实现的。如下: public partial class _Default : System.Web.UI.Page public partial class _Default 分部类使用关键字partial来定义,当一个类中的代码非常多时,可以使用分部类来进行拆分,这对代码的阅读很有好处,而且不会影响调用。不过现在我们前后端分离,后端代码要做到单一职责原则,不会有很多大的类,所以这个特性很少用到。
静态类
静态类中的公用方法必须也是静态的,可以由类名直接调用,不需要实例化,比较适用于编写一些工具类。如System.Math类就是静态类。工具类有一些特点,如:所有成员都是静态的、不需要被继承、不需要进行实例化。在C#1中我们可以通过如下代码来实现: //声明为密封类防止被继承 public sealed class StringHelper { //添加私有无参构造函ˉ数防止被实例化,如果不添加私有构造函数 //会自动生成共有无参构造函数 private StringHelper(){}; public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } } C#2中可以使用静态类来实现: public static class StringHelper { public static int StringToInt32(string input) { int result=0; Int32.TryParse(input, out result); return result; } }
属性的访问级别
在C#1中声明属性,属性中的get和set的访问级别是和属性一致,要么都是public要么都是private,如果要实现get和set有不同的访问级别,则需要用一种变通的方式,自己写GetXXX和SetXXX方法。在C#2中可以单独设置get和set的访问级别,如下: private string _name; public string Name { get { return _name; } private set { _name = value; } } 需要注意的是,不能讲属性设置为私有的,而将其中的get或是set设置成公有的,也不能给set和get设置相同的访问级别,当set和get的访问级别相同时,我们可以直接设置在属性上。
命名空间别名
命名空间可以用来组织类,当不同的命名空间中有相同的类时,可以使用完全限定名来防止类名的冲突,C#1中可以使用空间别名来简化书写,空间别名用using关键字实现。但还有一些特殊情况,使用using并不能完全解决,所以C#2中提供了下面几种特性: 命名空间修饰符语法 全局命名空间别名 外部别名 我们在构建命名空间和类的时候,尽量避免出现冲突的情况,这个特性也较少用到。
友元程序集
当我们希望一个程序集中的类型可以被外部的 某些 程序集访问,这时如果设置成Public,就可以被所有的外部程序集访问。怎样只让部分程序集访问,就要使用友元程序集了,具体参考之前的博文《C#:友元程序集(http://blog.fwhyy.com/2010/11/csharp-a-friend-assembly/)》
可空类型
可空类型就是允许值类型的值为null。通常值类型的值是不应该为null的,但我们很多应用是和数据库打交道的,而数据库中的类型都是可以为null值的,这就造成了我们写程序的时候有时需要将值类型设置为null。在C#1中通常使用”魔值“来处理这种情况,比如DateTiem.MinValue、Int32.MinValue。在ADO.NET中所有类型的空值可以用DBNull.Value来表示。C#2中可空类型主要是使用System.Nullable 的泛型类型,类型参数T有值类型约束。可以像下面这样来定义可空类型: Nullable<int> i = 20; Nullable<bool> b = true; C#2中也提供了更方便的定义方式,使用操作符?: int? i = 20; bool? b = true;
迭代器
C#2中对迭代器提供了更便捷的实现方式。提到迭代器,有两个概念需要了解 可枚举对象和枚举器,实现了System.Collections.IEnumerable接口的对象是可枚举对象,这些对象可以被C#中的foreach进行迭代; 实现了System.Collections.IEnumeror接口的对象被称为枚举器。在C#1中实现迭代器非常繁琐, 看下面一个例子: public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { return new PersonEnumerator(this); } private string this[int index] { get { return _names[index]; } set { _names[index] = value; } } } public class PersonEnumerator : IEnumerator { private int _index = -1; private Person _p; public PersonEnumerator(Person p) { _p = p; } public object Current { get { return _p._names[_index]; } } public bool MoveNext() { _index++; return _index < _p._names.Length; } public void Reset() { _index = -1; } } C#2中的迭代器变得非常便捷,使用关键字yield return关键字实现,下面是C#2中使用yield return的重写版本: public class Test { static void Main() { Person arrPerson = new Person("oec2003","oec2004","oec2005"); foreach (string p in arrPerson) { Console.WriteLine(p); } Console.ReadLine(); } } public class Person:IEnumerable { public Person(params string[] names) { _names = new string[names.Length]; names.CopyTo(_names, 0); } public string[] _names; public IEnumerator GetEnumerator() { foreach (string s in _names) { yield return s; } } }
匿名方法
匿名方法比较适用于定义必须通过委托调用的方法,用多线程来举个例子,在C#1中代码如下: private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(DoWork)); thread.Start(); } private void DoWork() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action<string>(this.ChangeLabel),i.ToString()); } } private void ChangeLabel(string i) { label1.Text = i + "/100"; } 使用C#2中的匿名方法,上面的例子中可以省去DoWork和ChangeLabel两个方法,代码如下: private void btnTest_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(delegate() { for (int i = 0; i < 100; i++) { Thread.Sleep(100); this.Invoke(new Action(delegate() { label1.Text = i + "/100"; })); } })); thread.Start(); }
固定缓存区大小
编译指令
委托的协变逆变
C#3.0革新代码的写作方式 LINQ
自动实现的属性
这个特性非常简单,就是使定义属性变得更简单了。代码如下: public string Name { get; set; } public int Age { private set; get; }
隐式类型的局部变量和扩展方法
隐式类型的局部变量是让我们在定义变量时可以比较动态化,使用var关键字作为类型的占位符,然后由编译器来推导变量的类型。 扩展方法可以在现有的类型上添加一些自定义的方法,比如可以在string类型上添加一个扩展方法ToInt32,就可以像“20”.ToInt32()这样调用了。 具体参见《C#3.0学习(1)—隐含类型局部变量和扩展方法(http://blog.fwhyy.com/2008/02/learning-csharp-3-0-1-implied-type-of-local-variables-and-extension-methods/)》。 隐式类型虽然让编码方便了,但有些不少限制: 被声明的变量只能是局部变量,而不能是静态变量和实例字段; 变量在声明的同时必须初始化,初始化值不能为null; 语句中只能声明一个变量;
对象集合初始化器
简化了对象和集合的创建,具体参见《C#3.0学习(2)—对象集合初始化器(http://blog.fwhyy.com/2008/02/learning-c-3-0-2-object-collection-initializer/)》。
隐式类型的数组
和隐式类型的局部变量类似,可以不用显示指定类型来进行数组的定义,通常我们定义数组是这样: string[] names = { "oec2003", "oec2004", "oec2005" }; 使用匿名类型数组可以想下面这样定义: protected void Page_Load(object sender, EventArgs e) { GetName(new[] { "oec2003", "oec2004", "oec2005" }); } public string GetName(string[] names) { return names[0]; }
匿名类型
匿名类型是在初始化的时候根据初始化列表自动产生类型的一种机制,利用对象初始化器来创建匿名对象的对象,具体参见《C#3.0学习(3)—匿名类型(http://blog.fwhyy.com/2008/03/learning-csharp-3-0-3-anonymous-types/)》。
Lambda表达式
实际上是一个匿名方法,Lambda表达的表现形式是:(参数列表)=>{语句},看一个例子,创建一个委托实例,获取一个string类型的字符串,并返回字符串的长度。代码如下: Func<string, int> func = delegate(string s) { return s.Length; }; Console.WriteLine(func("oec2003")); 使用Lambda的写法如下: Func<string, int> func = (string s)=> { return s.Length; }; Func<string, int> func1 = (s) => { return s.Length; }; Func<string, int> func2 = s => s.Length; 上面三种写法是逐步简化的过程。
Lambda表达式树
是.NET3.5中提出的一种表达方式,提供一种抽象的方式将一些代码表示成一个对象树。要使用Lambda表达式树需要引用命名空间System.Linq.Expressions,下面代码构建一个1+2的表达式树,最终表达式树编译成委托来得到执行结果: Expression a = Expression.Constant(1); Expression b = Expression.Constant(2); Expression add = Expression.Add(a, b); Console.WriteLine(add); //(1+2) Func<int> fAdd = Expression.Lambda<Func<int>>(add).Compile(); Console.WriteLine(fAdd()); //3 Lambda和Lambda表达式树为我们使用Linq提供了很多支持,如果我们在做的一个管理系统使用了Linq To Sql,在列表页会有按多个条件来进行数据的筛选的功能,这时就可以使用Lambda表达式树来进行封装查询条件,下面的类封装了And和Or两种条件: public static class DynamicLinqExpressions { public static Expression<Func<T, bool>> True<T>() { return f => true; } public static Expression<Func<T, bool>> False<T>() { return f => false; } public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>()); return Expression.Lambda<Func<T, bool>> (Expression.Or(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>()); return Expression.Lambda<Func<T, bool>> (Expression.And(expr1.Body, invokedExpr), expr1.Parameters); } } 下面是获取条件的方法: public Expression<Func<Courses, bool>> GetCondition() { var exp = DynamicLinqExpressions.True<Courses>(); if (txtCourseName.Text.Trim().Length > 0) { exp = exp.And(g => g.CourseName.Contains(txtCourseName.Text.Trim())); } if (ddlGrade.SelectedValue != "-1") { exp=exp.And(g => g.GradeID.Equals(ddlGrade.SelectedValue)); } return exp; }
LINQ
Linq是一个很大的话题,也是NET3.5中比较核心的内容,有很多书籍专门来介绍Linq,下面只是做一些简单的介绍,需要注意的是Linq并非是Linq To Sql,Linq是一个大的集合,里面包含: Linq To Object:提供对集合和对象的处理; Linq To XML:应用于XML; Linq To Sql:应用于SqlServer数据库; Linq To DataSet: DataSet; Linq To Entities:应用于SqlServer之外的关系数据库,我们还可以通过Linq的扩展框架来实现更多支持Linq的数据源。 下面以Linq To Object为例子来看看Linq是怎么使用的: public class UserInfo { public string Name { get; set; } public int Age { get; set; } } public class Test { static void Main() { List<UserInfo> users = new List<UserInfo>() { new UserInfo{Name="oec2003",Age=20}, new UserInfo{Name="oec2004",Age=21}, new UserInfo{Name="oec2005",Age=22} }; IEnumerable<UserInfo> selectedUser = from user in users where user.Age > 20 orderby user.Age descending select user; foreach (UserInfo user in selectedUser) { Console.WriteLine("姓名:"+user.Name+",年龄:"+user.Age); } Console.ReadLine(); } } 可以看出,Linq可以让我们使用类似Sql的关键字来对集合、对象、XML等进行查询。
C#4.0良好的交互性
可选参数
VB在很早就已经支持了可选参数,而C#知道4了才支持,顾名思义,可选参数就是一些参数可以是可选的,在方法调用的时候可以不用输入。看下面代码: public class Test { static void Main() { Console.WriteLine(GetUserInfo()); //姓名:ooec2003,年龄:30 Console.WriteLine(GetUserInfo("oec2004", 20));//姓名:ooec2004,年龄:20 Console.ReadLine(); } public static string GetUserInfo(string name = "oec2003", int age = 30) { return "姓名:" + name + ",年龄:" + age.ToString(); } }
命名参数
命名实参是在制定实参的值时,可以同时指定相应参数的名称。编译器可以判断参数的名称是否正确,命名实参可以让我们在调用时改变参数的顺序。命名实参也经常和可选参数一起使用,看下面的代码: static void Main() { Console.WriteLine(Cal());//9 Console.WriteLine(Cal(z: 5, y: 4));//25 Console.ReadLine(); } public static int Cal(int x=1, int y=2, int z=3) { return (x + y) * z; } 通过可选参数和命名参数的结合使用,我们可以减少代码中方法的重载。
动态类型
C#使用dynamic来实现动态类型,在没用使用dynamic的地方,C#依然是静态的。静态类型中当我们要使用程序集中的类,要调用类中的方法,编译器必须知道程序集中有这个类,类里有这个方法,如果不能事先知道,编译时会报错,在C#4以前可以通过反射来解决这个问题。看一个使用dynamic的小例子: dynamic a = "oec2003"; Console.WriteLine(a.Length);//7 Console.WriteLine(a.length);//string 类型不包含length属性,但编译不会报错,运行时会报错 Console.ReadLine(); 您可能会发现使用dynamic声明变量和C#3中提供的var有点类似,其他他们是有本质区别的,var声明的变量在编译时会去推断出实际的类型,var只是相当于一个占位符,而dynamic声明的变量在编译时不会进行类型检查。 dynamic用的比较多的应该是替代以前的反射,而且性能有很大提高。假设有一个名为DynamicLib的程序集中有一个DynamicClassDemo类,类中有一个Cal方法,下面看看利用反射怎么访问Cal方法: namespace DynamicLib { public class DynamicClassDemo { public int Cal(int x = 1, int y = 2, int z = 3) { return (x + y) * z; } } } static void Main() { Assembly assembly = Assembly.Load("DynamicLib"); object obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Type type = obj.GetType(); MethodInfo method = type.GetMethod("Cal"); Console.WriteLine(method.Invoke(obj, new object[] { 1, 2, 3 }));//9 Console.ReadLine(); } 用dynamic的代码如下: Assembly assembly = Assembly.Load("DynamicLib"); dynamic obj = assembly.CreateInstance("DynamicLib.DynamicClassDemo"); Console.WriteLine(obj.Cal()); Console.ReadLine(); 在前后端分离的模式下,WebAPI接口的参数也可以采用dynamic来定义,直接就可以解析前端传入的json参数,不用每一个接口方法都定义一个参数类型。不好的地方就是通过Swagger来生产API文档时,不能明确的知道输入参数的每个属性的含义。 C#4中还有一些COM互操作性的改进和逆变性和协变性的改进,我几乎没有用到,所以在此就不讲述了。
C#5.0简化的异步编程 异步async/await
异步处理
异步处理是C#5中很重要的一个特性,会涉及到两个关键字:async和await,要讲明白这个需要单独写一篇来介绍。 可以简单理解为,当Winform窗体程序中有一个耗时操作时,如果是同步操作,窗体在返回结果之前会卡死,当然在C#5之前的版本中有多种方法可以来解决这个问题,但C#5的异步处理解决的更优雅。 public async Task<int> ExampleMethodAsync() { // . . . . await responseStream.CopyToAsync(content); using (WebResponse response = await webReq.GetResponseAsync()) } private static async Task<int> CountCharactersAsync(int id, string address) { var wc = new WebClient(); Console.WriteLine($"开始调用 id = {id}:{Watch.ElapsedMilliseconds} ms"); var result = await wc.DownloadStringTaskAsync(address); Console.WriteLine($"调用完成 id = {id}:{Watch.ElapsedMilliseconds} ms"); return result.Length; }
循环中捕获变量
与其说是一个特性,不如说是对之前版本问题的修复,看下面的代码: public static void CapturingVariables() { string[] names = { "oec2003","oec2004","oec2005"}; var actions = new List<Action>(); foreach(var name in names) { actions.Add(() => Console.WriteLine(name)); } foreach(Action action in actions) { action(); } } 这段代码在之前的C#版本中,会连续输出三个oec2005,在C#5中会按照我们的期望依次输出oec2003、oec2004、oec2005。 如果您的代码在之前的版本中有利用到这个错误的结果,那么在升级到C#5或以上版本中就要注意了。
调用者信息特征
我们的程序通常是以release形式发布,发布后很难追踪到代码执行的具体信息,在C#5中提供了三种特性(Attribute), 允许获取调用者的当前编译器的执行文件名、所在行数与方法或属性名称。代码如下: static void Main(string[] args) { ShowInfo(); Console.ReadLine(); } public static void ShowInfo( [CallerFilePath] string file = null, [CallerLineNumber] int number = 0, [CallerMemberName] string name = null) { Console.WriteLine($"filepath:{file}"); Console.WriteLine($"rownumber:{number}"); Console.WriteLine($"methodname:{name}"); } 调用结果如下: filepath:/Users/ican_macbookpro/Projects/CsharpFeature/CsharpFeature5/Program.cs rownumber:12 methodname:Main
C#6.0
Null条件运算符
在C#中,一个常见的异常就是“未将对象引用到对象的实例”,原因是对引用对象没有做非空判断导致。在团队中虽然再三强调,但依然会在这个问题上栽跟头。下面的代码就会导致这个错误: class Program { static void Main(string[] args) { //Null条件运算符 User user = null; Console.WriteLine(user.GetUserName()); Console.ReadLine(); } } class User { public string GetUserName() => "oec2003"; } 要想不出错,就需要对user对象做非空判断 if(user!=null) { Console.WriteLine(user.GetUserName()); } 在C#6中可以用很简单的方式来处理这个问题 //Null条件运算符 User user = null; Console.WriteLine(user?.GetUserName()); 注:虽然这个语法糖非常简单,也很好用,但在使用时也需要多想一步,当对象为空时,调用其方法返回的值也是空,这样的值对后续的操作会不会有影响,如果有,还是需要做判断,并做相关的处理。
字符串嵌入
字符串嵌入可以简化字符串的拼接,很直观的就可以知道需要表达的意思,在C#6及以上版本中都应该用这种方式来处理字符串拼接,代码如下: //字符串嵌入 string name = "oec2003"; //之前版本的处理方式1 Console.WriteLine("Hello " + name); //之前版本的处理方式2 Console.WriteLine(string.Format("Hello {0}",name)); //C#6字符串嵌入的处理方式 Console.WriteLine($"Hello {name}");
只读自动属性
自动属性初始化表达式
using static
nameof 表达式
异常筛选器
使用索引器初始化索引
C#7.0
out变量
此特性简化了out变量的使用,之前的版本中使用代码如下: int result = 0; int.TryParse("20", out result); Console.WriteLine(result); 优化后的代码,不需要事先定义一个变量 int.TryParse("20", out var result); Console.WriteLine(result);
模式匹配
这也是一个减少我们编码的语法糖,直接看代码吧 public class PatternMatching { public void Test() { List<Person> list = new List<Person>(); list.Add(new Man()); list.Add(new Woman()); foreach (var item in list) { //在之前版本中此处需要做类型判断和类型转换 if (item is Man man) Console.WriteLine(man.GetName()); else if (item is Woman woman) Console.WriteLine(woman.GetName()); } } } public abstract class Person { public abstract string GetName(); } public class Man:Person { public override string GetName() => "Man"; } public class Woman : Person { public override string GetName() => "Woman"; } 详细参考官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/pattern-matching
本地方法
可以在方法中写内部方法,在方法中有时需要在多个代码逻辑执行相同的处理,之前的做法是在类中写私有方法,现在可以让这个私有方法写在方法的内部,提高代码可读性。 static void LocalMethod() { string name = "oec2003"; string name1 = "oec2004"; Console.WriteLine(AddPrefix(name)); Console.WriteLine(AddPrefix(name1)); string AddPrefix(string n) { return $"Hello {n}"; } }
异步mian方法
这个最大的好处是,在控制台程序中调试异步方法变得很方便。 static async Task Main() { await SomeAsyncMethod(); }
private protected 访问修饰符
可以限制在同一个程序集中的派生类的访问,是对protected internal的一种补强,protected internal是指同一程序集中的类或派生类进行访问。
其他相关特性
元组优化 弃元 Ref局部变量和返回结果 通用的异步返回类型 数字文本语法改进 throw表达式 默认文本表达式 推断元组元素名称 非尾随命名参数 数值文字中的前导下划线 条件ref表达式
C#8.0
可空引用类型
引用类型将会区分是否可空,可以从根源上解决 NullReferenceException。 #nullable enable void M(string? s) { Console.WriteLine(s.Length); // 产生警告:可能为 null if (s != null) { Console.WriteLine(s.Length); // Ok } } #nullable disable
异步流
考虑到大部分 Api 以及函数实现都有了对应的 async版本,而 IEnumerable<T>和 IEnumerator<T>还不能方便的使用 async/await就显得很麻烦了。 但是,现在引入了异步流,这些问题得到了解决。 我们通过新的 IAsyncEnumerable<T>和 IAsyncEnumerator<T>来实现这一点。同时,由于之前 foreach是基于IEnumerable<T>和 IEnumerator<T>实现的,因此引入了新的语法await foreach来扩展 foreach的适用性。 async Task<int> GetBigResultAsync() { var result = await GetResultAsync(); if (result > 20) return result; else return -1; } async IAsyncEnumerable<int> GetBigResultsAsync() { await foreach (var result in GetResultsAsync()) { if (result > 20) yield return result; } }
范围和下标类型
C# 8.0 引入了 Index 类型,可用作数组下标,并且使用 ^ 操作符表示倒数。 不过要注意的是,倒数是从 1 开始的。 Index i1 = 3; // 下标为 3 Index i2 = ^4; // 倒数第 4 个元素 int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6" 除此之外,还引入了 “..” 操作符用来表示范围(注意是左闭右开区间)。 var slice = a[i1..i2]; // { 3, 4, 5 } 关于这个下标从 0 开始,倒数从 1 开始,范围左闭右开。
模式匹配表达式
典型的模式匹配语句,只不过没有用“match”关键字,而是沿用了了“switch”关键字 object figure = ""; var area = figure switch { Line _ => 0, Rectangle r => r.Width * r.Height, Circle c => c.Radius * 2.0 * Math.PI, _ => throw new UnknownFigureException(figure) }; C# 8.0中的模式匹配相对C# 7.0来说有了进一步的增强,对于如下类: class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } 首先来看C# 7.0中一个经典的模式匹配示例: static string Display(object o) { switch (o) { case Point p when p.X == 0 && p.Y == 0: return "origin"; case Point p: return $"({p.X}, {p.Y})"; default: return "unknown"; } } 在C# 8.0中,它有更加精简的写法。
Switch表达式
在C# 8.0中,可以利用新的switch方式成模式匹配: static string Display(object o) => o switch { Point p when p.X == 0 && p.Y == 0 => "origin", Point p => $"({p.X}, {p.Y})", _ => "unknown" }; 它利用一条switch语句完成了模式匹配,第一样看上去要简洁一些。不过,它还有更多更简单的写法。
Property patterns
可以直接通过在属性上指定值作为判定条件, static string Display(object o) => o switch { Point { X: 0, Y: 0 } => "origin", Point p => $"({p.X}, {p.Y})", _ => "unknown" }; 也可以将属性值传递出来。 static string Display(object o) => o switch { Point { X: 0, Y: 0 } => "origin", Point { X: var x, Y: var y } => $"({x}, {y})", _ => "unknown" };
Positional parrerns
利用解构函数,可以写出更加精简的表达式。 static string Display(object o) => o switch { Point(0, 0) => "origin", Point(var x, var y) => $"({x}, {y})", _ => "unknown" }; 如果没有类型转换,则可以写得更加简单了: static string Display(Point o) => o switch { (0, 0) => "origin", (var x, var y) => $"({x}, {y})" };
非空判断
如果只是判断空和非空,则有最简单的模式: { } => o.ToString(), null => "null"
Tuple patterns
也支持直接对ValueTuple进行模式匹配,用起来非常灵活。 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition, hasKey) switch { (Opened, Close, _) => Closed, (Closed, Open, _) => Opened, (Closed, Lock, true) => Locked, (Locked, Unlock, true) => Closed, _ => throw new InvalidOperationException($"Invalid transition") };
递归模式语句
现在可以这么写了(patterns 里可以包含 patterns) IEnumerable<string> GetEnrollees() { foreach (var p in People) { if (p is Student { Graduated: false, Name: string name }) yield return name; } }