异常是在程序执行期间出现的问题。C# 中的异常是对程序运行时出现的特殊情况的一种响应,比如尝试除以零。
C# 异常处理有四个关键词:try、catch、finally 和 throw。
try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。
throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
C# 中的异常类主要是直接或间接地派生于 System.Exception 类。
System.IO.IOException 处理 I/O 错误。
System.IndexOutOfRangeException 处理当方法指向超出范围的数组索引时生成的错误。
System.ArrayTypeMismatchException 处理当数组类型不匹配时生成的错误。
System.NullReferenceException 处理当依从一个空对象时生成的错误。
System.DivideByZeroException 处理当除以零时生成的错误。
System.InvalidCastException 处理在类型转换期间生成的错误。
System.OutOfMemoryException 处理空闲内存不足生成的错误。
System.StackOverflowException 处理栈溢出生成的错误。
创建用户自定义异常
System.ApplicationException 类支持由应用程序生成的异常。所以程序员定义的异常都应派生自该类
System.SystemException 类是所有预定义的系统异常的基类
C# 预处理器指令:
预处理器指令指导编译器在实际编译开始之前对信息进行预处理。
预处理器指令都是以 # 开始,预处理器指令不是语句,所以它们不以分号(;)结束。一个预处理器指令必须是该行上的唯一指令。
C# 预处理器指令
#define 它用于定义一系列成为符号的字符。
#undef 它用于取消定义符号。
#if 它用于测试符号是否为真。
#else 它用于创建复合条件指令,与 #if 一起使用。
#elif 它用于创建复合条件指令。
#endif 指定一个条件指令的结束。
#line 它可以让您修改编译器的行数以及(可选地)输出错误和警告的文件名。
#error 它允许从代码的指定位置生成一个错误。
#warning 它允许从代码的指定位置生成一级警告。
#region 它可以让您在使用 Visual Studio Code Editor 的大纲特性时,指定一个可展开或折叠的代码块。
#endregion 它标识着 #region 块的结束。
#define和#undef
#define可以看做是声明的一个变量,但此变量没有真正的值,仅存在。 #define单独用没什么意义,一般是和#if结合使用。
#undef作用就是删除#define的定义
#define与#undef声明必须放在C#源文件的开头位置,即程序集的引用的上方。
#if,#elif,#else和#endif
#elif(=else if)和#else指令可以用在#if中,和C#中的if,else if,else含义相同。
#if和#elif支持一组逻辑运算符"!","==","!="和"||",如果符号存在,则为true。
#warning和#error
当编译器遇到这两条指令时,会分别产生警告和错误。如果编译器遇到#warning指令,会显示该指令后的文本,之后继续编译。
如果遇见#error指令,也会显示指令后面的文本。但会立刻退出编译,不会产生IL代码。(其实和编译器的警告和错误意义相同)
#region和#endregion
代码缩进和指定该代码块的名称,使得代码可以更好的布局
#line
这条指令很少用到。作用就是:如果代码在编译之前,要使用某些软件包改变输入的代码,就可以使用它。
(其实就是更改代码的行号)
C# 索引器(Indexer)
索引器(Indexer) 允许一个对象可以像数组一样被索引。
语法:访问修饰符 类型 this[int index]
{
get
{
// 返回 index 指定的值
}
set
{
// 设置 index 指定的值
}
}
索引器的行为的声明在某种程度上类似于属性(property)。就像属性(property),您可使用 get 和 set 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。
索引器(Indexer)重载
索引器(Indexer)可被重载。索引器声明的时候也可带有多个参数,且每个参数可以是不同的类型。没有必要让索引器必须是整型的。C# 允许索引器可以是其他类型
C#可空类型
int? a = null;
int? b = 10;
int c = a ?? 5;
Console.WriteLine(c);
c = b ?? 5;
Console.WriteLine(c);
不安全的代码
当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块。
指针变量:内存位置的直接地址,在使用指针存储其他变量地址之前声明指针。
int *ip; /* 指向一个整数 */
double *dp; /* 指向一个双精度数 */
float *fp; /* 指向一个浮点数 */
char *ch /* 指向一个字符 */
fixed关键字
由于C#中声明的变量在内存中的存储受垃圾回收器管理;因此一个变量(例如一个大数组)有可能在运行过程中被移动到内存中的其他位置。如果一个变量的内存地址会变化,那么指针也就没有意义了。
解决方法就是使用fixed关键字来固定变量位置不移动。
static unsafe void Main(string[] args)
{
fixed(int *ptr = int[5]) {//...}
}
stackalloc
在unsafe不安全环境中,我们可以通过stackalloc在堆栈上分配内存,因为在堆栈上分配的内存不受内存管理器管理,因此其相应的指针不需要固定。
static unsafe void Main(string[] args)
{
int *ptr = stackalloc int[1] ;
}
使用指针访问数组元素
在 C# 中,数组名称和一个指向与数组数据具有相同数据类型的指针是不同的变量类型。例如,int *p 和 int[] p 是不同的类型。可以增加指针变量 p,因为它在内存中不是固定的,但是数组地址在内存中是固定的,所以不能增加数组 p。因此,如果需要使用指针变量访问数组数据,可以像在 C 或 C++ 中所做的那样,使用 fixed 关键字来固定指针。
C# 泛型(Generic)
泛型(Generic) 允许编写一个可以与任何数据类型一起工作的类或方法,通过数据类型的替代参数编写类或方法的规范,编译器遇到类的构造函数或方法的函数调用时,它会生成代码来处理指定的数据类型。
泛型(Generic)的特性:
有助于最大限度地重用代码、保护类型的安全以及提高性能。
可以创建泛型集合类。.NET 框架类库在 System.Collections.Generic 命名空间中包含了一些新的泛型集合类。可以使用这些泛型集合类来替代 System.Collections 中的集合类。
可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
可以对泛型类进行约束以访问特定数据类型的方法。
关于泛型数据类型中使用的类型的信息可在运行时通过使用反射获取。
泛型(Generic)方法:static void Function<T>(T data){}
函数签名由函数的名称和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。
泛型方法的重载,重写:当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。方法签名的识别规则。
泛型(Generic)委托:delegate void MyDele<T>(T t);
泛型(Generic)接口:interface IFace<T>{ }
泛型(Generic)的优点:提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型。
泛型约束:定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的几种类型施加限制。 如果客户端代码尝试使用约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 通过使用 where 上下文关键字指定约束。
泛型约束:定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的几种类型施加限制。 如果客户端代码尝试使用约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 通过使用 where 上下文关键字指定约束。
C#协变和抗变
与原始类型转换方向相同的可变性就称作协变(covariant):可以理解成:父类 -> 子类。父类的对象用子类替换,也可以理解成子类当父类用。
一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!:可以理解成:子类 -> 父类。子类的对象用父类替换,也可以理解成父类当子类用。抗变也常常翻译为逆变。
之前学习的哪些属于协变,哪些属于抗变?
为什么要有协变、逆变?
通常只有具备继承关系的对象才可以发生隐式类型转换,协变和逆变可以使得更多的类型之间能够实现隐式类型转换、类型安全性有了保障。
泛型接口和泛型委托
.Net 4.0之后,支持协变和抗变的有两种类型:泛型接口和泛型委托。
泛型接口:修饰符out支持对类型T的协变,修饰符in支持对类型T的抗变。
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变。例如Func<int,string>,类型参数string这个参数类型支持协变,对int这个参数类型支持抗变。
如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
泛型委托与泛型接口基本一致??
C# 特性(Attribute)
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。
[attribute(positional_parameters, name_parameter = value, ...)]
特性(Attribute)的名称和值是在方括号内规定的,放置在它所应用的元素之前。positional_parameters 规定必需的信息,name_parameter 规定可选的信息。
预定义特性(Attribute)
.Net 框架提供了三种预定义特性:AttributeUsage,Conditional,Obsolete
AttributeUsage:预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。
Conditional
这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符
它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。
[Conditional("DEBUG")]
Obsolete
这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。
[Obsolete(
message
)]
[Obsolete(
message,
iserror
)]
参数 message,是一个字符串,描述项目为什么过时的原因以及该替代使用什么。
参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
调用者信息特性
调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息
这三个特性名称为CallerFilePath、CallerLineNumber和CallerMemberName这些特性只能用于方法中的可选参数
DebuggerStepThrough 特性
在单步调试代码时,如果希望调试器不要进入某些方法。只想执行该方法,然后继续调试下一行。DebuggerStepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。 在自己的代码中,这是最常使用的特性。有些方法很小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。但使用该特性时要十分小心,因为你并不想排除那些可能含有bug的代码。
关于DebuggerStepThrough要注意以下两点:
1.该特性位于System.Diagnostics命名空间
2.该特性可用于类、结构、构造函数、方法或访问器
创建自定义特性:
特性类的一些要点如下:用户自定义的特性类叫做自定义特性,所有特性类都派生自System.Attribute。
创建并使用自定义特性包含四个步骤:
声明自定义特性
构建自定义特性
在目标程序元素上应用自定义特性
通过反射访问特性
一个新的自定义特性应派生自 System.Attribute 类。:public class DeBugInfo : System.Attribute
给它起一个以后缀Attribute结尾的名字。
安全起见,通常建议你声明一个sealed的特性类(sealed密封类,不能被继承)
public sealed class MyAttributeAttribute : System.Attribute{…}
由于特性持有目标的信息,所有特性类的公共成员只能是:字段,属性,构造函数
特性和其他类一样,都有构造函数。每一个特性至少必须有一个公共构造函数。
和其他类一样,如果你不声明构造函数,编译器会为我们产生一个隐式、公共且无参的构造函数,特性的构造函数和其他构造函数一样,可以被重载,声明构造函数时必须使用类全名,包括后缀。我们只可以在应用特性时使用短名称
构建一个自定义特性,该特性将存储调试程序获得的信息.
构造函数的使用:普通类:myclass m = new myclass();命令语句"在这里创建新的类"。特性类: [MyAttribute("Holds a value")];声明语句"这个特性和这个目标相关联,如果需要构造特性,使用这个构造函数"
构造函教需要的任何位置参数都必须放在命名参数之前。
限制特性的使用:有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性。
应用该特性。
在应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式
如果应用的特性构造函数没有参数,可以省略圆括号。