@
讯享网

- Write没有在字符串后面添加换行符,而WriteLine则在每个字符串后面添加了换行符。如图:


- 从命令行读取和输入的例子:
讯享网

params是一个计算机函数,表示函数的参数是可变个数的,即可变的方法参数,用于表示类型相同,但参数数量不确定。
C#开发语言中 params 是关键字,params主要的用处是在给函数传参数的时候用,就是当函数的参数不固定的时候。 在函数的参数数目可变而执行的代码差异很小的时候很有用!C#语法规定,params后边必定跟数组。作用是把不定数量的、同类型的参数装入这个数组中.
(1)托管代码:
运行在CLR(CLR是一个通用语言架构,它定义了一个代码运行的环境)下的代码就是托管代码,诸如C#、VB.NET 写的代码都会先编译成MSIL(MS中间代码),并运行在CLR的子集CLI(Common Language Infrastructure)中,最终根据不同的平台使用JIT(just in Time)编译成机器代码。
与Java机制不同在于Java是经过一次编译和一次解释运行,C#是经过两次编译运行,这两个阶段分别为:源代码编译为托管代码,托管代码编译为微软平台的专用语言,又称机器语言。
(2)非托管代码:
非托管的代码也叫本地代码(native),是由操作系统管理的。
高级语言编写的程序必须经过一定的步骤编译为机器语言才能被机器理解和运行。
这一系列步骤为:预处理、编译、汇编、链接。
(3)托管代码和非托管代码的区别:
1、托管代码是一种中间语言,运行在CLR上;非托管代码被编译为机器码,运行在机器上。
2、托管代码独立于平台和语言,能更好的实现不同语言平台之间的兼容;非托管代码依赖于平台和语言。
3、托管代码可享受CLR提供的服务(如安全检测、垃圾回收等),不需要自己完成这些操作;非托管代码需要自己提供安全检测、垃圾回收等操作。
console.ReadLine()是等待输入,并按回车继续。
console.WriteLine()是向控制台窗口产生输出。
Console.ReadKey()是等待键盘输入,退出程序。使调试时能看到输出结果。如果没有此句,命令窗口会一闪而过。
在C#语言中,共有五种访问修饰符:public、private、protected、internal、protected internal。
(1)作用范围:

(2)在c#中,
- 类、结构的默认修饰符是internal。
- 类中所有的成员默认修饰符是private。
- 接口默认修饰符是internal。
- 接口的成员默认修饰符是public。
- 命名空间、枚举类型成员默认修饰符是public。
- 委托的默认修饰符是internal。
从某一个类型模板创建实际的对象,就称为实例化该类型。
某些类型变量的值;
程序当前的执行环境;
传递给方法的参数。
栈是一个内存数组,是一个LIFO(Last-In First-Out,后进先出)的数据结构。
堆和栈的区别主要有五大点,分别是:
(1)申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;
(2)申请大小的不同。栈获得的空间较小,而堆获得的空间较大;
(3)申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;
(4)存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;
(5)底层不同。栈是连续的空间,而堆是不连续的空间。
数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。
类型被分为两种:值类型和引用类型,这两种类型的对象在内存中的存储方式不同。
值类型只需要一段单独的内存,用于存储实际的数据。
引用类型需要两段内存,第一段存储实际的数据,它总是位于堆中;第二段是一个引用,指向数据在堆中的存放位置。
对于值类型,数据存放在栈里;对于引用类型,实际数据存放在堆里而引用存放在栈里。

分别是本地变量、字段、参数、数组元素。
本地变量:在方法的作用域保存临时数据,不是类型的成员。
字段:保存和类型或类型实例相关的数据,是类型的成员。
参数:用于从一个方法到另一个方法传递数据的临时变量,不是类型的成员。
数组元素:(通常是)同类数据项构成的有序集合的一个成员,可以为本地变量,也可以为类型的成员。
一个简单的变量声明至少需要一个类型和一个名称,如int a,其中int为类型,a为名称。
一句话:字段、属性都是变量,只是为了区分和数据安全设置的。
字段的使用场景:与类或者对象关系密切,建议使用private修饰。
属性的使用场景:对字段进行封装,提供get/set关键字,进行访问。
变量的使用场景:与类或者对象关系不密切,常常在方法或者语句块中使用。
字段和属性是相对于类而言的,而变量相对于方法或者语句块而言,可以在任何地方使用。

虽然常量成员表现得像一个静态量,但不能将常量声明为静态static,如:static const int a=3.14这句语法是错误的。
const是静态常量,readonly是动态常量。
静态常量是指编译器在编译时候会对常量进行解析,并将常量的值替换成初始化的那个值;而动态常量的值则是在运行的那一刻才获得的,编译器编译期间将其标示为只读常量,而不用常量的值代替,这样动态常量不必在声明的时候就初始化,而可以延迟到构造函数中初始化。
区别:可以通过静态常量与动态常量的特性来说明:
1)const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化 。
2)const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候。
此外const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类中。
1. this指代当前实例
this关键字在类中使用,是对当前实例的引用,它只能被用在下列类成员的代码块中:
(1)实例构造函数;
(2)实例方法;
(3)属性和索引器的实例访问器。
值得注意的是,静态成员并不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字,确切来说,this用于下列目的:
用于区分类的成员和本地变量或参数;
作为调用方法的实参。
2. this用作扩展方法
扩展方法能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。C#扩展方法第一个参数指定该方法作用于哪个类型,并且该参数以 this 修饰符为前缀。
需要注意的几点:
(1)扩展方法(this 需要扩展的类 命名),如:public static void ExtensionEat(this Person person);
(2)扩展方法必须是静态类的一个静态方法。
(3)调用扩展方法,必须用对象来调用 。
如图看一段代码:
运行结果:

3. this用作索引器:
讯享网
索引器的索引值(Index)类型不限定为整数,如上两个图代码中的整形和字符串型索引。
索引器与属性的区别:
(1)索引器的命名只能为this,而属性可以任意命名(开头字母大写);
(2)索引器可以重载,属性不可以;
(3)索引器不可用static进行声明,属性可以。
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽基类成员,这也是继承的主要功能之一,很实用。派生类屏蔽基类成员的关键字是new,在派生类中屏蔽一些基类成员的一些要点如下:
(1)要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
(2)通过在派生类中声明新的带有相同签名的函数成员,可以隐藏或屏蔽继承的基类的函数成员。注意:签名由名称和参数列表组成,不包括返回类型。
(3)要让编译器知道你在故意屏蔽继承的成员,实用new修饰符。否则,程序会成功编译,但会警告你隐藏了一个继承的成员。
(4)也可以屏蔽静态成员。
当使用基类引用访问派生类对象时,得到的是基类的成员。虚方法可以使基类的引用访问“升至”派生类级别。可以使用基类引用调用派生类的方法,只需要满足以下条件:
(1)派生类的方法和基类的方法拥有相同的签名和返回类型;
(2)基类的方法使用virtual标注;
(3)派生类的方法使用override标注。
可对比以下两图:



如,一个简单的继承类程序:
讯享网
运行结果:

而另外一种形式的构造函数初始化语句(通过this)可以让编译器使用该类中的其他构造函数。
readonly字段只可以在构造函数中初始化,如果在其他方法中初始化一个readonly字段(即使这个方法只被构造函数调用),会得到一个编译错误。

编译器可以通过初始化语句提供的信息推断出数据类型。
(1)传统定义变量是已经知道变量的类型,如: int a = 1; string b = “hello”;char c='x';
而var关键字不用预先知道变量的类型,是根据给变量赋值来判定变量属于什么类型;如var a =1, 则a是整型;var b = “hello”,则b是字符型;var c='x',则c也是字符型。
(2)简言之,var可以代替任何类型,编辑器会根据上下文来判断使用者具体想用什么类型,当无法确定自己将使用什么类型时,就可以使用var。
(3)使用var关键字有一些重要的条件:
- 必须在定义时初始化。必须是var a=“abc”的形式,不能是var a; a=“abc”的形式;
- 只能用于本地变量,不能用于字段;
- 只能在变量声明中包含初始化时使用;
- 一旦编译器推断除变量的类型,它就是固定且不能更改的。
当值参数为值类型时,值被复制,产生一个独立的数据项,当值参数为引用类型时,引用被复制,实参和形参都引用堆中的同一个对象。
使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。
引用类型作为值参数和引用参数:
(1)将引用类型对象作为值参数传递 :如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束之后,新对象也将不复存在。
(2)将引用类型作为引用参数传递 :如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
引用参数ref修饰的参数必须对其赋初值,但是初值不能是常量(即不能用const修饰),因为按引用传递可能会改变参数的值。在函数使用out参数时,必须看做是尚未赋值(不晓得为什么),实参传递给形参的值在函数执行时会丢失,参考以下代码段:



抽象类是指设计为被继承的类,抽象类只能被用作其他类的基类。
(1)不能创建抽象类的实例;
(2)抽象类必须使用abstract修饰符来声明,如下图:

(3)抽象类可以包含抽象成员和普通的非抽象成员,即抽象类的成员可以是抽象成员和普通成员的任意组合。
(4)抽象类自己可以派生另外一个抽象类,如下图:

(5)任何派生自抽象类的类必须使用override关键字实现该类的所有抽象成员,除非派生类自己也是抽象类。
看一段代码:


静态类中所有的成员都是静态的,静态类用于存放不受实例数据影响的函数或数据。静态类的一个常见用途就是创建一个包含一组数学方法和值的数学库。
关于静态类需要注意的几个地方:
(1)类本身必须被标记为static;
(2)类的所有成员必须是静态的;
(3)类可以有一个静态构造函数,但不能有实例构造函数,不能创建该类的实例;
(4)静态类是隐式封装的,也就是说,不能继承静态类。
讯享网

结构和类非常相似,都有数据成员和函数成员,最重要的区别是:
(1)类是引用类型,而结构是值类型;
(2)结构是隐式封装的,这意味着他们不能被派生。
结构也可以有实例构造函数和静态构造函数,但不能有析构函数。
static修饰的静态字段被类的所有实例共享,所有实例都访问同一内存位置。如果该内存位置的值被一个实例改变了,这种变化对于所有的实例都改变。


(1)枚举是一个值类型, 包含一组命名的常量, 枚举类型用 enum 关键字定义。如,声明一个简单的枚举类:

(2)默认情况下, enum 的类型是 int。
(3)动态获得枚举类型的信息:
Enum.GetNames:该方法返回一个包含所有枚举名的字符串数组,如下图:
Enum.GetValues:为了获得枚举的所有值, 可以使用 Enum.GetValues 。 Enum.GetValues 返回枚举值的一个数组。 为了获得整数值, 需要把它转换为枚举的底层类型, 为此应使用 foreach语句,如下图:

(4)Flags特性:没理解(后续学习)
(1)C#不支持动态数组,也就是说,数组一旦被创建,大小就固定了。
(2)C#中的各种数组:

(3)数组中一些可用的方法和属性:

自己以前写过的委托笔记
委托就是可以用方法名调用另一方法的便捷方法,可以理解为一个”命令”。
委托是一种用户自定义的类型,它也是引用类型,委托持有一个或者多个方法,以及一些列预定义操作。
1.delegate委托
先看一代码:
讯享网
运行结果:


因此使用委托的步骤:
(1)声明一个委托类型;
(2)使用该委托类型声明一个委托变量;
讯享网
(3)创建委托类型的变量,把它赋值给委托变量。新的委托对象包含指向某个方法的引用,这个方法和第一步定义的签名和返回类型保持一致;
讯享网
再看一段代码,稳固下委托的使用步骤:
运行结果:

调用带有返回值的委托:

如:
讯享网
运行结果:

调用带有引用参数的委托:

2.Action委托
Action委托签名不提供返回类型(也就是说Action委托是没有返回值的),它具有Action、Action<T1,T2>、Action<T1,T2,T3>……Action<T1,……T16>多达16个的重载,其中传入参数均采用泛型中的类型参数T,涵盖了几乎所有可能存在的无返回值的委托类型。
代码示例:

3.Func委托
Func具有Func
讯享网

4.Predicate委托
- Predicate是返回bool型的泛型委托;
- Predicate
表示传入参数为int,返回bool的委托; - Predicate有且只有一个参数,返回值固定为bool;

5.这几种委托的区别:
- Delegate至少0个参数,至多32个参数,可以无返回值,也可以指定返回值类型;
- Func可以接受0个至16个传入参数,必须具有返回值;
- Action可以接受0个至16个传入参数,无返回值;
- Predicate只能接受一个传入参数,返回值为bool类型;
匿名方法是在初始化委托时内联(inline)声明的方法。



Lambda表达式的出现很好地替代了匿名语法,它是匿名方法的简写形式,用来代替匿名方法。


(1)类名使用 BigCamelCase 大驼峰风格:
如:BaseClass,DerivedClass,SecondDerivedClass。
(2)方法名也用BigCamelCase大驼峰风格:
如:GetHttpMessage(),GetValue()。
(3)参数名、成员变量、局部变量都统一使用 lowerCamelCase 小驼峰风格:
如:localValue,inputUserId。
(4)常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长:
如:MAX_STOCK_COUNT,CACHE_EXPIRED_TIME。
(5)抽象类命名使用 Abstract开头 或 Base 结尾 ;异常类命名使用 Exception 结尾:
抽象类是特殊类,不能被实例化,抽象方法只能存在于抽象类中
访问修饰符 abstract class 类名
{
访问修饰符 abstract void 方法名();
}
抽象方法是一种特殊虚方法,只能声明,不能实现;
(6)测试类命名以它要测试的类的名称开始,以 Test 结尾。
(7)接口和实现类的命名:接口以I开头,实现类用 Impl 结尾:
如:XOrderImpl 实现 IOrder 接口。
(8)枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开:
如:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。
(1)使用 += 运算符注册事件;使用 -= 运算符取消订阅。
(2)C# 中使用事件机制实现线程间的通信。
(3)事件模型的5个组成部分
事件拥有者(event source)(类对象)(有些书将其称为事件发布者);
事件成员(event)(事件拥有者的成员)(事件成员就是事件本身,事件不会主动发生,其只会在事件拥有者的内部逻辑的触发下发生。);
事件响应者(event subscriber)(类对象)(有些书将其称为事件订阅者);
事件处理器(event handler)(事件的响应者的成员)(根据拿到的事件参数/信息对事件进行处理);
事件订阅(委托类型)。
例如,“裁判员开枪,运动员开始跑步。”
在上面这个例子中,事件拥有者是裁判员,事件成员是开枪,事件响应者是运动员,事件处理是开始跑步。
(4)代码举例:
讯享网

(5)再如代码举例:

1.什么是接口
接口是使用interface关键字声明的数据类型。
2.接口的作用是是什么
- 作为一个客观的规范
比如,如果把接口比做一个合同,这个合同规定了你能做什么事情,但是没有规定你怎么做。那么实现了这个接口的人就相当于履行合同的人,这个人必须按照合同的规定去做事情,但是不同的人在做这些事情时可以有不同的实现。
讯享网
- 实现多态
接口的主要目的是为不相关的类提供通用的处理服务,实现多态。
多态的定义:同一操作作用于不同的对象,可以有不同的的解释,产生不同的执行结果,这就是多态性。
比如下面代码,公司的每个人都是一个对象,有份工作需要对公司所有租房的人进行发放补助。一般来说,我们需要一一访问每个人才能知道他有没有租房,但是,倘若我们定义了一个接口,并且规定继承这个接口的人一定是租房了的,那么我只需要检查你是否实现这个接口就行。
事实上,很多时候,一一访问是做不到的,因为业务是多变的,但是功能是固定的,因此接口就能很好地弥补这种情况,在主体框架中只对接口进行编程,而不需要关注实现,这样才能让系统更加稳固。
3.接口的特点是什么
- 接口是抽象的行为,规定了能做什么,没规定怎么做。
- 实现了接口必须实现接口的所有成员。
4.接口如何使用
- 类继承接口 Class A : interface B,interface C
类需要实现继承接口中所有的方法,支持多继承。- 接口继承接口 interface A :interface B,interface C
接口不能实现继承接口的任何方法,支持多继。
6.接口和抽象类的区别
- 接口的主要作用是定义类型之间的契约,实现了接口的类必须按照接口定义的契约来实现它的成员,从而可以实现不同类型之间的通用性和互换性。抽象类的主要作用是为了实现多态性,它可以为子类提供一组基础功能,并要求子类必须实现一些具体的方法。抽象类可以包含一些具体的实现,但同时也可以包含一些抽象的成员,子类必须实现这些抽象成员。
- 使用接口的主要场景是当你需要定义一组通用的规范或契约时。比如说,你可以定义一个 IDisposable 接口,规定实现该接口的类型必须实现 Dispose 方法,用于释放资源。又比如,你可以定义一个 IComparer 接口,规定实现该接口的类型必须实现 Compare 方法,用于比较两个对象的大小。使用抽象类的主要场景是当你需要为子类提供一组基础功能,并要求子类必须实现一些具体的方法时。比如说,你可以定义一个 Animal 抽象类,规定所有动物都必须具有 Eat 和 Sleep 方法,但对于不同的动物,它们的实现方法是不同的,因此你可以定义一个 Dog 类和一个 Cat 类,分别继承 Animal 类,并实现它的抽象方法。
- 接口和抽象类都可以用来定义一组抽象的方法和属性,但它们的应用场景有所不同。当你需要定义一组通用的规范或契约时,应该使用接口;当你需要为子类提供一组基础功能,并要求子类必须实现一些具体的方法时,应该使用抽象类。
(1)装箱和拆箱
- 装箱:将值类型转换为引用类型的操作。
- 拆箱:相应地将引用类型转换成值类型。

(2)is运算符(用来检测转换是否成功,不像as操作符那样直接转换):
讯享网
注意: is运算符只可用于引用、装箱、拆箱三种转换,不可用于用户自定义转换。
(3)as运算符:
as运算符和强制转换运算符类似,只是不抛出异常。如果转换失败,它返回空而不是抛出异常。
语法如下:
源表达式 as 目标类型
注意: 目标类型必须是引用类型。

(6)当长类型的数据转换成短数据时,数据可能会丢失,需要我们进行显示地强制转换。
泛型:多种类型可以共享一组代码。泛型允许我们声明类型参数化的代码,可以用不同的类型进行实例化,也就是说,我们可以用“类型占位符”来写代码,然后在创建类的实例时指明真实的类型。
(1)C#提供了5中泛型,包括:类、结构、接口、委托和方法。(注意:前四种是方法,而方法则是成员。)
(2)泛型类不是实际的类,而是类的模板,也就是说,我们要先从他们构建实际的类类型,然后创建这个构建后的类类型的实例。这个过程如图:

(3)泛型类代码
看一段代码:
讯享网

(4)泛型方法:
泛型方法具有类型参数列表和可选的约束。
- 泛型方法有两个参数列表:
封闭在圆括号内的方法参数列表和封闭在尖括号内的类型参数列表。 - 要声明泛型方法,需要:在方法名称之后和方法参数列表之前防止类型参数列表,并在方法参数列表后放置可选的约束子句。
如图:

- 泛型方法的调用:

- 泛型方法举例:
代码:

(5)扩展方法和泛型类:
扩展方法可以和泛型类结合来使用,它允许我们将类中的静态方法关联到不同的泛型类上,还允许我们像调用类构造实例的实例方法一样来调用方法。

代码举例:
讯享网

(6) 泛型结构

(7) 泛型委托

代码举例:
讯享网

(8)泛型接口:
代码示例1:泛型类继承泛型接口

代码示例2:非泛型类继承泛型接口
讯享网


如下面一段代码:

本地变量是不会自动初始化的,而字段是会自动初始化的。
例如double rad是本地变量,如果没有赋值,就不会自动初始化;
字段则可以自动初始化。

(1)LINQ(发音为link)表示语言集成查询;
(2)LINQ是.net框架的扩展,它允许我们可以使用SQL查询数据库的方式来查询数据集合;
(3)使用LINQ,可以从XML文档、数据库、程序对象的集合中查找数据;
(4)匿名类型经常用于LINQ查询的结果之中;
(5)如下是一段匿名类型的对象:
讯享网

(7)join(联结)子句:
LINQ中的Join接受两个集合然后创建一个新的集合,每一个元素包含两个原始集合中的原始成员。
看一段代码:
讯享网

再看一段代码:

(8)orderby子句:
orderby子句接受一个表达式并根据表达式顺序返回结果项。
讯享网

(9)select ...group子句:

(10)group子句:
group子句按照一些标准进行分组。
(11)查询延续:into子句:
into子句可以接受查询的一部分结果并赋予另外一个名字,从而可以在查询的另一部中使用。

(12)再看一段代码:

(13)标准查询运算符:






(14)再看一段代码:
讯享网

1.进程和线程分别是什么?谈谈进程和线程的区别与联系。
(1)进程:构成运行程序的资源的集合。这些资源包括虚地址空间,文件句柄和许多其他程序运行所需的东西。
(2)线程:在进程的内部,系统创建了一个称为线程的内核对象,它代表了真正执行的程序。
(3)进程和线程的区别与联系:
- 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束;
- 线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程来执行程序的不同部分;
- 如果一个进程拥有多个线程,他们将共享进程的资源;
系统为处理器执行所规划的单元是线程,不是进程。
2.使用CancellationTokenSource 取消多线程
CancellationTokenSource 用于取消多线程操作。
使用步骤:
(1)声明CancellationTokenSource 对象;
(2)实例化 CancellationTokenSource 对象,此对象管理取消通知并将其发送给单个取消标记。并进行注册回调事件;
注意:想再次启动线程,必须重新再new CancellationTokenSource();因为取消了一次CancellationTokenSource.Cancel(),CancellationToken.IsCancellationRequested的标记一直为true;
(3)Cancel()方法调用会设置cancellationManage.IsCancellationRequested为True;
调用 CancellationTokenSource.Cancel 方法以提供取消通知。 这会将 CancellationToken.IsCancellationRequested 取消标记的每个副本上的属性设置为 true ;
代码举例:

3. 异步编程
首先,一个描述很详细的博客链接: 异步编程
4.Task.Run 和 Task.Factory.StartNew 区别
不推荐他使用 Task.Factory.StartNew ,因为 Task.Run 是比较新的方法。
需要知道 是在 dotnet framework 4.5 之后才可以使用,但是 可以使用比 更多的参数,可以做到更多的定制。
可以认为 是简化的 的使用,除了需要指定一个线程是长时间占用的,否则就使用
对比说明如下:
1.创建新线程
下面来使用两个函数创建新的线程:
讯享网
这时 foo 的创建就在另一个线程,需要知道 Task.Run 用的是线程池,也就是不是调用这个函数就会一定创建一个新的线程,但是会在另一个线程运行。
可以看到,两个方法实际上是没有差别,但是比较好看,所以推荐使用。
2.等待线程
创建的线程,如果需要等待线程执行完成再继续,那么可以使用 await 等待。
输出结果:
讯享网
但是需要说的是这里使用 await 主要是给函数调用的外面使用,上面代码在函数里面使用 await 函数是 void 那么和把代码放在 task 里面是相同。
但是如果把 void 修改为 Task ,那么等待线程才有用。
除了使用 await 等待,还可以使用 WaitAll 等待。
讯享网
使用 WaitAll 是在调用 WaitAll 的线程等待,也就是先在线程 1 运行,然后异步到线程3 运行,这时线程1 等待线程2运行完成再继续,所以输出结果:
3.长时间运行
两个函数最大的不同在于 可以设置线程是长时间运行,这时线程池就不会等待这个线程回收。
讯享网
所以在需要设置线程是长时间运行的才需要使用 不然就使用
调用 就和使用下面代码一样:
实际上 可以认为是对 封装,使用简单的默认的参数。如果需要自己定义很多参数,就请使用 定义参数。
5.TaskCompletionSource的使用
1.TaskCompletionSource简介以及简单使用方法
MSDN链接:添加链接描述
TaskCompletionSource生成Task方法,使用TaskCompletionSource很简单,只需要实例化它即可。TaskCompletionSource有一个Task属性,可以对该属性暴露的task做操作,比如让它wait或者ContinueWith等操作。当然,这个task由TaskCompletionSource完全控制。
大多数时候,只在目标方法要调用基于事件API,又要返回Task的时候使用。比如下面的ApiWrapper方法,该方法要返回Task
讯享网
再看一个例子:TaskCompletionSource中有一个SetResult方法,当该方法被调用后。就会让await等待的代码继续往下执行。
输出结果:
讯享网
2.TaskCompletionSource 的 TrySetResult 是线程安全
在创建一个 TaskCompletionSource 期望让等待的逻辑只会被调用一次,而调用的是多线程,可以使用 TrySetResult 方法,这个方法是线程安全,只会让 TaskCompletionSource 被调用一次。
在多个线程调用 TaskCompletionSource 的 TrySetResult 方法,只有一个线程能进入设置,其他线程将会拿到返回 false 的值
测试代码如下:
输出结果:
讯享网
6. c#判断代码是否执行超时的几种方式
1.使用Task
2.使用Thread
讯享网
7.C# Monitor和Lock的定义及区别
1.Monitor对象
- Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,当然在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
- Monitor的常用属性和方法:
- Enter(Object) 在指定对象上获取排他锁。
- Exit(Object) 释放指定对象上的排他锁。
- IsEntered 确定当前线程是否保留指定对象锁。
- Pulse 通知等待队列中的线程锁定对象状态的更改。
- PulseAll 通知所有的等待线程对象状态的更改。
- TryEnter(Object) 试图获取指定对象的排他锁。
- TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
- Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
2.Lock关键字
- 如果在使用多线程时,在相同的时间内有多个线程同时执行相同的方法,也许就存在数据安全的问题,如多个线程之间对于相同的内存进行同时的读取和修改。为了让多线程每次只能有一个线程执行,可以使用的方法有很多。
在C#里面可以使用关键词lock加上一个对象作为锁定,在进入lock的逻辑,只能有一个线程获取锁,因此在lock里面的代码只能被一个线程同时执行。
lock锁的究竟是什么?是lock下面的代码块吗,不,是locker对象。我们想象一下,locker对象相当于一把门锁(或者钥匙),后面代码块相当于屋里的资源。哪个线程先控制这把锁,就有权访问代码块,访问完成后再释放权限,下一个线程再进行访问。注意:如果代码块中的逻辑执行时间很长,那么其他线程也会一直等下去,直到上一个线程执行完毕,释放锁。
Lock关键字实际上是一个语法糖,它将Monitor对象进行封装,给object加上一个互斥锁,A进程进入此代码段时,会给object对象加上互斥锁,此时其他B进程进入此代码段时检查object对象是否有锁?如果有锁则继续等待A进程运行完该代码段并且解锁object对象之后,B进程才能够获取object对象为其加上锁,访问代码段。
例如,以下代码就是标准的锁定方法的代码:
- Lock关键字封装的Monitor对象结构如下:
讯享网
- 锁定的对象应该声明为private static object obj = new object();尽量别用公共变量和字符串、this、值类型。
3.Monitor和Lock的区别
- Lock是Monitor的语法糖。
- Lock只能针对引用类型加锁。
- Monitor能够对值类型进行加锁,实质上是Monitor.Enter(object)时对值类型装箱。
- Monitor还有其他的一些功能。
代码示例:
8.Thread.Abort()和Thread.ResetAbort()
先看一个问题,如下,要在Main方法中如何操作才能执行Foo方法finally中的语句?
讯享网
答案之一:使用一个线程调用的方式,调用之后结束线程,此时就会输出
引深一些:
Thread类中的Abort方法用于终止正在运行的线程。它可以强制终止线程,而不管线程是否是Sleep中。在执行了Abort方法后,被终止的线程就会继续运行try catch finally 块中代码,因为是强行终止,catch块中之后的语句就不再会执行。除非在catch块中执行Thread.ResetAbort这个静态方法。
先举例没有ResetAbort()的情况:
讯享网
输出结果:
讯享网
输出结果:
现在解释一下,在默认(不调用Thread.ResetAbort())的情况下, finally块后的代码是执行不到的,这是由于 ThreadAbortException这个异常非常特殊,它会在finally块的最后(如果没有finally块,则是在catch块的最后)重新扔出一个 ThreadAbortException异常。(不过这个异常在外部抓不到,它仅仅是为了退出线程用的)。
9.Task线程的开始、暂停、继续、取消
配合使用CancellationTokenSource和ManualResetEvent来实现线程的开始、暂停、继续、取消功能。
先看效果:

代码如下(此代码例子中还有同步、并行、异步、事件的举例代码):

代码已经放在仓库,地址为:仓库链接
- MainWindow.xaml代码:
讯享网
- MainWindowViewModel.cs代码:
- Book.cs代码:
讯享网
- Data.cs代码:
10.Task的一些用法详解
Task是微软在.Net 4.0时代推出来的,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool,所以一经问世,基本ThreadPool就被取代了。
task有很多封装好的API,比如:
- WaitAll:等待提供的所有 System.Threading.Tasks.Task 对象完成执行过程。
讯享网
- WaitAny:等待提供的任一 System.Threading.Tasks.Task 对象完成执行过程。
- ContinueWith:创建一个在目标 System.Threading.Tasks.Task 完成时异步执行的延续任务。
讯享网
这里分别开启了两个线程t1、t2,在t1里面等待1秒,t2里面等待2秒,所以执行WaitAny时先等到ti完成,WaitAll时会等到t2完成.最终输出结果如下:
- Wait:等待 System.Threading.Tasks.Task 完成执行过程。
- Start:启动 System.Threading.Tasks.Task,并将它安排到当前的 System.Threading.Tasks.TaskScheduler中执行;
带返回值的使用方式:
讯享网
11.Thread类中IsBackground属性
12.C# 前台线程和后台线程的区别(重要知识点)
前台线程和后台线程唯一区别:应用程序必须运行完所有的前台线程才会完全退出,若前台线程未执行完成,关闭应用程序后,应用程序并没有完全退出,在任务管理器中还存在此进程;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
- 在任何时候我们都可以通过线程的IsBackground属性改变线程的前后台属性:
- thread.IsBackground=false;前台线程
- thread.IsBackground=true;后台线程
- 应用程序的主线程以及使用Thread构造的线程都默认为前台线程
线程池线程也就是使用 ThreadPool.QueueUserWorkItem()和Task工厂创建的线程都默认为后台线程。
线程由程序员创建,可是创建的方式不同,总体来说有两种,一种是个人构造,也就是使用thread类new线程对象创建,这一类线程是大部分程序员知道的,也叫专用线程;还有一种是由CLR创建,这一类线程主要存在于线程池中,也叫线程池线程。对于这两种线程的好坏,建议最好使用线程池线程,不要大量使用专用线程。
从回收的角度来看又可分为前台线程和后台线程:
- 后台线程:后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。【后台,存在于黑暗之中默默无闻,它的消亡和存在,别人也感受不到】
- 前台线程:前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。
13.向线程里面传递数据
讯享网
(1)try语句:
try语句用来指明因避免出现异常而被保护的代码片段,并在发生异常时提供代码处理异常。

(2)try块后面必须跟catch块或finally块组合使用,不能单独使用
(3)代码举例:
运行结果:

(1)预处理指令指示编译器如何处理源代码。
(2)预处理指令:

(3)#define和#undef只能用在源文件的第一行,也就是任何C#代码之前使用,在C#代码开始后,#define和#undef就不能再使用。
(1)元数据:有关程序及其类型的数据称为元数据,它们保存在程序的程序集中。
(2)程序在运行时,可以查看其他程序集或其本身的元数据。一个运行的程序可以查看本身的元数据和其他程序的元数据的行为叫做反射。
(3)反射在System.Reflection命名空间中。
(4)Type类
(5)特性:
特性是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。
特性的目的就是告诉编译器把程序结构的某组元数据嵌入到程序集。
(6)Obsolete特性:


(7)Conditional特性
(8)看一段代码,反射中SetValue和GetValue以及克隆的使用:
讯享网
运行结果:

(1)字符串string类型的一些成员:
(2)Spilt方法:
该方法很有用,该方法会将一个字符串分割成若干个子字符串,并将他们以数组的形式返回。将一组按照预定位置分隔字符串的分隔符传给Spilt方法,就可以指定如何处理输出数组中的空元素(当然,原始字符串依然不会改变)。代码举例如下:
讯享网

(3)StringBuilder类
StringBuilder类位于System.Text命名空间中,它可以帮助程序员动态、有效地产生字符串,并避免创建许多副本。代码举例如下:

(4)把字符串解析为数据值
解析允许我们接受表示值的字符串,并转化为实际的值,通过Parse静态方法。如:
讯享网

但是,Parse方法有一个缺点,当转换不成功(即不能把string类型转化为其他类型)时会抛出一个异常,而在编程中要尽量避免异常。TryParse方法可以解决这个问题。


(1)List < T >是泛型集合,用法如下代码所示:

(2)List的一些常用属性和方法:

(3)可以用List
讯享网

异:(1)类型不同:
类是引用类型,在堆上分配地址;
结构是值类型,在栈上分配地址;
(2)继承性不同:
类:是完全可扩展的,也可以继承其他类和接口,自身也能被继承;
结构:不能从另外一个结构或者类继承,本身也不能被继承(但能够继承接口,方法和类继承接口一样)。
(3)内部结构不同:
类: 有默认的构造函数和析构函数,可以使用访问修饰符 ,必须使用new 初始化;
结构: 没有默认的构造函数,但是可以添加构造函数,没有析构函数 ,可以不使用new 初始化。
同:基类型都是对象(object)
不同点:
不能直接实例化接口。
接口不包含方法的实现。
接口可以多继承,类只能单继承。
类定义可以在不同的源文件之间进行拆分。
相同点:
接口、类都可以从多个接口继承。
接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
接口和类都可以包含事件、索引器、属性。

(1)GetType方法:获取当前变量的类型对象

(2)typeof运算符,在此笔记第41条已提到。
因为C#中所有的类都直接或者间接派生自Object类,因此Object类类是C#中唯一的非派生类。
当声明一个字符串变量时有一些字符是不能以平常的方式包含在变量中的。为了解决这个问题,C#提供了两种不同的C#转义字符方法::
(1)第一种方法是使用

代码举例一可参考笔记第56条;
代码举例二:
讯享网

注意:当出现"\n"时,结果是显示的是 ,而不会出现换行
(2)第二种C#转义字符方法是使用@

代码举例:

(1)抽象成员是抽象类中的成员,抽象成员是指设计为被覆写的函数成员,抽象成员具有以下特征:
- 抽象成员必须是函数成员,字段和常量不能为抽象成员;
- 抽象成员必须用Abstract标记;
- 抽象成员不能实现代码块;
- 抽象成员必须被子类用override关键字重写;
例如下图:

(2)虚成员和抽象成员的区别:
抽象方法是只有方法名称,没有方法体(也就是没有方法具体实现),子类必须用override关键字重写父类抽象方法。
虚函数有方法体,但是子类可以覆盖,也可不覆盖。
先看代码展示:
讯享网

十二字口诀:“先静后构,静外到内,构内到外”;
先静后构:
一个类同时存在静态构造函数和普通构造函数,对象实例化后,先执行静态构造函数(先静),再执行普通构造函数(后构);
故上文对象A实例后输出的是SA baseA。
静外到内:
父类和子类都存在静态构造函数的时候,实例化子类后,先执行子类静态构造函数(静外),再执行父类静态构造函数(到内);
上文B继承了A,B和A同时存在静态构造函数但是由于一个静态构造函数在一个应用程序的完整生命周期中,最多只会被自动执行一次,因此基类的静态构造函数只会在基类被初始化的时候调用且调用一次,所以实例化B后,静态构造函数输出结果是是SB ,而不是SB SA。
构内到外:
实例化子类后,先执行父类的普通构造函数(构内),再执行子类的构造函数(到外);
上文B继承了A,实例化B后,普通构造函数输出结果是baseA baseB。
- 对于构造函数重载,先执行基类的构造函数,再执行子类的构造函数;
- 对于析构函数,先执行子类的析构函数,再执行基类的析构函数。


隐式转换:类型的转换不会丢失数据或精度。

讯享网


- 接口中不能声明常量和字段,抽象类中可以声明任何类成员;
- 在接口中只能定义成员,但不能具体实现,在抽象类中除了抽象方法外,其他成员允许有具体的实现;
- 接口中没有实例构造函数,也就是说没有构造函数,抽象类中有构造函数;
- 接口成员不能使用任何访问修饰符,抽象类中的类成员可以使用任意的访问修饰符;
- 继承接口的类或结构必须隐式或显式实现接口中的所有成员,否则需要将实现类定义为抽象类,并将接口中未实现的成员以抽象的方式实现,继承抽象类的类必须重写实现抽象类中的所有抽象方法,或者抽象类继承抽象类,可以重写部分抽象方法;
- 接口不能作为派生类继承,抽象类可以继承非抽象类或抽象类;
- 接口可以作为基类来多继承:接口、类和结构,抽象类可以作为基类只能实现单继承,只能让非抽象类或者抽象类继承。
(1)显式转换:类型的转换可能会丢失数据或精度,语言不会自动做的转换需要用到显示转换。
(2)隐式转换:类型的转换不会丢失数据或精度,因此语言会自动给做的转换叫做隐式转换。
(3)装箱:值类型到引用类型的隐式转换。任何值类型都可以被隐式转换为object类型,System.ValueType或InterfaceT;
(4)拆箱:把装箱后的对象转换回值类型的过程。拆箱是显式转换。
讯享网
输出结果:
讯享网
结果如图:n1的值为1,n2的值为2

今天看项目代码,在IOC(控制反转)这部分知识这边卡了壳,问了下同事,终于理解了它的用法。
首先将一个类声明为单例模式下的类,然后将它放进IOC容器中,这样每次从IOC容器中取出的时候,就不用再重复实例化该类,减少内存的占用。
因为传统的实例化都是通过用new关键字实例化,当频繁地在不同文件中使用该类的时候,就可能需要不断地通过new来实例化,极大消耗了内存占用;而通过将单例模式下的类注入IOC容器后,每次用该类的时候可以通过IOC.Get方法取出该类(不管从IOC容器中取多少次,只会对该类进行一次实例化,不会重复实例化)。
发现了string类中一个特别好用的方法string.Join,先直接看效果:

看出来了,string.Join(",", arrays)这部分代码将arrays数组中每个成员用","分隔开,方便快捷。
下面是该方法原型:
public static String Join

- 摘要:
// 串联集合的成员,其中在每个成员之间使用指定的分隔符。 - 参数:
// separator:
// 要用作分隔符的字符串。只有在 values 具有多个元素时,separator 才包括在返回的字符串中。
// values:
// 一个包含要串联的对象的集合。
1.using指令
讯享网
2.using别名
3.using语句
定义一个范围,在范围结束时处理(释放)对象。
注意:只有使用了IDisposible接口的对象才可以用using进行管理。
只在一定的范围内有效,出了这个范围时,自动调用IDisposable接口释放掉using语句块中的内容,当然并不是所有的类都适用,只有实现了IDisposable接口的类才可以使用。
using语句处理实现IDisposable的对象,并在作用域的末尾调用Dispose方法。
讯享网
用Environment.GetEnvironmentVariable方法读取环境变量,并返回一个string类型的结果。
1.使用vs2022新建一个.net core控制台应用,并选择.net6.0时会有一个是否选择使用顶级语句的按钮,如图:


那么什么才是顶级语句?
- 直接在C#文件中直接编写入口方法的代码,不用类,不用Main。经典写法仍然支持。反编译一下了解真相。
- 同一个项目中只能有一个文件具有顶级语句。
- 顶级语句中可以直接使用await语法,也可以声明函数,如:

1.将 global 修饰符添加到 using 前,这个命名空间就应用到整个项目,不用重复using。
2.通常创建一个专门用来编写全局using代码的C#文件。
3.如果csproj中启用了
讯享网
这里的示例,用 record 声明了两个 model,第二个 model 声明的时候使用了简化的写法record RecordPerson2(string Name, int Age); 这样的声明意味着,构造方法有两个参数,分别是 string Name 和 int Age,并对应着两个属性,属性的声明方式和 RecordPerson 一样 public string Name { get; init; } 都是一个 get 一个 init。
对于 record 支持一个 with 表达式,来修改某几个属性的值,这对于有很多属性都相同的场景来说是及其方便的,来看一下上面示例的输出结果。

2.在C#9.0中增加了记录(record)类型的语法,编译器会为我们自动生成Equals、GetHashcode等方法。
sealed的英文意思就是密封,禁止的意思,故名思义,就是由它修饰的类或方法将不能被继承或是重写。在c#中sealed关键字可以用来修饰类和方法。
1.sealed关键字修饰类
当对一个类应用 sealed 修饰符时,此修饰符会阻止其他类从该类继承。

如上图,sealed关键字修饰了类B,类B可以继承自类A,但是类C无法从类B继承。
2.sealed关键字修饰方法或属性
当sealed修饰方法时,表示该方法不能被重写。
讯享网
1.通过接口注入
接口注入:
相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见。具体思路是先定义一个接口,包含一个设置依赖的方法。然后依赖类,继承并实现这个接口。
2.通过属性访问器Settter注入
- UML图:

- 代码:
讯享网

3.通过构造函数注入
构造函数注入:
通过客户类的构造函数,向客户类注入服务类实例。
构造注入(Constructor Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。
- UML图:

- 代码:

C#中,yield关键字的作用是将当前集合中的元素立即返回,只要没有yield break,方法还是会继续执行循环到迭代结束。
- yield return是一次一个的返回,yield return例子:
讯享网

通过单步调试即可发现,enumerableFuc方法每次被调用就会返回一个数据,第一次调用enumerableFuc方法会返回1,第二次调用会返回2,第三次调用会返回3。
- yield break用于结束返回(终止迭代),yield break例子:

通过此代码可以看出,此代码返回的结果是1和2,不是1、2和3,因为用了yield break,因此enumerableFuc方法中的第四行yield return 3这一行没有被执行。
- 引用拷贝:只复制对象的地址,并不会创建一个新的对象;
- 浅拷贝:浅拷贝会创建一个对象,并进行属性复制,但对于引用类型的属性,只会复制其对象地址;
- 深拷贝:深拷贝会完全复制整个对象,包括引用类型的属性。
浅拷贝代码举例,代码如下:
讯享网
输出结果:
讯享网
输出结果:
在C#中,string类型和数组类型也属于引用类型,而string类型比较特殊(具体可查资料),因此只需要手动克隆List类型的Hobbies属性即可实现深克隆。
1.Lazy简介
通过Lazy关键字,我们可以声明某个对象为仅仅当第一次使用的时候,再初始化,如果一直没有调用,那就不初始化,省去了一部分不必要的开销,提升了效率,同时Lazy是天生线程安全的。
2.应用场景
- 对象创建成本高且程序可能不会使用它;
- 对象创建成本高,且希望将其创建推迟到其他高成本操作完成后。
例如,假定程序在启动时加载多个对象实例,但是只需立即加载其中一部分。 可以通过推迟初始化不需要的对象,直到创建所需对象,提升程序的启动性能。
3.Lazy基本用法
用法1:构造时使用默认的初始化方式
在使用Lazy时,如果没有在构造函数中传入委托,则在首次访问值属性时,将会使用Activator.CreateInstance来创建类型的对象,如果此类型没有无参数的构造函数时将会引发运行时异常。
讯享网
输出结果:
用法2:构造时使用指定的委托初始化
讯享网
用法3:利用Lazy关键字来构造一个单例类
用法:
讯享网
代码举例:
运行结果:
讯享网
1.何时使用Dictionary而不是List
- 通常情况下,我们可以用int类型的索引来从数组或集合中来查询所需要的数据,但是当索引不是int(如string和double),这时就需要使用Dictionary字典。
- 当要存储的东西很多、列表很长时,可以使用Dictionary字典,字典的查询效率很高(List集合是循环遍历的查找方式,而字典是哈希查找)。
2.关于使用Dictionary时的注意事项
- 字典Dictionary在名称空间System.Collections.Generic下;
- 字典是一组键(Key)到一组值(Value)的映射;
- 键必须是唯一的且不能为空;
- 键和值可以是任何数据类型
3.Dictionary的一些简单用法
举个例子:
你有你自己的身份证号,一报身份证号,你应该知道是你了
你也有名字,当然名字复杂点,并且不是唯一,没有数字来得方便,
所以,窗口句柄就相当于身份证号,每个窗口都有一个编号,操作系统用这个编号来发送消息的.这就是操作系统的消息机制。
一个窗口如果里面有组件的话,那么每个组件也会有窗口句柄,这里的窗口提的是WINDOW,不带那个S的,表示的就是一个框,所以说,翻译上的不同,我认为也可以翻译成"框句柄",这比较符合实情,接下来,就可以对这个句柄进行操作了。
如果可以隐藏一个窗口,就发送消息让他隐藏,这里就用到API,当然API是比较多的,所有的功能都是通过API实现的。
更专业一点:
在Windows中,是一个32为无符号整数值,句柄是一个系统内部数据结构的引用,例如,当你操作一个窗口,或说是一个Delphi窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号窗口,就此,你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口极小化为图标,等等。实际上许多Windows API函数把句柄作为它的第一个参数,如GDI(图形设备接口)句柄、菜单句柄、实例句柄、位图句柄等等,不仅仅局限于窗口函数。
在实际编程中,IEnumerable接口常常和yield关键字配合使用。
比如像下面这样的方法,通过yield return每次返回一个数据,即产生了一个数据就返回一个:

又或者像下面这样的用法:

在一些返回集合数据的接口中,我们经常能看到IEnumerable接口的身影,那什么是Enumerable呢?首先它跟C#中的enum关键字所表达的意思是不同的, 从翻译上来看:可枚举的,展开来说就是它的数据是一枚一枚可以让我们列举出来。就像人们排队去打疫苗,排队的人就是可枚举的,他们有的开车,有走着,有早有晚全都按照先来后到的顺序排好队,当医生开始进行打疫苗的工作时,他并不关心有多少人在排队,也不关心是否有人迟到,当然也不能越过第一个人让其后边的人先进来打,他只能说“请下一个人进来打疫苗”,如果没人响应医生就等着,直到有人进来开始接种,当前这个人完成接种后,医生继续叫下一个人,直到所有人都打完疫苗。这样的情景在编程中就体现为对Enumerable数据的操作。
下面看看IEnumerable接口中都有什么东西:

IEnumerable< T >泛型接口中:

这两个接口中都有一个IEnumerator的接口,我们称之为枚举器,

IEnumerator接口有三个成员,Current就是保存的当前数据对象,MoveNext方法则是将指针指向下个对象(类似打疫苗情景中的"下一位"),Reset则是将指针复位,泛型版接口则对其内部名为Current的成员指定了类型。也就是说通过枚举我们可以获取一个枚举器,通过枚举器我们能找到一个个数据对象,明白了这一点,我们就能大体上了解如何通过IEnumerable来获取数据了。
如下代码中有一个返回IEnumerable< string >的方法,用来模拟数据的产生,其中用到了一个yield关键字,yield return就是部分返回(产生了一个数据,就返回一个),这个方法最终的运行效果就是一秒钟返回一个当前时间构成一个IEnumerable
讯享网
最终输出结果:
1.反射
反射就是我们在只知道一个对象的外部而不了解内部结构的情况下,可以知道这个对象的内部实现。
可以通过Type类获取程序集、模块、类的相关信息,可以看到Type类继承自IReflect接口,如下:


代码演示:
讯享网
输出结果:
得到一个Type类型对象有三种方法:object.GetType()、Type.GetType()、typeof()
使用object.GetType()必须先创建一个实例,而后两种不需要创建实例,但使用typeof运算符仍然需要知道类型的编译时信息,Type.GetType()静态方法不需要知道类型的编译时信息,所以是首选方法。
2.特性
特性的使用很简单,在结构声明的上一行,用"[]"扩起特性类名即可:
讯享网
3.反射和特性配合使用
讯享网
属性的本质就是方法,get和set两个方法构成。
- 字段只管存值,不管对数据的操作,字段一定是占用内存的。
属性可以占用内存,也可以不占用,当属性中封装了字段时,那么属性会占用内存,当不封装字段而是做了其他操作时,是可以不占用内存的。 - 字段是给类自己内部用的,属性是给外部调用这个类的时候用的。
字段一般都声明为private私有的,而属性一般都声明为public公有的。 - 属性跟字段最根本区别就在于属性是类似于方法,字段就是变量。通过属性的set和get函数可以限制字段的一些功能,以达到某种目的。
感觉师傅给我讲的关于属性和字段的区别以及属性的优点,我还是没有理解核心区别,今天先写总结到这里,日后再有更深层次的理解了,继续完善。
今天师傅给我讲了些项目代码中事件和委托的使用以及区别,并让我学会多使用evnet事件(可以少用action委托,多用事件),晚上回去作了些总结,记录如下:
先分析委托因为封装不充分而产生的缺点:
- 错误使用赋值操作符导致原本的委托链被覆盖:
如果一个委托通过 "+=" 绑定了多个方法,那么当委托通过 "=" 绑定方法时,之前所有通过 “+=” 方式绑定的方法都将被覆盖。 - 在类的外部也可以调用委托:
尤其是委托可以在类的外部(即不同类之间)使用,就会显得很混乱,而且会容易造成错误操作。
总结下来就是:委托封装的不好,没有很严格规范的使用规则,事件则解决了这些问题:
事件本质是对委托的封装,事件只能在类内调用,+=和-=是事件允许的唯一运算符,可以为事件定义事件访问器。有两个访问器add和remove,声明事件的访问器和声明一个属性差不多:
关于序列化的详细知识可参考此篇文章C# Xml进行序列化与反序列化
- 序列号的概念
序列化就是把一个对象保存到一个文件或数据库字段中去,反序列化就是在适当的时候把这个文件在转化成原来的对象使用。对象的序列化不是类的序列化。对象的序列化表明C#提供了将运行的对象(实时数据)写入硬盘文件或数据库中,此功能可以运用在需要保留程序运行时状态信息的环境下。 - 使用序列化的两个重要的原因
第一个原因:是将对象的状态永久保存在存储媒体中,以便可以在以后重新创建精确的副本;
第二个原因:是通过值将对象从一个应用程序域发送到另一个应用程序域中。
前提是要将对象的声明为可以序列化。 - 最主要的作用有
第一个作用:在进程下一次启动的时侯读取上一次保存的对象的信息。
第二个作用:在不同的AppDomain或进程之间传递数据。
第三个作用:在分布式应用系统中传递数据。
序列化是把一个内存中的对象的信息转化成一个可以持久化保存的形式,以便于保存或传输,序列化的主要作用是不同平台之间进行通信,常用的序列化有json、xml、文件等。 - 代码举例:
讯享网

输出结果:
- 通过FileInfo和DirectoryInfo类来读取文件和文件夹属性
包括:查看文件属性,创建文件,移动文件,重命名文件,判断路径是否存在,创建目录。 - 通过File来读写文件。
- 使用流来读写文件
FileStream,StreamReader(读取流,读取数据),StreamWriter(写入流,向别人传输)。
1.使用FileInfo查看文件属性
讯享网
代码示例:
2.使用DirectoryInfo查看文件夹属性
代码示例:
讯享网
3.使用File来读写文件
代码示例:
4.使用FileStream流来读写文件
FileStream(文件流) 这个类主要用于二进制文件中读写,也可以使用它读写任何文件(包括一些图像、音频文件)。
文件流FileStream 位于命名空间System.IO下,主要用来操作文件流,与File类的读取写入相比File类读取文件时是一次性读取,在操作大型文件时容易导致内存飙升,FileStream类则可以对一个文件分多次进行读取,每次只读取一部分,节省内存空间。FileStream就像把水缸里的水一瓢一瓢的取出来,而不像File类一次性倒出来,因此FileStream对电脑的内存占用资源占用方面相对较小,使用范围更广。
5.使用StreamReader和StreamWriter流读写文本文件
StreamReader(流读取器)和StreamWriter(流写入器)专门用于读写文本文件。
示例一:
讯享网
示例二:
6.MemoryStream流
数据 —> 通过inputStream()、Read()转化为流 —> 通过outputStream()、Writer()转化为数据,就像流是通过一个小细管道传输的,input输入管道变为流,output输出管道变为数据,流类读取数据为流,流将流写入数据。
C#中的MemoryStream是一个实现了Stream类的内存流类,用于在内存中读写数据。
下面是一些常用的属性和方法:
属性:
讯享网
方法:
代码举例:
讯享网
7.C#中操作表格
如下例子,向一个csv文件中写入数据并读取:
以下代码需要注意一点:
注意File.Create方法返回的是一个流,必须要调用Close方法关闭流,否则会报异常提示正在被其他的线程占用,因此不能用File.Create(),要用File.Create.Close方法
运行结果:
讯享网
8.C# 使用File.Create方法创建文件时,报进程被占用
在一个程序里偶然用了System.IO.File.Create去创建文件,运行时一直报错(进程被占用),后来在网上找到了解决办法,引用了一下。
判断是否有当前的文件存在,不存在则进行创建,在进行操作:
但是当我运行到发现没有当前的文件,就直接创建当前文件,之后直接进行操作,出问题了直接报出异常,当前文件正在另一个进程中使用……仔细一看 System.IO.File.Create(fileName)返回的类型是FileStream,ND文件流,文件流不关闭不出异常那才叫怪呢。
讯享网
方法二:
9.C# 以非独占方式打开文件(FileShare)
使用C#开发中,当一个程序正在读写某个文件,另一个程序则无法操作此文件。
使用FileStream类,其中的FileShare参数可设置文件的共享方式:
- FileShare.None 谢绝共享当前文件
- FileShare.Read 允许别的程序读取当前文件
- FileShare.Write 允许别的程序写当前文件
- FileShare.ReadWrite 允许别的程序读写当前文件
下面代码中将文件的FileShare属性设置为Read,程序在读写文件时,其他程序可以查看此文件
每次以追加的方式向文件中写入数据,并且给本程序赋予读写的权限来操作该文件,但是其他程序只能读取该文件
讯享网
XML指可扩展标记语言,XML被设计用来传输和存储数据。XML被设计用来结构化、存储以及传输信息。
1.MemoryStream和XmlSerializer配合来序列化和反序列化
1.C# json 转 xml 字符串
2.dotnet C# 如何让 Json 序列化数组时序列化继承类的属性
3.dotnet 手动解决 json 解析中不合法字符串
4.dotnet 使用 Newtonsoft.Json 输出枚举首字符小写
讯享网
输出结果:
讯享网
输出结果:
这个用的比较多的是在 out 参数后,比如输出的out类型的参数用不到,就可以直接out var _。
讯享网
1.?符号
如果去掉int?numb=null中的?,在声明的时候就会报错。

2.?.符号
讯享网
根据字符串的原理,如果进行不断的拼接,将会带来一点性能损耗,原因是每次拼接都会创建新的字符串对象。
如上面代码将会创建大量中间的字符串对象,而最终需要的对象仅仅只有一个字符串。一个优化的方法就是使用 StringBuilder 代替 string 此时能提升不少的性能。
需要将一个 URI 和另一个 URI 拼接如 https://blog.XXX.com/post/123 和 /api/12 拼接,拿到绝对路径 https://blog.XXX.com/api/12 可以使用下面方法:
讯享网
输出结果:
今日遇到需求如下:查找一个集合类中某个属性的最大值和最小值,想使用Linq来实现。
需求复现代码如下:
讯享网
代码运行结果如下:
什么时候该用where什么时候该用select。其实也挺简单的,就是假如后面的表达式返回的是true 或者false的bool值的时候就用where,要是后面能直接得到值就用select。
比如,以上代码Select(e=>e.Power)表达式中e=>e.Power返回的是值,因此就用select。
语法糖可以理解为,编码过程中写了一个关键字,但是编译的时候会把它编译成别的东西,主要是用来提升开发效率。
例如,C#中的async和await就是一组语法糖。
当一个集合作为入参传入另外一个方法时,我们首先需要判空处理,以免在空集合上处理引发异常,目前想到了以下几种判空处理的方式:
1.是否为null
如果一个集合没有实例化,那集合就是null,判null的常用以下几种方式:
- == null
讯享网
- is null
2.是否为空
在上面我们先判断了集合不为null以后,如果还需要检查集合是否有元素,也是有多种方式可以实现:
- stuList.Count == 0
讯享网
- !stuList.Any() // 确定序列是否包含任何元素。
// 返回结果:
// true 如果源序列中不包含任何元素,则否则为 false。
那如果既想检查他是否为空又想检查是否为null,这时候我们就可以用 ?. 来操作了。
讯享网
上面那个就是 ?? 的用法。
1.?的使用
- 定义数据类型可为空,目的是用于对 int,double,bool 等无法直接赋值为 null 的类型进行 null 的赋值:
讯享网
- 用于判断对象是否为 null,如果对象为 null,则不管调用什么都不会抛出异常,直接返回 null:
2.??的使用
- ??用于判断一个变量为 null 返回一个指定的值:
讯享网
1. 这道题要考察的知识点:

2.问题
先看下面这段代码有没有问题?
这段代码实际是有问题的,如下:

3.分析
如果非常清楚属性的本质的话,那么上述代码可以进行转换,将属性转换为普通方法。(属性的本质就是方法嘛)
讯享网
果然上述的改写只是将属性的写法转为本质写法-写成两个普通的方法。结果还是报一样的编译错误。

再修改一下:
输出结果:
讯享网
压根就没变,当然没有变啊,因为v2是副本,你更改的只是副本,并没有改变t中的v,同理,通过t.GetVector()也是一个副本,没有意义。
所以C#语法在对于这种情况,帮我们做了一个处理,如果写了这样的代码,直接给出编译报错。C#还是很智能的。就是如果我们错误的进行一个无意义的操作,会直接给出提示。这里给C#语法赞一个。
4.闲谈
这一段代码虽然好像很简单,但是真的藏的很深。因为枚举值传递是副本传递,再进行赋值操作没有意义。这一道很简单的问题,但是考察的东西真的很多很多。
1.用字符串分隔
运行结果:
讯享网
2.用多个字符来分隔
运行结果:
讯享网
3.用单个字符来分隔
运行结果:
讯享网
1.概述
Image 类为Bitmap(位图) 和 Metafile(矢量图) 的类提供功能的抽象基类。Image类不能直接创建对象的,但Image.FromFile()返回的是Bitmap或者Metafile的对象。
初始化Image:
2.属性
- PixelFormat:获取此 Image 的像素格式。
- RawFormat:获取此 Image 的文件格式。
- Size:获取此图像的宽度和高度(以像素为单位)。
- Width:获取此 Image 的宽度(以像素为单位)。
- Height:获取此 Image 的高度(以像素为单位)。
3.方法
- FromFile(String):从指定的文件创建 Image。
- FromStream(Stream):从指定的数据流创建 Image。
- GetBounds(GraphicsUnit):以指定的单位获取图像的界限。
- GetThumbnailImage(Int32, Int32, Image+GetThumbnailImageAbort, IntPtr):返回此 Image 的缩略图。
- RotateFlip(RotateFlipType):旋转、翻转或者同时旋转和翻转 Image。
- Save(Stream, ImageFormat):将此图像以指定的格式保存到指定的流中。
- Save(String, ImageFormat):将此 Image 以指定格式保存到指定文件。
4.绘制图片
讯享网
5.缩放
6.获取缩略图
讯享网
7.旋转
8.双倍缓冲
讯享网
9.格式转换与保存
使用Export和Import特性标签主要是为了让程序进行解耦。
代码举例如下:
讯享网
输出结果:
C# Math 类主要用于一些与数学相关的计算。
一些常用的方法如下:
Concurrent命名空间下提供多个线程安全集合类方案。
C#中线程安全集合汇总:

1.ConcurrentDictionary
对于c#中使用的List
ConcurrentDictionary的用法和Dictionary类似。
2.BlockingCollection
1.BlockingCollection简介
多线程操作集合时,ConcurrentQueue 是我常用的,一直用得也挺爽,突然发现了 BlockingCollection,原来还可以更简单。
BlockingCollection ,与经典的阻塞队列数据结构类似,能够适用于多个任务添加和删除数据,提供阻塞和限界能力,它是一个自带阻塞功能的线程安全集合。BlockingCollection
2.常用方法和属性
讯享网
3.代码举例
先看这一段代码的作用:
BlockingCollection.GetConsumingEnumerable 方法是关键,这个方法会遍历集合取出数据,一旦发现集合空了,则阻塞自己,直到集合中又有元素了再开始遍历。
1.例子1
一个任务往容器里面添加数据,另一个任务把数据从容器中取出,进行处理。
下面的代码很简单,使用BlockingCollection定义一个消息队列,然后使用AddMessage方法向队列中添加消息。重点看一下Process方法,里面写了一个死循环,里面调用BlockingCollection的Take方法,当队列中如果没有消息时,则阻塞队列,所以并不会一直循环。等到有新消息进来时,它就会继续处理。还有一个,我们在这个类中使用单独的线程来作执行Process方法。
讯享网
输出结果:
1.例子2
还可以将Action委托作为消息放到队列中,这样可以实现一个任务执行器。
讯享网
输出结果:
3.ConcurrentQueue
线程安全的集合使用Concurrent开头的集合就可以了。 多线程操作集合时,可以考虑用ConcurrentQueue。
1.ConcurrentQueue简介
ConcurrentQueue队列是一个高效的线程安全的队列。
ConcurrentQueue表示线程安全的先进先出 (FIFO) 集合。
经典的多线程应用问题就是:有一个或多个线程(生产者线程)产生一些数据,还有一个或者多个线程(消费者线程)要取出这些数据并执行一些相应的工作。
2.常用方法和属性
讯享网
- .Net所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于Stream,数据库的连接,GDI+的相关对象,还有Com对象等等,这些资源并不是受到.Net管理而统称为非托管资源。而对于内存的释放和回收,系统提供了垃圾回收器(GC-Garbage Collector),而至于其他资源则需要手动进行释放。
- .Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要GC回收;后者是分配在堆上,因此它的内存释放和回收需要通过GC来完成。GC的全称为“Garbage Collector”,顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被GC回收。也就是说,一个引用类型对象所占用的内存需要被GC回收,需要先成为垃圾。那么.Net如何判定一个引用类型对象是垃圾呢,.Net的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。GC对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。
- 托管和非托管的资源指的是存储在托管或本机堆中的对象。
- C#编程的一个优点是程序员不需要担心具体的内存管理,垃圾回收器会自动处理所有的内存清理工作。尽管垃圾收集器释放存储在托管堆中的托管对象,但不释放本机堆中的对象。必须由开发人员自己释放它们。
垃圾回收器的出现意味着,通常不需要担心不再需要的对象,只要让这些对象的所有引用都超出作用域,并允许垃圾回收器在需要时释放内存即可。但是,垃圾回收器不知道如何释放非托管的资源(例如,文件句柄、网络连接和数据库连接)。 托管类在封装对非托管资源的直接或间接引用时,需要制定专门的规则,确保非托管的资源在回收类的一个
实例时释放。
在定义一个类时,可以使用两种机制来自动释放非托管的资源。这些机制常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。这两种机制是:
- 声明一个析构函数(或终结器),作为类的一个成员;
- 在类中实现System.IDisposable接口;
众所周知,C#在大部分情况下,内存都是由.Net托管的,而有一些特殊的类,它能够进行一些非托管的操作,这通常用于在和c/C++代码进行交互时。Marshal是一个方法集合,主要应用在C#和非托管代码交互时,主要有如下方法:
- 分配非托管内存
- 复制非托管内存块
- 将托管类型转换为非托管类型
- 其他方法(与非托管代码交互时)
使用 Marshal 做出可以快速释放内存的大数组
使用方法:
讯享网
这行代码会将内存减少到几M左右。
注意:析构函数只能被GC来调用。
.NET里面有一个GC.Collect()吧,它的功能就是强制对所有代进行垃圾回收。
首先举个例子:
讯享网
.NET CLR中对于大于85000字节的内存既不像引用类型那样分配到普通堆上,也不像值类型那样分配到栈上,而是分配到了一个特殊的称为LOH的内部堆上,这部分的内存只有在GC执行完全回收,也就是回收二代内存的时候才会回收。因此,考虑如下情形:假设你的程序每次都要分配一个大型对象(大于85000字节),但却很少分配小对象,导致2代垃圾回收从不执行,即使这些大对象不再被引用,依然得不到释放,最终导致内存泄漏。示例代码:
讯享网
以上有三个按钮,Add Buffer、Remove Buffer 和 Force GC。每点一次 Add Buffer 会申请 1 GB 的内存,并添加到 mBuffer 中。每点一次 Remove Buffer 会从 mBuffer 中移除 1 GB 的内存。当点击 Force GC 的时候会强制执行一次完全的垃圾回收。
现在我点击4次Add Buffer按钮,BlankApp2进程的内存大约达到了4个G,如图:

接着,我再点击4次 Remove Buffer,发现内存并没有释放。紧接着,我点击了一次Force GC,发现内存马上就被释放到了98.7MB,如图:

在C#中,推荐使用System.IDisposable接口替代析构函数。
提示:实现Dispose方法的时候,一定要加上“GC.SuppressFinalize( this )”语句,避免再让GC调用对象的析构函数。
IDisposable接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。
IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。MyClass类的Dispose()方法的实现代码如下:
Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。
例如下面的例子展示了析构函数和Dispose方法的调用顺序:
讯享网
运行结果:
显然,以上代码,在出了Create函数外,myClass对象的析构函数没有被立刻调用,而是等显示调用GC.Collect才被调用。
对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。例如如下代码:
运行结果:
讯享网
那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,按 照前面的GC流程来说,GC对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的 Dispose函数,虽说被执行了,但是GC还是需要执行析构函数,那么一个完整的Dispose函数,应该通过调用 GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。
例子如下:
运行结果:
讯享网
显然加上了GC.SuppressFinalize(this)后,对象的析构函数没有被调用。
表格总结:

1.内存溢出和内存泄漏简介
内存溢出:通俗理解就是内存不够,系统中存在无法回收的内存或程序运行要用到的内存大于能提供的最大内存,从而造成“Out of memory”之类的错误,使程序不能正常运行。
内存泄露:内存泄漏指由于疏忽或错误造成程序不能释放或不能及时释放已经不再使用的内存的情况,是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存不能回收和不能及时回收,最后可能导致程序运行缓慢或者崩溃的问题。
造成内存泄漏的可能原因:
1、你的对象仍被引用但实际上却未被使用。 由于它们被引用,因此GC将不会收集它们,这样它们将永久保存并占用内存。
2、当你以某种方式分配非托管内存(比如用Marshal静态类来操作内存,但没有垃圾回收)并且不释放它们。
2.使用未退订的event事件造成的内存泄漏
注意当使用+=来订阅事件的时候,在不用的时候要用-=来取消订阅,这样才能取消对事件的引用以减少内存泄漏现象。
3.静态变量
静态变量中的成员所占的内存,如果不手动处理是不会释放内存的,单态模式的对象也是静态的,所以需要特别注意。因为静态对象中的成员所占的内存不会释放,如果此成员是以个对象,同时此对象中的成员所占的内存也不会释放,以此类推,如果此对象很复杂,而且是静态的就很容易造成内存泄露。
C#中使用AppDomain.CurrentDomain.BaseDirectory来获取程序的运行路径。例如:
输出结果:
讯享网

首先项目要引用System.Management库。









版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/157329.html