目录
原码、反码、补码(https://www.cnblogs.com/nsy101010/p/17284034.html)
4.2.1内置的引用类型有:object、dynamic 和 string。
4.2.2用户自定义引用类型有:class、interface 或 delegate。
5.string字符串(笔记在二:二、C#字符串(string)_摆烂的少年的博客-CSDN博客)
6.类型转换(笔记在二:二、C#字符串(string)_摆烂的少年的博客-CSDN博客)
11.静态常量(Const)和动态常量(Readonly)的区别
17. IO操作(( IO操作系列_摆烂的少年的博客-CSDN博客)
18.进程(C# 线程(Thread) -博客园 (cnblogs.com))
19.Thread、ThreadPool、Task之间的关系
19.3 Task多线程和异步(Task和async/await)
19.3.4 同步线程设置Task.RunSynchronously();
19.3.5 Task的阻塞方法(Wait/WaitAll/WaitAny)
19.3.6 Task的Wait/WaitAny/WaitAll方法
19.3.7 Task的延续操作(WhenAny/WhenAll/ContinueWith)
19.3.8 Task的任务取消(CancellationTokenSource)
-
cs文件结构
![](https://img-blog.csdnimg.cn/img_convert/f0bb2f40b2097cce017a0b440d43a0f1.png)
-
程序的第一行using System; - using 关键字用于在程序中包含System 命名空间。一个程序一般有多个using语句。
-
下一行是namespace声明。一个namespace是一系列的类。WebApplication1命名空间包含了类Program。。下一行是 class声明。类Program包含了程序使用的数据和方法声明。类一般包含多个方法。方法定义了类的行为。在这里,Program类只有一个Main方法。
-
下一行定义了Main方法,是所有C#程序的入口点。Main方法说明当执行时类将做什么动作。.Main方法通过方法体中的语句指定它的行为。
命名空间的好处:
-
解决命名冲突。
-
方便查找类,方便维护代码
注意:
-
一个cs文件的结构大约是由以上几个部分组成,但是根据不同的需求和作用,会有不同的内容;
-
C#是大小写敏感的。
-
所有的语句和表达式必须以分号)结尾
-
程序的执行从Main方法开始。
-
与Java不同的,文件名可以不同于类的名称。
-
基本语法
注意:
-
C#是大小写敏感的。
-
所有的语句和表达式必须以分号(G)结尾。
-
与Java不同的是,文件名可以不同于类的名称。
-
C#是一种面向对象的编程语言。在面向对象的程序设计方法中,程序由各种对象组成。相同种类的对象通常具有相同的类型。
案例:
-
以动物为例,”猫“和”狗“,同样是人类,故个生物的类型相同都是属于动物类。
关键字
-
关键字,是对编译器有特殊意义的预定义保留标示符,它们不能在程序中用作标示符
-
using 关键字
-
在任何C#程序中的第一条语句都是:
-
例如:using system;
-
using 关键字用于在程序中包含命名空间。
-
一个程序可以包含多个using语句。
-
class关键字
-
class关键字用于声明一个类。
原码、反码、补码(https://www.cnblogs.com/nsy101010/p/17284034.html)
-
C#的注释方式
多行注释
/*
多行注释语法
*/
单行注释
//单行注释
XML文档注释用
只需要在类或者方法前输入///回车即可自动输入注释主体
///<summary>
/// xml注释,可以用来帮助开发者生成源码文档。
/// summary一般概要, main是应用程序的入口方法。
/// </summary>
///<param name="args">参数1</param>
region折叠代码注释
#region 变量
代码块
#endregion
注释的作用:
-
解释:说明代码作用
-
注销:将暂时不需要的代码注销
元组:
static void Main(string[] args) {
(string,int) a1 = ("张三丰",56);
Console.WriteLine($"名字:{a1.Item1}\t年龄:{a1.Item2}");
(string name,int age)a2 = ("张三",22);
Console.WriteLine($"名字:{a2.name}\t年龄:{a2.age}");
var b1 = (name: "张三丰", age: 56);
Console.WriteLine($"名字:{b1.name}\t年龄:{b1.age}");
b1.name = "张三三";
Console.WriteLine($"名字:{b1.name}\t年龄:{b1.age}");
var b2 = (1,2,3,4,5,6,7,8,9,0,"李思思");
Console.WriteLine("b2的值:{0}",b2.ToString());
//元组数据互换
//使用元组可一行代码交换两个变量的值,非常简洁
string x = "xxxx";
string y = "yyyy";
(x,y) = (y,x);
Console.WriteLine($"x的值:{x},y的值:{y}");//输出:x的值:yyyy,y的值:xxxx
}
4.C#的基本数据类型
4.1值类型(Value types)
![](https://img-blog.csdnimg.cn/img_convert/1d47b2cc5166c7cbd2c8a81160cbbe62.png)
最大值 最小值
int 2147483647 int -2147483648
uint 4294967295 uint 0
byte 255 byte 0
sbyte 127 sbyte -128
short 32767 short -32768
ushort 65535 ushort 0
long 9223372036854775807 long -9223372036854775808
ulong 18446744073709551615 ulong 0
float 3.402823E+38 float -3.402823E+38
double 1.79769313486232E+308 double -1.79769313486232E+308
decimal 79228162514264337593543950335 decimal -79228162514264337593543950335
4.2引用类型(Reference types)
4.2.1内置的引用类型有:object、dynamic 和 string。
-
object:对象类型
-
当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱。
object obj;
obj = 100; // 这是装箱
-
dynamic:动态类型
-
可以存储任何类型的值在动态数据类型变量中。
-
这些变量的类型检查是在运行时发生的。
//语法声明
dynamic <variable_name> = value;
//例子
dynamic d = 20;
-
String:字符串类型
String str = "abc";
//C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:
string str = @"C:\Windows";
等价于:
string str = "C:\\Windows";
4.2.2用户自定义引用类型有:class、interface 或 delegate。
4.3指针类型(Pointer types)
//指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。
//声明指针类型的语法:
type* identifier;
//例如:
char* cptr;
int* iptr;
5.string字符串(笔记在二:二、C#字符串(string)_摆烂的少年的博客-CSDN博客)
6.类型转换(笔记在二:二、C#字符串(string)_摆烂的少年的博客-CSDN博客)
7.函数(方法)
-
函数的高级参数(ref、out)
-
ref:
引用传递--->将参数/变量传入一个函数中处理,再将处理完成的值带出(就是使用这个参数传递的形参进入函数后,假如函数里面会对函数外面的实参做修改操作,那么修改的值就会影响函数外的实参变量)。不加ref参数时,函数里对变量进行修改,不会影响到函数外给函数传递实参的变量的值(函数下面再调用实参的值时,还是实参变量进入方法修改之前的变量值,函数里修改参数值不会返回给函数外)
![](https://img-blog.csdnimg.cn/img_convert/83209b9d30de95001bae1133537f3326.png)
![](https://img-blog.csdnimg.cn/img_convert/47da2e17e636a0b6fb034b5edc549c53.png)
-
out:
把函数中处理的结果返回出函数(相当于return的作用)
static void Main(string[] args){
int a = 10;
int b = 20;
int max;
int min;
A(a,b,max,min);
//输出max的值是:30,min的值是:10
Console.WriteLine("max的值是:{0},min的值是:{1}",{max},{min});
}
public void A(int a,int b,out int max,out int min){
max = a+b;
min = b-a;
}
8.数组
8.1一维数组
int [] a = new int[5] { 99, 98, 92, 97, 95};
8.2 二维数组
int [,] a = new int [2,4] {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
};
//如果我想获取数组中6的值
Console.WriteLine(a[1,2]);
8.3 多维数组
//三维数组举例(还有四维、五维....)
int[, ,] a = new int[2, 2, 3]{
{
{1,2,3},{4,5,6}
},
{
{7,8,9},{2,3,4}
}
};
//如果我想获取数组中9的值
Console.WriteLine(a[1,0,2]);
8.4 交叉数组
int[][] scores = new int[5][];
for (int i = 0; i < scores.Length; i++)
{
scores[i] = new int[4];
}
//或者初始化一个交错数组
int[][] scores = new int[2][]{new int[]{92,93,94},new int[]{95,96,97,98}};
//如果我想获取数组中94的值
Console.WriteLine(a[{0}][{2}]);
8.5 实例化类对象数组
student[] arr = new student[2]{
new Student("张三”,18),
new Student("李四”,19)
};
Console.WriteLine ({arr[0].Name},{arr[0].Age});
Console.WriteLine ({arr[1].Name},{arr[1].Age}") ;
9.类
详细可以参考:(C# 类教程-类 - ITPOW)
9.1 修饰符
9.1.1 访问修饰符
-
public :公开的公共的
-
private:私有的,只能在当前类的内部访问
-
protected:受保护的,只能在当前类的内部以及该类的子类中访问。
-
internal:只能在当前项目中访问。在同一个项目中,internal和public的权限是一样。
-
protected internal:protected+internal
能够修饰类的访问修饰符只有两个:public、internal。
在继承中,当可访问性不一致时,子类的访问权限不能高于父类的访问权限,否则会暴漏父类的成员。
![](https://img-blog.csdnimg.cn/img_convert/c7b2d3bcc5917234954ef0a883e4968a.png)
![](https://img-blog.csdnimg.cn/img_convert/ac190720bf178271dd72fcbd07c94e71.jpeg)
9.1.2 其他修饰符
-
partial部分类
在合作开发时,一个类可能由多个人写,每个人都写在不同的文件中,这时可以使用partial来修饰类,表示当前只是该类的一部分。
-
sealed密封类
丁克,不能被继承
-
static
静态和非静态的区别
1.在非静态类中,既可以有实例成员,也可以有静态成员。
2.在调用实例成员的时候,需要使用对象名.实例成员;
3. 在调用静态成员的时候,需要使用类名.静态成员名;
总结:
静态成员必须使用类名去调用,而实例成员使用对象名调用。
静态函数中,只能访问静态成员,不允许访问实例成员。
实例函数中,既可以使用静态成员,也可以使用实例成员。
静态类中只允许有静态成员,不允许出现实例成员。
10.静态类
10.1静态类定义方式:
-
在类修饰符和class关键字中间,添加static关键字。(推荐)
-
也可以在修饰符前添加static来标识一个类是一个静态类。
10.2.静态类的特点:
-
不能实例化。原因:静态类只有一个“副本”,说明静态类在分配空间时只有一份。
-
静态类只能包含静态成员。静态类是不能包含实例成员(字段,属性,方法,事件)。
-
静态类中构造函数必须是静态的,且不能有修饰符。
-
静态类中的构造函数只在第一次使用静态类时被调用一次。普通类的构造函数,每次实例化都会被调用)
-
静态类默认是密封的,所以不能继承。
-
静态类中的属性或者方法只能使用类本身来访问(例如:类名.属性(例如:Class.age;)/类名.方法();)
-
实例(普通)方法可以访问静态成员和静态方法
-
静态方法中只能访问静态成员和属性,不能访问实例方法和成员。
特别提醒:
类中的成员在定义时,没有顺序,但建议先定义字段,属性,构造函数,再定义方法。
不能为静态类创建实例对象,所以静态类中构造函数写了也没用。
11.静态常量(Const)和动态常量(Readonly)的区别
参考笔记 三: CSDN
12.Struct(结构体)和Enum(枚举)
12.1 结构体:struct
-
在 C# 中,结构体是值类型数据结构。它使得一个单一变量可以存储各种数据类型的相关数据。
-
struct 关键字用于创建结构体。结构体是用来代表一个记录。
-
为了定义一个结构体,您必须使用 struct 语句。struct 语句为程序定义了一个带有多个成员的新的数据类型。
例如,可以按照如下的方式声明 Book 结构:
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
public class testStructure
{
public static void Main(string[] args)
{
Books Book1; /* 声明 Book1,类型为 Books */
/* book 1 详述 */
Book1.title = "C#软件开发";
Book1.author = "某某某";
Book1.subject = "C#";
Book1.book_id = 6495407;
Console.WriteLine(Book1.title);//C#软件开发
Console.WriteLine(Book1.author);//某某某
Console.WriteLine(Book1.subject);//C#
Console.WriteLine(Book1.book_id);//6495407
}
}
C#结构体菜鸟教程
12.2 枚举:enum
-
enum枚举定义的关键字。
-
枚举是一组命名整型常量(使用枚举时,不能实例化)。枚举类型是使用enum关键字声明的。
-
C# 枚举是值类型。换句话说,枚举包含自己的值,且不能继承或被继承。
-
枚举方法也可以单独定义在一个类中,使用时直接在这个类的同一个项目下的控制台类中Console.WriteLine(Course.后端)即可获取这个枚举其中某一项的值。例如:
-
可以直接调用枚举值,也可以根据给枚举值设置的数值获取获取该枚举:
public enum Demo
{
A = 1,
B = 2,
C = 3,
D = 4
}
使用时:
public void Test()
{
Demo d = (Demo)int.Parse(1); // d : A
//或者
Demo d = (Demo)1; // d :A
}
![](https://img-blog.csdnimg.cn/img_convert/1b9ba5b1f0d593c2521b38da99413470.png)
namespace Day14_C
{
//枚举
public class Program
{
static void Main(string[] args)
{
Console.WriteLine(Color.Red);//Red
Console.WriteLine((int)Color.Red);//10 枚举方法里没有设置值时,默认就是0
int num = -10;
if (num == (int)Color.Red)
{
Console.WriteLine($"颜色是:{Color.Red}");
}
else if (num == (int)Color.Green)
{
Console.WriteLine($"颜色是:{Color.Green}");
}
else if (num == (int)Color.Blue)
{
Console.WriteLine($"颜色是:{Color.Blue}");
}
else{
Console.WriteLine($"颜色是:{Color.Block}");
}
Console.WriteLine(Course.后端);
}
public enum Color {
//枚举中的项(元素),命名大驼峰。
//枚举中的整型数字,可以随意设置,但不能相同(唯一)。
//枚举中的整型数字,可以省略,默认整型数字从0开始,每一个枚举项在此基础加1
//没有给值时,默认就是从0开始
//Red,
//Green,
//Blue,
//Block
//设置值时,先后没有顺序,项的值只有整数,不能有浮点数
Red = -10,
Green = 20,
Blue = 30,
Block = 40
}
}
}
13.事件(event)和委托(delegate)
13.1 委托:delegate
-
13.1.1委托实例
-
其实就是调用者的委托: 调用者调用委托,然后委托调用目标方法
-
间接的把调用者和目标方法解耦合了
namespace Day16_C_委托
{
delegate int DG(int x);
internal class Program
{
static int One(int x) {
Console.WriteLine("委托调用One方法:{0}",x + 10);
return x+10;
}
static int Two(int y) {
Console.WriteLine($"委托调用Two方法:{0}", y + 10);
return y+20;
}
static void Main(string[] args)
{
DG dg = One;
dg(100);
Console.Read();
}
}
}
-
案例
namespace ConsoleApp1
{
delegate int DG(int x);
internal class PracticeDelegate
{
static int Add(int x) {
return x + x;
} static int Sub(int x) {
return x - x;
} static int Div(int x) {
return x / x;
}
public static void Method1(int[] arr , DG dg) {
for (int i = 0;i<arr.Length;i++) {
arr[i] = dg(arr[i]);
}
}
static void Main(string[] args) {
//DG dg = Add;
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Method1(arr,Add);
for (int i = 0;i<arr.Length;i++) {
Console.WriteLine($"集合中{i+1}处的元素的值是:{arr[i]}");
}
Console.Read();
}
}
}
-
13.1.2 多播委托
-
所有的委托实例都具有多播的能力。一个委托实例可以引用一组目标方法。
![](https://img-blog.csdnimg.cn/img_convert/d329d4f138c6cd1dcaa38b73249e6d6b.png)
-
委托是不可变的
-
使用+=或-=操作符时,实际上是创建了新的委托实例,并把它赋给当前的委托变量。
namespace ConsoleApp1
{
delegate int MoreDelegate(int a,int b);
internal class PracticeDelegate2
{
static int Method1(int a,int b) {
return a + b;
}
static int Method2(int a,int b) {
return a - b;
}
static void Main(string[] args)
{
int a = 20;
int b = 10;
MoreDelegate md = Method1;
md += Method2;
int result1 = md(a,b);
Console.WriteLine(result1);//20+10=30 20-10=10 输出结果:10
md -= Method2;
int result2 = md(a,b);
Console.WriteLine(result2);//20+10=30 输出结果:30
Console.Read();
}
}
}
-
如果多播委托的返回类型不是void,那么调用者从最后一个被调用的方法来接收返回值。前面的方法仍然会被调用,但是其返回值就被弃用了。
-
13.1.3实例方法目标和静态方法目标
-
当一个实例方法被赋值给委托对象的时候,这个委托对象不仅要保留着对方法的引用,还要保留着方法所属实例的引用。
-
System. Delegate的Target属性就代表着这个实例。
-
如果引用的是静态方法,那么Target属性的值就是null。
![](https://img-blog.csdnimg.cn/img_convert/fb384db9d035bd3cd22337f55c0f06ba.png)
-
13.1.4 泛型委托类型
-
委托类型可以包含泛型类型参数
public delegate T Transformer<T>(T arg);
![](https://img-blog.csdnimg.cn/img_convert/6e1cc3eae309665f81ea776541f8433a.png)
13.2 事件:Event
-
事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。
-
使用委托的时候,通常会出现两个角色,一个广播者,一个订阅者
-
使用委托的时候,通常会出现两个角色,一个广播者,一个订阅者
-
广播者这个类型包含一个委托字段,广播者通过调用委托来决定什么时候进行广播。
-
订阅者是方法目标的接收者,订阅者可以决定何时开始或结束监听,方式是通过在委托上调用+=和-=
-
一个订阅者不知道和不干扰其它的订阅者。
-
事件就是将上述模式正式化的一个语言特性。
-
事件是一种结构,为了实现广播者/订阅者模型,它只暴露了所需的委托特性的部分子集。
-
事件的主要目的就是防止订阅者之间互相干扰。
-
13.2.1 声明事件
-
最简单的声明事件的方式就是在委托前面加上event关键字
namespace ConsoleApp1
{
delegate double EventTest(double a,double b);
internal class Event1
{
public event EventTest ET;
}
}
-
事件在内部是如何工作的
![](https://img-blog.csdnimg.cn/img_convert/fe3a58e46fdd73f89bebd1170982d786.png)
-
标准的事件模式
-
为编写事件,.NET定义了一个标准的模式
-
System.EventArgs,一个预定义的框架类,除了静态的Empty属性之外,它没有其它成员。
-
EventArgs是为事件传递信息的类的基类。
-
例子:
![](https://img-blog.csdnimg.cn/img_convert/aca8fe2011f799ac1b7904fd570f04ee.png)
-
为事件选择或者定义委托
-
返回类型是void;
-
接收两个参数,第一个参数类型是object,第二参数类型是EventArgs的子类。
-
第一个参数表示事件的广播者,第二个参数包含需要传递的信息;
-
名称必须以EventHandler结尾。
![](https://img-blog.csdnimg.cn/img_convert/05c6c026d8e14ec32480ed2f34152880.png)
-
13.2.1 泛型事件
-
13.2.3 例子
namespace EventProctice
{
//定义委托
public delegate void DelegateKitchen(string person,string dish);
//饭店类(发布者)
public class Restaurant
{
#region 定义事件
//排队事件
public event DelegateKitchen? EventWaiting;
//备菜事件
public event DelegateKitchen? EventPrepare;
//菜在制作中事件
public event DelegateKitchen? EventBeforeMake;
//制作完成事件
public event DelegateKitchen? EventAfterMake;
//打包事件
public event DelegateKitchen? EventPackaged;
#endregion
public void Kitchen(string person,string dish) {
#region 排队逻辑
EventWaiting?.Invoke(person,dish);
Console.WriteLine($"内部:{person}的{dish},排队");
Thread.Sleep(100);
#endregion
#region 备菜逻辑
EventPrepare?.Invoke(person,dish);
Console.WriteLine($"内部:{person}的{dish},备菜");
Thread.Sleep(100);
#endregion
#region 制作逻辑
EventBeforeMake?.Invoke(person,dish);
Console.WriteLine($"内部:{person}的{dish},制作");
Thread.Sleep(100);
EventAfterMake?.Invoke(person,dish);
#endregion
#region 包装逻辑
//EventWaiting?.Invoke(person,dish);
Console.WriteLine($"内部:{person}的{dish},包装");
Thread.Sleep(100);
EventPackaged?.Invoke(person, dish);
#endregion
}
}
}
namespace EventProctice
{
//接收者类
public class KitchMethod
{
public static void Restaurant_EventPackaged1(string person,string dish){
Console.WriteLine($"包装完毕事件:通知骑手,{dish}已经包装完毕,来取餐");
}
public static void Restaurant_EventPackaged(string person ,string dish) {
Console.WriteLine($"包装完毕事件:通知{person},{dish}已经包装完毕");
}
public static void Restaurant_EventAfterMake(string person, string dish)
{
Console.WriteLine($"制作完成事件:通知{person},{dish}已经制作完毕,正在包装");
}
public static void Restaurant_EventWaiting(string person,string dish) {
Console.WriteLine($"等待事件:通知{person},{dish}已接单,正在等待制作");
}
}
}
namespace EventProctice
{
internal class Program
{
static void Main(string[] args)
{
Restaurant restaurant = new Restaurant();
restaurant.EventWaiting += KitchMethod.Restaurant_EventWaiting;
restaurant.EventAfterMake += KitchMethod.Restaurant_EventAfterMake;
restaurant.EventPackaged += KitchMethod.Restaurant_EventPackaged;
restaurant.EventPackaged += KitchMethod.Restaurant_EventPackaged1;
restaurant.Kitchen("小云","红烧鱼");
Console.ReadKey();
}
}
}
-
输出结果:
![](https://img-blog.csdnimg.cn/img_convert/5747b2e8c5debf06ecf2c453d58b070d.png)
14.继承
-
当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类。
-
继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。
14.1基类和派生类
-
一个类可以派生自多个类或接口,这意味着它可以从多个基类或接口继承数据和函数。
-
C# 中创建派生类的语法如下:
using System;
namespace InheritanceApplication
{
//基类(父类)
class Shape
{
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
protected int width;
protected int height;
}
// 派生类(子类)
class Rectangle: Shape
{
public int getArea()
{
return (width * height);
}
}
class RectangleTester
{
static void Main(string[] args)
{
Rectangle Rect = new Rectangle();
Rect.setWidth(5);
Rect.setHeight(7);
// 打印对象的面积
Console.WriteLine("总面积: {0}", Rect.getArea());//输出结果:35
Console.ReadKey();
}
}
}
14.2 C# 多重继承
-
多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。
-
与单一继承相对,单一继承指一个类别只可以继承自一个父类。
-
C# 不支持多重继承。但是可以使用接口来实现多重继承。
using System;
namespace InheritanceApplication
{
class Shape
{
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
protected int width;
protected int height;
}
// 基类 PaintCost
public interface PaintCost
{
int getCost(int area);
}
// 派生类
class Rectangle : Shape, PaintCost
{
public int getArea()
{
return (width * height);
}
public int getCost(int area)
{
return area * 70;
}
}
class RectangleTester
{
static void Main(string[] args)
{
Rectangle Rect = new Rectangle();
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 打印对象的面积
Console.WriteLine("总面积: {0}", Rect.getArea());//输出结果:35
Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area));//输出结果:
Console.ReadKey();
}
}
}
14.3 抽象类: Abstract
-
C#允许把类、属性和函数声明为abstract。
-
抽象类不能实例化,抽象类可以包含普通属性和抽象属性,普通函数和抽象函数。
-
抽象函数就是只有函数定义没有函数体的函数。显然,抽象函数本身也是虚拟(virtual)的。
-
14.3.1 示例演示
-
①、创建一个鸟的抽象类
// 鸟的抽象类
abstract class Bird // 含有抽象属性和方法,就一定是抽象类
{
// 鸟速度的属性
public double Speed { get; set; }
// 鸟体重的属性
public abstract double Weight { get; set; }
// 鸟飞翔的抽象方法
public abstract void Fly();
}
-
②、创建一个麻雀类,继承鸟的抽象类
// 创建麻雀的类,继承自鸟
class Sparrow : Bird // 继承了抽象类,此时必须要求实现抽象属性和方法
{
// 麻雀体重的属性
public override double Weight { get; set; }
// 麻雀飞翔的方法
public override void Fly()
{
Console.WriteLine("麻雀的飞翔~");
}
}
-
③、实例化麻雀和鸟
// 用派生类声明和构造
Sparrow sparrow = new Sparrow();
sparrow.Fly(); //输出结果:麻雀的飞翔~
// 用抽象类声明,用派生类构造
Bird bird = new Sparrow();
bird.Fly(); //输出结果:麻雀的飞翔~
-
14.3.2 抽象类的作用
-
当我们在写基类时,有的属性、方法是一定要被重写的,在基类中实现并没有意义。这时我们就可以将这种属性、方法写作抽象属性、抽象方法,并将基类改作抽象类,这样我们在写派生类时,直接对没有实现的抽象属性、抽象方法进行重写(override)即可。
-
14.3.3抽象类总结
-
抽象类中的抽象属性和抽象方法必须是公有的,因此必须有public修饰符。
-
派生类必须重写(override)抽象类中的所有抽象属性和抽象方法,如果没有全部重写(override),那么派生类必须是抽象类。
-
抽象类中可以有非抽象属性和非抽象方法,也可以是私有或者公有,但是如果是私有的话,派生类就不能访问,这样也就无意义了,所以一般情况下都设置为公有。
-
有抽象属性或抽象方法的类一定是抽象类,抽象类中的属性或方法不一定都是抽象的。
-
14.3.4抽象类与接口的区别
-
抽象类和接口都包含可以由派生类继承的成员,它们都不能直接实例化。
-
它们的派生类只能继承一个基类(所谓的单继承,多接口继承),即:只能直接继承一个抽象类,但是可以继承任意多个接口。
-
抽象类中可以定义成员的具体实现,但是接口却不行。
-
抽象类中可以包含字段,构造函数,析构函数,静态成员等,接口中不可以。
-
抽象类中的成员可以是私有的(只要它们不是抽象的),受保护的,内部的或者受保护的内部成员,但是接口中的成员必须是公共的(默认就是公共的)。
14.4 接口类: interface
-
抽象类在某种程度上与接口类似,但是它们大多只是用在当只有少数方法由基类声明由派生类实现时。
-
接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。
-
接口不能实例化,也不能通过类本身来调用接口中的方法。必须实现接口才能使用接口的方法。
-
抽象类不能直接实例化,但允许派生出具体的,具有实际功能的类。
-
接口里只能定义属性、方法、事件、索引器这些成员
-
抽象类中的方法不能实现,没有方法体。接口中的方法可以实现,但是不建议有方法体
-
接口中有方法体的方法,接口继承类不能继承、也重写不了(所以在接口中没必要实现方法)
-
14.4.1定义接口
//接口使用 interface 关键字声明,它与类的声明类似。
//接口声明默认是 public 的。
interface IMyInterface
{
void MethodToImplement();
}
-
14.4.2实现接口
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
-
继承接口后,我们需要实现接口的方法 MethodToImplement() , 方法名必须与接口定义的方法名一致。
14.5密封类: sealed
(参考:C#之密封类(详解) - 瞿亮 - 博客园 (cnblogs.com))
14..6 部分类:partial
-
partial 关键字允许把类、结构、方法或者接口放在多个文件中。
-
一般情况下,某种类型的代码生成器生成了一个类的某部分,所以把这类放在多个文件中是有益的。
-
处理大型项目的时候,使用一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理(相当于支持并行处理,很实用)
-
假定要给类添加一些从工具中自动生成的内容。如果重新运行该工具,前面所做的修改就会丢失。
-
partial 关键字有助于把类分开放在两个文件中,而对不由代码生成器定义的文件进行修改。
-
partial 关键字的用法是:把Partial放在class、struct、或者inteface关键字前面。在下面的举例中 SampleClass类驻留在两个不同的源文件 SampleClassAutogenerated.cs 和 SampleClass.cs中。
在使用partial需要注意以下一些情况:
1、使用partial 关键字表明可在命名空间内定义该类、结构或接口的其他部分;
2、所有部分都必须使用partial 关键字;
3、各个部分必须具有相同的可访问性,如public、private 等;
4、如果将任意部分声明为抽象的,则整个类型都被视为抽象的;
5、如果将任意部分声明为密封(sealed修饰符)的,则整个类型都被视为密封的;
6、如果任意部分声明继承基类时,则整个类型都将继承该类;
7、各个部分可以指定不同的基接口,最终类型将实现所有分部声明所列出的全部接口;
8、在某一分部定义中声明的任何类、结构或接口成员可供所有其他部分使用;
//SampleClassAtuogenerated.cs
partial class SampleClass
{
public void MethodOne(){}
}
//SampleClass.cs
partial class SampleClass
{
public void MethodTwo(){}
}
-
编译包含这两个源文件项目时,会创建一个SampleClass类,它有两个方法MethodOne() 和 MethodTwo()。
如果声明类时 使用了下面的关键字,则这些关键字就必须应用于 同一个类型的的所有部分。
1、public
2、private
3、protected
4、internal
5、abstract
6、sealed
7、new
8、一般约束
-
在嵌套的类型中,只要partial关键字位于class关键字的前面,就可以嵌套部分类。在把部分类编译到类型中时,属性,XML注释,接口,泛型类型的参数属性和成员会合并。有如下两个源文件:
//SampleClassAutogenerated.cs
[CustomAttribute]
partial class SampleClass:SampleBaseClass,IsampleClass
{
public void MethodOne(){}
}
//SampleClass.cs
[AnotherAttribute]
partial class SampleClass:IOtherSampleClass
{
public void MethodTwo(){}
}
编译后,等价的源文件变成:
[CustomAttribute]
[AnotherAttribute]
partial class SampleClass:SampleBaseClass,ISampleClass,IOtherSampleClass
{
public void MethodOne(){}
public void MethodTwo(){}
}
注意:
尽管partial 关键字很容易创建跨多个文件的巨大的类,且不同的开发人员处理同一个类的不同文件,但是关键字并不用于这个目的。在这种情况下,最好把大类拆分成几个小类,一个类只用于一个目的。
-
部分类可以包含部分方法。如果生成的代码应该调用可能不存在的方法,这就是非常有用的。
-
扩展部分类的程序员可以决定创建部分方法的自定义实现代码,或者什么也不做。
-
下面的代码片段包含一个部分类,其方法MethodOne调用APartialMethod方法。APartialMethod方法用partial关键字声明:因此不需要任何实现代码。如果没有实现代码,编译器将删除这个方法调用:
//SampleClassAtuogenerated.cs
partial class SampleClass
{
public void MethodOne()
{
APartiaMethod();
}
public partial void APartialMethod();
}
-
部分方法在实现可以放在部分类的任何其他地方,如下面的代码片段所示。
-
有了这个方法,编译器就在MethodOne内创建代码,调用这里声明的APartialMethod:
//SampleClass.cs
partial class SampleClass : IOtherSampleClass
{
public void APartilMethod()
{
//implementation of APartialMethod
}
}
部分方法必须是void类型,否则编译器在没有实现了代码的情况下无法删除调用。
15.封装
-
封装 被定义为"把一个或多个项目封闭在一个物理的或者逻辑的包中"。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。
-
抽象和封装是面向对象程序设计的相关特性。抽象允许相关信息可视化,封装则使开发者实现所需级别的抽象。
-
C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。
-
public:所有对象都可以访问;
-
private:对象本身在对象内部可以访问;
-
protected:只有该类对象及其子类对象可以访问
-
internal:同一个程序集的对象可以访问;
-
protected internal:访问限于当前程序集或派生自包含类的类型。
![](https://img-blog.csdnimg.cn/img_convert/76bd6d98d685d2696c2829a5b7697c7f.png)
16.多态
-
多态是同一个行为具有多个不同表现形式或形态的能力。
-
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。
-
多态性可以是静态的或动态的。在静态多态性中,函数的响应是在编译时发生的。
-
在动态多态性中,函数的响应是在运行时发生的。
-
在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。
![](https://img-blog.csdnimg.cn/img_convert/60a219697a7be607f7c20a7fc0a12d7b.png)
16.1父类方法重写
-
方法修饰符:override
-
在父类子类中,方法的名字,参数都一样。
-
调用方法的时候,优先调子子类的方法。如果子类中没有方法,则调用父类的。
-
注意:
-
重写方法和被重写方法必须具有 相同的方法名和参数列表
-
重写方法的返回值类型必须和被重写方法的返回值类型相同或者是其子类
-
重写方法的不能缩小被重写方法的访问权限
16.2 父类方法重载
-
在同一个类中,方法名字必须一样。
-
参数列表不一样。
-
返回值类型可以相同,也可以不相同。
16.3 虚方法:virtual
-
在我们的C#编程中,把一个基类函数声明为virtual,这样我们就可以在它的任何派生类中重写该函数。
-
同时,我们在其派生类中重写另外一个函数时,要使用override关键字显示声明。重写之后,原来的方法就不存在了。
-
16.3.1 示例讲解
-
①、首先,我们创建基类Person与派生类Student。并添加它俩相同的方法Run,我们设置基类Person的Run方法为虚函数,并让其派生类Student的Run方法进行重写。
class Person
{
public virtual void Run()
{
Console.WriteLine("我是Person的成员方法,我可以跑~");
}
}
class Student : Person
{
public override void Run()
{
Console.WriteLine("我是Student的成员方法,我可以跑~");
}
}
-
②、实例化一个学生stu1,调用它的Run方法。
Student stu1 = new Student();
stu1.Run();//输出结果:我是Student的成员方法,我可以跑
-
③改变上面的代码,看一下效果。
Student stu1 = new Student();
stu1.Run();//输出结果:我是Student的成员方法,我可以跑
Person p1 = new Person();
p1.Run();//输出结果:我是Person的成员方法,我可以跑
Person p2 = new Student();
p2.Run();//输出结果:我是Student的成员方法,我可以跑
注意:
我们在派生类里面重写虚函数之后,不管在哪里调用,只要是用派生类的构造函数实例化的对象,它都是调用重写之后的方法。
但是如果我们调用的是基类构造的对象,此时它的派生类并没有重写虚方法,此时仍然调用基类的虚方法。
16.4 隐藏方法
-
子类默认会把父类的中所有的方法继承过来,如果不想要哪个方中法,可以隐藏,重新在子类中创建了新的同名方法。方法隐藏使用new关键字。new新的。
-
隐藏和重写达到的目的是一一样,都是修改了父类的方法。但意义不一样。
-
隐藏是先把继承过来的隐藏掉,再创建。而重写是修改父类的虛方法。
-
如果签名相同的方法在基类和派生类中都进行了声明,但是该方法没有分别声明为virtual和override,派生类就会隐藏基类方法(要使用new关键字进行声明)。
-
隐藏的话,也就是看不到了,实际这个方法还存在。
-
16.4.1 案例讲解
//基类(父类)
class Person
{
public void Run()
{
Console.WriteLine("我是Person的成员方法,我可以跑~");
}
}
//派生类(子类)
class Student : Person
{
public void Run()
{
Console.WriteLine("我是Student的成员方法,我可以跑~");
}
}
注意:其派生类中的方法,new可写可不写,此时基类的方法为隐藏方法,如下:
public new void Run(){}
//或者
public void Run(){}
-
去掉了virtual和override,此时这两个方法的签名完全相同,我们再用其派生类进行调用。
Student stu1 = new Student();
stu1.Run();//输出结果:我是Student的成员方法,我可以跑~
-
改变上面的代码,看一下会效果
Student stu1 = new Student();
stu1.Run();//输出结果:我是Student的成员方法,我可以跑~
Person p1 = new Person();
p1.Run();//输出结果:我是Person的成员方法,我可以跑~
Person p2 = new Student();//基类声明子类对象
p2.Run();//输出结果:我是Person的成员方法,我可以跑~
1.如果使用派生类声明的对象,调用隐藏方法会调用派生类的。
2.如果 使用基类声明对象,那么就会调用基类的隐藏方法。
-
16.4.2 虚方法与隐藏方法的区别
1、重写和隐藏的定义
-
重写:继承时发生,在派生类中重新定义基类中的方法,派生类中的方法和基类的方法是一样的 。例如:基类方法声明为virtual(虚方法),派生类中使用override声明此方法的重写。
-
隐藏:基类方法不做声明(默认为非虚方法),在派生类中使用new声明此方法的隐藏(new可写可不写)。
2、重写和隐藏的区别
-
重写(virtaul)时,定义的变量为基类或派生类, 赋值为派生类时,皆调用基类的重写方法(也就是调用子类的方法,会从子类中查找有重写则调用 ,没则调用基类方法)。
-
隐藏(new)时,定义的变量为基类,则调用基类的方法(不管赋值是派生类还是基类),定义的变量为派生类则调用派生类的方法。
17. IO操作(( IO操作系列_摆烂的少年的博客-CSDN博客)
18.进程(C# 线程(Thread) -博客园 (cnblogs.com))
18.1 进程的概念
-
程序在服务器上运行时,占据的计算资源合集,称之为进程
-
进程之间不会相互干扰---进程间的通信比较困难(分布式)
-
进程是计算机操作系统中正在运行的程序的实例。通过任务管理器可以查看运行进程。前台进程:用户可以看见相应的操作界面,如:浏览器进程等
-
后台进程:用户默认是“看不见”操作界面。服务中大部分都是后台进程;如:杀毒软件后台偷偷的扫描你的硬盘。
18.2 线程的概念
-
线程是操作系统能够进行运算调度的最小单位。
-
响应操作的最小执行流,线程也包含自己的计算资源,线程是属于进程的
-
一个进程可以有多个线程
18.3 多线程的概念
-
一个进程里面,有多个线程并发执行
18.3 进程和线程的关系
注意:
1. 线程必须包含在进程内。一个进程中可以包含多个线程。多个线程可以并行执行。
2.线程在进程中并行执行的操作,称为多线程。
并行===非阻塞===体验好
并行===阻塞===体验不好
3.资源分配给进程,同一个进程的所有线程共享该进程所有资源。
4.CPU分配给线程,即真正在处理器运行的是线程。
5. 线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步。
进程间通信方式:(1)消息传递(2)共享存储(3)管道通信
19.Thread、ThreadPool、Task之间的关系
19.1.什么是thread和threadPool
-
当我们提及多线程的时候会想到thread和threadpool,这都是异步操作。
-
threadpool其实就是thread的集合,具有很多优势。不过在任务多的时候CPU中的全局队列会存在竞争而消耗资源。
-
thread默认为前台线程,主程序必须等线程跑完才会关闭,而threadpool相反。
-
threadpool默认是后台程序。
总结:
threadpool确实比thread性能优,但是两者都没有很好的api区控制。
如果线程执行无响应就只能等待结束,从而诞生了task任务。
19.2 Thread与ThreadPool
-
Thread
-
前台线程:主程序必须等待线程执行完毕后才可退出程序。Thread默认为前台线程,也可以设置为后台线程
-
后台线程:主程序执行完毕后就退出,不管线程是否执行完毕。ThreadPool默认为后台线程
-
线程消耗:开启一个新线程,线程不做任何操作,都要消耗1M左右的内存
-
ThreadPool
-
ThreadPoll是线程池 其目的是为了减少开启新线程消耗的资源(使用线程池中的空闲线程,不必在开启新线程,以及统一管理线程(线程池中的线程执行完毕后,回归到线程池里,等待新任务).
总结:
ThreadPool性能优于Thread
但是Thread和ThreadPool对线程的控制都不是很好,例如线程等待(线程执行一段时间无响应后,直接停止线程,释放资源 等 都没有直接的API来控制 只能通过硬编码来实现.
同时ThreadPool使用的是线程池全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能。
19.3 Task多线程和异步(Task和async/await)
19.3.1 什么是异步
同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。
异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。
回到顶部
19.3.2 Task介绍
-
task简单地看就是任务,那和thread有什么区别呢?
-
Task的背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll。
-
因为它使用的不是线程池的全局队列,而是使用的本地(内存)队列,使线程之间的资源竞争减少。
-
同时Task提供了丰富的API来管理线程、控制。但是相对前面的两种耗内存,Task依赖于CPU对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。
总结:
Task其实就是在ThreadPool的基础上进行一层封装
ThreaPool启动的线程不好判断线程的执行情况,但Task可以,很好地解决了这个问题。Task不等于Thread,只是微软默认实现ThreadPoolTaskScheduler是依赖于线程池的,因为该类的可访问性为internal,所以我们在实际编码中无法直接在代码中new这么一个Scheduler出来,只能通过TaskScheduler.Default间接的来使用
Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一个ThreadPool的例子吧
static void Main(string[] args)
{
for (int i = 1; i <=10; i++)
{
//ThreadPool执行任务
ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
Console.WriteLine($"第{obj}个执行任务");
}),i);
}
Console.ReadKey();
}
上边的代码通过ThreadPool执行了10个任务,执行结果为:
![](https://img-blog.csdnimg.cn/img_convert/eac7e120faa8123b8e4691d5f1d65304.png)
ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。
19.3.3 Task创建多个线程的方法
#region Task创建方式,都是异步启动
//创建方式1
//初始化(创建)一个任务
Task t1 = new Task(() =>{
Console.WriteLine("1111111打印Task1中的实例化的t1");
});
//启动任务
t1.Start();
//创建方式2(推荐使用)
//创建任务,并运行
Task t2 = Task.Run(() => {
Console.WriteLine("222222Task2");
});
//创建方式3
//工厂创建模式
TaskFactory tf = new TaskFactory();
//创建新的任务并启动
tf.StartNew(() => { Console.WriteLine("333333打印TaskFactory1"); });
tf.StartNew(() => { Console.WriteLine("333333打印TaskFactory2"); });
#endregion
#region Task 有返回值的,与上面用法一样
Task<string> t1 = new Task<string>(() => {
return "1111111打印Task1中的实例化的t1";
});
//启动任务
t1.Start();
string result = t1.Result;//获取线程的返回值
Console.WriteLine(result);
#endregion
19.3.4 同步线程设置Task.RunSynchronously();
允许跨线程修改UI界面:CheckForIllegalCrossThreadCalls = false;(默认为true。不建议使用,不稳定。详细参考:忽略跨线程访问的错误)
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("同步111111");
});
task1.RunSynchronously();//同步设置
//task1.Start();
Task task2 = new Task(() =>
{
Console.WriteLine("同步22222");
});
task2.RunSynchronously();//同步设置
//task2.Start();
19.3.5 Task的阻塞方法(Wait/WaitAll/WaitAny)
-
Thread阻塞线程的方法
使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:
static void Main(string[] args){
Thread th1 =new Thread(()=>{
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");});
th1.Start();
Thread th2 =new Thread(()=>{
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");});
th2.Start();//阻塞主线程
th1.Join();
th2.Join();
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:
![](https://img-blog.csdnimg.cn/img_convert/bcb115ebcf5cadf09b816d093db41e86.png)
19.3.6 Task的Wait/WaitAny/WaitAll方法
-
Thread的Join方法可以阻塞调用线程,但是有一些弊端:
-
①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;
-
②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。
-
Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。
-
Task.Wait() 表示等待task执行完毕,功能类似于thead.Join();
-
Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞;
-
Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看例子:
static void Main(string[] args){
Task task1 =new Task(()=>{
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 =new Task(()=>{
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//阻塞主线程。task1,task2都执行完毕再执行主线程
//执行【task1.Wait();task2.Wait();】可以实现相同功能
Task.WaitAll(new Task[]{ task1,task2});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
执行结果如下:
![](https://img-blog.csdnimg.cn/img_convert/cdcf89eb8a4d4e8ea12de9683d38b916.png)
如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】
19.3.7 Task的延续操作(WhenAny/WhenAll/ContinueWith)
-
上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。
-
我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?
-
这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。
-
task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作,
-
task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。
static void Main(string[] args){
Task task1 =new Task(()=>{
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 =new Task(()=>{
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//task1,task2执行完了后执行后续操作
Task.WhenAll(task1, task2).ContinueWith((t)=>{
Thread.Sleep(100);
Console.WriteLine("执行后续操作完毕!");
});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作。
![](https://img-blog.csdnimg.cn/img_convert/650f34f6c10fd9de5bf819938adf2750.png)
上边也可以通过 :
-
Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)
-
Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 。
static void Main(string[] args){
Task task1 =new Task(()=>{
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 =new Task(()=>{
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//通过TaskFactroy实现
Task.Factory.ContinueWhenAll(new Task[]{ task1, task2 },(t)=>{
Thread.Sleep(100);
Console.WriteLine("执行后续操作");
});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();}
19.3.8 Task的任务取消(CancellationTokenSource)
-
Thread取消任务执行
在Task前我们执行任务采用的是Thread。
Thread怎么取消任务呢?
一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:
static void Main(string[] args){
bool isStop =false;
int index =0;//开启一个线程执行任务
Thread th1 =new Thread(()=>{while(!isStop){
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
th1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
isStop =true;
Console.ReadKey();
}
-
Task取消任务执行
Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。
static void Main(string[] args){
CancellationTokenSource source =newCancellationTokenSource();
int index =0;//开启一个task执行任务
Task task1 =newTask(()=>{while(!source.IsCancellationRequested){
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
//source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
source.Cancel();
Console.ReadKey();}
CancellationTokenSource的功能不仅仅是取消任务执行。
我们可以使用 source.CancelAfter(5000) 实现5秒后自动取消任务。
也可以通过 source.Token.Register(Action action) 注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。
static void Main(string[] args){
CancellationTokenSource source =newCancellationTokenSource();
//注册任务取消的事件
source.Token.Register(()=>{
Console.WriteLine("任务被取消后执行xx操作!");
});
int index =0;//开启一个task执行任务
Task task1 =new Task(()=>{while(!source.IsCancellationRequested){
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
source.CancelAfter(5000);
Console.ReadKey();}
执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:
![](https://img-blog.csdnimg.cn/img_convert/ce98c30988dedf0f00b58248d48ddfd3.png)
最后看上一篇跨线程的例子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:
public partial classForm1: Form
{
publicForm1(){
InitializeComponent();
}
private void mySetValueBtn_Click(object sender, EventArgs e){
Task.Run(()=>{
Action<int> setValue=(i)=>{
myTxtbox.Text = i.ToString();
};
for(int i =0; i <1000000; i++){
myTxtbox.Invoke(setValue,i);
}
});
}
}
19.3.9 异步方法(async/await)
在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看一个获取文件内容的例子:
![](https://img-blog.csdnimg.cn/img_convert/43154fb32d2329721eefc32407c504eb.png)
上边的例子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。
注意一个小问题:
异步方法中方法签名返回值为Task<T>,代码中的返回值为T。
上边例子中GetContentAsync的签名返回值为Task<string>,而代码中返回值为string。牢记
这一细节对我们分析异步代码很有帮助。
异步方法签名的返回值有以下三种:
① Task<T>:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task<TResult>;
② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;
③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。
小结:
-
到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。
-
通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);
-
ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。
-
async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。
19.4 Task详细讲解参考:
20. Asp .Net MVC学习笔记
https://www.cnblogs.com/yaopengfei/category/1087077.html
附加:
数组、字典和其他集合类型
- 数组:就像一列有固定数量的停车位,每个停车位只能停一辆特定类型的车。
- 列表:更像一个动态的停车场,可以根据需要增加或减少停车位。
- 字典:就像一个大楼的邮箱,每个邮箱有唯一的标签和里面的邮件。
- 队列:就像排队等候,先来的先得到服务。
- 栈:就像一叠盘子,最后放上去的盘子会先被取下。
- 散列集:就像一个房间里的人,每个人都是唯一的,但你不能预测他们站在房间里的哪个位置。
1. 数组(Array)
- 定义方式: 使用方括号
[]
。- 特性: 长度固定,元素类型相同。
- 访问: 使用索引,从
0
开始。- 示例:
int[] numbers = new int[5] {1, 2, 3, 4, 5};- 常用操作:
Length
获取长度,SetValue
和GetValue
设置和获取值。2. 列表(List)
- 定义方式: 使用
List<T>
类。- 特性: 动态大小,元素类型相同。
- 访问: 使用索引,从
0
开始。- 示例:
List<int> numbers = new List<int> {1, 2, 3, 4, 5};- 常用操作:
Add
,Remove
,Count
,Contains
。3. 字典(Dictionary)
- 定义方式: 使用
Dictionary<TKey, TValue>
类。- 特性: 键值对存储,键唯一。
- 访问: 使用键。
- 示例:
Dictionary<string, int> age = new Dictionary<string, int>
{
{"Alice", 30},
{"Bob", 40}
};- 常用操作:
Add
,Remove
,ContainsKey
,TryGetValue
。4. 队列(Queue)
- 定义方式: 使用
Queue<T>
类。- 特性: 先进先出(FIFO)。
- 访问: 不能使用索引。
- 示例:
Queue<int> numbers = new Queue<int>();- 常用操作:
Enqueue
,Dequeue
,Peek
,Count
。5. 栈(Stack)
- 定义方式: 使用
Stack<T>
类。- 特性: 后进先出(LIFO)。
- 访问: 不能使用索引。
- 示例:
Stack<int> numbers = new Stack<int>();- 常用操作:
Push
,Pop
,Peek
,Count
。6. 散列集(HashSet)
- 定义方式: 使用
HashSet<T>
类。- 特性: 元素唯一,无序。
- 访问: 不能使用索引。
- 示例:
HashSet<int> numbers = new HashSet<int> {1, 2, 3};- 常用操作:
Add
,Remove
,Contains
,Count
。总结一下
类型 长度是否固定 元素是否唯一 是否有序 可通过索引访问 使用场景 数组 是 否 是 是 固定大小 列表 否 否 是 是 动态内容 字典 否 键是 否 否(通过键访问) 配置设置 队列 否 否 是(FIFO) 否 打印队列、等待列表 栈 否 否 是(LIFO) 否 撤销操作、深度优先搜索 散列集 否 是 否 否 停止词、唯一标识符集