导图社区 CSharp思维导图
Csharp又被简称为C#,是微软公司在发布的一种新的编程语言。本图介绍了初级课程、中级课程、高级课程,值得收藏学习哦!
编辑于2021-09-07 22:13:12C#
初级课程
概述
.NET框架
 工具: Visual Studio .NET兼容的编译器(C#,VB .NET,F#...) 调试器 网站开发技术ASP.NET WCF
Base Class Library(基类库)
通用基础类 - 文件操作 字符串操作
集合类 - 列表 字典
线程和同步类 - 多线程程序
XML类 - 创建,读取以及操作XML文档
编译过程
 编译过程解析: .Net源文件包括C# VB 等... 编译器产生程序集(dll文件或者exe) 程序集中包括CIL(公共中间语言)
运行过程

编译和运行过程

CLR概述
 托管代码是.Net框架编写的代码,需要在CLR的环境下运行 非托管代码不在CLR控制之下,比如Win32 C/C++ DLL 成为非托管代码
C#的演化

C#编程
1、利用VS创建一个C#的控制台工程 2、点击F5运行或者使用点击启动 3、查看控制台输出
程序分析
 1行、告诉编译器使用System命名空间中的类型 3行、声明一个新的命名空间,名字为Simple,下面的类属于这个命名空间 5行、声明一个新的类、类型,名字叫做Program 7行、声明一个方法,名称为Main,Main方法就是类的一个成员,Main是一个特殊函数,编译器把它作为程序的起始点 9行、这个是一个单独的简单的语句,这一行就是Main方法的方法体语句以分号结束;这个语句使用命名空间System下的Console类把一个字符串输出到控制台;没有第一行的using语句,编译器就找不到Console类
计算机使用基本知识
常用快捷键 Home/End/PageUp/PageDown Shift Ctrl + A,S,Z,X,C,V,Home,End F5/Ctrl+F5 文字的删除 Del/Backspace 插入模式和覆盖模式 全角字符和半角字符 英文字符和中文字符
标示符
 标示符是一种字符串,用来命名如变量 方法 参数和许多后面要讲解的程序结构 标识符不能和关键字重复
字母 下划线 可以用在任何位置
数字不能放在首位
@字符只能放在标示符的首位
C#命名规范
Camel命名法
首个单词的首字母小写,其余单词的首字母大写(enemyHp)
Pascal命名规范
每个单词的第一个字母都大写(EnemyHp) 如果使用到英文单词的缩写,全部使用大写(PI HP MP)
变量使用Camel命名,方法和类使用Pascal命名规范
关键字
关键字:用来定义C#语言的字符串记号  1、关键字不能被用来做变量名或者其他形式的标识符,除非以@字符开始 2、所有C#关键字全部由小写字母组成
Main方法
1、每个C#程序的可执行起始点在Main中的第一条指令 2、Main方法首字母大写 3、Main方法最简单的形式: static void Main(){ }
每个C#程序必须带一个Main方法(函数)
语句
语句是描述一个类型或告诉程序去执行某个动作的一条源代码指令,语句以分号结束。 int var1 = 5; System.Console.WriteLine("The value of var1 is {0}",var1);
块
块是一个由大括号包围起来的0条或多条语句序列,它在语法上相当于一条语句。 { int var1 = 5; System.Console.WriteLine("The value of var1 is {0}",var1); } 块的内容: 1,某些特定的程序结构只能使用块 2,语句可以以分号结束,但块后面不跟分号 
从程序中输出文本
控制台窗口是一个简单的命令提示窗口,允许程序显示文本并从键盘接受输入。BCL提供一个Console的类(在System命名空间下),该类包含了输入和输出到控制台窗口的方法。
Console.Write
Write: Write是Console类的成员,它把一个文本字符串发送到程序的控制台窗口。最简单的使用,Write把文本的字符串字面量发送到窗口,字符串必须使用双引号括起来。实例:Console.Write("This is a trivial text."); 
Console.WriteLine
WriteLine是Console的另外一个成员,它和Write实现相同的功能,但会在每个输出字符串的结尾添加一个换行符。 System.Console.WriteLine("Hello world1."); System.Console.WriteLine("Hello world2."); System.Console.WriteLine("Hello world3.");
接受用户输入
Console.ReadLine()
这个方法可以从键盘上读取一行的字符串输入,返回的是一个字符串。 示例:接受用户从控制台输入的两个数字,并计算和,输出到控制台 赋值运算 = 返回值 方法的调用和表达式都会有返回值,返回给程序 用户输入 我们可以通过Console.ReadLine()从控制台接受一行 用户输入的字符串 类型转换 把一个整形数字的字符串转成一个整形 string str="234324"; int i = Convert.ToInt32(str); 把一个浮点类型的字符串转成一个浮点类型 float f=Convert.ToFloat("34.34");
Convert.ToInt32()
Convert.ToFloat()
格式化字符串
当利用Write和WriteLine方法输出的时候,可以对字符串进行格式化输出,什么是格式化输出呢? Console.WriteLine("两个数相加{0}+{1}={2}",3,34,34); 
多重标记和值
下面的语句使用了3个标记,但只有两个值 Console.WriteLine("Three integers are {1},{0} and {1}",3,5); 但是记住标记不能引用超出替换值列表长度以外位置的值 
变量和表达式
变量
计算机程序的运行其实就是对数据的操作,数据是什么?比如数字,文字,图片这些在计算机中都是数据,那么数据怎么在计算机中存储呢? 答案:通过变量 你可以把计算机内存中的变量,当成一个盒子,盒子里面存储着东西,可以放入或者取出 “存储数据的盒子”
变量的声明
声明就是创建。 声明变量需要指定类型和变量名 <type> <name>; type表示使用什么类型的盒子,来存储数据 name表示存储这个盒子的名字 实例:(每一个声明都是一条语句,语句以;结束) int age; int hp; string name;
简单类型
整数

小数

非数值类型

变量的命名约定
遵守命名规范可以让程序结构更加清晰,更易于阅读 规范:第一个单词以小写字母开头,以后每个单词的首字母大写 变量的命名遵守Camel命名法(驼峰命名法)。首字母小写,以后每个单词的首字母大写。
Camel命名法
字面值
表示文本和数字的。 
char
char表示一个字符,字母 数字 @#¥%……&*()一个汉字
string
string是一个char的数组,数组以后介绍,现在先把string认为字符的集合
转义字符列表
转义字符是有特殊功能的字符。 
字符的Unicode值
Unicode是一个16进制的数字,表示这个字符在内存中以哪个数字存储 也可以使用Unicode来代表一个转义字符 (\u加上十六进制值) "I\'s siki!" "I\u0027s siki!"
@
如果我们不想去识别字符串中的转义字符,可以在字符串前面加一个@符号(除了双引号其他转义字符都不在识别) 举例:"I'm a good man. \n You are bad girl!",使用两个引号表示一个引号 @字符的两个作用示例: 1,默认一个字符串的定义是放在一行的,如果想要占用多行 2,用字符串表示路径 "c:\\xxx\\xx\xxx.doc" 使用@"c:\xxx\xx\xxx.doc"更能读懂
使用@不识别转义字符。
多变量声明和赋值
我们可以使用一条语句声明多个类型一样的变量 string name1,name2; 在多变量声明中,可以在变量后面跟上=,对其中的一个变量或者部分或者全部变量进行初始化 注意: 变量在使用之前必须初始化 怎么判断变量有没有使用,当你从变量的盒子里面取东西的时候,就是要使用这个变量的时候,初始化就是先往这个盒子里面放入东西,才能去取 第一次给变量赋值,就叫做初始化
表达式
前面说了如何创建变量,下面就是处理他们了,C#包含了许多进行这类处理的运算符。 把变量和字面值和运算符组合起来就是表达式 运算符的分类 一元运算符 处理一个操作数 二元运算符 处理两个操作数 三元运算符 处理三个操作数
数学运算符
加减乘除。  最后的两个单独的+和-;相当于正负 数学运算符只能处理数字;除了 字符串相加 char可以用来做数学运算 char变量实际现在内存中存储的是数字
关于数学运算的结果的类型
当两边的操作数类型一致的时候,返回的结果跟操作数的类型一样 当两边的操作数类型不一致的时候,返回的结果跟类型大的操作数保持一致,这样做编译器是为了保证结果可以存的下,因为如果其中有一个类型大的操作数,很可能结果也是一个比较大的数值这样小类型是存不下的。
自增、自减
 ++总是使操作数加1 --总是使操作数减1
赋值运算符
用来向变量盒子存东西的运算符。 
运算符的优先级

括号可以用来重写优先级,括号内的优先级最高
流程控制
我们至今写的代码都是一行接着一行,自上而下进行。但是有的时候我想根据程序中当时的环境执行不同的代码,或者有的时候需要重复执行某段代码。这两种方法就是需要用到流程控制中的分支和循环。 分支:有条件的执行代码 循环:重复执行相同的代码
布尔运算
布尔运算符

处理布尔值

条件布尔运算符

布尔赋值运算符

按位运算符
& 和 |
goto语句
在程序中我们可以给代码加上标签,这样就可以使用goto语句直接调到标签的地方去执行。 goto语句的语法: goto <labelName>; 标签定义: <labelName>: 实例: int myInteger = 5; goto myLabel; myInteger ++; myLabel: Console.WriteLine(myInteger);
三元运算符
语法 <test> ? <resultIfTrue> : <resultIfFalse> 示例 string resStr = (myInteger<10) ? "Less than 10" :"Greater than or equal to 10"
: ... ? ...
分支-if语句
使用if语句可以有条件的执行某段代码。 if的语法 if(<test>) <code executed if <test> is true> 先执行<test>,如果结果是true就执行 案例: bool var1 = true; if(var1) Console.WriteLine(var1);
if...else...
if else语法 if(<test>) <code executed if <test> is true> else <code executed if <test> is false> 如果if和else要执行的代码有多行,可以加上{}组成一个块 if(<test>){ <code executed if <test> is true> }else{ <code executed if <test> is false> }
if...elif...else...
if (){ }else if(){ }else if(){ }else{ } else if可以有0或者多个 else 只能有0或者1个
switch语句
switch语句类似于if语句,switch可以用来将测试变量跟多个值进行比较。switch的语法结构如下: switch (<testvar>){ case <comparisonVal1>: <code to execute if <testvar> == <comparisionVal1>> break; case <omparisonVal2>: <code to execute if <testvar> == <comparisionVal2>>; break; ... case <comparisionN>: <code to execute if <testvar>==<comparisionValN>>; break; default: <code to execute if <testvar>!=<comparisionVals>> break; } <testvar> 这里不管直接放一个字面值还是变量,它的类型是数值类型跟char类型
do循环
语法结构 do{ <code to be looped>; }while(<test>); <test>返回的是一个bool值(循环的条件判断)
while循环
语法结构 while(<test>){ <code to be looped> }
for循环
语法结构 for(<initialization;<condition>;<operation>>){ <code to loop> } <initialization>是初始化,这里可以定义一个变量,也可以给变量赋值 <condition>是判断是否执行循环的条件 <operation>每次执行完循环都会执行operation代码
循环中断
break(终止当前循环)
循环的中断,使用break立即跳出循环。
continue(终止当前循环继续下一个循环)
使用continue,只会终止当次循环,继续运行下次循环
return 跳出循环(跳出函数)
接受用户输入 ,如果输入0,就使用return 跳出循环。
goto (可以直接跳到某一个位置)
接受用户输入,如果输入的0,就使用goto退出循环。
变量类型
类型转换
隐式转换 编译器自动识别 , 不需要我们写更多的代码 显示转换 需要我们告诉编译器,什么类型转换成什么类型
隐式转换
short i = 34; int j =i; 当小盒子放入大盒子的时候,可以自动进行类型转换  
显式转换
int i = 33434; short j = i; 当把大盒子里面的数据放入小盒子里面的时候,有可能小盒子装不下,所以编译器不允许直接这样写。 我们可以通过显示转换的方式 short j = (short) i;
Convert命令
前面说的显示转换跟隐式转换都是针对于数字来说的。 如果要把一个数字字符串转换成一个数字类型(整形或者浮点型),就要用到Convert里面的方法 
复杂的变量类型
除了简单的变量类型之外,C#还提供了3个复杂的变量:枚举,结构和数组。
枚举
枚举类型的定义 enum <typeName>{ <value1>, <value2>, <value3>, ... <valueN> } 枚举类型的声明 <typeName> <varName>; 枚举类型的赋值<varName>=<typeName>.<value>;
使用枚举的原因
在游戏中我们可以定义一个int类型来存储状态 0 游戏暂停 1游戏失败 2游戏胜利 3开始菜单,但是这样不容易阅读,而且容易出错。 这个时候我们通过枚举类型来解决 enum GameState{ Pause, Failed, Success, Start } 在游戏中定义一个 GameState state = GameState.Start;
含义与赋值
枚举类型中的每一个值,其实都是一个整数,默认为int类型  默认情况下枚举类型中的值从0开始,我们可以直接在枚举类型定义的时候 使用=运算符给每一个枚举指定一个特定的值 
结构/结构体
如果我们要表示一个向量的话 需要定义,三个float类型 x y z 这样比较麻烦,不方便管理,我们可以使用结构 定义结构 struct <typeName>{ <memberDeclarations> } 其中<memberDeclarations>是结构体的成员,每个成员的声明如下 <type> <name>; struct Vector3{ float x; float y; float z; }
数组
前面所有的类型都有一个共同点:他们都存储一个值(结构存储一组值),这样会带来不方便,假如我们要存储几个类型相同的值。 案例:我们要存储玩游戏10次的得分: int score1 = 34; int score2 = 34; int score3 = 56; ... int score10=3434; 另外一种方式是使用数组,数组的声明 <baseType>[] <name>; 数组是一个变量的索引列表,这个索引是一个整数,第一个条目的索引是0,第二个是1,以此类推... int[] scores; 声明了分数的数组(int类型的数组)
数组的初始化
int[] scores; 第一种方式 scores ={34,34,3,43,43,4,34}; 第二种方式 scores = new int[10]; 里面的每一个元素按照类型的默认值赋值 第三种方式 scores = new int[10]{123,12,34,56,77,89,85,6,45634,34}; 数组的访问 <arrayName>[条目索引]
数组的遍历
遍历数组中的每一个元素并输出。 第一种方式for for(int i =0;i<array.Length;i++){ } 第二种方式while循环 int i=0; while(i<array.Length){ //use array[i] i++; } 第三种方式foreach foreach(int temp in array){
字符串的处理
字符串可以当做字符char类型的数组 1,通过数组遍历字符串的每一个字符 name[index]访问指定字符串指定索引位置处的字符 str.Length访问到字符串的长度(有多少个字符)
ToLower() ToUpper()
Trim() TrimStart() TrimEnd()
Split()
函数
1,如果想要重复执行某段代码,那就需要写重复的代码 2,目前我们写的代码,写多了,结构非常混乱,不容易阅读 所以函数来了,函数也叫做方法
定义和使用函数
定义函数 static void Write(){ Console.WriteLine("Text output from function ."); } 函数的使用 static void Main(){ Write();
返回值
控制台应用程序函数的定义形式如下: static <returnType> <FunctionName>(){ ... return <returnValue>; } 这里需要注意的是<returnValue>必须是<returnType>类型的,当<returnType>为void,表示这个函数不需要返回值,函数体不需要return语句,也可以写return;中断函数
参数
为什么使用参数。当我们定义一个函数,用来求得两个数字的和,这个时候函数怎么知道求哪两个数字呢? 答案:通过函数的参数。 结构: static <returnType> <FunctionName>(<parameType> <paramName>, ...){ ...; return <returnValue>; }
参数数组
定义一个函数,用来取得数字的和,但是数字的个数不确定。 解决方案: 1,定义一个函数,参数传递过来一个数组; 2,定义一个参数个数不确定的函数,这个时候我们就要使用参数数组。 除了参数数组,所有函数的参数都是固定的,那么调用的时候,参数是一定要传递的 //这里定义了一个int类型的参数数组,参数数组和数组参数(上面的)的不同,在于函数的调用,调用参数数组的函数的时候,我们可以传递过来任意多个参数,然后编译器会帮我们自动组拼成一个数组,参数如果是上面的数组参数,那么这个数组我们自己去手动创建 //参数数组就是帮我们 减少了一个创建数组的过程
结构函数
我们不但在结构中定义数据,还可以包含函数的定义。 struct CustomerName{ public string firstName; public string lastName; } CustomerName myName; myName.firstName = "siki"; myName.lastName = "Liang"; Console.WriteLine("My name is "+myName.firstName+" "+myName.lastName); 在结构体中定义函数,实现得到名字 struct CustomerName{ public string firstName; public string lastName; public string GetName(){ return firstName+" "+lastName; } } 当我们在结构体中定义一个函数的时候,这个函数就可以通过结构体声明的变量来调用,这个函数可以带有参数,那么调用的时候必须传递参数,这个函数,可以使用结构体中的属性。
函数的重载overload
为什么使用函数重载? 假设我们有一个函数用来实现求得一个数组的最大值 static int MaxValue(int[] intArray){ .... return; } 这个函数只能用于处理int数组,如果想处理double类型的话需要再定义一个函数 static double MaxValue(double[] doubleArray){ ... return; } 函数名相同,参数不同,这个叫做函数的重载(编译器通过不同的参数去识别应该调用哪一个函数) //编译器会根据你传递过来的实参的类型去判定调用哪一个函数
委托
委托的定义
委托(delegate)是一种存储函数引用的类型。 委托的定义指定了一个返回类型和一个参数列表 定义了委托之后,就可以声明该委托类型的变量,接着就可以把一个返回类型跟参数列表跟委托一样的函数赋值给这个变量。 委托的使用分两步 定义 声明(变量) 结构体,枚举的使用同上都分为定义和声明 整数类型数组类型字符串类型都是直接声明变量的,因为类型的定义已经完成了(CLR中已经完成定义)
委托的使用
delegate double MyDelegate(double param1,double param2); static double Multiply(double param1,double param2){ return param1*param2; } static double Divide(double param1,double param2){ return param1/param2; } double param1 = 34; double param2 =2; MyDelegate de; de = Multiply; de(param1,param2); de = Divide; de(param1,param2);
中级课程
调试和错误处理
错误
代码中难免存在错误,不论程序员多么优秀,程序总会出现问题,有些问题,比如变量名字写错,导致编译器无法编译(语法错误),有的时候我们的逻辑在某些方面有瑕疵,也会产生错误,这类错误成为语义错误(逻辑错误)。 我们接下来学习如何在程序出错之后,使用VS提供给我们的调试功能,找到错误的原因,修改代码。(调试) 以及学习c#中的错误处理技术,对可能发生错误的地方采取预防措施,并编写弹性代码来处理可能会发生的致命错误。(错误处理)
正常模式下的调试
正常模式指的是不会影响程序的正常运行。 1,在VS中我们使用Console.Write(或者WriteLine)方法向控制台输出变量的值,通过这个我们可以查看变量的值是否符合我们的预期来调试错误。 2,在Unity中我们使用Debug.Log("") Debug.LogError("") Debug.LogWarn(""),向unity的Console窗口输出信息,帮助我们调试错误。
中断模式下的调试
中断模式指我们可以暂停程序的执行,然后查看程序中的状态,也可以让程序继续执行。 如何让程序中断? 断点 断点是什么? 断点是源代码中自动进入中断模式的一个标记,当遇到断点的时候,程序会进入中断模式。 如何插入断点? 1,右击代码行,选择breakpoint(断点) -> insert breakpoint(插入断点) 2,光标定位到代码行,选择菜单上的Debug(调试)->Toggle Breakpoint(切换断点) 3,光标定位到代码行,按下F9键,在此按下F9是取消断点 4,在需要添加断点的行首位置,直接单击,再次单击取消断点
窗口 Breakpoints
我们可以通过 (调试-窗口-断点),打开断点窗口,这个窗口显示了当前项目中添加了的所有的断点,我们可以在这里定位断点的位置,也可以去删除断点。
监视变量的内容
在中断模式下查看变量值最简单的方式,就是把鼠标指向源代码中的变量名,此时会出现一个工具提示,显示该变量的信息。 中断模式下的窗口(左下角),有三个选项卡 错误列表 -程序运行中发生的所有错误的列表 局部变量 -当前运行环境中所有的局部变量的值 监视 -监视某个变量的值的变化 在上面的几个窗口中不但可以观察变量值的变化,还可以直接去修改变量中存储的值
调用堆栈和即时窗口
在中断模式下,可以在右下角看到调用堆栈和即时窗口 在调用堆栈窗口下我们可以观察到当前代码执行到哪一行了,并且可以看到这个代码的是被什么语句调用的 即时窗口我们可以在这里输入一些命令,查看变量的值,修改变量的值,可以输入表达式查看结果
单步执行代码
在中断模式下我们可以单步执行代码,单步执行带有有两种 逐过程和逐语句,他们两个都是一条语句一跳语句的执行,区别在于逐过程遇到函数,不会进入函数内部,而把函数当成一条语句去执行。
错误处理(异常处理)
我们上面讨论了再开发过程中如何查找和修正错误,使这些错误不会再发布的代码中出现,但有时,我们知道可能会有错误发生,但不能100%的肯定他们不会发生,此时最好能预料到错误的发生,编写足够健壮的代码以处理这些错误,而不必中断程序的执行。 错误处理就是用于这个目的。下面学习异常和处理他们的方式。
预处理指令
#define
#undef
#if #else #endif
一般用于控制程序执行流程
#error #warning
#error"这是一个错误" #warning"这是一个警告"
#region #endregion
可以对一段代码做一个标记,可以是程序看起来更加的简介、清晰 #region 字段 public string name; public int num; #endregion
异常处理
代码应当具有健壮性。
异常
异常是在运行期间代码中产生的错误。 示例: int[] myArray = {1,2,3,4}; int myEle = myArray[4];//数组下标越界 运行到这里的时候,会出现异常,这个异常的定义已经在CLR中定义好了。如果我们不去处理这个异常,那么当异常发生的时候,程序会终止掉,然后异常后面的代码都无法执行。
异常处理(捕捉异常) try ... catch ... finally
我们处理异常的语法结构如下(包含了三个关键字 try catch finally) try{ ... } catch( <exceptionType> e ){ ... } finally{ } 其中catch块可以有0或者多个,finally可以有0或者1个 但是如果没有catch块,必须有finally块,没有finally块,必须有catch块。 catch块和finally块至少有一个存在,也可以同时存在;处理异常可以保证程序的运行输出处理结果。 每个模块的用法: try块包含了可能出现异常的代码(一条或者多条语句) catch块用来捕捉异常,当代码发生异常,那么异常的类型和catch块中的类型一样的时候,就会执行该catch块,如果catch块的参数不写,表示发生任何异常都执行这个catch块 finally块包含了始终会执行的代码,不管有没有异常产生都会执行
类Class
基类 - 派生类(父类 - 子类)
修饰符
修饰符,用来类型或者成员的关键字。修饰符可以指定方法的可见性。  public 和private修饰字段和方法的时候,表示该字段或者方法能不能通过对象去访问,只有public的才可以通过对象访问,private(私有的)只能在类模板内部访问。 protected 保护的,当没有继承的时候,它的作用和private是一样的,当有继承的时候,protected表示可以被子类访问的字段或者方法
类型的修饰符
public class ... class ... 前者可以在别的项目下访问,后者不行
其他修饰符
 static可以修饰字段或者方法,修饰字段的时候,表示这个字段是静态的数据,叫做静态字段或者静态属性,修饰方法的时候,叫做静态方法,或者静态函数 使用static修饰的成员,只能通过类名访问 当我们构造对象的时候,对象中只包含了普通的字段,不包含静态字段
定义 class
访问修饰符:public 或 internal
class Box { }
封装
实例化
实例化是指在面向对象的编程中,把用类创建对象的过程称为实例化。是将一个抽象的概念类,具体到该类实物的过程。实例化过程中一般由类名 对象名 = new 类名(参数1,参数2...参数n)构成。 Csharp中必须将类实例化后才可以通过对象来调用类中的成员变量和方法。 注意:静态成员变量和方法只有通过类名调用,不能通过对象调用,因为它不属于对象。
Box box=new Box()
构造函数
构造函数名与类名相同;当我们将类实例化时(即创建对象时),调用构造函数;若不写构造函数,系统将自动创建构造函数。构造函数必须在类中写 实例化 Box box=new Box() 中的Box()就是构造函数;与构造函数中的 public Box() 对应。 构造函数中我们可以自定义参数;相应的实例化中的构造函数的参数也应当做相应的改变。
public Box() { }
析构函数
当我们在销毁函数时,调用析构函数。 析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。
Box(){ }
继承 :
类1:类2 表示 类1继承于类2;类1具有了类2所有的成员属性和方法。 注意:private 定义的成员变量和方法是不能被继承的。 C#不能多重继承 一些语言(C++)支持所谓的 “多重继承”,即一个类派生自多个类。 使用多重继承的优点是有争议的:一方面,毫无疑问,可 以使用多重继承编写非常复杂、 但很紧凑的代码,。另一方面,使用多重实现继承的代码常常很难理解和调试。 如前所述,简化健壮代码的编写工作是开发 C#的重要设计 目标。 因此,C#不支持多重实现继承。 而 C#允许类型派生自多个接 口— — 多重接口继承。 这说明,C#类可以派生自另一个类和任意多个接口。更准确地说, System.Object 是一个公共的基类,所 以每个 C#(除了Object类之外)都有一个基类,还可以有任意多个基接口。
class 类1:类2{}
接口继承
接口继承: 表示一个类型只继承了函数的签名,没有继承任何实现代码。 在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
实现继承
表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。 在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。 在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承非常有用。 如果要声明派生自另一个类的一个类,就可以使用下面的语法: class MyDerivedClass : MyBaseclass { // functions and data members here } 如果类(或 结构)也 派生 自接 口,则用逗号分隔列表中的基类和接 口: public class MyDerivedClass: MyBaseClass , IInterface1 , IInterface2 { // etc. }
派生类的构造函数
1,在子类中调用父类的默认构造函数(无参)(会先调用父类的,然后是子类的) public class MyDerivedClass{ public MyDerivedClass():base(){ //do something } } 在这里 :base()可以直接不写,因为默认会调用父类中的默认构造函数 2,调用有参数的构造函数 public class MyDerivedClass{ public MyDerivedClass(string name):base(name){ //do something } }
多态
静态多态
在编译时具有多种形态
函数的重载 OverLoad
在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能,这就是重载函数。重载函数常用来实现功能类似而所处理的数据类型不同的问题。不能只有函数返回值类型不同。 为什么使用函数重载? 假设我们有一个函数用来实现求得一个数组的最大值 static int MaxValue(int[] intArray){ .... return; } 这个函数只能用于处理int数组,如果想处理double类型的话需要再定义一个函数 static double MaxValue(double[] doubleArray){ ... return; } 函数名相同,参数不同,这个叫做函数的重载(编译器通过不同的参数去识别应该调用哪一个函数) //编译器会根据你传递过来的实参的类型去判定调用哪一个函数
运算符的重载
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。 示例:重载加法运算符+ class Box { public int length; public int width; public int heihth; } public static Box operator + (Box a,Box b) { Box box=new Box(); box.length=a.length+b.length; } public void DisPlay(){ //输出长度 Console.WriteLine(this.length); } static void main(string[] args) { Box box1=new Box(); box1.length=10; Box box2=new Box(); box.length=20; (box1+box2).Display(); //(box1+box2)返回的Box的对象,然后直接调用Dispay方法 Console.ReadLine(); }
动态多态
在运行时具有多种形态
虚方法 virtual/override
把一个基类函数声明为virtual,就可以在任何派生类中重写该函数: class MyBaseClass{ public virtual string VirtualMethod(){ return "Method is called in base class"; } } 在派生类中重写另外一个函数时,要使用override关键字显示声明 class MyDerivedClass:MyBaseClass{ public override string VirtualMethod(){ return "Method is called in derivedclass."; } } 我们在子类里面重写虚函数之后,不管在哪里调用都是调用重写之后的方法
隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但是该方法没有分别声明为virtual和override,派生类就会隐藏基类方法。(要使用new关键字进行声明) 基类 class MyBaseClass{ public int MyMethod(){ } } 派生类(在派生类中把基类同名的方法隐藏掉了) class MyDerivedClass :MyBaseClass{ public new void MyMethod() { } }
抽象类
C#允许把类和函数声明为 abstract。 抽象类不能实例化,只能将继承它的子类实例化,抽象类可以包含普通函数和抽象函数。 抽象函数就是只有函数定义没有函数体,只有在子类中定义函数体,函数必须通过override关键字声明。 显然,抽象函数本身也是虚拟的Virtual(只有函数定义,没有函数体实现)。 类是一个模板,那么抽象类就是一个不完整的模板,我们不能使用不完整的模板去构造对象。 abstract class Building{ public abstract decimal CalculateHeatingCost(); class Building2 : Building { public override int area() { ................... } }
定义和实现接口 interface
定义接口(飞翔功能) public interface IFlyHandler{ public void Fly(); //只有函数名,没有函数体 } 实现接口 public class Type1Enemy:IFlyHandler{ } 定义一个接口在语法上跟定义一个抽象类完全相同,但不允许提供接口中任何成员的实现方式,也就是在接口中声明方法,在继承接口的类中去实现该方法;在一般情况下,接口只能包含方法,属性,索引器和事件的声明。 接口不能有构造函数,也不能有字段,接口也不允许运算符重载。 接口定义中不允许声明成员的修饰符,接口成员都是公有的 在Csharp中,类不能实现多重继承;但接口可以多重继承,即一个类可以继承多个接口。
派生的接口
接口可以彼此继承,其方式和类的继承方式相同 public interface A{ void Method1(); } public interface B:A{ void Method2(); }
this和base关键字
this可以访问当前类中定义的字段,属性和方法,有没有this都可以访问,有this可以让IDE-VS编译器给出提示,另外当 方法的参数跟字段重名的时候,使用this可以表明访问的是类中的字段,base可以调用父类中的公有方法和字段,有没有 base都可以访问,但是加上base.IED工具会给出提示,把所有可以调用的字段和方法罗列出来方便选择
密封类和密封方法
C#允许把类和方法声明为 sealed。 对于类 ,这表示不能继承该类;对于方法表示不能重写该方法。 sealed FinalClass { // etc } 什么时候使用 密封类和密封方法? 防止重写某些类导致代码混乱 商业原因
可空类型
集合
Hashtable
.Add()
利用键值对的形式添加
hashtable.Add("001","C#");
.Sort()
.Count
.Remove()
Dictionary
以键值对方式进行添加操作
List
ArrayList
.Add()
.Sort()
.Count
泛型
通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化。 class ClassA<T>
泛型类定义
定义一个类,这个类中某些字段的类型是不确定的,但在构造的时候这些类型可以确定下来。 class ClassA<T>{ private T a; private T b; public ClassA(T a,T b){ this.a = a ;this.b = b; } public T GetSum(){ return a+“”+b; } }
泛型方法定义
定义泛型方法就是定义一个方法,这个方法的参数的类型可以是不确定的,当调用这个方法的时候再去确定方法的参数的类型。 public static T GetSum<T>(T a,T b){ return a+""+b; //实现任意类型组拼成字符串的方法 } GetSum<int>(23,12); GetSum<double>(23.2,12);
列表List
当我们有很多类型一样的数据的时候,前面我们一般使用数组来进行管理,但是这样有个缺点就是数组的大小是固定的。如果我们很多类型一样的数据,比如游戏得分,我们可以集合类来进行管理,比如列表List,我们可以使用列表List很方便的添加数据,删除数据还有其他对数据的操作。
列表List的创建
创建列表(列表可以存储任何类型的数据,在创建列表对象的时候首先要指定你要创建的这个列表要存储什么类型的)(泛型) List<int> scoreList = new List<int>(); new List<int>(){1,2,3} new List<string>(){"one","two"} var scoreList = new List<int>();
列表的使用
往列表中插入数据 scoreList.Add(12); scoreList.Add(45); 如何取得列表中的数据?列表中的数据跟数组有点相似,索引从0开始 ,可以通过索引来访问 scoreList[0] //访问添加到列表中的第一个数据
列表的遍历
遍历列表有两种方式: 1,for循环,遍历所有的索引,通过索引访问列表中的元素 for(int i=0;i<list.Count;i++){ //循环体list[i] } 2,foreach遍历 foreach(int temp in list){ //依次取得list中的每一个元素赋值给temp,并执行循环体 //循环体 temp
列表其他
1. 列表内部数据是使用数组进行的存储,一个空的列表内部会有一个长度为0的数组,当给列表中添加元素的时候,列表的容量会扩大为4,如果添加第5个的时候,列表的大小会重新设置为8,如果添加第9个元素,列表容量会扩大为16,依次增加。当列表的中的容量发生改变的时候,它会创建一个新的数组,使用Array.Copy()方法将旧数组中的元素复制到新数组中。为了节省时间,如果事先知道要存储的数据的个数,就可以利用列表的构造函数指定列表的容量大小,比如下面的 List<int> intlist = new List<int>(10);创建了一个初始容量为10的列表,当容量不够用的时候,每次都会按照原来容量的2倍进行扩容。 我们可以通过Capacity属性获取和设置容量 intList.Capacity = 100; 2. 注意容量和列表中元素个数的区别,容量是列表中用于存储数据的数组的长度通过Capacity获取,列表中的元素是我们添加进去需要管理的数据,通过Count获取
集合类MyList的方法和属性
Capacity 获取容量大小
Add() 方法添加元素
Insert() 方法插入元素
[index] 访问元素(索引器)
Count 属性访问元素个数
RemoveAt() 方法移除指定位置的元素
IndexOf() 方法取得一个元素所在列表中的索引位置
LastIndexOf()上面的方法是从前往后搜索,IndexOf()是从后往前搜索,搜索到满足条件的就停止。
Sort() 对列表中是元素进行从小到大排序
索引器
通过[index]这种形式去访问数据,就是索引器。
高级课程
字符串类string
System.String(string是这个类的别名) System.Text.StringBuilder
创建字符串
string s = "www.devsiki.com";
获取字符串长度
s.Length(属性)
比较字符串是否一样
s=="www.devsiki.com"
字符串连接
s="http://"+s;
索引字符串中字符
stringName[index] s[0] s[3]
字符串更多方法 str.
CompareTo()方法,比较字符串的内容
注意返回结果是int型; 当两字符串相等时,返回0; 当被比较字符串在字母表中的排序靠前的时候,返回1;否则返回-1。
Replace()用另一个字符或者字符串替换字符串中给定的字符或者字符串
Replace(‘被替换字符’,‘替换字符’) 也可用于删除字符串中的空格。
Split()在出现给定字符的地方,把字符串拆分成一个字符串数组
Split('a') a为需要在此拆分的给定字符
SubString()在字符串中检索给定位置的子字符串
SubString(a,b) a:起始字符索引 b:结束字符索引
对原字符串没有影响
ToLower()把字符串转换成小写形式
string str = s.ToLower;
ToUpper()把字符串转换成大写形式
Trim()删除首尾的空白
string str=s.Trim(); 一般用于用户在注册输入账号时,一般不允许账号首尾留有空白,删除账号的首位空白。
Concat()方法,合并字符串
CopyTo()方法,把字符串中指定的字符复制到一个数组中
Format()方法,格式化字符串
IndexOf()方法,取得字符串第一次出现某个给定字符串或者字符的位置
int index = str.IndexOf("字符串"); 应用:可以使用这个方法判断当前字符串是否包含一个子字符串,如果不包含,返回-1;如果包含返回一个字符的索引。
IndexOfAny()方法
Insert()把一个字符串实例插入到另一个字符串实例的制定索引处
Join()合并字符串数组,创建一个新字符串
StringBuilder类
位于System.Text命名空间下。
创建StringBuilder对象
StringBuilder sb = new StringBuilder("www.taikr.com"); StringBuilder sb = new StringBuilder(20); StringBuilder sb = new StringBuilder("www.devsiki.com",100); 关于StringBuilder对象创建的时候的内存占用
Append()方法,给当前字符串追加一个字符和字符串
str.Append("string");
Insert()追加特定格式的字符串
Remove()从当前字符串中删除字符
str.remove(a,b);
Replace()在当前字符串中,用某个字符或者字符串全部替换另一个字符或者字符串
str.Replace("a","b"); 把a字符串替换成b字符串
ToString()把当前stringBuilder中存储的字符串,提取成一个不可变的字符串
str.ToString();
正则表达式Regular Expression
正则表达式作用: 检索:通过正则表达式,从字符串中获取我们想要的部分 匹配:判断给定的字符串是否符合正则表达式的过滤逻辑(如:判断用户输入的密码是否合法,判断用户输入的邮箱格式是否合法) 正则表达式组成: 正则表达式就是由普通字符以及特殊字符(成为元字符)组成的文字模式。该模式描述在查找文字主体时待匹配的一个或多个字符串。
操作正则表达式的方法
位于System.Text.RegularExpressions下的Regex类
定位元字符
^匹配必须出现在字符串的开头或行的开头
区配开始 ^ string str = "I am Blue cat"; Console.WriteLine(Regex.Replace(str, "^","准备开始:"));
$匹配必须出现在以下位置:字符串结尾、字符串结尾处的 \n 之前或行的结尾
区配结束 $ string str = "I am Blue cat"; Console.WriteLine(Regex.Replace(str, "$", " 结束了!"));
基本语法元字符
在正则表达式中,\是转义字符. * 是元字符 如果要表示一个\ . *字符的话,需要使用\\ \. \*
. 匹配除换行符以外的任意字符
\w 匹配字母、数字、下划线、汉字(指大小写字母、0-9的数字、下划线_)
\W \w的补集( 除“大小写字母、0-9的数字、下划线_”之外)
\s 匹配任意空白符(包括换行符/n、回车符/r、制表符/t、垂直制表符/v、换页符/f)
\S \s的补集(除\s定义的字符之外)
\d 匹配数字(0-9数字)
\D 表示\d的补集(除0-9数字之外)
反义字符
\B 匹配不是单词开头或结束的位置
[ab] 匹配中括号中的字符
[a-c] a字符到c字符之间是字符
[^x] 匹配除了x以外的任意字符
[^adwz] 匹配除了adwz这几个字符以外的任意字符
重复描述字符
{n} 匹配前面的字符n次
{n,} 匹配前面的字符n次或多于n次
{n,m} 匹配前面的字符n到m次
string pattern = @"^\d{5,12}$"; //从开头到结尾只能是5到12位的数字。
? 重复零次或一次
+ 重复一次或更多次
* 重复零次或更多次
择一匹配符
| 将两个匹配条件进行逻辑“或”(Or)运算。
例1. string s = "23爱仕达发发发adsda$#)"; string pattern = @"\d|[a-z]"; MatchCollection col = Regex.Matches(s, pattern); foreach (Match match in col) { Console.WriteLine(match); } Console.ReadKey(); 例2. string s = "zhangshan;lisi,wangwu.zhouliu"; string pattern = @"[;,.]"; string[] resArray = Regex.Split(s, pattern); foreach (var s1 in resArray) Console.WriteLine(s1); Console.ReadKey(); 等价于: string s = "zhangshan;lisi,wangwu.zhouliu"; string pattern = @"[;]|[,]|[.]"; string[] resArray = Regex.Split(s, pattern); foreach (var s1 in resArray) Console.WriteLine(s1); Console.ReadKey();
用小括号来指定子表达式(也叫做分组)
示例一: Console.WriteLine("请输入一个任意字符串,测试分组:"); string inputStr = Console.ReadLine(); string strGroup1 = @"a{2}"; Console.WriteLine("单字符重复2两次替换为22,结果为:"+Regex.Replace(inputStr, strGroup1,"22")); //重复 多个字符 使用(abcd){n}进行分组限定 string strGroup2 = @"(ab\w{2}){2}"; Console.WriteLine("分组字符重复2两次替换为5555,结果为:" + Regex.Replace(inputStr, strGroup2, "5555")); 示例二: 校验IP4地址(如:192.168.1.4,为四段,每段最多三位,每段最大数字为255,并且第一位不能为0) string regexStrIp4 = @"^(((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?))$"; Console.WriteLine("请输入一个IP4地址:"); string inputStrIp4 = Console.ReadLine(); Console.WriteLine(inputStrIp4 + " 是否为合法的IP4地址:" + Regex.IsMatch(inputStrIp4, regexStrIp4)); Console.WriteLine("请输入一个IP4地址:"); string inputStrIp4Second = Console.ReadLine(); Console.WriteLine(inputStrIp4 + " 是否为合法的IP4地址:" + Regex.IsMatch(inputStrIp4Second, regexStrIp4));
委托delegate
委托是一个类型,这个类型可以赋值一个方法的引用。 “就是可以将方法作为参数进行传递”
委托声明
定义委托的语法如下: delegate void IntMethodInvoker(int x); 定义委托的其他案例: delegate double TwoLongOp(long first,long second); delegate string GetAString();
使用委托
private delegate string GetAString(); static void Main(){ int x = 40; GetAString firstStringMethod = new GetAString(x.ToString); //ToString方法用来把数据转换成字符串。 Console.WriteLine(firstStringMethod()); }
Action委托
Action委托引用了一个void返回类型的方法,T表示方法参数,先看Action委托有哪些 Action Action<in T> Action<in T1,in T2> Action<in T1,in T2 .... inT16> "有参无返回"
Func委托
Func引用了一个带有一个返回值的方法,它可以传递0或者多到16个参数类型,和一个返回类型 Func<out TResult> Func<in T,out TResult> Func<int T1,inT2,,,,,,in T16,out TResult> “有参有返回” 案例: delegate double DoubleOp(double x); 如何用Func表示 Func<double,double> //最后一个类型是返回值类型
冒泡排序(委托)
对集合进行排序
对集合进行排序,冒泡排序 bool swapped = true; do{ swapped = false; for(int i =0;i<sortArray.Length -1;i++){ if(sortArray[i]>sortArray[i+1]){ int temp= sortArray[i]; sortArray[i]=sortArray[i+1]; sortArray[i+1]=temp; swapped = true; } } }while(swapped); 这里的冒泡排序只适用于int类型的。
通用排序法
泛型+委托 class Employee{ public Employ(string name,decimal salary){ this.Name = name; this.Salary = salary; } public string Name{get;private set;} public decimal Salary{get;private set;} public static bool CompareSalary(Employee e1,Employee e2){ return e1.salary>e2.salary; } } public static void Sort<T>( List<T> sortArray,Func<T,T,bool> comparision ){ bool swapped = true; do{ swapped = false; for(int i=0;i<sortArray.Count-1;i++){ if(comparision(sortArray[i+1],sortArray[i])){ T temp = sortArray[i]; sortArray[i]=sortArray[i+1]; sortArray[i+1]=temp; swapped = true; } } }while(swapped); } static void Main(){ Employee[] employees = { new Employee("Bunny",20000), new Employee("Bunny",10000), new Employee("Bunny",25000), new Employee("Bunny",100000), new Employee("Bunny",23000), new Employee("Bunny",50000), }; Sort(employees,Employee.CompareSalary); 输出 }
多播委托
多播委托:委托也可以包含多个方法。使用多播委托就可以按照顺序调用多个方法,多播委托只能得到调用的最后一个方法的结果,一般我们把多播委托的返回类型声明为void。 Action action1 = Test1; action2+=Test2; action2-=Test1; 多播委托包含一个逐个调用的委托集合,如果通过委托调用的其中一个方法抛出异常,整个迭代就会停止。
匿名方法
匿名方法(本质上是一种方法,只是方法没有名字):一种使用委托的方式,不用去定义一个方法。任何使用委托变量的地方都可以使用匿名方法赋值,一般使用匿名方法进行参数回调。 Func<int,int,int> plus = delegate (int a,int b){ int temp = a+b; return temp; }; int res = plus(34,34); Console.WriteLine(res); 在这里相当于直接把要引用的方法直接写在了后面,优点是减少了要编写的代码,减少代码的复杂性
Lambda表达式
Lambda表达式: 从C#3.0开始,可以使用Lambda表达式代替匿名方法。只要有委托参数类型的地方就可以使用Lambda表达式。刚刚匿名方法的例子可以修改为 Func<int,int,int> plus = (a,b)=>{ int temp= a+b;return temp; }; //Lambda表达式的参数是不需要声明类型的 int res = plus(34,34); Console.WriteLine(res); Lambda运算符“=>”的左边列出了需要的参数,如果是一个参数可以直接写 a=>(参数名自己定义),如果多个参数就使用括号括起来,参数之间以,间隔。 多行语句: 1. 如果Lambda表达式只有一条语句,在方法块内就不需要花括号和return语句,编译器会自动添加return语句 Func<double,double> square = x=>x*x; 添加花括号,return语句和分号是完全合法的 Func<double,double> square = x=>{ return x*x; } 2. 如果Lambda表达式的实现代码中需要多条语句,就必须添加花括号和return语句。 Lambda表达式外部的变量:(一般不用) 通过Lambda表达式可以访问Lambda表达式块外部的变量。这是一个非常好的功能,但如果不能正确使用,也会非常危险。示例: int somVal = 5; Func<int,int> f = x=>x+somVal; Console.WriteLine(f(3));//8 somVal = 7; Console.WriteLine(f(3));//10 这个方法的结果,不但受到参数的控制,还受到somVal变量的控制,结果不可控,容易出现编程问题,用的时候要谨慎。
事件
事件含义: 事件(event)基于委托,为委托提供了一个发布/订阅机制,我们可以说事件是一种具有特殊签名的委托。 事件(Event)是类或对象向其他类或对象通知发生的事情的一种特殊签名的委托. 事件的声明: public event 委托类型 事件名; 事件使用event关键词来声明,他的返回类值是一个委托类型。 通常事件的命名,以名字+Event 作为他的名称,在编码中尽量使用规范命名,增加代码可读性。 总结: -事件是一种特殊的委托,或者说是受限制的委托,是委托一种特殊应用,只能施加+=,-=操作符。二者本质上是一个东西。 event ActionHandler Tick; // 编译成创建一个私有的委托示例, 和施加在其上的add, remove方法. -event只允许用add, remove方法来操作,这导致了它不允许在类的外部被直接触发,只能在类的内部适合的时机触发。委托可以在外部被触发,但是别这么用。 -使用中,委托常用来表达回调,事件表达外发的接口。 -委托和事件支持静态方法和成员方法, delegate(void * pthis, f_ptr), 支持静态返方法时, pthis传null.支持成员方法时, pthis传被通知的对象. -委托对象里的三个重要字段是, pthis, f_ptr, pnext, 也就是被通知对象引用, 函数指针/地址, 委托链表的下一个委托节点.
LINQ
from...in... join...in...on...equals... from...in... | let ... | where.... orderby...Thenby...(descending) select | group... by... into ; 【与数据库中SQL语句查询相像】
LINQ表达式
LINQ表达式写法 //用LINQ查询武林等级大于8级的武林高手 var res = from m in masterList // from后面跟上查询的集合 where m.Level > 8 // where 后面跟上查询的条件 select m ; //表示m的结果集合返回;若只想返回名字,则select m.Name; foreach (var temp in res) { Console.WriteLine(temp); }
Lambda表达式(拓展方法)
扩展方法的写法【Lambda表达式】 var res = masterList.Where(m => m.Level > 8); //Lambda表达式 foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } //过滤方法 static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
集合联合查询
LINQ表达式方法
// 取得所学功夫的杀伤力大于90的武林高手【具体可参考数据库中的联合查询语句原理】 var res = from m in masterList from k in kongfuList where m.Kungfu == k.Name && k.Power > 90 select m; foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
Lambda表达式方法(拓展方法)
// 取得所学功夫的杀伤力大于90的武林高手 var res = masterList.SelectMany(m => kongfuList, (m, k) => new { master = m,kongfu =k}).Where(x=>x.master.Kungfu==x.kongfu.Name && x.kongfu.Power>90); foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
对查询结果进行排序 orderby
var res = from m in masterList where m.Level > 8 && m.Menpai == "丐帮" orderby m.Age // 默认升序排列 select m; foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
降序 descending
orderby m.Age descending // 降序排列
多字段排序
orderby m.Level,m.Age // 按照多字段进行排序,如果字段属性相同,就按照第二个属性排序
排序拓展方法
// 先按照等级排,在按照年龄排 var res = masterList.Where(m => m.Level > 8).OrderBy(m => m.Level).ThenBy(m => m.Age); //注意不能连续用orderby;若想“先按照...排,再按照什么排”需要用ThenBy foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
join on联合查询
var res = from m in masterList join k in kongfuList on m.Kungfu equals k.Name / / 注意这里不使用“==”,而是equals where k.Power > 90 select new { master = m, kongfu = k }; foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
分组查询 into
// 按照功夫分组查询修炼此功的人数并排序 var res = from k in kongfuList join m in masterList on k.Name equals m.Kungfu into groups orderby groups.Count() select new {kongfu=k,count=groups.Count() }; foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; }
group by分组操作
var res = from m in masterList group m by m.Menpai into g select new { count = g.Count(), key = g.Key }; //g.Key 表示是按照哪个属性分的组 foreach (var temp in res) { Console.WriteLine(temp); } Console.ReadKey(); } static bool Tset1(MartialArtsMaster master) { if (master.Level > 8) return true; return false; } 注意与into分组的不同之处
量词操作符 any all
不是用于查询的,而是用于判断的 bool res = masterList.Any(m => m.Menpai == "丐帮"); // Any只要有一个条件满足即可返回True,否则返回False Console.WriteLine(res); bool res = masterList.All(m => m.Menpai == "丐帮"); // All只有全部条件均满足才返回True,否则返回False Console.WriteLine(res);
LINQ知识点图

反射和特性
元数据
有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中。 程序在运行时,可以查看其它程序集或其本身的元数据
反射
一个运行的程序查看本身的元数据或者其他程序集的元数据的行为叫做反射。
Type类
Type位于System.Reflection命名空间下。 BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。由于 Type是抽象类,因此不能利用它去实例化对象。 一个类中的数据是存储在对象中的,但是Type对象只存储类的成员。 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。 程序中用到的每一个类型都会关联到独立的Type类的对象。 不管创建的类型有多少个示例,只有一个Type对象会关联到所有这些实例。
System.Type类部分成员
Name(属性)返回类型的名字
Namespace(属性)返回包含类型声明的命名空间
Assembly(属性)返回声明类型的程序集。
GetFields(方法)返回类型的字段列表
FieldInfo[] array = type.GetFields(); foreach (FieldInfo info in array) { Console.WriteLine(info.Name + " "); } 注意:只能获取公有字段
GetProperties(方法)返回类型的属性列表
GetMethods (方法)返回类型的方法列表
通过type对象可以获取它对应所属类的所有成员(public)
获取Type对象两种方式
Type t = myInstance.GetType();
通过类的实例来获取Type对象。 在object类有一个GetType的方法,返回Type对象,因为所有类都是从object继承的,所以我们可以在任何类型上使用GetType()来获取它的Type对象
Type t = typeof(ClassName);
直接通过typeof运算符和类名获取Type对象。 获取里面的属性 FieldInfo[] fi = t.GetFields(); foreach(FieldInfo f in fi){ Console.WriteLine(f.Name+" "); }
Assembly类
Assembly类在System.Reflection命名空间中定义,它允许访问给定程序集的元数据,它也包含了可以加载和执行程序集。
加载程序集
Assembly assembly1 = Assembly.Load("SomeAssembly");
根据程序集的名字加载程序集,它会在本地目录和全局程序集缓存目录查找符合名字的程序集。
Assembly assembly2 = Assembly.LoadFrom(@"c:\xx\xx\xx\SomeAssembly.dll")
这里的参数是程序集的完整路径名,它不会在其他位置搜索。
Assembly对象的使用
获取程序集的全名
string name = assembly1.FullName;
遍历程序集中定义的类型
Type[] types = theAssembly.GetTypes(); foreach(Type definedType in types){ // }
遍历程序集中定义的所有特性
Attribute[] definedAttributes = Attribute.GetCustomAttributes(someAssembly);
特性(attribute)
特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。 将应用了特性的程序结构叫做目标 设计用来获取和使用元数据的程序(对象浏览器)叫做特性的消费者 .NET预定了很多特性,我们也可以声明自定义特性
特性的创建和使用
 我们在源代码中将特性应用于程序结构; 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中; 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。 关于特性的命名规范,特性名使用Pascal命名法(首字母大写),并且以Attribute后缀结尾,当为目标应用特性时,我们可以不使用后缀。例如对于SerializableAttribute和MyAttributeAttribute这两个特性,我们把他们应用到结构是可以使用Serializable和MyAttribute。
Obsolete特性
一个程序可能在其生命周期中经历多次发布,而且很可能延续多年。在程序生命周期的后半部分,程序员经常需要编写类似功能的新方法替换老方法。处于多种原因,你可能不再使用哪些调用过时的旧方法的老代码。而只想用新编写的代码调用新方法。旧的方法不能删除,因为有些旧代码也使用的旧方法,那么如何提示程序员使用新代码呢?可以使用Obsolete特性将程序结构标注为过期的,并且在代码编译时,显示有用的警告信息。 [Obsolete(“这个方法过时了”)] //表示这个方法被弃用了 static void Oldmethod() { Console.WriteLine("OLldMethod"); } [Obsolete("Use method SuperPrintOut",true)] //这个特性的第二个参数表示是是否应该标记为错误,true为标记为错误,而不仅仅是警告。 static void PrintOut(string str){ Console.WriteLine(str); }
Conditional特性
Conditional特性允许我们包括或取消特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。 定义方法的CIL代码本身总是会包含在程序集中,只是调用代码会被插入或忽略。 注意:Conditional 位于命名空间 using System.Diagnostics下; #define DoTrace //定义一个宏,此时便可以调用方法,若注释掉,则不能调用 class Program{ [Conditional("DoTrace")] static void TraceMessage(string str){ Console.WriteLine(str); } static void Main(){ TraceMessage("Start of Main"); Console.WriteLine("Doing work in Main.") TraceMessage("End of Main"); } }
调用者信息特性
注意:该特性位于命名空间using System.Runtime.CompilerServices;下 调用者信息特性可以访问文件路径,代码行数,调用成员的名称等源代码信息。 1. 这个三个特性名称为CallerFilePath,CallerLineNumber和CallerMemberName 2. 这些特性只能用于方法中的可选参数 public static void PrintOut(string message,[CallerFilePath] string filename="",[CallerLineNumber]int lineNumber = 0,[CallerMemberName]string callingMember=""){ Console.WriteLine("Message:"+message); Console.WriteLine("Line :"+lineNumber); Console.WriteLine("Called from:"+callingMember); Console.WriteLine("Message :"+message); }
DebuggerStepThrough特性
注意:该特性位于 System.Diagnostics 命名空间下 该特性可用于类,结构,构造方法,方法或访问器 我们在单步调试代码的时候,常常希望调试器不要进入某些方法。我们只想执行该方法,然后继续调试下一行。DebuggerStepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。有些方法小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。要小心使用该特性,不要排除了可能出现bug的代码。 class Program{ int _x=1; int X{ get{return _x;}; [DebuggerStepThrough] set{ _x=_x*2; _x+=value; } } public int Y{get;set;} [DebuggerStepThrough] void IncrementFields(){ X++; Y++; } static void Main(){ Program p = new Program(); p.IncrementFields(); p.X = 5; Console.WriteLine("P.X:"+p.X+" p.Y:"+p.Y); Console.ReadKey(); } }
CLSCompliant
声明可公开的成员应该被编译器检查是否符合CLS。兼容的程序集可以被任何.NET兼容的语言使用
Serializable
声明结构可以被序列化
NonSerialized
声明结构不可以被序列化
DLLImport
声明是非托管代码实现的
WebMethod
声明方法应该被作为XML Web服务的一部分暴露
AttributeUsage
声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上
自定义特性
应用特性的语法和之前见过的其他语法很不相同。你可能会觉得特性跟结构是完全不同的类型,其实不是,特性只是某个特殊结构的类。所有的特性类都派生自System.Attribute。 声明自定义特性: 声明一个特性类和声明其他类一样。有下面的注意事项 声明一个派生自System.Attribute的类 给它起一个以后缀Attribute结尾的名字 (安全起见,一般我们声明一个sealed的特性类) 特性类声明如下: public sealed class MyAttributeAttribute : System.Attribute{ ... 特性类的公共成员可以是 字段 属性 构造函数
进程,任务和同步
进程
线程
对于所有需要等待的操作,例如移动文件,数据库和网络访问都需要一定的时间,此时就可以启动一个新的线程,同时完成其他任务。一个进程的多个线程可以同时运行在不同的CPU上或多核CPU的不同内核上。 线程是程序中独立的指令流。在VS编辑器中输入代码的时候,系统会分析代码,用下划线标注遗漏的分号和其他语法错误,这就是用一个后台线程完成。Word文档需要一个线程等待用户输入,另一个线程进行后台搜索,第三个线程将写入的数据存储在临时文件中。运行在服务器上的应用程序中等待客户请求的线程成为侦听器线程。 进程包含资源,如Window句柄,文件系统句柄或其他内核对象。每个进程都分配的虚拟内存。一个进程至少包含一个线程。 一个应用程序启动,一般会启动一个进程,然后进程启动多个线程。
进程和线程的一个简单解释
进程和线程的简单解释: 1,计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。 2,如果工厂的电力有限一次只能供给一个车间使用。也就是说一个车间开工的时候,其他车间就必须停工。背后的含义就是。单个CPU一次只能运行一个任务。(多核CPU可以运行多个任务) 3,进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 4,一个车间里,可以有很多工人,他们协同完成一个任务。 5,线程就好比车间里的工人。一个进程可以包括多个线程。 6,车间的控件是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享空间。 7,进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 8,一个防止他人进入的简单方法,就是门口加一把锁(厕所)。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。 9,还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。 10,这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。 不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。 11,操作系统的设计,因此可以归结为三点: (1)以多进程形式,允许多个任务同时运行; (2)以多线程形式,允许单个任务分成不同的部分运行; (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
开启线程
异步委托
创建线程的一种简单方式是定义一个委托,并异步调用它。 委托是方法的类型安全的引用。Delegate类 还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。 接下来定义一个方法,使用委托异步调用(开启一个线程去执行这个方法) namespace 线程_委托方式发起线程 { class Program { //一般我们会为比较耗时的操作,开启单独的线程去执行,比如下载操作 static int Test(int i,string str) { Console.WriteLine("test"+i+str); return 100; } static void Main(string[] args) {//在main线程中执行 一个线程里面语句的执行顺序从上到下 Func<int,string,int> a = Test; //通过委托 开启一个线程 IAsyncResult ar = a.BeginInvoke(100,"siki",null,null); //开启一个线程 Console.WriteLine("main"); //可以认为线程是同时执行的(异步执行) while (ar.IsCompleted == false) //如果当前线程没有执行完毕 { Console.Write("."); Thread.sleep(10); //控制子线程的检测频率 } int res = a.EndInvoke(ar);//取得异步线程的返回值 Console.WriteLine(res); Console.ReadKey(); } } }
等待句柄
IAsyncResult.AsyncWaitHanlde 当我们通过BeginInvoke开启一个异步委托的时候,返回的结果是IAsyncResult,我们可以通过它的AsyncWaitHandle属性访问等待句柄。这个属性返回一个WaitHandler类型的对象,它中的WaitOne()方法可以等待委托线程完成其任务,WaitOne方法可以设置一个超时时间作为参数(要等待的最长时间 ms(毫秒)),如果发生超时就返回false。 static void Main(){ TakesAWhileDelegate d1 = TakesAWhile; IAsyncResult ar = d1.BeginInvoke(1,3000,null,null); while(true){ Console.Write("."); if(ar.AsyncWaitHanle.WaitOne(50,false)){ Console.WriteLine("Can get result now"); break; } } int result = d1.EndInvoke(ar); Console.WriteLine("Res:"+result); }
异步回调(常用)
等待委托的结果的第3种方式是使用异步回调。在BeginInvoke的第三个参数中,可以传递一个满足AsyncCallback委托的方法,AsyncCallback委托定义了一个IAsyncResult类型的参数其返回类型是void。对于最后一个参数,可以传递任意对象,以便从回调方法中访问它。(我们可以设置为委托实例,这样就可以在回调方法中获取委托方法的结果) static void Main(){ TakesAWhileDelegate d1 = TakesAWhile; d1.BeginInvoke(1,3000,TakesAWhileCompleted,d1); while(true){ Console.Write("."); Thread.Sleep(50); } } static void TakesAWhileCompleted(IAsyncResult ar){//回调方法是从委托线程中调用的,并不是从主线程调用的,可以认为是委托线程最后要执行的程序 if(ar==null) throw new ArgumentNullException("ar"); TakesAWhileDelegate d1 = ar.AsyncState as TakesAWhileDelegate; int result = d1.EndInvoke(ar); Console.Write("Res:"+result); } 用Lambda表达式实现: 等待委托的结果的第3种方式是使用异步回调。在BeginInvoke的第三个参数中,可以传递一个满足AsyncCallback委托的方法,AsyncCallback委托定义了一个IAsyncResult类型的参数其返回类型是void。对于最后一个参数,可以传递任意对象,以便从回调方法中访问它。(我们可以设置为委托实例,这样就可以在回调方法中获取委托方法的结果) static void Main(){ TakesAWhileDelegate d1 = TakesAWhile; d1.BeginInvoke(1,3000,ar=>{ int result = d1.EndInvoke(ar); Console.WriteLine("Res:"+result); },null); while(true){ Console.Write("."); Thread.Sleep(50); } }
第一类发起线程方法
Thread类
注意:Thread类位于 using System.Threading 命名空间下 使用Thread类可以创建和控制线程。Thread构造函数的参数是一个无参无返回值的委托类型。通过Thread.CurrentThread.ManagedThreadId 可以获取线程ID static void Main(){ var t1 = new Thread(ThreadMain); //创建出Thread对象,这个线程并没有启动 t1.Start(); //开始,开始去执行线程 Console.WriteLine("This is the main thread."); } static void ThreadMain(){ Console.WriteLine("Running in a thread."); } 在这里哪个先输出是无法保证了线程的执行有操作系统决定,只能知道Main线程和分支线程是同步执行的。在这里给Thread传递一个方法,调用Thread的Start方法,就会开启一个线程去执行,传递的方法。 利用Lambda表达式的方法: 上面直接给Thread传递了一个方法,其实也可以传递一个Lambda表达式。(委托参数的地方都可以使用Lambda表达式) static void Main(){ var t1 = new Thread( ()=>Console.WriteLine("Running in a thread, id : "+Thread.CurrentThread.ManagedThreadId) ); t1.Start(); Console.WriteLine("This is the main Thread . ID : "+Thread.CurrentThread.ManagedThreadId) }
第二类发起线程方法
前台线程和后台线程
只有一个前台线程在运行,应用程序的进程就在运行,如果多个前台线程在运行,但是Main方法结束了,应用程序的进程仍然是运行的,直到所有的前台线程完成其任务为止。 在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。 在用Thread类创建线程的时候,可以设置IsBackground属性,表示它是一个前台线程还是一个后台线程。 看下面例子中前台线程和后台线程的区别: class Program{ static void Main(){ var t1 = new Thread(ThreadMain){IsBackground=false}; t1.Start(); Console.WriteLine("Main thread ending now."); } static void ThreadMain(){ Console.WriteLine("Thread +"+Thread.CurrentThread.Name+" started"); Thread.Sleep(3000); Console.WriteLine("Thread +"+Thread.CurrentThread.Name+" started"); } } 后台线程用的地方:如果关闭Word应用程序,拼写检查器继续运行就没有意义了,在关闭应用程序的时候,拼写检查线程就可以关闭。 当所有的前台线程运行完毕,如果还有后台线程运行的话,所有的后台线程会被终止掉。
线程的优先级
线程有操作系统调度,一个CPU同一时间只能做一件事情(运行一个线程中的计算任务),当有很多线程需要CPU去执行的时候,线程调度器会根据线程的优先级去判断先去执行哪一个线程,如果优先级相同的话,就使用一个循环调度规则,逐个执行每个线程。 在Thead类中,可以设置Priority属性,以影响线程的基本优先级 ,Priority属性是一个ThreadPriority枚举定义的一个值。定义的级别有Highest ,AboveNormal,BelowNormal 和 Lowest。
控制线程
1,获取线程的状态(Running还是Unstarted,,,),当我们通过调用Thread对象的Start方法,可以创建线程,但是调用了Start方法之后,新线程不是马上进入Running状态,而是出于Unstarted状态,只有当操作系统的线程调度器选择了要运行的线程,这个线程的状态才会修改为Running状态。我们使用Thread.Sleep()方法可以让当前线程休眠进入WaitSleepJoin状态。 2,使用Thread对象的Abort()方法可以停止线程。调用这个方法,会在终止要终止的线程中抛出一个ThreadAbortException类型的异常,我们可以try catch这个异常,然后在线程结束前做一些清理的工作。 3,如果需要等待线程的结束,可以调用Thread对象的Join方法,表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止。
线程池
创建线程需要时间。 如果有不同的小任务要完成,就可以事先创建许多线程 , 在应完成这些任务时发出请求。 这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。 不需要 自己创建线程池,系统已经有一个ThreadPool类管理线程。 这个类会在需要时增减池中线程的线程数,直到达到最大的线程数。 池中的最大线程数是可配置的。 在双核 CPU中 ,默认设置为1023个工作线程和 1000个 I/o线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。 如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
线程池示例
线程池示例: static void Main(){ int nWorkerThreads; int nCompletionPortThreads; ThreadPool.GetMaxThreads(out nWorkerThreads,out nCompletionPortThreads); Console.WriteLine("Max worker threads : " +nWorkerThreads+" I/O completion threads :"+nCompletionPortThreads ); for(int i=0;i<5;i++){ ThreadPool.QueueUserWorkItem(JobForAThread); } Thread.Sleep(3000); } static void JobForAThread(object state){ for(int i=0;i<3;i++){ Console.WriteLine("Loop "+i+" ,running in pooled thread "+Thread.CurrentThread.ManagedThreadId); Thread.Sleep(50); } } 示例应用程序首先要读取工作线程和 I/o线程的最大线程数,把这些信息写入控制台中。接着在 for循 环中,调 用ThreadPool.QueueUserWorkItem方法,传递一个WaitCallBack类型的委托 ,把 JobForAThread方 法赋予线程池中的线程。 线程池收到这个请求后,就会从池中选择一个线程 来调用该方法。 如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。 如果线程池己经在运行,且有一个空闲线程来完成该任务,就把该作业传递给这个线程。 使用线程池需要注意的事项: 线程池中的所有线程都是后台线程 。 如果进程的所有前台线程都结束了,所有的后台线程就会停止。 不能把入池的线程改为前台线程 。 不能给入池的线程设置优先级或名称。 入池的线程只能用于时间较短的任务。 如果线程要一直运行(如 Word的拼写检查器线程),就应使用Thread类创建一个线程。
第三类发起线程方法
任务
在.NET4 新的命名空间System.Threading.Tasks包含了类抽象出了线程功能,在后台使用的ThreadPool进行管理的。任务表示应完成某个单元的工作。这个工作可以在单独的线程中运行,也可以以同步方式启动一个任务。 任务也是异步编程中的一种实现方式。 启动任务的三种方式: TaskFactory tf = new TaskFactory(); Task t1 = tf.StartNew(TaskMethod); Task t2 = TaskFactory.StartNew(TaskMethod); Task t3 = new Task(TaskMethod); t3.Start(); 我们创建任务的时候有一个枚举类型的选项TaskCreationOptions
连续任务
如果一个任务t1的执行是依赖于另一个任务t2的,那么就需要在这个任务t2执行完毕后才开始执行t1。这个时候我们可以使用连续任务。 static void DoFirst(){ Console.WriteLine("do in task : "+Task.CurrentId); Thread.Sleep(3000); } static void DoSecond(Task t){ Console.WriteLine("task "+t.Id+" finished."); Console.WriteLine("this task id is "+Task.CurrentId); Thread.Sleep(3000); } Task t1 = new Task(DoFirst); Task t2 = t1.ContinueWith(DoSecond); Task t3 = t1.ContinueWith(DoSecond); Task t4 = t2.ContinueWith(DoSecond); Task t5 = t1.ContinueWith(DoError,TaskContinuationOptions.OnlyOnFaulted);
任务的层次结构
我们在一个任务中启动一个新的任务,相当于新的任务是当前任务的子任务,两个任务异步执行,如果父任务执行完了但是子任务没有执行完,它的状态会设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态就变成RunToCompletion static void Main(){ var parent = new Task(ParentTask); parent.Start(); Thread.Sleep(2000); Console.WriteLine(parent.Status); Thread.Sleep(4000); Console.WriteLine(parent.Status); Console.ReadKey(); } static void ParentTask(){ Console.WriteLine("task id "+Task.CurrentId); var child = new Task(ChildTask); child.Start(); Thread.Sleep(1000); Console.WriteLine("parent started child , parent end"); } static void ChildTask(){ Console.WriteLine("child"); Thread.Sleep(5000); Console.WriteLine("child finished "); }
第四类发起线程方法
线程问题
争用条件问题
public class StateObject{ private int state = 5; public void ChangeState(int loop){ if(state==5){ state++;//6 Console.WriteLine("State==5:"+state==5+" Loop:"+loop);//false } state = 5; } } static void RaceCondition(object o ){ StateObject state = o as StateObject; int i = 0; while(true){ state.ChangeState(i++); } } static void Main(){ var state = new StateObject(); for(int i=0;i<20;i++){ new Task(RaceCondition,state).Start(); } Thread.Sleep(10000); }
使用lock(锁)解决争用条件的问题
注意:只能用于锁定一个引用类型,不能用于锁定值类型。 static void RaceCondition(object o ){ StateObject state = o as StateObject; int i = 0; while(true){ lock(state){ state.ChangeState(i++); } } } 另外一种方式是锁定StateObject中的state字段,但是我们的lock语句只能锁定个引用类型。因此可以定义一个object类型的变量sync,将它用于lock语句,每次修改state的值的时候,都使用这个一个sync的同步对象。就不会出现争用条件的问题了。下面是改进后的ChangeState方法 private object sync = new object(); public void ChangeState(int loop){ lock(sync){ if(state==5){ state++; Console.WriteLine("State==5:"+state==5+" Loop:"+loop); } state = 5; } }
死锁问题
public class SampleThread{ private StateObject s1; private StateObject s2; public SampleThread(StateObject s1,StateObject s2){ this.s1= s1; this.s2 = s2; } public void Deadlock1(){ int i =0; while(true){ lock(s1){ lock(s2){ s1.ChangeState(i); s2.ChangeState(i); i++; Console.WriteLine("Running i : "+i); } } } } public void Deadlock2(){ int i =0; while(true){ lock(s2){ lock(s1){ s1.ChangeState(i); s2.ChangeState(i); i++; Console.WriteLine("Running i : "+i); } } } } } var state1 = new StateObject(); var state2 = new StateObject(); new Task(new SampleTask(s1,s2).DeadLock1).Start(); new Task(new SampleTask(s1,s2).DeadLock2).Start();
解决方法:在编程的开始设计阶段,设计锁定顺序
网络编程
socket编程-tcp服务器端
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Sockets; using System.Net; namespace socket_tcp { class Program { static void Main(string[] args) { //1.创建socket Socket tcpSever = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.绑定ip和端口号 10.23.19.13 IPAddress ipaddress = new IPAddress(new byte[] { 10,23,19,13 }); EndPoint point = new IPEndPoint(ipaddress,7788); //ipendpoint是对ip+端口号做了一层封装的类 tcpSever.Bind(point);//向操作系统申请一个可用的ip跟端口号 用来做通信 //3.开始监听 tcpSever.Listen(100); //参数是最大连接数 Console.WriteLine("开始监听"); Socket clientSocket = tcpSever.Accept(); // //暂停当前线程,直到有一个客户端连接过来,之后进行下面的代码 Console.WriteLine("一个客户端连接多来了")//使用返回的socket跟客户端做通信 string message = "hello 欢迎你"; byte[] data = Encoding.UTF8.GetBytes(message); //对字符串做编码,得到一个字符串的字节数组 clientSocket.Send(data); Console.WriteLine("想客户端发送一条数据"); byte[] data2 = new byte[1024];//创建一个字节数组用来当作容器,去承接客户端发来的数据 int length = clientSocket.Receive(data2); string message2 = Encoding.UTF8.GetString(data2,0, length); //把字节数据转化成一个字符串 Console.WriteLine("接受到了一个从客户端发过来的消息" + message2); Console.ReadKey(); } } }
socket编程-tcp客户端
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Sockets; using System.Net; namespace socket_tcp_客户端 { class Program { static void Main(string[] args) { //1.创建socket Socket tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.发起建立连接的请求 IPAddress ipaddress = IPAddress.Parse("192.168.0.112"); //可以把一个字符串的ip地址转化成一个ipadress的对象 EndPoint point = new IPEndPoint(ipaddress); tcpClient.Connect(point); //通过ip:端口号 定位一个要连接的服务器端 byte[] data = new byte[1024]; int length = tcpClient.Receive(data); //这里传一个byte数组,实际上这个data数组用来接收数据,length返回值表示接收了多少字节的数据 string message = Encoding.UTF8.GetString(data,0,length); //只把接受到的数据做一个转化 Console.WriteLine(message); //向服务器端发送消息 string message2 = Console.ReadLine(); //读取用户输入,把输入发送到服务器端 tcpClient.Send(Encoding.UTF8.GetBytes(message2)); //把一个字符串转化成字节数组,然后发送到服务器端 } } }
文件操作
下面的类用于浏览文件系统和执行操作,比如移动,复制和删除文件。 System.MarshalByRefObject 这个是.NET类中用于远程操作的基对象类,它允许在应用程序域之间编组数据。 FileSystemInfo 这是表示任何文件系统对象的基类 FileInfo和File 这些类表示文件系统上的文件 DirectoryInfo和Directory 表示文件系统上的文件夹 Path 包含用于处理路径名的一些静态方法 DriveInfo 它的属性和方法提供了指定驱动器的信息
表示文件和文件夹的.NET类
我们有两个用于表示文件夹的类和两个用于表示文件的类 Directory(文件夹)和File(文件)类只包含静态方法,不能被实例化。如果只对文件夹或文件执行一个操作,使用这些类就很有效,省去了去实例化.NET类的系统开销。 DirectoryInfo类和FileInfo类实现与Directory和File相同的公共方法,他们拥有一些公共属性和构造函数,这些类的成员都不是静态的。需要实例化这些类,之后把每个实例与特定的文件夹或者文件关联起来。如果使用同一个对象执行多个操作,使用这些类比较合适,这是因为在构造的时候他们将读取合适文件系统对象的身份验证和其他信息,无论每个对象调用了多少方法,都不需要再次读取这些信息。
FileInfo和DirectoryInfo类
1,完成一个文件的拷贝 FileInfo myFile = new FileInfo(@"c:\pxx\xx\xxx\xxx.txt"); myFile.CopyTo(@"d:\xx\xx.txt");//拷贝文件 对应的File处理方式 File.Copy(@"c:\xxx\xx\xx\xx.txt",@"d:\xx\xx\xx.txt"); 2,判断一个文件夹是否存在 DirectoryInfo myFolder = new DirectoryInfo(@"c:\program files"); myFolder.Exists 对于FileInfo,或者DirectoryInfo进行构造的时候,如果传递了一个不存在的文件或者文件夹路径,这个时候不会出现异常,只有当你使用这个文件或者文件夹的时候才会出现问题。 FileInfo和DirectoryInfo的对象都可以通过Exists属性判断这个文件或者文件夹是否存在。
FileInfo和DirectoryInfo的属性列表
CreationTime 创建文件或文件夹的时间 DirectoryName(用于FileInfo) 包含文件夹的完整路径 Parent(用于DirectoryInfo) 指定子目录的父目录 Exists 文件或文件夹是否存在 Extension 文件的扩展名,对于文件夹,它返回空白 FullName 文件或文件夹的完整路径名 LastAccessTime 最后一次访问文件或文件夹的时间 LastWriteTime 最后一个修改文件或文件夹的时间 Name 文件或文件夹的名称 Root (仅用于DirectoryInfo) 路径的根部分 Length (仅用于FileInfo) 返回文件的大小(以字节为单位)
FileInfo和DirectoryInfo的方法列表
Create() 创建给定名称的文件夹或者空文件,对于FileInfo,该方法会返回一个流对象,以便于写入文件。 Delete() 删除文件或文件夹。对于文件夹有一个可以递归的Delete选项 MoveTo() 移动或重命名文件或文件夹 CopyTo() (只用于FileInfo)复制文件,文件夹没有复制方法,如果想要复制完整的目录树,需要单独复制每个文件和文件夹 GetDirectories() (只适用于DirectoryInfo)返回DirectoryInfo对象数组,该数组表示文件夹中包含的所有文件夹 GetFiles() (只适用于DirectoryInfo)返回FileInfo对象数组,该数组表示文件夹中所有的文件 GetFileSystemInfos() (只适用于DirectoryInfo)返回FileInfo和DirectoryInfo对象,它把文件夹中包含的所有对象表示为一个FileSystemInfo引用数组
案例
创建时间,最后一次访问时间和最后一次写入时间都是可写入的。 FileInfo test = new FileInfo(@"c:\myfile.txt"); Console.WriteLine(test.Exists); Console.WriteLine(test.CreationTime); test.CreationTime = new DataTime(2010,1,1,7,20,0); Console.WriteLine(test.CreationTime);
Path类
我们不能去实例化Path类,Path类提供了一些静态方法,可以更容易的对路径名执行操作。 Console.WriteLine(Path.Combine(@"c:\my documents","Readme.txt")); 在不同的操作系统上,路径的表示是不一样的 windows上是 \ , 在Unix就是/ ,我们可以使用Path.Combine连接两个路径,不用关心在哪个系统上。
静态字段
AltDirectorySeparatorChar 提供分割目录的字符,在windows上使用 \ 在Unix上用 / DirectorySpeparatorChar 提供分割目录的字符,在windows上使用 / 在Unix上用 \ PathSeparator 提供一种与平台无关的方式,来指定划分环境变量的路径字符串,默认为分号 VolumeSepartorChar 提供一种与平台无关的方式,来指定容量分割符,默认为冒号
读写文件
在.NET 2.0扩展了File类,通过File可以读写文件。
读取文件
在.NET 2.0扩展了File类,通过File可以读写文件。 1,File.ReadAllText(FilePath);根据文件路径读取文件中所有的文本 2,File.ReadAllText(FilePath,Encoding);//Encoding可以指定一个编码格式 Encoding.ASCII; 3,File.ReadAllBytes()方法可以打开二进制文件把内容读入一个字节数组 4,File.ReadAllLines() 以行的形式读取文件,一行一个字符串,返回一个字符串的数组
写入文件
我们读取文件有ReadAllText() ReadAllLines()和ReadAllBytes()这几个方法,对应的写入文件的方法有WriteAllText() WriteAllLines()和WriteAllBytes()
流
流是一个用于传输数据的对象,数据可以向两个方向传输: 如果数据从外部源传输到程序中,这就是读取流 如果数据从程序传输到外部源中,这就是写入流 外部源可能是 一个文件 网络上的数据 内存区域上 读写到命名管道上 读写内存使用System.IO.MemorySystem 处理网络数据使用System.Net.Sockets.NetworkStream
与流相关的类
FileStream(文件流) 这个类主要用于二进制文件中读写,也可以使用它读写任何文件。 StreamReader(流读取器)和StreamWriter(流写入器)专门用于读写文本文件 
FileStream类读写二进制文件
FileStream实例用于读写文件中的数据,要构造FileStream实例,需要提供下面的4中信息: 要访问的文件 - 一般提供一个文件的完整路径名 表示如何打开文件的模式 - 新建文件或打开一个现有文件,如果打开一个现有的文件,写入操作是覆盖文件原来的内容,还是追加到文件的末尾? 表示访问文件的方式 - 只读 只写 还是读写 共享访问 - 表示是否独占访问文件,如果允许其他流同时访问文件,则这些流是只读 只写 还是读写文件
构造函数的参数
构造函数的参数的取值 FileMode( 打开模式) Append,Create,CreateNew,Open,OpenOrCreate和Truncate FileAccess(读取还是写入) Read,ReadWrite和Write FileShare(文件共享设置) Delete,Inheritable,None,Read,ReadWrite和Write 注意 如果文件不存在 Append Open和Truncate会抛出异常 如果文件存在 CreateNew会抛出异常 Create 和 OpenOrCreate Create会删除现有的文件,新建一个空的文件,OpenOrCreate会判断当前是否有文件,没有的话才会创建
创建FileStream文件流
FileStream fs = new FileStream(@"c:\xx\xx.doc",FileMode.Create); FileStream fs2 = new FileStream(@"c:\xx\xx.doc",FileAccess.Write); FileStream fs3 = new FileStream(@"c:\xx\xx.doc",FileAccess.Write,FileShare.None); 通过FileInfo打开文件流 FileInfo myfile1 = new FileInfo(@"c:\xx\xx.doc"); FileStream fs4= myfile1.OpenRead(); FileInfo myfile2 = new FileInfo(@"c:\xx\xx.doc"); FileStream fs5= myfile2.OpenWrite(); FileInfo myfile3 = new FileInfo(@"c:\xx\xx.doc"); FileStream fs6= myfile3.Open(FileMode.Append,FileAccess.Write,FileShare.None); FileInfo myfile4 = new FileInfo(@"c:\xx\xx.doc"); FileStream fs7= myfile4.Create();
FileStream文件流的关闭
当我们使用完了一个流之后,一定要调用fs.Close();方法去关闭流,关闭流会释放与它相关联的资源,允许其他应用程序为同一个文件设置流。这个操作也会刷新缓冲区。
从文件流中读取内容和写入内容
从文件流读取内容 1,int nextByte = fs.ReadByte();读取一个字节(0-255)的数据,返回结果,如果达到流的末尾,就返回-1 2,int nBytesRead = fs.Read(ByteArray,0,nBytes);//读取多个字节,第一个是存放的数组,第二个参数是开始存放的索引,第三个参数是要读取多少个字节。返回的结果是读取的自己的实际个数,如果达到流的末尾 返回-1 向文件流写入内容 1,byte NextByte = 100;fs.WriteByte(NextByte);把一个字节写入文件流中 2,fs.Write(ByteArray,0,nBytes); 把一个自己数组写入文件流中 参数同上
读写文本文件
我们对文本文件的读写一般使用StreamReader和StreamWriter,因为不同的文本有不同的编码格式,这个StreamReader会帮我们自动处理,所以我们不需要关心文本文件的编码是什么 1,创建文本的读取流(会检查字节码标记确定编码格式) StreamReader sr = new StreamReader(@"c:\xx\ReadMe.txt"); 2,指定编码格式 StreamReader str = new StreamReader(@"c:\xx\xx.txt",Encoding.UTF8); (可取的编码格式 ASCII Unicode UTF7 UTF8 UTF32) 3,在文件流的基础上创建文本读取流 FileStream fs = new FileStream(@"c:\xx\xx.txt",FileMode.Open,FileAccess.Read,FileShare.None); StreamReader sr = new StreamReader(fs); 4,通过文件信息创建文本读取流-第二种方式 FileInfo myFile = new FileInfo(@"c:\xx\xx.txt"); StreamReader sr = myFile.OpenText(); 流的关闭 sr.Close();
读取文本文件
1,string nextLine = sr.ReadLine();//读取一行字符串 2,string restOfStream = sr.ReadToEnd();//读取流中所有剩余的文本内容 3,int nextChar = sr.Read();//只读取一个字符 4, int nChars = 100; char[] charArray = new char[nChars]; int nCharsRead = sr.Read(charArray,0,nChars); 读取多个字符,第一个参数是要存放的字符数组,第二个参数是从数组的哪一个索引开始放,第三个参数是读取多少个字符 返回值是实际读取的字符的个数
文本写入流StreamWriter-创建
StreamWriter的创建 1,StreamWriter sw = new StreamWriter(@"c:\xx\xx.txt");(默认使用UTF-8编码) 2,StreamWriter sw = new StreamWriter(@"c:\xx\xx.txt",true,Encoding.ASCII) 第二个参数表示是否以追加的方式打开,第三个参数是编码方式 3,通过FileStream创建StreamWriter FileStream fs = new FileStream(@"c:\xx\xx.txt",FileMode.CreateNew,FileAccess.Write,FileShare.Read); StreamWriter sw = new StreamWriter(fs); 4,通过FileInfo创建StreamWriter FileInfo myFile = new FileInfo(@"c:\xx\xx.txt"); StreamWriter sw = myFile.CreateText(); 所有流用完之后关闭 sw.Close();
文本写入流StreamWriter-写入
1,写入一行字符 string nextLine = "x xx x x x x ";sw.Write(nextLine); 2,写入一个字符 char nextChar = 'a'; sw.Write(nextChar); 3,写入字符数组 char[] charArray = ..; sw.Write(charArray); 4,写入字符数组的一部分 sw.Write(charArray,StartIndex,Length); 1:要写入的数组 2:开始索引 3写入长度
XML操作
XML 指可扩展标记语言 XML 被设计用来传输和存储数据。XML 被设计用来结构化、存储以及传输信息。
xml文档展示
-----------------------------xml文档 <?xml version="1.0" encoding="ISO-8859-1"?> <note> <to>George</to> <from>John</from> <heading>Reminder</heading> <body>Don't forget the meeting!</body> </note> 这个 XML 文档仍然没有做任何事情。它仅仅是包装在 XML 标签中的纯粹的信息。我们需要编写软件或者程序,才能传送、接收和显示出这个文档。
xml标签
第一行是 XML 声明。它定义 XML 的版本 (1.0) 和所使用的编码 (ISO-8859-1 = Latin-1/西欧字符集)。 下一行描述文档的根元素(像在说:“本文档是一个便签”):<note> 接下来 4 行描述根的 4 个子元素(to, from, heading 以及 body): <to>George</to> <from>John</from> <heading>Reminder</heading> <body>Don't forget the meeting!</body> 最后一行定义根元素的结尾: </note>
XML 文档形成一种树结构
XML 文档必须包含根元素。该元素是所有其他元素的父元素。 XML 文档中的元素形成了一棵文档树。这棵树从根部开始,并扩展到树的最底端。 所有元素均可拥有子元素: <root> <child> <subchild>.....</subchild> </child> <child> <subchild>.....</subchild> </child> </root>
XML 元素
XML 元素指的是从(且包括)开始标签直到(且包括)结束标签的部分。 <bookstore> <book category="CHILDREN"> <title>Harry Potter</title> <author>J K. Rowling</author> <year>2005</year> <price>29.99</price> </book> <book category="WEB"> <title>Learning XML</title> <author>Erik T. Ray</author> <year>2003</year> <price>39.95</price> </book> </bookstore> <bookstore> 和 <book> 都拥有元素内容,因为它们包含了其他元素。<author> 只有文本内容,因为它仅包含文本。 只有 <book> 元素拥有属性 (category="CHILDREN")。
xml语法规则
所有 XML 元素都须有关闭标签 <p>This is a paragraph</p> XML 标签对大小写敏感,标签 <Letter> 与标签 <letter> <Message>这是错误的。</message> <message>这是正确的。</message> XML 必须正确地嵌套 <b><i>This text is bold and italic</b></i> <b><i>This text is bold and italic</i></b> XML 文档必须有根元素 <root> <child> <subchild>.....</subchild> </child> </root> XML 的属性值须加引号 <note date=08/08/2008> <to>George</to> <from>John</from> </note> <note date="08/08/2008"> <to>George</to> <from>John</from> </note> XML 中的注释 <!-- This is a comment -->
XML 命名规则
XML 元素必须遵循以下命名规则: 1. 名称可以含字母、数字以及其他的字符 2. 名称不能以数字或者标点符号开始 3. 名称不能以字符 “xml”(或者 XML、Xml)开始 4. 名称不能包含空格 可使用任何名称,没有保留的字词。 实例: <skills> <skill> <id>2</id> <name lang="cn">天下无双</name> <damage>123</damage> </skill> <skill> <id>3</id> <name lang="cn">永恒零度</name> <damage>93</damage> </skill> <skill> <id>4</id> <name lang="cn">咫尺天涯</name> <damage>400</damage> </skill> </skills>
C#操作XML
在C#中使用控制台程序,用 XMLDocument进行xml操作,包括查询,增加,修改,删除和保存。 <skills> <skill> <id>2</id> <name lang="cn">天下无双</name> <damage>123</damage> </skill> <skill> <id>3</id> <name lang="cn">永恒零度</name> <damage>93</damage> </skill> <skill> <id>4</id> <name lang="cn">咫尺天涯</name> <damage>400</damage> </skill> </skills>
JSON操作
JSON 是存储和交换文本信息的语法。类似 XML。 JSON 比 XML 更小、更快,更易解析。JSON跟XML一样是一种是数据格式。 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于ECMAScript的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C++、C#、Java、JavaScript、Perl、Python等)。这些特性使JSON成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成(网络传输速率)。 { "employees": [ { "firstName":"Bill" , "lastName":"Gates" }, { "firstName":"George" , "lastName":"Bush" }, { "firstName":"Thomas" , "lastName":"Carter" } ] } JSON 是轻量级的文本数据交换格式 JSON 独立于语言 * JSON 具有自我描述性,更易理解 * JSON 使用 JavaScript 语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。
JSON 语法规则
数据在键值对中 数据由逗号分隔 花括号保存对象 方括号保存数组 JSON 名称/值对 JSON 数据的书写格式是:名称/值对。 名称/值对组合中的名称写在前面(在双引号中),值对写在后面(同样在双引号中),中间用冒号隔开:"firstName":"John" JSON 值可以是: 数字(整数或浮点数) 字符串(在双引号中) 逻辑值(true 或 false) 数组(在方括号中) 对象(在花括号中) null 注意:使用泛型去解析json时 json里面对象的键必须跟定义的类里面的字段或者属性保持一致
JSON数据结构
json简单说就是javascript中的对象和数组,所以这两种结构就是对象和数组两种结构,通过这两种结构可以表示各种复杂的结构 1、对象:对象在js中表示为“{}”括起来的内容,数据结构为 {key:value,key:value,...}的键值对的结构,在面向对象的语言中,key为对象的属性,value为对应的属性值,所以很容易理解,取值方法为 对象.key (c# 对象[key])获取属性值,这个属性值的类型可以是 数字、字符串、数组、对象几种。 2、数组:数组在js中是中括号“[]”括起来的内容,数据结构为 ["java","javascript","vb",...],取值方式和所有语言中一样,使用索引获取,字段值的类型可以是 数字、字符串、数组、对象几种。 经过对象、数组2种结构就可以组合成复杂的数据结构了。
LitJson学习
1,JsonMapper.ToObject(string json) -- 把json数据转化成JsonData对象 JsonData data = new JsonData(); data...; data.ToJson(); --把JsonData对象转化成json数据 2,JsonMapper.ToObject<T>(T t1) -- 把json数据转化成对应的类对象 JsonMapper.ToJson(object o) --把一个对象中的数据转化成json数据 (在unity中解析json文件) 参考litjson官网的Quickstart
Excel操作
1,使用OLEDB操作Excel 关于OLEDB介绍参考 http://www.cnblogs.com/moss_tan_jun/archive/2012/07/28/2612889.html 2,连接字符串 "Provider=Microsoft.Jet.OLEDB.4.0;" + "Data Source=" + fileName + ";" + ";Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1\""; "Provider=Microsoft.ACE.OLEDB.12.0;" + "Data Source=" + fileName + ";" + ";Extended Properties=\"Excel 12.0;HDR=YES;IMEX=1\""; 示例: //加载Excel public static DataSet LoadDataFromExcel(string filePath) { try { string strConn; strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + filePath + ";Extended Properties='Excel 8.0;HDR=False;IMEX=1'"; OleDbConnection OleConn = new OleDbConnection(strConn); OleConn.Open(); String sql = "SELECT * FROM [Sheet1$]";//可是更改Sheet名称,比如sheet2,等等 OleDbDataAdapter OleDaExcel = new OleDbDataAdapter(sql, OleConn); DataSet OleDsExcle = new DataSet(); OleDaExcel.Fill(OleDsExcle, "Sheet1"); OleConn.Close(); return OleDsExcle; } catch (Exception err) { MessageBox.Show("数据绑定Excel失败!失败原因:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information); return null; } }