c#笔记
游戏开发之路
基础理论
数据结构和算法
设计模式
计算机网络 计算机图形学
操作系统 编译原理
基础技能
C#Unity
就业必备
UI系统
热更新技术
平台相关
网络游戏开发
程序基础
主流语言的应用领域
C:嵌入式硬件开发
C++:(虚幻)游戏客户端、服务器、软件
C#:(unity)游戏客户端、服务器、软件、网站
Java:安卓、服务器、软件、网站
JavaScript: H5游戏、网站、服务器
PHP:网站、服务器
Python:网站、服务器、辅助开发
SQL:数据库
Go:服务器
Objective-C:苹果(mac、ios)相关
Swift:苹果(mac、ios)相关
市场需求——手游,unity,c#
常用的IDE(集成开发环境)软件
Visual Studio:一般Windows操作系统使用的软件都由它来进行开发,可用于开发基于C、C++、C#等等语言的软件
一个解决方案,,多个项目
Eclipse和IntelliJlDEA:一般主要用于开发Java语言的相关软件
Android Studio:谷歌推出,主要用于开发安卓应用
Xcode:苹果推出,主要用于开发苹果应用
API
程序代码之间的相互调用——API的相互调用
API: (Application Programming Interface,应用程序接口)是一些预先定义的代码逻辑。
用来提供应用程序与开发人员基于某软件或硬件得以访问的一组例程,而又无需访问源码,或理解内部工作机制的细节。
注释
1.两杠注释://用于注释一行信息
2.星号注释:/*用于注释多行信息/*
3.三杠注释:///用于注释类class、命名空间namespace等等
工具栏
快捷键
1.注释Ctrl+k,ctrl+c
2.取消注释Ctrl+k,ctrl+u
代码块
1.命名空间(面向对象)——工具包
引用工具包:using System;
2.类(面向对象)——工具
3.函数——可以做的事情
主函数:程序的主入口main
c#入门
C#基础语法
环境搭建、变量、类型转换、运算符条件分支语句、循环语句
控制台 输入输出
打印内容 自动换行
——通过+号 拼接打印
双引号“”字符串
变量不用引号
//不输入内容 只换行
Console.WriteLine();
Console.WriteLine("hello");
打印内容 不换行
Console.Write("please input");
读取输入(回车——输入完毕)
//等待玩家输入完毕后(按回车键)才会继续执行后面的代码
//可以输入很多信息直到回车结束
Console.ReadLine();
检测玩家是否按键
//只要按了键盘上的任意键就会认为输入结束
Console.ReadKey();
折叠代码
#region``````#endregion
配对出现
//将中间包裹的代码折叠起来,避免代码太乱
输入#region
之后按tab
键 自动补全
//本质是编辑器提供给我们的预处理指令
//它只会在编辑时有用,发布了代码或执行代码它会被自动删除
#region 知识点一
//折叠代码
Console.WriteLine("hello");
#endregion
变量
申明变量
变量类型 变量名 = 初始值;
变量类型(14种)
——关键字,存储的类型,存储的范围
- 有符号的整形变量(正负数包括0)
-
sbyte -128~127
-
int -21亿~21亿多
-
short -32768~32767
-
long -900万兆~900万兆
- 无符号的整形变量(正数和0)
-
byte 0~255
-
uint 0~42亿多的一个范围
-
ushort O~65535之间的一个数
-
ulong 0~18百万兆之间的数
- 浮点数(小数)
四舍五入
根据编译器不同有效数字也可能不一样
有效数字是从左到右从非0数开始算有效数字的
- float
存储7/8位有效数字
在后面加f或者F——c#中申明的小数默认是double的类型,加f是告诉系统它是float类型
- double
存储15~17位有效数字
- decimal
存储27~28位的有效数字,不建议使用
在后面加m或者M
- 特殊类型
-
bool 真假
-
char 字符 单个字符——单引号’‘
-
string 字符串 多个字符——双引号”“
——
——
数字用int,小数用float,字符串用string,真假用bool
多个同类型变量申明:
变量类型变量名=初始值,变量名=初始值,变量名=初始值…;
变量的存储空间(内存中)
单位转换
1byte = 8bit
1KB =1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
通过sizeof
方法可以获取变量类型所占的内存空间(单位:字节)
int size = sizeof(double);
变量类型——内存空间
变量类型 | 范围 | 内存空间 |
---|---|---|
//有符号 | ||
sbyte | -128~127 | 1 |
int | -21亿多~21亿多 | 4 |
short | -3万多~3万多 | 2 |
long | -9百万兆多~9百万兆多 | 8 |
//无符号 | ||
byte | 0~255 | 1 |
uint | 0~42亿多 | 4 |
ushort | 0~6万多 | 2 |
ulong | 0~18百万兆多 | 8 |
//浮点数 | ||
float | 7~8位有效数字 | 4 |
double | 15~17位有效数字 | 8 |
decimal | 27~28位有效数字 | 16 |
//特殊 | ||
bool | true,false | 1 |
char | 单个字符 | 2 |
string | 字符串长度是可变的,不定 | sizeof不能得到string类型所占的内存大小 |
变量的本质
变量的本质是2进制
计算机中所有数据的本质都是二进制,0和1
数据传递只能通过电信号,只有开和关两种状态。用0和1来表示这两种状态
- bit(位)
计算机中的存储单位最小为bit,只能表示0和1
1bit就是1个数,要不是0要不是1
- byte(字节)
由8个bit组成的存储单位。一个字节为8位
十进制》二进制——对二取余,倒着
二进制》十进制——从右往左看最右边的一位为第0位,如果该位不为0则加上2的n位次方
有符号 无符号 整形 区别——有符号 第一位 表示符号(符号位)
变量命名规范
1.不能重名
2.不能以数字开头
3.不能使用程序关键字命名
4.不能有特殊符号(下划线_除外)
建议的命名规则:变量名要有含义—>用英文(拼音)表示变量的作用
不建议的命名规则:用汉字命名
驼峰命名法——首字母小写,之后单词首字母大写(变量)
帕斯卡命名法——所有单词首字母都大写(函数、类)
——c#区分大小写
常量
申明常量
const 变量类型 变量名 = 初始值;
1.必须初始化
2.不能被修改
用处:数学相关固定的数
转义字符
\字符
enter上面的反斜线\
目标 | 写法 | 结果 |
---|---|---|
//常用转义字符 | ||
单引号 | \’ | ’ |
双引号 | \" | " |
换行 | \n | |
斜杠(计算机文件路径) | \\ | \ |
//不常用转义字符 | ||
制表符 | \t | tab键(4个空格) |
光标退格 | \b | 光标退格再继续输出 |
空字符 | \0 | 没用 |
警报音 | \a | 系统发出声音 |
取消转义字符:
在字符串“”之前加@
shift+2
类型转换
隐式转换
不同类型之间自动转换
大范围装小范围
- 相同大类型之间的转换
//可以用大范围装小范围的类型
//不能够用小范围的类型去装大范围的类型
类型 | 规则(大——>小) |
---|---|
有符号 | long——>int——>short——>sbyte |
无符号 | ulong——>uint——>ushort——>byte |
浮点数 | double——>float |
decimal这个类型没有办法用隐式转换的形式 | |
特殊类型 | bool char string不存在隐式转换 |
- 不同大类型之间的转换
类型转换 | 条件 | |
---|---|---|
//无符号和有符号之间 | ||
有符号——>无符号 | x | 无符号不能装负数 |
无符号——>有符号 | √ | 大范围装小范围 |
//浮点数和整数(有、无符号)之间 | ||
整数——>浮点数 | √ | 任何类型的整数 |
超过一定范围 自动使用科学计数法(e) | ||
浮点数——>整数 | x | 整数不能存小数 |
//特殊类型 | ||
bool | x | |
char——>部分整形和浮点数 | √ | char类型是一个16位无符号整数,用于表示Unicode字符的编码值 |
取值范围是0到65535 | ||
可以隐式转换为比它宽度更大的数值类型int、long、ushort、ulong等 | ||
对应的数字是一个ASCII码 | ||
string | x |
- 隐式转换规则 总结
double——> float——>整数(无符号、有符号)——>char
decimal——>整数(无符号、有符号)——>char
long——>int——>short——>sbyte
ulong——>uint——>ushort——>byte
string和bool不参与隐式转换规则的
无符号没法隐式存储有符号的
有符号的可以隐式存储无符号(范围大小)
显示转换
手动处理强制转换
1.括号强转
将高精度的类型强制转换为低精度
常用于转数值
变量类型 变量名 = ( 变量类型 ) 变量;
注意:精度问题 范围问题
相同大类的整形——范围问题
无符号和有符号——范围问题
浮点和整形——精度问题(浮点数强转成整形时,直接抛弃掉小数点后面的小数
char和数值类型
bool和string不能括号强转
2.Parse法
把字符串类型转换为对应的类型
变量类型.Parse("字符串")
字符串必须能够转换成对应类型,否则报错
string不用
3.Convert法
更准确的将各个类型之间进行相互转换
Convert.To目标类型(变量或常量)
填写的变量或常量必须正确否则出错
每一个类型都存在对应的Convert中的方法
sbyte sb5 = Convert. ToSByte( "1");
short s5 = Convert.ToInt16("1");
int i5 = Convert.ToInt32("1");
long 15 = Convert.ToInt64( "1");
byte b6 = Convert.ToByte( "1");
ushort us5 = Convert.ToUInt16( "1");
uint ui5 = Convert.ToUInt32( "1");
ulong ul5 = Convert. ToUInt64( "1");
float f5 = Convert.ToSingle( "13.2");
double d5 = Convert.ToDouble( "13.2");
decimal de5 = Convert.ToDecimal( "13.2");
bool bo5 = Convert.ToBoolean( "true" );
char 5 = Convert.ToChar("A");
string str5 = Convert.ToString(123123);
字符串转对应类型,要合法合规
精度比括号强转好一点,会四舍五入
bool类型可以转成数值类型,true对应1,false对应0
4.其它类型转string
拼接打印
变量.toString();
字符串拼接时自动会调用tostring
Console.WriteLine( "123123"+1 + true);
异常捕获
避免当代码报错时,造成程序卡死的情况
try——catch——finally
//必备部分
try
{
//希望进行异常捕获的代码块,放到try中
//如果try中的代码报错了,不会让程序卡死
}
catch//catch(Exception e)
{
//如果出错了会执行catch中的代码来捕获异常
//catch(Exception e)具体报错跟踪,通过e得到具体的错误
}
//可选部分
finally
{
//最后执行的代码,不管有没有出错都会执行其中的代码
}
代码基本结构中不需要加分号; 在里面写代码逻辑时每一句代码才加:
try
{
string str = Console.ReadLine();
int i = int.Parse(str);
console.writeLine(i);
}
catch
{
Console.WriteLine("请输入合法数字");
}
运算符
算数运算符
1.赋值符号
先看右侧再看左侧
把右侧的值赋值给左侧的变量
2.算数运算符
算数运算符,用于数值类型变量计算的运算符
返回结果是数值
用自己计算:先算右侧结果再赋值给左侧变量
连续运算:先算右侧结果再赋值给左侧变量
初始化时就运算:先算右侧结果再赋值给左侧变量
-
加+
-
减-
-
乘*
-
除/
默认的整数是int,除法运算会丢失小数点后的小数
用浮点数来存储除法结果,在运算时要有浮点数
- 取余%
3.算数运算符的 优先级
在混合运算时的运算顺序
乘除取余,优先级高于加减——先算乘除取余,后算加减
括号可以改变优先级,优先计算括号内内容——多组括号,先算最里层括号,依次往外算
4.算数运算符的 复合运算符
固定写法——
-
+=
-
-=
-
*=
-
/=
-
%=
复合运算符:用于自己—自己进行运算
复合运算符只能进行一种,不能混合运算
5.算数运算符的 自增减1
a = a + 1;
a += 1;
-
a++;//先用再加
-
++a;//先加再用
-
a–;//先用再减
-
–a;//先减再用
字符串拼接
- 方法1
用加号+拼接
用复合运算符+=拼接(先看右边结果)
自动调用toString
string str = "";
str += "1"+4 + true;//14true
str += 1 +2 +3 + 4;//10 先计算右边
str += ""+ 1+ 2 + 3 + 4;//1234
str += 1 +2 +""+3 +4;//334
str += 1 +2 +""+(3 +4);//37 括号改变运算顺序
- 方法2
string . Format("待拼接的内容",内容1,内容2,......)
要被拼接的内容,用占位符替代,{数字}
数字:0~n 依次往后
自动调用toString
- 控制台打印拼接
Console.WriteLine(“A{0}B{1}C{2}”,1,true,false);
Console.Write(“A{0}B{1}C{2}”,1,true,false);
可以多,不能少填
条件运算符
- 条件运算符的基本应用
用于比较两个变量或常量
-
大于>
-
小于<
-
等于==
-
不等于!=
-
大于等于>=
-
小于等于<=
条件运算符一定存在左右两边的内容
左边内容 条件运算符 右边内容
比较的结果返回的是一个 bool类型的值,true和false
如果比较的条件满足那就返回true,不满足就返回false
=先算右边再赋值给左边
- 各种应用写法
变量和变量比较
变量和数值(常量)比较
数值和数值比较
计算结果比较
//条件运算符的优先级低于算数运算符
//先计算再比较
- 不能进行范围比较
判断是否在某两个值之间
1< a< 6//在C#都不能这样写
要判断一个变量是否在两个数之间,要结合逻辑运算符的知识点
- 不同类型之间的比较
不同数值类型之间可以随意进行条件运算符比较
只要是数值就能够进行条件运算符比较,比较大于小于等于等等
特殊类型char string bool只能同类型进行==和!=比较
不仅可以和自己类型进行== !=还可以和数值类型进行比较还可以和字符类型进行大小比较
逻辑运算符
对bool类型进行逻辑运算
逻辑运算符优先级,低于 条件运算符,算术运算
- 逻辑与
符号&& 并且
有假则假,同真为真
在没有括号的情况下从左到右
有括号先看括号内
- 逻辑或
符号|| 或者
有真则真,同假为假
- 逻辑非
符号!
取反,真变假,假变真
逻辑非的优先级较高
- 混合使用 优先级问题
逻辑非!优先级最高
&&(逻辑与)优先级高于||(逻辑或)
逻辑运算符 优先级, 低于 算数运算符, 条件运算符(逻辑非除外)
- 短路规则
逻辑或||有真则真
逻辑与&&有假则假
只要逻辑与或者逻辑或,左边满足了条件
右边的内容已经不重要,抛弃后面不去计算
位运算符
用数值类型进行计算
连接两个数值
将数值转换为2进制再进行
对位运算
位数不同 补0
- 位与 &
有0则0
结果 小于等于最小数
- 位或 │
有1则1
- 异或 ^
相同为0 不同为1
- 位取反 ~
写在数值前面
0变1,1变0
某个类型完整的二进制,补全,完全取反
如int32位,第一位符号位,1》负数,负数二进制》转码 补码
- 左移<< 右移>>
数字 << 移动的位数
左移几位,右侧加几个0
右移几位,右侧去掉几个数
三目运算符
套路:3个空位2个符号
空位 ? 空位 : 空位;
bool类型 ? bool类型为真返回内容 : bool类型为假返回内容;
三目运算符会有返回值,这个返回值类型必须一致,并且必须使用!
第一个空位始终是结果为bool类型的表达式, bool变量,条件表达式,逻辑运算符表达式
第二三个空位什么表达式都可以,只要保证他们的结果类型是一致的
条件分支语句
if
满足条件时执行一些代码
if( bool类型值)
{
满足条件要执行的代码写在if代码块中;
}
1.if语句的语法部分,不需要写分号
2.if语句可以嵌套使用
if…else
产生两条分支,满足条件做什么,不满足条件做什么
if( bool类型值)
{
满足条件执行的代码;
}
else
{
不满足条件执行的代码;
}
if…else if. …else
产生n条分支,选择最先满足的一个条件,就做什么
if( bool类型值 )
{
满足条件执行的代码;
}
else if( bool类型值 )
{
满足条件执行的代码;
}
else
{
不满足条件执行的代码:
}
中间可以有n个else if语句代码块
else是可以省略的
条件判断从上到下,执行满足了第一个后,之后的都不会执行了
switch
switch(变量)
{
//变量==常量执行case和break之间的代码
case常量:
满足条件执行的代码逻辑;
break;
case常量:
满足条件执行的代码逻辑;
break;
//case可以有无数个
default:
如果上面case的条件都不满足就会执行default中的代码;
break;
}
常量1.必须初始化2.不能修改
只能写一个值,不能去写一个范围,不能写条件运算符,逻辑运算符
switch只判断变量是否等于某一个固定值
default可省略
可自定义常量const
满足某些条件时做的事情是一样的,可以使用贯穿
贯穿,不写case后面配对的break
满足break前其中一个条件就会执行之后的代码
循环语句
while
让顺序执行的代码,不停的循环执行某一代码块的内容
让代码可以被重复执行
bool类型,变量,条件运算符,逻辑运算符
while(bool类型的值)
{
//当满足条件时就会执行while语句块中的内容
//当代码逻辑执行完会回到while循环开头,再次进行条件判断
}
死循环
不停的执行循环中的逻辑
死循环只有在学习控制台程序时,会频繁使用
进入Unity过后基本不会使用死循环
1.可能因为内存问题造成程序崩溃闪退2.造成程序卡死
流程控制关键词
break:跳出循环
continue:回到循环开始,继续执行while(true)
break和continue和循环配合使用的,和if语句没关
do while
while循环是先判断条件再执行
do while循环是先斩后奏,先至少执行一次循环语句块中的逻辑,再判断是否继续do
do
{
//循环代码逻辑;
}while (bool类型的值);
while语句存在一个重要的分号
for
for(初始表达式 ; 条件表达式 ; 增量表达式){
//循环代码逻辑;
}
第一个空(初始表达式):一般声明一个临时变量,用来计数用
第二个空(条件表达式)︰表明进入循环的条件一个bool类型的结果 (bool变量,条件运算符,逻辑运算符,算术运算符)
第三个空(增量表达式):用第一个空中的变量进行自增减运算
第一次进入循环时,才会调用第一个空中的代码
每次进入循环之前,都会判断第二个空中的条件,满足才会进入循环逻辑
执行完循环语句块中的逻辑后,最后执行第三个空中的代码
每个空位规则
- 第一个空位:申明变量,可以连续申明——可以写在for前
- 第二个空位:进入条件,只要是bool结果的表达式都可以——不写可以写死循环
- 第三个空位:执行—次循环逻辑过后要做的事情——可以写在循环语句内
for循环可以准确得到一个范围中的所有数
项目补充
//输入一个键并赋值
char c = Console.ReadKey().KeyChar;
//输入一个键并赋值 且不在控制台显示
char c = Console.ReadKey(true).KeyChar;
//清除控制台
Console.Clear();
//设置光标位置 控制台左上角为0,0 右侧是x正方向 下方是y正方向
Console.SetCursorPosition(10,5);
//设置文字颜色
Console.ForegroundColor = ConsoleColor.Red;
//设置背景色 配合Clear使用 填充窗口颜色
Console.BackgroundColor = ConsoleColor.White;
Console.Clear();
//光标显隐
Console.CursorVisible = false;
//关闭控制台
Environment.Exit(0);
c#基础
复杂数据类型
复杂数据(变量)类型特点
1.数据集合
一般是多个数据(变量)集合在一起构成的数据
2.自定义
一般可以自己取名字,可以自定义的数据(变量)
枚举:整形常量的集合,可以自定义
数组:任意变量类型顺序存储的数据
结构体:任意变量的数据集合,可以自定义
枚举
被命名的整形常量的集合
—般用它来表示状态 类型等等
申明枚举,申明枚举变量,是两个概念
申明枚举:创建—个自定义的枚举类型
申明枚举变量:使用申明的自定义枚举类型,创建一个枚举变量
申明枚举
枚举名 以E
或者E_
开头
枚举中包裹的整形常量,第一个默认值是0,下面会依次累加1
可以自定义数值
自定义枚举项名字 = 100,
enum E_自定义枚举名
{
自定义枚举项名字,
自定义枚举项名字1,
自定义枚举项名字2,
}
-
namespace语句块中(常用)
-
class语句块中 struct语句块中
枚举不能在函数语句块中申明
申明枚举变量
自定义的枚举类型变量名 = 默认值;//自定义的枚举类型.枚举项
函数内
与switch配合使用
类型转换
1.枚举和int互转
- 枚举转int
int i = (int)playerType;
- int转枚举
playerType = 0;
2.枚举和string互转
- 枚举转string
string str = playerType. Tostring();
- string转枚举
Parse后第一个参数︰你要转为的是哪个枚举类型
第二个参数:用于转换的对应枚举项的字符串
转换完毕后是一个通用的类型,用括号强转成我们想要的目标枚举类型
playerType = (E_PlayerType) Enum.Parse( typeof(E_PlayerType), "other""):
作用
在游戏开发中,对象很多时候会有许多的状态
比如玩家动作状态——需要用一个变量或者标识来表示,当前玩家处于的是哪种状态
可能会使用int来表示他的状态,1行走2待机3跑步4跳跃……等等
枚举可以帮助我们清晰的分清楚状态的含义
数组
存储一组相同类型数据的集合
分为一维、多维、交错数组
一般情况,一维数组就简称为数组
一维数组
1数组的申明
变量类型,数组名,数组长度,数组内容
变量类型[] 数组名;
int[]arr1;
只是申明了一个数组,但是并没有开房
变量类型可以是所有变量类型
类型一致
变量类型[]数组名= new 变量类型 [数组的长度]
int[] arr3 = new int[5] ;//后面必须申明,初始化
arr1=new int[5];
变量类型[] 数组名= new 变量类型 [数组的长度] {内容1,内容2,内容3,.......;
int[] arr3 = new int[5] {1,2,3,4,5 };
变量类型[] 数组名= new 变量类型[] {内容1,内容2,内容3,.......);
int[] anr4 = new int[] {1,2,3,4,5,6,7,8,9};
后面的内容就决定了数组的长度 “房间数""
5.
变量类型[] 数组名= {内容1,内容2,内容3,.......];
int[] arr5 = {1,3,4,5,6};
2数组的使用
1.数组的长度
数组变量名.Length
array. Length
2.获取数组中的元素
数组的下标和索引,从0开始的
不能越界 范围 0~(length-1)
array[0]
3.修改数组中的元素
类型不能变
4.遍历数组
for 循环
下标自增 0 ~ array. Length
for (int i =0; i < array . Length; i++)
{
Console.WriteLine(array[i]);
}
5.增加数组的元素
数组初始化以后是不能够直接添加新的元素
搬家——使用新的数组,再改变原来数组的指向
int[]array2 = new int[6];
for (int i = 0; i < array.Length; i++)//遍历搬家
{
array2[i]= array[i];
}
array = array2;//原数组指向新数组空间(原空间自动销毁)
//原数组和新数组 都指向 新数组空间
array[5] = 999;//增加新元素
6.删除数组的元素
数组初始化以后,不能够直接删除元素的
搬家
int[]array3 = new int[5];
for (int i = 0; i < array3.Length; i++)
{
array3[i] = array[i];
}
array = array3;
7.查找数组中的元素
遍历,才能确定数组中是否存储了一个目标元素
int a = 3;
for (int i = 0; i < array. Length; i++)
{
if( a == array[i] )
{
Console.WriteLine(""和a相等的元素在{0}系引位置",i);
break;
}
}
二维数组
使用两个下标(索引引)来确定元素的数组
行标和列标——矩阵
控制台小游戏》地图格子
1数组的申明
变量类型[,] 二维数组变量名;
int[,]arr;
申明过后,初始化
2.
变量类型[,] 二维数组变量名= new 变量类型 [行,列];
int[,]arr2 = new int[3,3];
3.
变量类型[,〕 二维数组变量名= new 变量类型 [行,列] {{0行内容1,0行内容2......},{1行内容1,1行内容2......}};
int[,]arr3 = new int[3,3]{
{ 1,2,3 },
{ 4,5,6},
{ 7,8,9}};
变量类型[,] 二维数组变量名= new 变量类型 [,] {{0行内容1,0行内容2......},{1行内容1,1行内容2.....}};
int[,]arr4 = new int[,]{
{ 1,2,3 },
{ 4,5,6},
{ 7,8,9}};
变量类型[,] 二维数组变量名= {{0行内容1,0行内容2.......},{1行内容1,1行内容2.....}};
int[, ]arr5 = {
{ 1,2,3 },
{ 4,5,6},
{ 7,8,9}};
2数组的使用
1.二维数组的长度
行数 array.GetLength(0)
列数 array. GetLength(1)
2.获取二维数组中的元素
第一个元素的索引是0,最后一个元素的索引是长度-1
array[行索引,列索引]
3.修改二维数组中的元素
array[0,0] = 99;
4.遍历二维数组
嵌套循环
for (int i = 0; i < array. GetLength(0); i++)
{
for (int j = 0;j < array.GetLength(1); j++)
{
Console.WriteLine(array[i.j]);
}
}
5.增加数组的元素
数组声明初始化过后,不能再原有的基础上进行添加或者删除
增加一行 或者一列
搬家——使用新的数组,再改变原来数组的指向
int[,]array2 = new int[3,3];
for (int i = 0; i < array.GetLength(0); i++)
{
for (int j = 0; j < array. GetLength(1); j++)
{
array2[i,j]= array[i,j];
}
}
array = array2;
array[2,0]= 7;
array[2,1] = 8;
array[2,2]= 9;
6.删除数组的元素
数组初始化以后,不能够直接删除元素的
搬家
7.查找数组中的元素
遍历,才能确定数组中是否存储了一个目标元素
交错数组
交错数组是数组的数组,每个维度的数量可以不同
二维数组的每行的列数相同,交错数组每行的列数可能不同
1数组的申明
变量类型[][] 交错数组名;
int [][] arr1;
2.
娈量类型[][] 交错数组名= new 变量类型 [行数][];
int[][] arr2 = new int[3][];[][]
娈量类型[][] 交错数组名= new 变量类型 [行数][] {一维数组1,一维数组2,........ };
int [][] arr3 = new int[3][0] {
new int[]{1,2,3},
new int[] {1,2 },
new int[] { 1}};
4.
变量类型[][] 交错数组名= new 变量类型 [][] {一维数组1,一维数组2,........ };
int [][] arr3 = new int[][0] {
new int[]{1,2,3},
new int[] {1,2 },
new int[] { 1}};
变量类型[][] 交错数组名= {一维数组1,一维数组2,........ };
int [][] arr5 ={
new int[]{1,2,3},
new int[] {1,2 },
new int[] { 1}};
2数组的使用
1.数组的长度
行 array. GetLength(0)
得到某一行的列数 array[0].Length
2.获取交错数组中的元素
不要越界
array[0][1]
3.修改交错数组中的元素
array[0][1] = 99;
4.遍历交错数组
for (int i = 0; i < array.GetLength(0); i++)
{
for (int j = 0; j < array[i].Length; j++)
{
Console.write(array[i][j]+”“);
}
Console.writeLine();
}
5.增加交错数组的元素
6.删除交错数组的元素
7.查找交错数组中的元素
与一维数组,二维数组类似
值类型和引用类型
引用类型:string,数组(—维、二维、交错),类
值类型:无符号整形,有符号整形,浮点数,char ,bool ,结构体
- 赋值区别:
值类型 把内容拷贝给了对方,它变我不变
引用类型 让两者指向同一个值,它变我也变
- 存储区别:
值类型和引用类型 存储的内存区域是不同的,存储方式是不同的
值类型 栈空间—— 系统分配,自动回收,小而快
引用类型 堆空间—— 手动申请和释放,大而慢;;;;栈空间 存储 引用类型在堆空间中 的地址
- 特殊的引用类型 string:它变我不变
重新赋值时,会在堆空间中 重新分配空间,栈空间存的地址 指向 新的堆空间地址
频繁的改变string重新赋值,会产生内存垃圾
函数
函数基础
函数(方法)
本质 一块具有名称的代码块
可以使用函数(方法)的名称,来执行该代码块
函数(方法)是,封装代码,进行重复使用 的一种机制
函数(方法)的主要作用
1.封装代码
2.提升代码复用率(少写点代码)
3.抽象行为
函数写在
1.class语句块中
2.struct语句块中
函数定义基础
static 返回类型 函数名 (参数类型参数名,参数类型参数名2,.......)
{
函数的代码逻辑;
return 返回值;(如果有返回类型才返回)
}
-
static 不是必须的
-
返回类型
- 关键字void(表示没有返回值)
- 任意的变量类型14种变量类型+复杂数据类型(数组、枚举、结构体、类class)
-
函数名,帕斯卡命名法//MyName(帕斯卡命名法)
-
参数
-
不是必须的,可以有0~n个参数
-
参数的类型可以是任意类型14种变量类型+复杂数据类型(数组、枚举、结构体、类class)
-
多个参数,用逗号隔开
-
参数名 驼峰命名法 //myName(驼峰命名法)
-
-
返回值
- 当返回值类型不为void时,必须return返回对应类型的内容
(void也可以使用return) - retrun后面可以写一个表达式,结果和返回值类型一致
- return可以直接不执行之后的代码,直接返回到函数外部
- 当返回值类型不为void时,必须return返回对应类型的内容
函数使用
使用函数,直接写函数名(参数)
参数可以是 常量,变量,函数
参数一定是传一个能得到对应类型的表达式
有返回值的函数要 拿返回值来用 或者拿变量接收它的结果
可以直接调用,但是返回值就没用了
函数定义使用
1.无参 无返回值
static void SayHellow()
{
Console.writeLine("你好世界");
return;//没有返回值时(返回值类型是void)可以省略
}
2.有参 无返回值
static void sayYourName(string name)
{
console.WriteLine("你的名字是:{0}", name);
return;//可以省略
}
3.无参 有返回值
static string whatYourName()
{
return“唐老狮";//对应返回值类型
}
4.有参 有返回值
static int sum(int a, int b)
{
int c =a + b;
return a + b;
}
5.有参 有多返回值
函数的返回值一定是一个类型,只能是一个内容
使用复杂数据类型
static int[] calc(int a, int b)
{
int sum = a+ b;
int avg= sum / 2;
//int[] arr = { sum,avg };
//return arr;
return new int[] i sum, avg;
}
ref和out
函数参数的修饰符
函数申明和使用的时候,都要加在参数前面
当传入的值类型参数在内部修改时,或者引用类型参数在内部重新申明时,外部的值会发生变化
ref和out的区别
- ref传入的变量必须初始化,在内部可改可不改
- out传入的变量不用初始化,在内部必须修改该值(必须赋值)
变长参数
params 参数类型[] 参数名字
可以传入n个同一类型参数,n可以等于0
传入的参数会存在arr数组中
- params关键字后面必为数组
- 数组的类型可以是任意的类型
- 函数参数可以有别的参数和params关键字修饰的参数
- 函数参数中只能最多出现一个params关键字,并且一定是在最后一组,参数前面可以有n个其它参数
参数默认值
有参数默认值的参数,可选参数
当调用函数时可以不传入参数,不传就使用默认值作为参数的值
1.支持多参数默认值,每个参数都可以有默认值
2.如果要混用,可选参数必须写在普通参数后面
函数重载
在同一语句块(class或者struct)中函数(方法)名相同
- 参数的数量不同 或者
- 参数的数量相同,但参数的类型或顺序不同
作用:
1.命名一组功能相似的函数,减少函数名的数量,避免命名空间的污染
2.提升程序可读性
注意:
1.重载和返回值类型无关,只和参数类型,个数,顺序有关
2.调用时程序会自己根据传入的参数类型判断使用哪一个重载
不同情况:
- 参数数量不同
- 数量相同类型不同
- 数量相同顺序不同
- 增加ref 或者out(ref和out相当于同一个 变量类型)
递归函数
让函数自己调用自己
一个正确的递归函数
1.必须有结束调用的条件
2.用于条件判断的这个条件,必须改变,能够达到停止的目的
复杂数据类型—结构体
一种自定义变量类型,类似枚举,需要自己定义
数据和函数的集合,可以申明各种变量和方法
作用:用来表现存在关系的数据集,比如用结构体表现学生动物人类等等
一般写在namespace语句块中
结构体名 帕斯卡命名法
struct 自定义结构体名
{
第一部分 变量
第二部分 构造函数(可选)
第三部分 函数
}
- 变量
结构体申明的变量,不能直接初始化
变量类型可以写任意类型包括结构体,但是 不能是自己的结构体
- 函数方法
表现这个数据结构的行为
——注意在结构体中的方法目前不需要加static关键字
函数中可以直接使用结构体内部申明的变量
可以根据需求写无数个函数的
- 访问修饰符
修饰结构体中变量和方法是否能被外部使用
public公共的可以被外部访问
private私有的只能在内容使用
默认不写为private
- 构造函数
1.没有返回值
2.函数名必须和结构体名相同
3.必须有参数
4.如果申明了构造函数那么,必须在其中对所有变量数据初始化
构造函数一般是用于在外部方便初始化
关键字this代表自己
public student(int age)
{
this.age = age;
}
- 使用结构体
class Program
{
static void Main(string[] args)
{
student s1;
s1.age = 10;
s1.Speak();
Student s2 = new Student(18,true,2,"小红");
s2.Speak();
}
}
排序
将一组"无序”“的序列调整为”“有序”(升序 或者 降序)的序列
序列一般存储在数组中,所以排序往往是对数组进行排序
冒泡排序
两两相邻,不停比较,不停交换,比较n轮
两层循环,外层轮数,内层比较;两值比较,满足交换
//升序
//有几个数就比较多少轮
for (int m = ; m < arr.Length; m++)
{
//进一次循环就需要比较一轮
for (int n = 0;n< arr.Length - 1; n++)
{
//如果第n个数比第n+1个数大,交换位置
if (arr[n] > arr[n + 1])//升序
{
//第二步交换位置
//中间商不赚公价
int temp = arr[n];
arr[n] = arr[n + 1];
arr[n + 1]=temp;
}
}
}
优化1:
比过不比
确定位置的数字不用比较了
确定了一轮后极值(最大或者最小)已经放到了对应的位置(往后放),所以没完成n轮后面位置的数就不用再参与比较了
//升序
for (int m = ; m < arr.Length; m++)
{
//优化
for (int n = 0;n< arr.Length - 1 - m ; n++)
{
if (arr[n] > arr[n + 1])
{
int temp = arr[n];
arr[n] = arr[n + 1];
arr[n + 1]=temp;
}
}
}
优化2:
提前结束
bool值
内层循环中没有满足条件交换
for (int m= 0; m < arr.Length; m++)
{
isSort = false;//标识符
for (int n = 0; n < arr.Length - 1 - m ;n++)
{
if (arr[n] > arr[n + 1])
{
isSort = true;//改变标识符
int temp = arr[n];
arr[n] = arr[n + 1];
arr[n + 1]= temp;
}
}
//当一轮结束过后如果isSort这个标识还是false
//意味着已经是最终的序列了,不需要再判断交换了
if( !isSort )
{
break;
}
}
选择排序
新建中间商,记录索引;依次比较,找出极值(最大或最小),放入目标位置;比较n轮
两层循环,外层轮数,内层寻找初始索引记录极值;;内存循环外交换
//比较m轮
for (int m= 0; m< arr.Length; m++)
{
//第一步
//申明一个中间商来记录索引
//每一轮开始 默认第一个都是极值
int index = 0;
//第二步
//依次比较
//-m的目的是 排除上一轮已经放好位置的数
for (int n = 1; n < arr.Length - m; n++)
{
//第三步
//找出极值(最大值)
if (arr[index] < arr[n])
{
index = n;
}
}
//第四步
//放入目标位置
//Length - 1 -轮数
//如果当前极值所在位置 就是目标位置那就没必要交换了
if (index != arr.Length - 1 -m)
{
int temp = arr[index];
arr[index] = arr[arr.Length - 1 -m];
arr[arr.Length - 1 -]= temp;
}
}
c核心
面向对象:三大特性,七大原则
类(class关键词)
面向对象三大特性:封装+继承+多态
封装:用程序语言来形容对象
继承:复用封装对象的代码;儿子继承父亲,复用现成代码
多态:同样行为的不同表现,儿子继承父亲的基因但是有不同的行为表现
面向对象七大原则:
开闭原则、依赖倒转原则、里氏替换原则、单一职责原则、接口隔离原则、合成复用原则、迪米特法则
面向对象的概念
- 面向过程编程
以过程为中心的编程思想
分析出解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用的时候一个一个依次调用
- 面向对象编程
对现实世界理解和抽象的编程方法
万物皆对象,用程序来抽象(形容)对象
把相关的数据和方法组织作为一个整体来看待
从更高的层次来进行程序开发,更贴近事物的自然运行模式
套路:
用中文去形容一类对象,把一类对象的共同点提取出来
然后用程序语言把它翻译过来,带着对象的概念在程序中使用它们
好处:
提高代码复用率,提高开发效率,提高程序可拓展性,清晰的逻辑关系
面向对象—封装
类
具有相同特征,具有相同行为,一类事物的抽象类
是对象的模板,可以通过类创建出对象
类的关键词class
一般申明在namespace语句块中
命名:用帕斯卡命名法 同一个语句块中的不同类不能重名
可以使用三杠注释 ///
类的申明
(可以加 访问修饰符private public)class 类名
{
特征——成员变量
行为——成员方法
保护特征——成员属性
构造函数、析构函数
索引器
运算符重载
静态成员
}
(类)对象
类的申明 和 类对象的申明 是两个概念
-
类的申明 是申明对象的模板 用来抽象(形容)显示事物的
-
类对象的申明 是用来表示现实中的对象个体的
类是一个自定义的变量类型
实例化一个类对象是在申明变量
类的申明 类似枚举和结构体的申明——类的申明相当于申明了一个自定义变量类型
而对象 是类创建出来的——相当于申明一个指定类的变量
类创建对象的过程:实例化对象
类对象:引用类型
类对象(变量)的申明
类名 变量名;
类名 变量名= null; (null代表空)
类名 变量名= new 类名();
Person P;//相当于下一句,=null
Person P2 = null;//null代表空,分配栈空间,不分配堆内存空间
Person P3 = new Person();//相当于一个人对象
Person p4 = new Person();//相当于又是一个人对象
P3和P4虽然是来自一个类的实例化对象
但是他们的特征行为等等信息,都是他们独有的
他们没有共享了数据,彼此没有关系
成员变量
1.申明在类class语句块中
2.用来描述对象的特征
3.可创建任意变量类型
4.数量不做限制
5.是否赋值(初始化)根据需求来定
6.可以申明和类名 同名的变量(因为类变量是引用类型)(但是不能初始化)
7.成员变量的初始值:
值类型:数字类型默认值都是0,boo1类型false
引用类型:null
查看默认值的方法:default(变量类型)default(Person)
8.使用 点
Person p = new Person();
p.age = 10;
Console.writeLine(p.age);
外部只能使用public的变量
不修改会打印初始值(默认值)
访问修饰符
public:公共的 自己(内部)和别人(外部)都能访问和使用
private:私有的 自己(内部)才能访问和使用 不写默认为private
protected:保护的 自己(内部)和子类才能访问和使用
目前决定类内部的成员的访问权限
加在各种之前
访问修饰符 类名 变量名;
成员方法
成员方法(函数) 用来表现对象行为
1.申明在类class语句块中
2.是用来描述对象的行为的
3.规则和函数申明规则相同
4.受到访问修饰符规则影响
5.返回值参数不做限制
6.方法数量不做限制
注意:
1.成员方法不要加static(静态)关键字
2.成员方法必须实例化出对象,再通过对象来使用,,相当于该对象执行了某个行为
可以使用class内成员变量(成员方法在成员变量前后都可以,c#一般写在变量后面)
使用 点(和成员变量一样)
class Person
{
public Person[] friends;
///<summary>
///添加朋友的方法
///</summary>
///<param name="p">传人新朋友</param>
public void AddFriend(Person p)
{
if(friends == null)
{
friends = new Person[] { p };
}
else
{
//新建一个房子数组
Person[] newFriends = new Person[friends.Length + 1];
//搬家
for (int i = 0; i < friends.Length; i++)
{
newFriends[i] = friends[i];
}
//把新加的朋友放到最后一个
newFriends[newFriends.Length - 1]=p;
//地址重定向
friends = newFriends;
}
}
}
Person p = new Person();
p.name = "唐老狮";
Person p2 = new Person();
p2.name ="火山哥";
//使用 成员方法
p.AddFriend(p2);
for (int i = 0; i< p.friends. Length; i++)
{
Console.WriteLine(p.friends[i].name);//打印出 朋友的名字
}
构造函数
在实例化对象时,会调用的,用于初始化成员变量的函数
如果不写,默认存在一个无参构造函数
构造函数的写法
1.没有返回值
2.函数名和类名必须相同
3.没有特殊需求时一般都是public的
4.构造函数可以被重载(不同参数)
5.this代表当前调用该函数的对象自己,区别同名变量
注意:
如果不自己实现无参构造函数,而实现了有参构造函数,会失去默认的无参构造
类中是允许自己申明无参构造函数的,结构体是不允许
构造函数d 特殊写法:可以通过this重用构造函数代码
访问修饰符 构造函数名 (参数列表):this(参数1,参数2....)
只要是参数类型一致
public Person()//无参构造函数
{
name ="唐老师";
age =18;
}
public Person(string name)//有参构造函数
{
this.name = name;
}
public Person(int age, string name) :this()//先调用无参构造函数
{
Console.WriteLine( "两个参数构造函数");
}
public Person(int age, string name) :this(name)//先调用有参构造函数
{
Console.WriteLine( "Person两个参数构造函数");
}
析构函数
当引用类型的堆内存被回收时,会调用该函数
对于需要手动管理内存的语言((比如C++),需要在析构函数中做一些内存回收处理
但是c#中存在自动垃圾向收机制 GC,所以我们几乎不会怎么使用析构函数。除非你想在某一个对象被垃圾回收时,做一些特殊处理
在Unity开发中析构函数几乎不会使用,只做了解
//当引用类型的堆内存被回收时调用
~类名()
{
//在某一个对象被垃圾回收时,做一些特殊处理
}
内存——变成垃圾——回收垃圾
垃圾回收机制
垃圾回收,英文简写GC (Garbage collector)
垃圾回收的过程:
遍历堆(Heap)上动态分配的所有对象,
通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用
所谓的垃圾就是没有被任何变量,对象引用的内容
垃圾就需要被回收释放
垃圾回收有很多种算法:
引用计数(Reference counting)
标记清除(Mark Sweep)
标记整理(Mark Compact)
复制集合(copy collection)
注意:
GC只负责***堆(Heap)***内存的垃圾回收
引用类型都是存在堆(Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理
***栈(Stack)***上的内存是由系统自动管理的
值类型在栈(Stack)中分配内存的,他们有自己的申明周期,不用对他们进行管理,会自动分配和释放
C#中内存回收机制的大概原理
0代内存——>1代内存——>2代内存
代的概念:
代是垃圾回收机制使用的一种算法(分代算法)
新分配的对象都会被配置在第0代内存中
每次分配都可能会进行垃圾回收以释放内存(0代内存满时)
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步
1.标记对象从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象,不可达对象就认为是垃圾
2.搬迁对象压缩堆(挂起执行托管代码线程),释放未标记的对象,搬迁可达对象,修改引用地址
0代搬到1代,1代搬到2代
大对象总被认为是第二代内存,目的是减少性能损耗,提高性能
不会对大对象进行搬迁压缩,85000字节(83kb)以上的对象为大对象
手动触发垃圾回收的方法
一般情况下我们不会频繁调用,都是在Loading过场景时才调用GC.collect();
成员属性
1.用于保护成员变量,可以设置加密处理
2.为成员属性的获取和赋值 添加逻辑处理
3.解决3P的局限性
public——内外访问
private——内部访问
protected——内部和子类访问
属性可以让成员变量在外部
只能获取不能修改,或者,只能修改本能获取
属性的命名一般使用 帕斯卡命名法
访问修饰符 属性类型 属性名
public int Name
{
get
{
//和set不同,需要return返回值
//可以在返回之前添加一些逻辑规则
//意味着 这个属性可以获取的内容
//解密处理
return name - 5;
}
private set
{
//可以在设置之前添加一些逻辑规则
//value关键字 用于表示外部传入的值
//加密处理
name = value + 5;
}
}
使用成员属性
Person p =new Person();
p. Name =“唐老狮";//进入set
Console.WriteLine(p.Name);//进入get
成员属性中get和set前,可以加访问修饰符
1.默认不加,会使用属性申明时的访问权限
2.加的访问修饰符,要低于属性的访问权限
3.不能让get和set的访问权限,都低于属性的权限
4.get和set中,一般只加一个
get和set可以只有一个
只有一个时,没必要在前面加访问修饰符
一般情况下只会出现 只有get的情况,基本不会出现只有set
自动属性
外部能得不能改的特征
没有在get和set中写逻辑的需求或者想法
如果类中有一个特征是只希望外部能得不能改的,又没什么特殊处理,那么可以直接使用自动属性
public float Height
{
get;
//private set;
set;
}
索引器
让对象可以像数组一样,通过索引访问其中元素,使程序看起来更直观,更容易编写
主要作用:可以让我们以中括号的形式范围自定义类中的元素,规则自己定,访问时和数组一样
比较适用于在类中有数组变量时使用,可以方便的访问和进行逻辑处理
索引器申明
访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名.....]
{
//get,set 内部的写法和规则,和索引器相同
get{}
set{}
}
eg.索引器可以写逻辑
private Person[] friends;
public Person this[int index]
{
get
{
//可以写逻辑,根据需求来处理这里面的内容
//索引器中可以写逻辑
if( friends =- null
friends.Length - 1 < index)
{
return null;
}
return friends [index];
}
set
{
//value代表传入的值
//可以写逻辑,根据需求来处理这里面的内容
if( friends == null )
{
friends = new Person[] { value };
}
else if(index > friends.Length - 1)
{
//自己定了一个规则,如果索引越界就默认把最后一个朋友顶掉
friends[friends.Length - 1] = value;
}
friends [index] = value;
}
}
索引器使用
Person p = new Person();
p[0] = new Person();
索引器可以重载
重载——函数名相同,参数类型、数量、顺序不同
private int[,] array;//二维数组
public int this [int i, int j]
{
get
{
return array[i,i];
}
set
{
array[i,j] = value;
}
}
静态成员
静态关键字 static
用static修饰的成员变量、方法、属性等 称为静态成员
使用:直接用类名点出使用
//自定义静态成员
class Test
{
//静态成员变量
static public float PI = 3.1415926f;
//成员变量
public int testInt = 100;
//静态成员方法
public static float calccircle(f1oat r)
{
return PI * r*r;
}
//成员方法
public void TestFun()
{
Console.writeLine( "123");
}
}
可以不实例化,直接点出来使用——
程序中是不能无中生有的
要使用的对象,变量,函数都是要在内存中分配内存空间的
实例化对象,目的就是分配内存空间,在程序中产生一个抽象的对象
静态成员的特点:
程序开始运行时就会分配内存空间,可以直接使用。
静态成员和程序同生共死。
只要使用了它,直到程序结束时,内存空间才会被释放。
所以一个静态成员就会有自己唯一的一个“内存小房间”,这让静态成员就有了唯一性。
在任何地方使用都是用的小房间里的内容,改变了它也是改变小房间里的内容。
-
静态函数中不能使用非静态成员
-
非静态函数可以使用静态成员
静态的作用
静态变量:
1.常用唯一变量的申明
2.方便别人获取的对象申明
静态方法:
常用的唯一的方法申明,比如相同规则的数学计算相关函数
常量和静态变量
const(常量)可以理解为特殊的static(静态)
相同点
通过类名点出使用
不同点
1.const必须初始化,不能修改;static没有这个规则
2.const只能修饰变量、 static可以修饰很多
3.const一定是写在访问修饰符后面的, static没有这个要求
静态类
用static修饰的类,工具类,提供工具方法、拓展方法
只能包含静态成员,不能被实例化
作用
1.将常用的静态成员写在静态类中,方便使用
2.静态类不能被实例化,更能体现工具类的唯一性
Console就是一个静态类
静态构造函数
在构造函数加上 static修饰
特点
1.静态类和普通类都可以有
2.不能使用访问修饰
3.不能有参数
4.只会自动调用一次,第一次使用时
作用
在静态构造函数中,初始化静态变量
使用
1.静态类中的静态构造函数
2.普通类中的静态构造函数
//静态类中的静态构造函数
static class Staticclass
{
public static int testInt= 100;
public static int testInt2 = 10e;
static staticclass()//
{
Console.WriteLine(“静态构造函数");
}
}
//普通类中的静态构造函数
class Test
{
public static int testInt = 200;
static Test()//
{
Console.WriteLine(“静态构造"");
}
public Test()
{
Console.WriteLine("普通构造"");
}
}
拓展方法
为现有 非静态 变量类型,添加新方法
作用
1.提升程序拓展性
2.不需要再对象中重新写方法
3.不需要继承来添加方法
4.为别人封装的类型,写额外的方法
特点
1.一定是写在静态类中
2.一定是个静态函数
3.第一个参数为拓展目标
4.第一个参数用this修饰
访问修饰符 static 返回值 函数名 (this 拓展类名 参数名,参数类型 参数名,参数类型 参数名....)
static class Tools
{
//为拓展类名int拓展了一个成员方法
//成员方法是需要实例化对象后才能使用的
//value 代表使用该方法的实例化对象
public static void speakvalue(this int value)
{
//拓展的方法的逻辑
Console.WriteLine(”唐老狮为int拓展的方法”+value);
}
}
使用
int i = 10;
i.speakValue();
为自定义的类型拓展方法
如果与原来方法重名,使用原来方法
运算符重载
让自定义类和结构体能够使用运算符
使用关键字operator
特点
1.一定是一个公共的静态方法
2.返回值写在operator前
3.逻辑处理自定义
作用
让自定义类和结构体对象可以进行运算
1.条件运算符需要成对实现
2.—个符号可以多个重载
3.不能使用ref和out
public static 返回类型 operator 运算符(参数列表)
class Point
{
public int x;
public int y;
public static Point ogerator +(Point p1,Point p2) {
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
return p;
}
}
参数必须有一个和类相同
参数个数和运算符有关
可重载的运算符:
算数运算符
逻辑运算符——逻辑非
位运算符——位或与|&^取反~ 左移<< 右移>>
条件运算符——相关符号必须成对实现 ><, >=<=, ==!= 返回值一般是bool值,也可以是其他的
不可重载的运算符:
逻辑运算符
-
逻辑与&&
-
逻辑或Il
索引符[]
强转运算符()
特殊运算符
-
点.
-
三目运算符?:
-
赋值符号=
内部类
在一个类中再申明一个类
使用时要用包裹者点出自己
作用:亲密关系的变现
访问修饰符作用很大
class Person
{
public int age;
public string name;
public Body body;
public class Body//内部类
{
Arm leftArm;
Arm rightArm;
class Arm{}
}
}
Person P =new Person();
Person.Body body = new Person.Body();//点出使用
分部类
把一个类分成几部分申明
关键字partial
分部描述─个类,蹭加程序的拓展性
分部类可以写在多个脚本文件中
分部类的访问修饰符要一致
分部类中不能有重复成员
partial class Student
{
public bool sex;
public string name;
partial void Speak();//分部方法的申明
}
partial class student
{
public int number;
partial void Speak()
{
//分部方法的实现逻辑
}
}
分部方法
将方法的申明和实现分离
关键字partial
1.不能加访问修饰符,默认私有
2.只能在分部类中申明
3.返回值只能是void
4.可以有参数但不用out关键字
局限性大,了解即可
类的知识总结
class 类名
{
特征—成员变量
行为—成员方法
初始化调用—构造函数
释放时调用—析构函数
保护成员变量—成员属性
像数组一样使用—索引器
类名点出使用—静态成员
自定义对象可计算—运算符重载
内部类
}
静态类和静态构造函数-拓展方法
分部类-分部方法
面向对象—继承
继承的基本规则
一个类A继承一个类B
类A将会继承类B的所有成员
A类将拥有B类的所有特征和行为
被继承的类——称为父类、基类、超类
继承的类——称为子类、派生类
子类可以有自己的特征和行为
单根性——子类只能有一个父类
传递性——子类可以间接继承父类的父类
class 类名 : 被继承的类名
{
}
访问修饰符的影响:
public——公共:内外部访问
private——私有:内部访问,外部和子类不能用
protected——保护:内部和子类访问,外部不能用
命名空间中internal——内部的:只有在同一个程序集的文件中,内部类型或者是成员才可以访问
子类和父类的同名成员
C#中允许子类存在和父类同名的成员
在子类中申明变量时,在修饰符后、类型前加new
不建议使用
使用多态解决问题
里氏替换原则
面向对象七大原则中,最重要的原则
任何父类出现的地方,子类都可以替代
语法表现:父类容器装子类对象
因为子类对象包含了父类的所有内容
不能用子类装父类
方便进行对象存储和管理
class GameObject{}
class Player:GameObject{}
class Monster:GameObject{}
class Boss:GameObject{}
//用父类容器装载子类对象
GameObject player = new Player();
GameObject monster = new Monster();
GameObject boss = new Boss();
GameObject[] objects = new GameObject[] { new Player(),new Monster(),new Boss()};
//此时容器不能使用子类的特有参数方法
is:判断一个对象是否是指定类对象
返回值:bool,是为真,不是为假
as:将一个对象转换为指定类对象
返回值:指定类型对象
成功返回指定类型对象,失败返回null
类对象is类名——bool类型返回值:true和false
类对象as类名——返回值:对象或者null
if( player is Player )
{
//Player p = player as Player;
//p.PlayerAtk();
(player as Player).PlayerAtk();//简写
}
对于数组 更有用
继承中的构造函数
当申明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数
父类的父类的构造——>父类构造——>子类构造
1.父类的无参构造很重要
子类实例化时,默认自动调用的是父类的无参构造,所以如果父类无参构造被顶掉,会报错
–申明父类无参构造,或者–base指定父类有参构造
2.子类可以通过base关键字,代表父类,指定调用父类构造
区分base和this
class Son:Father
{
public Son(int i) : base(i)//指定父类构造函数
{
}
public Son(int i, string str):this(i)//通过this间接调用了父类构造函数
{
}
}
万物之父
关键字: object
object是所有类型的基类,它是一个类(引用类型)
1.可以利用里氏替换原则,用object容器装所有对象
2.可以用来表示不确定类型,作为函数参数类型
//引用类型
object o = new Son();
//用is as来判断和转换即可
if( o is son )
{
(o as Son). speak();
}
//值类型
object o2 = 1f;//用括号强转
float f1=(float)o2;
//特殊的string类型
object str = "123123"";
string str2 = str as string;//方法1 引用类型可以用as
string str3 = str.ToString();//方法2
//数组
object arr = new int[10];
int[] ar1 = arr as int[];//方法1 引用类型可以用as
int[] ar2 =(int[])arr;//方法2
装箱拆箱
装箱:用object存值类型
把值类型用引用类型存储,栈内存会迁移到堆内存中
拆箱:再把object转为值类型
把引用类型存储的值类型取出来,堆内存会迁移到栈内存中
好处:不确定类型时可以方便参数的存储和传递
坏处:存在内存迁移,增加性能消耗
object v= 3;//装箱
int intValue = (int)v;//拆箱
object可以存各种类型
可以用于变长参数
static void TestFun( params object[]array )
{
}
使用时需要拆箱
密封类
使用sealed
密封关键字修饰的类
让类无法再被继承
在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承,可以保证程序的规范性、安全性
以后制作复杂系统或者程序框架时,便能慢慢体会到密封的作用
面向对象—多态
Vob(没有这个词,方便记)
多种状态
让继承同一父类的子类们,在执行相同方法时,有不同的表现(状态)
同一父类的对象执行相同行为(方法),有不同的表现
让同一个对象有唯一行为的特征
编译时多态——函数重载,开始就写好的
运行时多态(vob、抽象函数、接口)
vob
v: virtual(虚函数)
o: override(重写)
b : base(父类)
父类 虚函数 可以被子类 重写
//父类
class Gameobject
{
//virtual虚函数,可以被子类重写
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}
//子类
class Player:Gameobject
{
//override重写虚函数
public override void Atk()
{
//base代表父类,可以通过base来保留父类的行为
//base.Atk(); 调用父类的行为
Console. WriteLine("玩家对象进行攻击");
}
}
//使用
GameObject p = new Player("唐老狮");
p.Atk();//使用子类行为
抽象类
被抽象关键字abstract修饰的类
1.不能被实例化的类
2.可以包含抽象方法
3.继承抽象类必须重写其抽象方法
封装的所有知识点,都可以在抽象类中书写
可以在抽象类中,写抽象函数
如何选择普通类还是抽象类
不希望被实例化的对象,相对比较抽象的类,可以使用抽象类
父类中的行为不太需要被实现的,只希望子类去定义具体的规则的,可以选择抽象类,然后使用其中的抽象方法来定义规则
整体框架设计时 使用
abstract class Thing
{
public string name;
}
class Water:Thing
{
}
//抽象类不能被实例化
//Thing t = new Thing();
//但是可以遵循里氏替换原则,用父类容器装子类
Thing t = new Water();
抽象方法
又叫纯虚方法
用abstract关键字修饰的方法
1.只能在抽象类中申明
2.在父类中 没有方法体,只能申明;在子类继承后实现
3.不能是私有的private,可以是public或者protected
4.继承后必须实现,用override重写
abstract class Fruits
{
public abstract void Bad();//申明,不能写逻辑
}
class Apple : Fruits
{
public override void Bad()//override重写
{
//实现
}
}
和虚方法区别:
抽象方法必须写在抽象类里面
虚方法是可以由我们子类选择性来实现的,抽象方法必须要实现
都可以被子类无限重写
都可以用base
接口
行为的抽象规范
也是一种自定义类型
接口是抽象行为的“基类”
关键字: interface
命名规范:帕斯卡前面加个I(大写i)
接口申明:
1.不包含成员变量
2.只包含方法、属性、索引器、事件
3.成员不能被实现
4.成员可以不用写访问修饰符,不能是私有的
如果写保护的,需要显示的实现
5.接口不能继承类,但是可以继承另─个接口
接口的使用:
1.类可以继承多个接口
2.类继承接口后,必须实现接口中所有成员
特点:
1.它和类的申明类似
2.接口是用来继承的
3.接口不能被实例化,但是可以作为容器存储对象
interface 接口名
{
}
interface IFly
{
//成员不能被实现
//方法
void Fly();
//属性。自动属性
string Name
{
get;
set;
}
//索引器
int this[int index]
{
get;
set;
}
//事件
event Action doSomthing;
}
接口的使用:
接口用来继承
1.类可以继承1个类,n个接口
2.继承了接口后必须实现其中的内容,并且必须是public的
3.实现的接口函数,可以加virtual,再在子类重写
4.接口也遵循里氏替换原则
class Person : Animal,IFly//类可以继承1个类,n个接口
{
//必须实现接口中的内容
//必须是public的
//方法
//可以加virtual,再在子类重写
public virtual void Fly()
{
}
//属性
public string Name
{
get;
set;
}
//索引器
public int this[int index]
{
get
{
return 0;
}
set
{
}
}
//事件
event Action doSomthing;
}
点小灯泡,自动补全,所有接口内容的实现;会有占位的内容
接口可以继承接口
接口继承接口时,不需要实现
待类继承接口后,类自己去实现所有内容
遵循里氏替换原则
显示实现接口
当一个类继承两个接口
但是接口中存在着同名方法时
或者protect修饰符时
显示实现接口时,不能写访问修饰符
接口名.行为名
继承类:是对象间的继承,包括特征行为等等
继承接口:是行为间的继承,继承接口的行为规范,按照规范去实现内容
由于接口也是遵循里氏替换原则,所以可以用接口容器装对象
可以实现,装载各种亳无关系但是却有相同行为的对象
密封方法
用密封关键字sealed 修饰的重写函数
让虚方法virtual或者抽象abstract方法,之后不能再被重写
和override一起出现,写在override前面
面向对象关联知识点
命名空间
用来组织和重用代码
命名空间就像是一个工具包,类就像是一件一件的工具,都是申明在命名空间中的
namespace 命名空间名
{
类
类
}
可以有同名的命名空间,相当于分开写partial,可以在不同文件
同一命名空间,不能有同名类
不同命名空间,可以有同名类
使用同名类,要指明出处
不同命名空间中相互使用,需要引用命名空间或指明出处
-
引用命名空间
using 命名空间名;
写在最前面 ;即使在一个文件也要引用 -
指明出处
命名空间名.方法
命名空间可以包裹命名空间
要引用命名空间里面的命名空间 using 命名空间.子命名空间;
子命名空间可以有同名成员,使用要指明出处
修饰类的修饰符
public一公共的
internal 一只能在该程序集中使用,命名空间中的类默认为internal
abstract一抽象类
sealed —密封类
partial—分部类
万物之父中的方法
object是所有类的基类,使用方法时可以Object点出使用,也可以直接写方法名
静态static方法
- Equals
Equals(参数1,参数2)
判断两个对象是否相等;引用类型相等,即指向一个对象
最终的判断权,交给左侧对象的Equals方法,
不管值类型引用类型都会,按照左侧对象Equals方法的规则来进行比较
- ReferenceEquals
ReferenceEquals(参数1,参数2)
比较两个对象是否是相同的引用,主要是用来比较引用类型的对象
值类型对象返回值始终是false
成员方法/普通方法
- GetType
在反射相关知识点中是非常重要的方法,返回的Type类型。
主要作用是获取对象运行时的类型Type
结合反射相关知识点,可以做很多关于对象的操作。
- Memberwiseclone
用于获取对象的浅拷贝对象
会返回一个新的对象Object,但是新对象中的引用变量会和老对象中一致
是保护类型,需要包裹一次
class Test
{
public int i = 1;//值变量 但是在class中存在堆空间中
public Test2 t2 = new Test2();//引用类型
public Test Clone()
{
return MemberwiseClone() as Test;//Memberwiseclone是保护类型方法,需要包裹一次,才能在外部使用
}
}
class Test2
{
public int i=2;
}
Test t2 = t.Clone();
/*克隆对象后
t.i =1
t.t2.i =2
t2.i =1
t2.t2.i =2
*/
t2.i = 20;
t2.t2.i = 21;
/*改变克隆体信息后
t.i =1
t.t2.i =21
t2.i =20
t2.t2.i =21
*/
虚virtual方法
可以override重写
- Equals
默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals
微软在所有值类型的基类System.ValueType中重写了该方法,用来比较值相等
可以重写该方法,定义自己的比较相等的规则
- GetHashCode
获取对象的哈希码
一种通过算法算出的,表示对象的唯一编码
不同对象哈希码有可能一样,具体值根据哈希算法决定
可以通过重写该函数来自己定义对象的哈希码算法
正常情况下,我们使用的极少,基本不用
- ToString
返回当前对象代表的字符串
可以重写它定义我们自己的对象转字符串规则,
该方法非常常用。
当我们调用打印方法时,默认使用的就是对象的ToString方法后打印出来的内容。
String
很多方法生成的新的字符串,不会改变原来字符串
- 字符串指定位置获取
-
字符串本质是char数组 索引器
可以用
str名称[index索引]
得到单个字符 -
转为char数组
str名称点出
.ToCharArray()
方法
char[]chars = str.ToCharArray();
Console.WriteLine(chars[1]);
- 字符串拼接
- 加法+
string.Format("{0}{1}",参数0,参数1)
- 正向查找字符位置
str名称.IndexOf("要找的字符")
返回int类型,索引值
找不到,返回-1
- 反向查找指定字符串位置
str名称.LastIndexOf("要找的字符")
返回int类型,正向的索引值
找不到,返回-1
- 移除指定位置后的字符
str名称.Remove(索引值)
移除包括索引值和以后的字符
str名称.Remove(开始字符的索引值,移除字符的个数)
执行两个参数进行移除
参数1开始位置
参数2字符个数
移除包括索引值开始,指定个数的字符
- 替换指定字符串
str名称.Replace("被替换的字符串","替换后的字符串")
能找到的都替换
- 大小写转换
- 转大写
str名称.ToUpper()
- 转小写
str名称.ToLower()
- 字符串截取
- 截取从指定位置开始之后的字符串
str名称.Substring(索引值)
留下的字符,包含原索引值对应字符
- 截取从指定位置开始之后,指定个数个的字符串
str名称.Substring(索引值,截取字符的个数)
参数一开始位置
参数二指定个数
截取越界,直接报错
- 字符串切割
str名称.split('字符')
返回的是数组
str = "1_1l2_2l3_3l5_1l6_1l7_218_3";
string[]strs = str.split('l');
for (int i = 0; i< strs.Length; i++)
{
Console.WriteLine(strs[i]);
}
StringBuilder
string是特殊的引用
每次重新赋值或者拼接时,会分配新的内存空间
如果一个字符串经常改变,会非常浪费空间
StringBuilder
C#提供的一个用于处理字符串的公共类
修改字符串而不创建新的对象
需要频繁修改和拼接的字符串可以使用它,可以提升性能
1.使用前需要引用命名空间
2.初始化,直接指明内容
StringBuilder str = new StringBuilder("123123123");
Console.WriteLine(str);
- 容量
初始容量大于字符串
StringBuilder存在一个容量的问题,每次往里面增加时会自动扩容
(扩容两倍,16,32…)
获得容量str.capacity
获得字符长度str.Length
- 增
str.Append("增加的字符串");
str.AppendFormat("{0){1}",100,999);
- 插入
str.Insert(插入位置索引值,“插入的内容");
- 删
str.Remove(开始位置索引值,删除的个数);
- 清空
str.Clear();
- 查
得到索引值位的字符
str[索引值]
- 改
str[索引值] =‘字符’
- 替换
str.Replace("被替换的字符串","替换的字符串");
- 重新赋值
str.Clear();
str.Append(“123123”);
StringBuilder方法没有string多
结构体和类的区别
结构体和类最大的区别是在存储空间上的
结构体是值,类是引用
存储位置一个在栈上,一个在堆上,
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。
结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率
由于结构体不具备继承的特性,所以它不能够使用protected保护访问修饰符。
细节区别
1.结构体是值类型,类是引用类型
2.结构体存在栈中,类存在堆中
3.结构体成员不能使用protected访问修饰符,而类可以
4.结构体成员变量申明不能指定初始值,而类可以
5.结构信不能申明无参的构造函数,而类可以
6.结构体申明有参构造函数后,无参构造不会被顶掉
7.结构体不能申明析构函数,而类可以
8.结构体不能被继承,而类可以
9.结构体需要在构造函数中初始化所有成员变量,而类随意
10.结构体不能被静态static修饰(不存在静态结构体),而类可以
11.结构体不能在自己内部申明和自已一样的结构体变量,而类可以
结构体的特别之处
结构体可以继承接口,因为接口是行为的抽象
如何选择结构体和类
1.想要用继承和多态时,直接淘汰结构体,比如玩家、怪物等等
2.对象是数据集合时,优先考虑结构体,比如位置、坐标等等
3.从值类型和引用类型赋值时的区别上去考虑,比如经常被赋值传递的对象,并且,改变赋值对象,原对象不想跟着变化时,就用结构体。比如坐标、向量、旋转等等
抽象类和接口的区别
- 抽象类和抽象方法
abstract修饰的类和方法
抽象类不能实例化
抽象方法只能在抽象类中申明,是个纯虚方法,必须在子类中实现
- 接口
interface
自定义类型
是行为的抽象
不包含成员变量
仅包含方法、属性、索引器、事件,成员都不能实现,建议不写访问修饰符,默认public
相同点
1.都可以被继承
2.都不能直接实例化
3.都可以包含方法申明
4.子类必须实现未实现的方法
5.都遵循里氏替换原则
区别
1.抽象类中可以有构造函数;接口中不能
2.抽象类只能被单—继承;接口可以被继承多个
3.抽象类中可以有成员变量;接口中不能
4.抽象类中可以申明成员方法,虚方法,抽象方法,静态方法;接口中只能申明没有实现的抽象方法
5.抽象类方法可以使用访问修饰符;接口中建议不写,默认public
如何选择抽象类和接口
表示对象的用抽象类,表示行为拓展的用接口
不同对象拥有的共同行为,往往使用接口来实现
举个例子:
动物是一类对象选择抽象类;而飞翔是一个行为,选择接口
拓展
多个脚本文件
Uml类图
七大原则
-
单一职责原则
-
开闭原则
-
里氏替换原则
-
依赖倒转原则
-
接口隔离原则
-
合成复用原则
-
迪米特法则
c进阶
f12查看——英文
微软中国官网——中文学习文档
0简单数据结构类
Arraylist
一个c#为我们封装好的类
本质是一个object类型的数组
ArrayList类帮助我们实现了很多方法,比如数组的增删查改
申明:需要引用命名空间
using System.Collections;
——以下arraylist名称为array
ArrayList array = new ArrayList();
- 增
可以加任意类型
array.Add(增加的内容);
范围增加,批量增加:把另一个list容器中的内容增加到后面
array.AddRange(array2);
插入
array.Insert(位置索引值,增加的内容);
- 删
移除指定元素,从头找,找到了就移除
array.Remove(指定元素);
移除指定位置的元素
array.RemoveAt(索引值);
清空
array.Clear;
- 查
1.得到指定位置的元素
array[索引值]
2.查看元素是否存在,返回值是bool类型
array.Contains(查找的元素)
3.正向查找元素位置
找到的返回值是位置索引值,找不到返回值是-1
array.IndexOf(查找的元素)
4.反向查找元素位置
返回的是从头开始的索引数
array.LastIndexOf(查找的元素)
- 改
array[索引值]=改后的元素
- 遍历
长度:array.Count
容量:array.Capacity
避免产生过多垃圾
for (int i = 0; i < array . count; i++)
{
Console.WriteLine(array[i]);
}
迭代器遍历
foreach (object item in array)
{
Console.WriteLine(item);
}
- 装箱拆箱
ArrayList本质上是一个可以自动扩容的object数组
装箱:往其中进行值类型存储
拆箱:将值类型对象取出来转换使用
所以ArrayList尽量少用,之后会学习更好的数据容器。
但是ArrayList优点,什么都可以装
Stack
Stack(栈)是一个c#为我们封装好的类
本质也是object[]数组,只是封装了特殊的存储规则
Stack是栈存储容器
先进后出:先存入的数据后获取,后存入的数据先获取
申明:需要引用命名空间
using System.Collections
——以下使用stack为Stack变量名称
Stack stack = new Stack();
- 增
压栈push
stack.Push(增加的元素);
- 取
弹栈pop
栈中不存在删除的概念,只有取的概念
stack.Pop()
取出最后放入的元素
- 查
1.栈无法查看指定位置的元素
只能查看栈顶的内容
stack.Peek()
返回栈顶的元素
2.查看元素是否存在于栈中
stack.Contains(查看的元素)
返回bool类型
- 改
栈无法改变其中的元素,只能压(存)和弹(取)
实在要改只有清空
stack.Clear();
- 遍历
1.长度
stack. Count
2.用foreach遍历
遍历出来的顺序:从栈顶到栈底
foreach (object item in stack)
{
Console.WriteLine(item);
}
3.将栈转换为object数组 ToArray方法
遍历出来的顺序:从栈顶到栈底
object array = stack.ToArray();
for (int i = 0; i < array . Length; i++)
{
Console.WriteLine(array[i]);
}
4.循环弹栈
while( stack.Count > 0)
{
object o = stack. Pop();
Console.WriteLine(o);
}
- 装箱拆箱
由于用万物之父object来存储数据,自然存在装箱拆箱。
当往其中进行值类型存储时就是在装箱
当将值类型对象取出来转换使用时就存在拆箱
Queue
c#为我们封装好的类
本质也是object[]数组,只是封装了特殊的存储规则
Queue是队列存储容器
先进先出
先存入的数据先获取,后存入的数据后获取
申明:需要引用命名空间
using System.Collections;
——以下Queue名称为queue
Queue queue = new Queue();
- 增
queue.Enqueue(增加的元素);
- 取
队列中不存在删除的概念
只有取的概念
取出先加入的对象
queue.Dequeue()
返回先加入的元素
- 查
1.查看队列头部元素,但不会移除
queue. Peek();
返回队列头部元素
2.查看元素是否存在于队列中
queue.Contains(查找的元素)
返回bool类型
- 改
队列无法改变其中的元素,只能进出队列
实在要改只有清空
queue.Clear();
- 遍历
1.长度
queue. Count
2.用foreach遍历
foreach (object item in queue)
{
Console.WriteLine(item);
}
3.将队列转换为object数组 ToArray方法
object[] array = queue. ToArray();
for (int i = 0; i < array. Length; i++)
{
Console.WriteLine(array[i]);
}
4.循环出列
while(queue.count>0)
{
object o = queue. Dequeue();
Console.WriteLine(o);
}
- 装箱拆箱
由于用万物之父object来存储数据,自然存在装箱拆箱。
当往其中进行值类型存储时,装箱
当将值类型对象取出来转换使用时,拆箱
Hashtable
哈希表,又称散列表
是基于键的,哈希代码组织起来的,键/值对
主要作用是提高数据查询的效率
使用键,来访问集合中的元素
申明:需要引用命名空间
using System.Collections
——以下Hashtable名称为hashtable
Hashtable hashtable = new Hashtable();
- 增
hashtable.Add(键,值);
键值都是Object类型
不能出现相同键(运行时会报错)
- 删
1.只能通过键去删除
hashtable.Remove(键);
删除不存在的键没反应
3.直接清空
hashtable.Clear();
- 查
1.通过键查看值
返回 键对应值
找不到会返回空
hashtable[键]
2.查看是否存在
返回bool类型
根据键检测
hashtable.Contains(键)
hashtable.ContainsKey(键)
根据值检测
hashtable.ContainsValue(值)
- 改
只能改键对应的值,无法修改键
hashtable[键]=值;
- 遍历
0.得到键值对对数
遍历中用不到
hashtable.Count
1.遍历所有键
hashtable.Keys
foreach (object item in hashtable.Keys)
{
Console.WriteLine(“键: "+item);
Console.WriteLine(“值:"+hashtable[item]);
}
2.遍历所有值
hashtable.Values
foreach (object item in hashtable.Values)
{
Console.WriteLine("值:" + item);
)
3.键值对一起遍历
DictionaryEntry
foreach (DictionaryEntry item in hashtable)
{
Console.WriteLine("键:"+ item.Key +"值:" + item.Value);
}
4.迭代器遍历法
IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator();
bool flag = myEnumerator . MoveNext();
while (flag)
{
Console.WriteLine("键: "+ myEnumerator.Key +“值:”+ myEnumerator.Value);
flag = myEnumerator. MoveNext();//游标下移
}
- 装箱拆箱
由于用万物之父Object来存储数据,自然存在装箱拆箱
当往其中进行值类型存储时,装箱
当将值类型对象取出来转换使用时,拆箱
1泛型
实现了类型参数化,达到代码重用目的
通过类型参数化,来实现同一份代码上操作多种类型
相当于类型占位符
定义类或方法时使用替代符代表变量类型,当真正使用类或者方法时再具体指定类型
泛型占位字母可以有多个,用逗号分开
一般是大写,多用T,可以是单词,使用帕斯卡命名法
使用时,按需求类型替换泛型占位字母
-
泛型类
class 类名 <泛型占位字母>泛型类中的函数使用泛型类的占位字母,不算泛型函数
-
泛型接口
interface 接口名 <泛型占位字母>
继承的时候相当于使用,要用类型替换泛型占位字母
-
泛型函数
函数名 <泛型占位字母> (参数列表)
有无泛型占位字母,属于不同方法
1.普通类中的泛型方法
class Test2
{
//1.可以用作参数类型
public void TestFun<T>( T value)
{
Console.writeLine(value);
}
//2.可以用泛型类型 在函数内做逻辑处理
public void TestFun<T>()
{
T t.= default(T);//default方法返回类型的默认值
}
//3.可以做返回值
public T TestFun<Te(string v)
{
return default(T);
}
2.泛型类中的泛型方法
不能使用泛型类的同名占位符
泛型的作用
1.不同类型对象的,相同逻辑处理
2.一定程度,避免装箱拆箱
举例:优化ArrayList
2泛型约束
让泛型的类型有一定的限制
使用时不符合要求会报错
关键字:where
where 泛型字母:(约束的类型)
写在class <泛型占位字母> 、函数名字 <泛型占位字母> (参数列表)之后
一共有6种
1.值类型
where 泛型字母:struct
2.引用类型
where 泛型字母:class
3.存在无参公共构造函数
where 泛型字母:new()
必须是public
在类中,默认存在无参公共构造函数;当存在有参构造函数会被顶掉
在结构体中,都存在无参公共构造函数
必须是非抽象类型,不能用抽象类
4.某个类本身或者其派生类(继承关系 子类)
where 泛型字母:类名
5.某个接口的派生类型( 类,接口)
where 泛型字母:接口名
不能是接口本身(接口不能new)
6.另一个泛型类型本身或者派生类型
where 泛型字母:另一个泛型字母
泛型类型和另一个泛型类型一样
or泛型字母是另一个泛型字母的派生类型
-
约束的组合使用
加,逗号
where 泛型字母: class , new()
注意可以组合的类型,组合的顺序
-
多个泛型有约束
不要加符号,空格后接着写where
where 泛型字母1:(约束的类型1) where 泛型字母2:(约束的类型2)
3常用泛型数据结构类
List
c#为我们封装好的类
本质是一个可变类型的泛型数组
List类帮助我们实现泛型数组的增删查改
申明:需要引用命名空间
using System.Collections.Generic;
——以下list名称为list
List<类型> list = new List<类型>();
——以下以int类型为例
(与arraylist类似)
- 增
list.Add(增加的元素);
list.AddRange(批量增加的元素-比如list2);
插入
list.Insert(位置,增加的元素);
- 删
1.移除指定元素
list.Remove(移除的元素);
2.移除指定位置的元素
list.RemoveAt(索引值);
3.清空
list.Clear();
- 查
1.得到指定位置的元素
list[索引值]
2.查看元素是否存在
list.Contains(查找的元素)
返回bool类型
3.正向查找元素位置
找到返回位置,找不到返回-1
list.IndexOf(查找的元素)
4.反向查找元素位置
找到返回位置,找不到返回-1
索引值从头数下标
list.LastIndexOf(查找的元素)
- 改
list[索引值]=修改后
- 遍历
长度
list.Count
容量//避免产生垃圾
list.Capacity
for (int i = 0; i < list.count; i++)
{
Console.WriteLine(list[i]);
}
foreach (int item in list)
{
Console.WriteLine(item);
}
Dictionary
拥有泛型的Hashtable
基于键的哈希代码组织起来的,键/值对
键值对类型从Hashtable的object,变为了可以自己制定的泛型
申明:需要引用命名空间
using System.Collections.Generic
Dictionary<键类型, 值类型> dictionary名称 = new Dictionary<键类型, 值类型>();
——以下以dictionary为例,键值对为int,string类型
Dictionary<int, string> dictionary = new Dictionary<int,string>();
- 增
不能出现相同键
dictionary.Add(键,值);
- 删
1.只能通过键去删除
删除不存在的键,没反应
dictionary.Remove(键);
2.清空
dictionary.Clear();
- 查
1.通过键查值
找不到直接报错
dictionary[键]
2.查看是否存在
//根据键检测
dictionary. ContainsKey(键)
//根据值检测
dictionary. ContainsValue(值)
- 改
dictionary[键]=修改后的值
- 遍历
1.遍历所有键
foreach (int item in dictionary. Keys)
{
Console.WriteLine(item);
Console.WriteLine(dictionary[item]);
}
2.遍历所有值
foreach (string item in dictionary.Values)
{
Console.WriteLine(item);
}
3.键值对一起遍历
foreach (KeyValuePair<int,string> item in dictionary)
{
Console.WriteLine("键:" + item.Key +"值:" + item.Value);
}
顺序存储和链式存储
数据结构
计算机存储、组织数据的方式(规则)
数据结构是指相互之间存在一种或多种特定关系的数据元素的集合
比如自定义的一个类,自己定义的数据组合规则
人定义的存储数据和表示数据之间关系的规则而已
常用的数据结构:数组、栈、队列、链表、树、图、堆、散列表
线性表
一种数据结构
由n个具有相同特性的数据元素的有限序列
比如数组、ArrayList、stack、Queue、链表等等
-顺序存储和链式存储是数据结构中两种存储结构
顺序存储
用一组地址连续的存储单元依次存储线性表的各个数据元素
数组Stack、Queue、List、ArrayLis,组织规则不同
链式存储
(链接存储):用一组任意的存储单元,存储线性表中的各个数据元素
单向链表、双向链表、循环链表
简单的单向链表——方法一
class LinkedNode<T>
{
public T value;
//这个存储下一个元素是谁,相当于钩子
public LinkedNode<T> nextNode;
}
使用——
LinkedNode<int> node = new LinkedNode<int>(1);
LinkedNode<int> node2 = new LinkedNode<int>(2);
node.nextNode = node2;
node2.nextNode = new LinkedNode<int>(3);
node2.nextNode.nextNode = new LinkedNode<int>(4);
方法二:封装方法
class LindedList<T>
{
public LinkedNode<T> head;//头节点
public LinkedNode<T> last;//尾节点
//增加节点
public void Add(T value)
{
//添加节点必然是new一个新的节点
LinkedNode<T> node = new LinkedNode<T>(value);
if( head == null )
{
head= node;
last = node;
}
else
{
last.nextNode = node;
last = node;
}
}
//移除节点
public void Remove(T value)
{
if( head == null )
{
return;
}
if( head.value.Equals(value) )
{
head = head.nextNode;
//如果头节点被移除,发现头节点变空
//证明只有一个节点那尾也要清空
if( head == null )
{
last = null;
}
return;
}
LinkedNode<T> node = head;
while(node.nextNode != null)
{
if( node.nextNode.value.Equals(value))
{
//让当前找到的这个元素的上一个节点
//指向自己的下一个节点
node.nextNode = node. nextNode.nextNode;
break;
}
node = node,nextNode;
}
}
}
使用——
LinkedList<int> link = new LindedList<int>();
//增加元素
link. Add(1);
//遍历循环打印
LinkedNode<int> node = link.head;
while( node != null)
{
Console.WriteLine( node.value);
}
//移除元素
link. Remove(1);
顺序存储和链式存储的优缺点
从增删查改的角度去思考
-
增: 链式存储 计算上优于顺序存储
(中间插入时链式不用像顺序一样去移动位置)
-
删: 链式存储 计算上优于顺序存储
(中间删除时链式不用像顺序一样去移动位置)
-
查: 顺序存储 使用上优于链式存储
(数组可以直接通过下标得到元素,链式需要遍历)
-
改: 顺序存储 使用上优于链式存储
(数组可以直接通过下标得到元素,链式需要遍历)
Linkedlist
c#为我们封装好的类’
本质是一个可变类型的泛型双向链表
申明:需要引用命名空间
using System.collections.Generic
LinkedList<类型> linkedList名称 = new LinkedList<类型>();
链表对象需要掌握两个类
—个是链表本身,一个是链表节点类 LinkedListNode
- 增
1.在链表尾部添加元素
linkedList.AddLast(增加的元素);
2.在链表头部添加元素
linkedList.AddFirst(增加的元素);
3.在某—个节点之后,添加一个节点
要指定节点,先得得到一个节点
//得到一个节点
LinkedListNode<int> n = linkedList.Find(查找的元素);
//增加一个节点
linkedList.AddAfter(n得到的节点 ,增加的元素);
4.在某一个节点之前,添加一个节点
要指定节点,先得得到一个节点
//得到一个节点
LinkedListNode<int> n = linkedList.Find(查找的元素);
//增加一个节点
linkedList.AddBefore(n得到的节点 ,增加的元素);
- 删
1.移除头节点
linkedList.RemoveFirst();
2.移除尾节点
linkedList.RemoveLast();
3.移除指定节点
无法通过位置直接移除
linkedList.Remove(移除的元素);
4.清空
linkedList.Clear();
- 查
节点类 LinkedListNode
1.头节点
LinkedListNode<int> first = linkedList.First;
2.尾节点
LinkedListNode<int> last = linkedList.Last;
3.找到指定值的节点
无法直接通过下标获取中间元素,只有遍历查找指定位置元素
LinkedListNode<int> node = linkedList.Find(查找的元素);
获得节点的值
node.Value
4.判断是否存在
linkedList.Contains(查找的元素)
返回bool类型
- 改
先得到节点,再得到值,修改值
LinkedListNode<int> node = linkedList.Find(查找的元素);
node.Value = 修改的值;
- 遍历
1.foreach遍历
foreach (int item in linkedList)
{
Console.WriteLine(item);
}
2.通过节点遍历
//从头到尾
LinkedListNode<int> nowNode = linkedList.First;
while (nowNode != null)
{
Console.WriteLine( nowNode. Value);
nowNode = nowNode.Next;
}
//从尾到头
LinkedListNode<int> nowNode = linkedList.Last;
while (nowNode != null)
{
Console.WriteLine( nowNode. Value);
nowNode = nowNode.Previous;
}
泛型栈
命名空间:using System.Collections.Generic;
使用上和之前的Stack一模一样
Stack<类型> stack = new Stack<类型>();
队列
命名空间:using System.Collections.Generic;
使用上和之前的Queue一模一样
Queue<类型> gueue = new Queue<类型>();
数据结构总结——
-
变量
- 无符号
byte ushort uiht ulong - 有符号
sbyte short int long - 浮点数
float double decimal - 特殊
char bool string
- 无符号
-
复杂数据容器
枚举enum
结构体struct
数组(一维、二维、交错) [] [,] [][]
类
-
数据集合
命名空间using System.Collections;
数据类型Object 装箱拆箱
-
ArrayList
object数据列表
-
Stack栈
先进后出
-
Queue队列
先进先出
-
Hashtable哈希表
键值对
-
-
泛型数据集合
命名空间using System.Collections.Generic;
- List列表 泛型队列
- Dictionary字典 泛型哈希表
- LinkedList 双向链表
- Statck泛型栈
- Queue泛型队列
1委托
函数(方法)的容器
表示函数(方法)的变量类型
用来存储、传递函数(方法)
本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)
不同的函数(方法)必须对应,和各自"格式"—致的委托
-
定义委托
-
装载函数
-
调用委托
申明
关键字:delegate
访问修饰符 delegate 返回值 委托名 (参数列表);
可以申明在namespace和class语句块中,更多的写在namespace中
访问修饰默认不写为public,在别的命名空间中也能便用,private其它命名空间就不能用了。一般使用public
委托不能重名(同一语句块中)
定义自定义委托
无参无返回值函数的容器
delegate void MyFun();
有参有返回值函数的容器
delegate int MyFun2(int a);
可以用泛型
delegate T MyFun2<T>(T t);
使用定义好的委托
委托变量是函数的容器
相当于参数类型
- 装载函数
委托名 名称 = new 委托名(函数名称);
委托名 名称 = 函数名称;
- 调用委托——调用函数
名称.Invoke(参数);
名称(参数);
1.作为类的成员
委托名 名称 ;
2.作为函数的参数
返回值类型 函数名(委托名称 变量名称) {}
使用时候传递函数名称
委托变量可以存储多个函数(多播委托)
增+(+=)
删-(-=)
多删不会报错,没有反应
空null
开始可以赋值null
可以多次存相同的函数,调用多次函数
系统定义好的委托
要引用命名空间using system;
-
Action无参无返回值
-
Action<类型1,类型2…>可以传n个参数,最多16个
-
Func泛型返回值
-
Func<类型1,类型2…T>可以传n个参数,最多16个;返回值写在后面
2事件
事件是基于委托的存在
事件是委托的安全包裹,让委托的使用更具有安全性
一种特殊的变量类型
申明语法:
访问修饰符 event 委托类型 事件名;
事件的使用:委托怎么用,事件就怎么用
事件相对于委托的区别:
在类外部,不能赋值=,不能直接等于null,不能调用;但是可以加减+=-=
只能作为成员,存在于类和接口以及结构体中;不能作为函数内临时变量
class Test
{
//委托成员变量用于存储函数的
public Action myFun;
//事件成员变量用于存储函数的
public event Action myEvent;
public Test()
{
//事件的使用和委托一模一样,只是有些细微的区别
myEvent = TestFun;//装载函数
myEvent += TestFun;//增
myEvent -= TestFun;//删
//使用函数
myEvent();
myEvent.Invoke();
myEvent = null;
}
}
事件的作用:
1.防止外部随意置空委托
2.防止外部随意调用委托
3.相当于对委托进行了一次封装,让其更加安全
3匿名函致
没有名字的函数
配合委托和事件进行使用
1.函数中传递委托参数时
2.委托或事件赋值时
delegate (参数列表)
{
//函数逻辑
};
赋值时要写分号,传递时不需要
-
申明匿名函数
-
调用匿名函数
匿名函数的使用:
1.无参无返回
Action a = delegate ()//必须要用委托容器装
{
//匿名函数逻辑
};
a();//调用
2.有参无返回
Action<参数类型> b = delegate (参数类型 参数名称)//必须要用委托容器装
{
//匿名函数逻辑
};
b(参数);//调用
3.无参有返回值
Func<返回值类型> c = delegate ()//必须要用委托容器装
{
//匿名函数逻辑
return 返回值;
};
//c()//调用 返回值
4.作为函数参数传递 | 作为函数返回值
//申明匿名函数
class Test
{
public Action action;
//作为参数传递时
public void Dosomthing(int a,Action fun)
{
Console.writeLine(a);
fun();
}
//作为返回值
public Action GetFun()
{
return delegate(){
Console.WriteLine("函数内部返回的一个匿名函数逻辑");
};
}
}
//调用匿名函数
//参数传递1
Test t = new Test();
Action ac = delegate()//先用委托装
{
Console.WriteLine("随参数传入的匿名函数逻辑");
};
t.Dosomthing(100,ac);
//参数传递2 合并简写
t.Dosomthing(100,delegate()
{
Console.WriteLine("随参数传入的匿名函数逻辑");
});
//返回值1
Action ac2 = t.GetFun();
ac2();
//返回值2 合并简写
t.GetFun()();//先调用返回值,再用返回的函数
匿名函数的缺点:
添加到委托或事件容器中后,不记录,无法单独移除
没有名字,不能指定删除,不能看逻辑一样看作一个
4Lambad表达式
可以理解为 匿名函数的简写
使用上和匿名函数一模一样
和委托或者事件配合使用
//匿名函数
delegate(参数列表){};
//lambad表达式
(参数列表)=>
{
//函数体
};
1.无参无返回
Action a = ()=>
{
//无参无返回值的lambad表达式
};
a();
2.有参
Action<int> a2 = (int value)=>
//参数类型可以省略,参数类型和委托或事件容器一致
//Action<int> a2 = (value)=>
{
//有参无返回值的lambad表达式
};
a2(100);
3.有返回值
Func<string,int> a3 = (value)=>
//参数类型可以省略,参数类型和委托或事件容器一致
//参数类型//返回值类型
{
//有参有返回值的lambad表达式
return 返回值;
};
其它传参使用等、缺点也是和匿名承数—样
闭包
内层的函数,可以引用包含在它外层的函数的变量
即使外层函数的执行已经终止
该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
class Test
{
public event Action action;
public Test()
{
int value = 10;
//当构造函数执行完毕,其中申明的临时变量value的生命周期被改变了
action= ()=>
{
Console.WriteLine(value);
};
for (int i=0; i < 10; i++)//临时变量i最终值为10,进不去循环
{
action += ()=>
{
Console.writeLine(i);//打印出来全是i的最终值 10
};
}
for (int i=0; i < 10; i++)
{
int index = i;//每次进入循环是新的临时变量 此index非彼index
action += ()=>
{
Console.writeLine(index);//打印0~9
};
}
}
}
List排序
List自带sort排序方法
ArrayList中也有Sort排序方法
list.Sort();
默认升序排列
自带的变量类型可以使用
自定义类的排序
需要继承接口
-
泛型接口
Icomparable<T>
-
非泛型接口
Icomparable
class Item : Icomparable<Item>//继承比较的泛型接口
{
public int money;
public Item(int money)//构造函数
{
this .money = money;
}
//比较方法
public int CompareTo(Item other)//接口中的方法
{
//返回值的含义
//小于0:
//放在传入对象的前面
//等于0:
//保持当前的位置不变
//大于0:
//放在传入对象的后面
//可以简单理解传入对象的位置就是
//如果你的返回为负数,就放在它的左边,也就前面
//如果你返回正数,就放在它的右边,也就是后面
//按照money的值 升序排列
if( this.money > other.money )
{
return 1;
}
else
{
return -1;
}
}
}
使用时候,点出sort方法
通过委托函数进行排序
在sort中传入委托函数
//在sort中传入委托函数
shopItems.Sort( SortShopItem);
static int SortshopItem( ShopItem a,ShopItem b )
{
//传入的两个对象,为列表中的两个对象
//进行两两的比较,用左边的和右边的条件比较
//返回值规则和之前一样
//0做标准,负数在左(前),正数在右(后)
if (a.id > b.id)
{
return 1;
}
else
{
return -1;
}
}
//匿名函数
shopitems.Sort(delegate (shopItem a, shopItem b)
{
if (a.id > b.id)
{
return -1;
}
else
{
return 1;
}
});
//lambad表达式,配合三目运算符
shopItems.Sort((a,b)=>{ return a.id > b.id ? 1 : -1;});
协变逆变
协变:
和谐的变化,自然的变化
因为里氏替换原则,父类可以装子类,所以子类变父类
比如string变成 object,感受是和谐的
逆变:
逆常规的变化,不正常的变化
因为里氏替换原则,父类可以装子类,但是子类不能装父类,所以父类变子类
比如object变成string,感受是不和谐的
-
协变:out
-
逆变:in
用于在泛型中,修饰泛型字母
只有泛型接口和泛型委托能使用
//用out修饰的泛型,只能作为返回值
delegate T Testout<out T>();
//用in修饰的泛型,只能作为参数
delegate void TestIn<in T>(T t);
结合里氏替换原则:
//协变:父类泛型委托 装子类泛型委托
//父类总是能被子类替换
//看起来就是son-> father
TestOut<Son> os = ()=>
{
return new Son();
};
TestOut<Father> of = os;
Father f = of();//实际上返回的是os里面装的函数,返回的是son
//逆变:子类泛型委托 装父类泛型委托
//父类总是能被子类替换
//看起来像是father->son
//明明是传父类,但是你传子类,不和谐的
TestIn<Father>iF = (value) =>
{
};
TestIn<Son> iS = iF;
iS(new Son());//实际上调用的是iF
多线程
进程(Process)
计算机中的程序关于某数据集合上的一次运行活动
是系统进行资源分配和调度的基本单位,是操作系统结构的基础
打开一个应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立运行,互不干扰
进程之间也可以相互访问、操作
操作系统
电脑操作系统:Windows、MacOS、Unix、Linux
手机操作系统:Android、lOS、HarmonyOS
线程
操作系统能够进行运算调度的最小单位。
被包含在进程之中,是进程中的实际运作单位
—条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
我们目前写的程序都在主线程中
线程就是代码从上到下运行的一条“管道”
线程之间可以相互独立运行,互不干扰
多线程
可以通过代码,开启新的线程
可以同时运行代码的多条““管道”,就叫多线程
语法:
线程类 Thread
需要引用命名空间using system.Threading;
- 申明一个新的线程
线程执行的代码,需要封装到一个函数中
Thread t = new Thread(封装线程的函数);
- 启动线程
t.Start();
- 设置为后台线程
当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
后台线程不会防止应用程序的进程被终止掉
如果不设置为后台线程,可能导致进程无法正常关闭
t.IsBackground = true;
- 关闭释放一个线程
如果开启的线程中,不是死循环,是能够结束的逻辑,那么不用刻意的去关闭它
t = null;
如果是死循环,想要中止这个线程有两种方式
4.1 -死循环中bool标识
static bool isRuning = true;//bool标识
Console.ReadKey();//按键后,执行后面代码;;——停止开启的其他线程
isRuning = false;//中止
Console.ReadKey();//按键后,执行后面代码;;停止主线程
static void NewThreadLogic()
{
//新开线程,执行的代码逻辑,在该函数语句块中
while(isRuning)//bool标识
{
Console.writeLine("新开线程代码逻辑");
}
}
4.2 -通过线程提供的方法Abort
注意:在.Net core版本中无法中止会报错——用try避免报错
try
{
t.Abort();
t = null;
}
catch
{
}
- 线程休眠
让线程休眠多少毫秒,1s = 1000毫秒
在哪个线程里执行,就休眠哪个线程
Thread.sleep( 1000);
线程之间共享数据
多个线程使用的内存是共享的,都属于该应用程序(进程)
当多线程,同时操作同一片内存区域时,可能会出问题
可以通过加锁的形式避免问题
lock(引用类型){
}
多线程的意义
专门处理一些复杂耗时的逻辑
比如寻路、网络通信等等
预处理器指令
编译器
一种翻译程序
用于将源语言程序翻译为目标语言程序
源语言程序:某种程序设计语言写成的,比如C#、C、C++、Java等语言写的程序
目标语言程序:二进制数表示的伪机器代码写的程序
预处理器指令
指导编译器,在实际编译开始之前,对信息进行预处理
以#开始
预处理器指令不是语句,不以分号;结束
常见的预处理器指令
#define
定义一个符号,类似一个没有值的变量
#undef
取消define定义的符号,让其失效
两者都是写在脚本文件最前面
一般配合if指令使用或配合特性
#if
#elif
#else
#endif
和if语句规则一样
一般配合#define定义的符号使用
用于告诉编译器,进行编译代码的流程控制
#define Unity4
#undef Unity4
//如果发现有unity4这个符号,那么其中包含的代码,就会被编译器翻译
//可以通过逻辑或和逻辑与,进行多种符号的组合判断
#if Unity4
Console.WriteLine("版本为unity4");
#elif Unity2017 && IoS
console.WriteLine("版本为Unity2017");
#else
Console.WriteLine("其它版本");
#warning 这个版本,不合法
#error 这个版本,不能运行
#endif
#warning
#error
告诉编译,是报警告,还是报错误
配合if使用
反射
程序集
经由编译器编译得到的,供进一步编译执行的,那个中间产物
WINDOWS系统中,后缀为dll(代码库文件)或者是 exe(可执行文件)的格式
程序集就是我们写的一个代码集合:我们现在写的所有代码,最终都会被编译器翻译为一个程序集供别人使用
元数据
用来描述数据的数据
程序中的类,类中的函数、变量等等信息,就是程序的元数据
有关程序以及类型的数据被称为元数据,它们保存在程序集中
反射
程序正在运行时,可以查看其它程序集或者自身的元数据。
一个运行的程序,查看本身或者其它程序的元数据的行为,就叫做反射
程序运行时通过反射,可以得到其它程序集或者自己程序集代码的各种信息类,函数,变量,对象等等,实例化它们,执行它们,操作它们
反射的作用:
因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
1.程序运行时得到所有元数据,包括元数据的特性
2.程序运行时,实例化对象,操作对象
3.程序运行时创建新对象,用这些对象执行任务
Type
——类的信息类
需要引用反射的命名空间
反射功能的基础
访问元数据的主要方式
使用 Type的成员,获取有关类型声明的信息
有关类型的成员(如构造函数、方法、字段、属性和类的事件)
获取Type
三种方法指向的堆内存一致
每一个类型的type是唯一的
- 万物之父object中的GetType()
Type type1= 变量名a.GetType();
- 通过typeof关键字传入类名
Type type2= typeof(类型变量int);
- 通过类的名字
类名必须包含命名空间,不然找不到
Type type3= Type.GetType("System.Int32");
得到类的程序集信息
type.Assembly
获取类中的所有公共成员
首先获取Type
然后得到所有公共成员(数组)
需要引用命名空间using System.Reflection;
MemberInfo[] infos名称 = t.GetMembers();
MemberInfo
类,GetMembers
方法
//以下Test是自定义的类
//一般用于得到别的程序集信息,即无法直接new的
//首先得到Type
Type t = typeof(Test);
//然后得到所有公共成员
//需要引用命名空间using system.Reflection;
MemberInfo[] infos = t.GetMembers();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine(infos[i]);
}
获取类的公共构造函数并调用
获取所有构造函数
获取其中一个构造函数,执行函数
ConstructorInfo
类,GetConstructors
方法
//1.获取所有构造函数
ConstructorInfo[] ctors = t.GetConstructors();
for (int i = 0; i < ctors.Length; i++)
{
Console.WriteLine(ctors[i]);
}
//2.获取其中一个构造函数并执行
//得构造函数,传入Type数组,数组中内容按顺序是,参数类型
//执行构造函数,传入 object数组,表示按顺序传入的参数
//2-1得到无参构造
ConstructorInfo info1 = t.GetConstructor(new Type[0]);//没有参数
//执行无参构造Invoke
//无参构造没有参数,传null
Test obj = info1.Invoke(null) as Test;//Object类型里氏转换
Console.WriteLine(obj.j);
//2-2得到有参构造
ConstructorInfo info2 = t.GetConstructor(new Type[] { typeof(int)});//有一个参数是int类型的
//执行有参构造
obj = info2. Invoke(new object[] { 2 })as Test;
Console.WriteLine(obj.str);
ConstructorInfo info3 = t.GetConstructor(new Type[] { typeof(int),typeof(string)});//和函数中参数类型顺序一致
//执行有参构造
obj = info3. Invoke(new object[] { 2 ,"ssss" })as Test;
Console.WriteLine(obj.str);
获取类的公共成员变量
FieldInfo
类,GetField(s)
方法
得到所有成员变量
得到指定名称的公共成员变量
通过反射GetValue
获取和SetValue
设置对象的值
//1.得到所有成员变量
FieldInfo[] fieidInfos = t.GetFields();
for (int i = 0; i < fieldInfos.Length; i++)
{
Console.WriteLine(fieldInfos[i]);
}
//2.得到指定名称的公共成员变量
FieldInfo infoJ = t.GetField("公共成员变量");
//3.通过反射获取和设置对象的值
Test test = new Test();
test.j = 99;
test.str = "2222";
//3-1通过反射获取对象的某个变量的值
Console.WriteLine(infoJ.GetValue(test));
//3-2通过反射设置指定对象的某个亦量的值
infoJ.SetValue(test,100); //对象,赋的值
获取类的公共成员方法
获取方法
MethodInfo
类,GetMethod(s)
方法
Invoke调用方法
//获得string类的方法
//通过Type类中的GetMethod方法得到类中的方法
// MethodInfo是方法的反射信息
Type strType = typeof(string);
//1.获取方法
//1.1获取所有方法
//如果存在方法重载,用Type数组表示参数类型
MethodInfo[] methods = strType. GetMethods();//得到所有方法
for (int i = 0; i < methods.Length; i++)
{
Console.WriteLine(methods[i]);
}
//1.2获取指定方法
MethodInfo substr = strType. GetMethod("Substring" ,
new Type[]{ typeof(int), typeof(int)} );//方法名称,参数类型
//2.调用该方法
//注意:如果是静态方法,Invoke中的第一个参数传null即可
string str = "Hello,world! ";
//第一个参数相当于是,哪个对象要执行这个成员方法
object result = subStr.Invoke(str,new object[] { 7,5 });//对象,参数
其他
-
得枚举
GetEnumName / GetEnumNames
-
得事件
GetEvent / GetEvents
-
得接口
GetInterface / GetInterfaces
-
得属性
GetProperty / GetPropertys
等等
Assembly
——程序集类
需要引用反射的命名空间
主要用来加载其它程序集,加载后才能用Type,来使用其它程序集中的信息
如果想要使用不是自己程序集中的内容
简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类
三种加载程序集的函数:
一般用来加载,在同一文件下的其它程序集
Assembly asembly2 = Assembly . Load(“程序集名称"");
一般用来加载,不在同一文件下的其它程序集
Assembly asembly = Assembly.LoadFrom(“包含程序集清单的文件的名称或路径");
Assembly asembly3 = Assembly.LoadFile("要加载的文件的完全限定路径");
路径中存在斜杠转义字符,要使用双斜杠\\
,或者@
全部取消转义
1.先加载一个,指定程序集
2.再加载,程序集中的一个类对象,之后才能使用反射
Activator
——用于快速实例化对象的类
用于将Type对象,快捷实例化为对象
先得到Type,然后快速实例化一个对象
CreateInstance
方法
1.无参构造
2.有参数构造
Type testType = typeof(Test);
//1.无参构造
Test testObj = Activator.CreateInstance(testType) as Test;
Console.WriteLine(testObj.str);
//2.有参数构造
testObj = Activator.createInstance(testType,99) as Test;
Console.WriteLine(testObj.j);
testObj = Activator.CreateInstance(testType,55,"111222") as Test;//对象,多个参数要一致
Console.WriteLine(testObj.j);
反射应用示例:
1.先加载一个指定程序集 Assembly
2.得到程序集中的数据 GetType
得到程序集中的成员 GetFie1d
实例化对象 CreateInstance
得到程序集中的方法 GetMethod
3.调用程序集的方法 Invoke
//1.先加载一个指定程序集
Assembly asembly = Assembly.LoadFrom(@"c: \Users \Desktop\CSharp进阶教学\Lesson18_练习题\bin\Debu\netcoreapp3.1\");//路径名称+文件名称
Type[] types = asembly. GetTypes();//得到程序集中所有数据
for (int i = 0; i < types. Length; i++)
{
Console.WriteLine(types[i]);//不清楚别的程序集中的内容,所以打印查看
}
//2.再加载程序集中的一个类对象之后,才能使用反射
Type icon = asembly. GetType( "Lesson18_练习题.Icon");//命名空间.类名
MemberInfo[] members = icon. GetMembers();
for (int i = 0; i < members.Length; i++)
{
Console.WriteLine(members[i]);//不清楚别的程序集中的内容,所以打印查看
}
//通过反射,实例化一个icon对象
//<<枚举:得到type——得到成员GetFie1d——得到值GetValue>>
//首先得到枚举Type,来得到可以传入的参数(此处用的程序集构造函数最后有枚举参数)
Type moveDir = asembly.GetType( "Lesson18_练习题.E_MoveDir");
Fie1dInfo right = moveDir.GetFie1d( "Right");//得到成员变量 通过枚举得到成员 得到的是反射信息,不是具体的值
//直接实例化对象
object iconobj = Activator.CreateInstance(icon,10,5,right.GetValue(null));//用Object装//GetValue没有对象,用null,得到值
//通过反射,得到对象中的方法
MethodInfo move = icon.GetMethod("Move");
MethodInfo draw = icon.GetMethod( "Draw");
MethodInfo clear= icon.GetNethod( "Clear");
while(true)
{
Thread.Sleep(1000);
//谁调用的对象,传的参数
//调用其他程序集的方法
clear.Invoke(iconobj,null);//清除
move.Invoke(iconobj, nul1);//移动
draw.Invoke( iconobj, nul1);//绘画
}
类库工程创建
库ddl
创建项目——选择c#类库(.NET Framework)
类库:纯写逻辑算法的工程,不控制台打印,不能运行
——点击生成——生成ddl文件 在外部资源管理器中
》之后可以运用反射来使用
特性
一种允许我们向程序的程序集添加元数据的语言结构
用于保存程序结构信息的某种特殊类型的类
特性提供功能强大的方法,以将声明信息与C#代码(类型、方法、属性等)相关联。特性与程序实体关联后,即可在运行时,使用反射查询特性信息
目的是告诉编译器,把程序结构的某组元数据,嵌入程序集中
它可以放置在几乎所有的声明中(类、变量、函数等等申明)
特性本质是个类
利用特性类为元数据添加额外信息
比如一个类、成员变量、成员方法等等,为他们添加更多的额外信息,之后可以通过反射来获取这些额外信息
自定义特性
继承 特性基类 Attribute
命名:基础名称+Attribute
(使用时默认省略)
特性的使用
[特性名(参数列表)]
本质上就是,在调用特性类的构造函数(参数列表和构造函数一样)
写在类、函数、变量上一行,表示他们具有该特性信息
可以写多个
通过反射使用
0.需要先gettype反射
MyClass mc = new Myclass();
Type t = mc.GetType();
1.IsDefined
方法:判断是否使用了某个特性
参数一:特性的类型
参数二:代表是否搜索继承链(属性和事件忽略此参数)
t.IsDefined(typeof(MycustomAttribute), false)
2.GetCustomAttributes
方法:获取Type元数据中的所有特性
参数:代表是否搜索继承链
object[]array = t.GetCustomAttributes(true);
for (int i = 0; i < array. Length; i++)//可以存在多个特性,所以是数组
{
if( array[i] is MyCustomAttribute )
{
Console.WriteLine((array[i] as MyCustomAttribute).infos);//得到特性的参数 这里的infos是自定义特性的参数
(array[i] as MyCustomAttribute).TestFun();//使用特性的方法这里的TestFun是自定义特性内的方法
}
}
限制自定义特性的使用范围
通过为特性类加特性,限制其使用范围
[AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct,AllowMultiple = true,Inherited = true)]
中间的|是位或
参数一:AttributeTargets
——特性能够用在哪些地方,可以选择后面的成员:
class类,field变量,struct结构体。。。
参数二:AllowMultiple
——是否允许多个特性实例用在同一个目标上,bool值
参数三:Inherited
——特性是否能被派生类和重写成员继承,bool值
系统自带特性
- 过时特性
[Obsolete("提示内容",false)]
用于提示用户,使用的方法等成员,已经过时,建议使用新方法
—般加在函数前的特性
参数一:调用过时方法时,提示的内容
参数二:bool值,true——使用该方法时会报错,false——使用该方法时直接警告
- 调用者信息特性
哪个文件调用?
CallerFilePath
特性
哪一行调用?
CallerLineNumber
特性
哪个函数调用?
CallerMemberName
特性
需要引用命名空间 using System.Runtime.CompilerServices;
一般作为函数参数的特性,一般设置参数的默认值
一般用在trycatch的catch中,用于得到异常捕获信息
public void Speakcaller(string str,
[callerFilePath]string fileName = "",
[callerLineNumber]int line =0,
[callerMemberName]string tanget = "")
{
}
- 条件编译特性
[Conditional("define的内容")]
和预处理指令#define配合使用
当define的内容存在时,能执行用特性修饰的内容(方法)
需要引用命名空间using System.Diagnostics;
主要可以用在学调试代码上
有时想执行有时不想执行的代码
- 外部Dll包函数特性
[DllImport("路径文件名称.dll")]
用来标记非 .Net(C#) 的函数
表明该函数在一个外部的DLL中定义。
一般用来调用C或者C++的Dll包写好的方法
需要引用命名空间using system.Runtime.InteropServices
迭代器
迭代器(iterator),又称光标(cursor)
程序设计的软件设计模式
迭代器模式提供一个方法,顺序访问一个聚合对象中的各个元素,而又不暴露其内部的标识
在表现效果上看,是可以在容器对象(例如链表或数组)上,遍历访问的接口
设计人员无需关心容器对象的内存分配的实现细节
可以用foreach遍历的类,都是实现了迭代器的
标准迭代器的实现方法
命名空间:using System.Collections;
关键接口:IEnumerator
, IEnumerable
IEnumerable
接口——GetEnumerator
方法
IEnumerable
接口——Current,Movenext,Reset
方法(光标默认-1)
可以通过同时继承IEnumerable
和IEnumerator
实现其中的方法
可以不继承IEnumerable
接口,必须要有public IEnumerator GetEnumerator()
方法;写接口方便直接调用方法
foreach 本质:
1.先获取in后面这个对象的 IEnumerator
会调用对象其中的GetEnumerator方法来获取
2.执行得到这个IEnumerator对象中的 MoveNext方法
3.只要MoveNext方法的返回值时true,就会去得到current,然后复制给item
class customList : IEnumerable,IEnumerator //继承接口
{
private int[] list;
//从-1开始的光标,用于表示数据得到了哪个位置
private int position = -1;
public CustomList()//构造函数
{
list = new int[] { 1,2,3,4,5,6,7,8 };//遍历是数据
}
/*IEnumerable GetEnumerator*/
public IEnumerator GetEnumerator()
{
Reset();//重置光标位置
return this;
}
/*IEnumerator Current*/
public object Current
{
get
{
return list[position];//得到光标位置
}
}
/*IEnumerator MoveNext*/
public bool MoveNext()
{
//移动光标
++position;
//是否溢出 溢出就不合法
return position < list.Length;
}
/*IEnumerator Reset*/
//重置光标位置,一般写在获取IEnumerator对象这个函数中
public void Reset()
{
position = -1;
}
}
用yield return语法糖实现迭代器
yield return
是c#提供给我们的语法糖
语法糖,也称糖衣语法
主要作用就是将复杂逻辑简单化,可以增加程序的可读性,从而减少程序代码出错的机会
关键接口:IEnumerable
命名空间:using System.Collections;
让想要通过foreach遍历的自定义类,实现接口中的方法GetEnumerator
即可
class customList : IEnumerable //继承一个接口
{
private int[] list;
public CustomList()//构造函数
{
list = new int[] { 1,2,3,4,5,6,7,8 };//遍历是数据
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < list.Length; i++)
{
//yield关键字配合迭代器使用
//可以理解为暂时返回保留当前的状态,一会还会在回来
yield return list[i];//实际自动生成规则
}
}
}
用yield return语法糖为泛型类实现迭代器
class CustomList<T> : IEnumerable//泛型类 继承一个接口
{
private T[] array;
public CustomList(params T[]array)//构造函数
{
this. array = array;
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < array. Length; i++)
{
yield return array[i];
}
}
}
特殊语法
var隐式类型
var,一种特殊的变量类型,可以用来表示任意类型的变量
根据初始化赋值,决定类型
可以用于不确定类型
1.var不能作为类的成员,只能用于临时变量申明时,一般写在函数语句块中
2.var必须初始化
匿名类型
var变量可以申明为自定义的匿名类型
类似于一个类,可以点出成员
只能有成员变量,不能有函数
var v = new { age = 10,money = 11, name ="小明"};
设置对象初始值
申明对象时,通过直接写大括号{}的形式,初始化公共成员变量和属性
相当于省略写法,实际先执行构造函数,再初始化值
可以有选择的初始化公共成员变量和属性
如果是无参构造,括号()可以省略
Person p = new Person { sex = true,Age = 18,Name ="唐老狮"};
Person p = new Person () { sex = true,Age = 18,Name ="唐老狮"};
Person p = new Person (100) { sex = true,Age = 18,Name ="唐老狮"};
设置集合初始值
申明集合对象时,通过大括号{},直接初始化内部属性
int[] array2 = new int[] { 1,2,3,4,5 };
List<int> listInt = new List<int>() { 1,2,3,4,5 };//括号()可以省略
List<Person> listPerson = new List<Person>() {
new Person ( 200),
new Person( 100){Age = 10},
new Person( 1){sex = true, name =“唐老狮"}
};
Dictionary<int,string> dic = new Dictionary<int,string>()
{
{1,"123"},
{2,"222"}
};
可空类型?
- 值类型:
1.值类型是不能赋值为空的
//int c = null;
2.申明时在 值类型 后面加?,可以赋值为空
int? c = 3;
3.判断是否为空
if( c.HasValue )//c为null返回false,有值返回true
{
//两种都可以获取c的值
Console.WriteLine(c);
Console.WriteLine(c.value);
}
4.安全获取可空类型值
GetValueOrDefault
方法的可选参数:如果为null,默认返回的值
如果为空,默认返回值类型的默认值
c.GetValueOrDefault()
如果为空,返回指定一个默认值
c.GetValueOrDefault(100)
不会给参数实际赋值
- 引用类型?:
如果是null,不会执行,也不会报错
可以简化写法
object o = null;
//1普通写法
if( o != null )
{
Console.WriteLine(o.Tostring());
}
//2使用?写法
//相当于是一种语法糖,能够帮助我们,自动去判断o是否为空
//如果是null,不会执行tostring,也不会报错
Console.WriteLine(o?.ToString());
int[] arrryInt = null;
Console.WriteLine(arrryInt?[0]);
Action action = null;
//1普通写法
if (action != null)
{
action();
}
//2使用?简化写法
action?.Invoke();
空合并操作符??
左边值 ?? 右边值
如果左边值为null,返回右边值;否则返回左边值
只要是可以为null的类型都能用
相当于三目运算符简化写法
//值类型为空 需要加?
int? intV = null;
//三目运算符?:
int intI = intv == null ? 100 : intv.value;
//空合并操作符??
intI = intv ?? 100;
//引用类型
string str = null;
str = str ?? "hahah";
Console.writeLine(str);
内插字符串$
关键符号:$
让字符串中可以拼接变量 {变量}
string name =“唐老狮";
Console.WriteLine($"好好学习,{name}");
单句逻辑简略写法
如果语句块内只有一句代码,可以省略大括号{},或者是省略为=>
//if判断
if (true)
Console.WriteLine( "123123");
//for循环
for (int i= 0; i < 10; i++)
Console.WriteLine(i);
//类中的属性
public string Name
{
get => "唐老狮"; //省略为=> 不可以写return
set => sex = true;
}
//函数
public int Add(int x, int y)=>x+y;
public void Speak(string str) =>Console.WriteLine(str);
值和引用类型
- 值类型
无符号: byte,ushort,uint,ulong
有符号:sbyte,short,int,long
浮点数:float, double, decimal
特殊:char, bool
枚举: enum
结构体:struct
- 引用类型
string
数组
class
interface
委托
值类型和引用类型的本质区别:
值的具体内容存在栈内存上
引用的具体内容存在堆内存上
如何判断值类型和引用类型?
F12进到类型的内部去查看//或者右键转到定义
是class就是引用
是struct就是值
语句块
1.命名空间
2.类、接口、结构体
3.函数、属性、索引器、运算符重载等(类、接口、结构体)
4.条件分支、循环
上层语句块:类、结构体
中层语句块:函数
底层的语句块:条件分支循环等
- 逻辑代码写在哪里?
函数、条件分支、循环-中底层语句块中
- 变量可以申明在哪里?
上、中、底都能申明变量
上层语句块中:成员变量
中、底层语句块中:临时变量
变量的生命周期
编程时大部分都是 临时变量
在中底层申明的临时变量(函数、条件分支、循环语句块等)
语句块执行结束,没有被记录的对象将被回收或变成垃圾
值类型:被系统自动回收(栈:先进后出,弹栈)(语句块结束就会释放)
引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾
想要不被回收或者不变垃圾,必须将其记录下来:
在更高层级记录,或者使用静态全局变量记录
结构体中的值和引用
结构体本身是值类型
前提:该结构体没有做为其它类的成员
-
在结构体中的值,栈中存储值具体的内容
-
在结构体中的引用,堆中存储引用具体的内容
引用类型始终存储在堆中
真正通过结构体,使用其中引用类型时,只是顺藤摸瓜
类中的值和引用
类本身是引用类型
-
在类中的值,堆中存储具体的值
-
在类中的引用,堆中存储具体的值
值类型跟着大哥走好,引用类型一根筋
数组中的存储规则
数组本身是引用类型
-
值类型数组,堆中房间存具体内容
-
引用类型数组,堆中房间存地址(引用包裹引用)
结构体继承接口
利用里氏替换原则,用接口容器装载结构体,存在装箱拆箱
interface ITest
{
int value
{
get;
set;
}
}
struct TestStruct : ITest //结构体继承接口
{
int value;
public int Value
{
get
{
return value;
}
set
{
this.value = value;
}
}
}
Teststruct obj1 =new Teststruct();//结构体:值类型
obj1.value = 1;
Teststruct obj2 = obj1;
obj2.Value = 2;//值类型 存在栈中 不影响
Console.WriteLine(obj1.Value);
Console.writeLine(obj2.Value);
ITest iobj1 = obj1;//用接口容器装载结构体(父类装子类) 装箱value 1 值变成引用类型
ITest iobj2 = iobj1;
iobj2.Value = 99;//引用类型 改2会影响1
Console.writeLine(iobj1.value);
Console.writeLine(iobj2.value);
Teststruct obj3 = (Teststruct)iobj1;//拆箱
排序进阶
插入排序
两个区域,排序区,未排序区
用一个索引值做分水岭
未排序区元素,与排序区元素比较,插入到合适位置,直到未排序区清空
排序开始前,首先认为第一个元素在排序区中,其它所有元素在未排序区中
排序开始后,每次将未排序区第一个元素取出,用于和排序区中元素比较(从后往前)
满足条件(较大或者较小),则排序区中元素往后移动一个位置。
所有数字都在一个数组中
所谓的两个区域,是一个分水岭索引
//第1步
//能取出未排序区的所有元素进行比较
//i=1的原因:默认第一个元素就在排序区
for (int i = 1; i < arr.Length; i++)
{
//第2步
//每一轮
//1.取出排序区的最后一个元素索引
int sortIndex = i - 1;
//2.取出未排序区的第一个元素
int noSortNum = arr[i];
//第3步
//在未排序区进行比较
//移动位置
//确定插入索引
//循环停止的条件:
//1.发现排序区中所有元素都已经比较完
//2.发现排序区中的元素不满足比较条件了
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
//只要进了这个while循环证明满足条件
//排序区中的元素就应该往后退一格
arr[sortIndex + 1] = arr[sortIndex];
//移动到排序区的前一个位置准备继续比较
--sortIndex;
}
//最终插入数字
//循环中知识在确定位置 和找最终的插入位置
//最终插入对应位置应该循环结束后
arr[ sortIndex + 1] = noSortNum;
}
- 为什么有两层循环
第一层循环:一次取出未排序区的元素进行排序
第二层循环:找到想要插入的位置
- 为什么第一层循环从1开始遍历
插入排序的关键是分两个区域,已排序区和未排序区
默认第一个元素在已排序区
- 为什么使用while循环
满足条件才比较
否则证明插入位置已确定,不需要继续循环
- 为什么可以直接往后移位置
每轮未排序数已记录
最后一个位置不怕丢
- 为什么确定位置后,是放在sortIndex +1的位置
当循环停止时,插入位置应该是,停止循环的索引加1处
- 基本原理:
两个区域
用索引值来区分未排序区与排序区元素
不停比较找到合适位置插入当前元素
- 套路写法:
两层循环
一层获取未排序区元素
一层找到合适插入位置
默认开头已排序
第二层循环外插入
希尔排序
插入排序的升级版
将整个待排序序列
分割成为若干子序列
分别进行插入排序
希尔排序对插入排序的升级:主要就是加入了一个步长的概念
通过步长,每次可以把原序列分为多个子序列
对子序列进行插入排序
每次希尔排序轮次,步长除以2,直到1
在极限情况下,可以有效降低普通插入排序的时间复杂度,提升算法效率
//第1步:确定步长
//基本规则:每次步长变化都是/2
//一开始步长就是数组的长度/2
//之后每一次都是在上一次的步长基础上/2
//结束条件是步长<=日
//1.第一次的步长是数组长度/2所以: int step = arr.length/2
//2.之后每一次步长变化都是/2索引:step /= 2
//3.最小步长是1所以:step > 6
for (int step = arr.Length / 2; step > 0; step/=2)
{
//第2步:执行插入排序
//注释的代码是普通插入排序的代码(大部分1改为步长step)
//i=1代码相当于代表取出来的排序区的第一个元素
//for (int i = 1; i < arr.Length; i++)
//i=step相当于代表取出来的排序区的第一个元素
for (int i = step; i < arr.Length; i++)
{
//得出未排序区的元素
int noSortNum = arr[i];
//得出排序区中最后一个元素索引
//int sortIndex = i - 1;
//i-step代表和子序列中已排序区元素一一比较
int sortIndex = i - step;
//进入条件
//首先排序区中还有可以比较的>=0
//排序区中元素满足交换条件,升序就是排序区中元素
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
//arr[sortIndex +1]= arr[sortIndex];
//代表移步长个位置代表子序列中的下一个位”
arr[sortIndex + step] = arr[sortIndex];
//--sortIndex;
//一个步长单位之间的比较
sortIndex -= step;
}
//找到位置过后真正的插入值
// arr[sortIndex + 1]= noSortNum;
//现在是加步长个单位
arr[sortIndex + step]= noSortNum;
}
//第1步:确定步长
for (int step = arr.Length / 2; step > 0; step/=2)
{
//第2步:执行插入排序
for (int i = step; i < arr.Length; i++)
{
int noSortNum = arr[i];
int sortIndex = i - step;
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
arr[sortIndex + step] = arr[sortIndex];
sortIndex -= step;
}
arr[sortIndex + step]= noSortNum;
}
- 基本原理:
设置步长
步长不停缩小
到1排序后结束
具体排序方式:插入排序原理
- 套路写法:三层循环
一层获取步长
—层获取未排序区元素
一层找到合适位置插入
归并排序
归并=递归+合并
数组分左右,左右元素相比较,满足条件放入新数组,一侧用完放对面
递归不停分,分完再排序,排序结束往上走,边走边合并,走到头顶出结果
归并排序分成两部分:
1.基本排序规则:
左右元素进行比较,依次放入新数组中,—侧没有了,另一侧直接放入新数组
2.递归平分数组:
不停进行分割,长度小于2停止,开始比较,一层一层向上比
//第一步:
//基本排序规则
//左右元素相比较
//满足条件放进去
//—侧用完直接放
public static int[]Sort(int[] left,int[] right)
{
//1.先准备一个新数组
int[]array = new int[ left.Length + right.Length];
int leftIndex = o;//左数组索引
int rightIndex = ;//右数组索引
//2.最终目的是要填满这个新数组
//不会出现两侧都放完还在进循环
//因为这个新数组的长度是根据左右两个数组长度计算出来的
for (int i = 0; i < array. Length; i++)
{
//左侧放完了 直接放对面右侧
if( leftIndex >= left.Length )
{
array[i] = right[rightIndex];
//已经放入了一个右侧元素进入新数组
//所以标识应该指向下一个
rightIndex++;
}
//右侧放完了直接放对面左侧
else if( rightIndex >= right.Length )
{
array[i] = left[ leftIndex];
//已经放入了一个左侧元素进入新数组
//所以标识应该指向下一个
leftIndex++;
}
else if( left[leftIndex] < right[rightIndex] )
{
array[i] = left[ leftIndex];
//已经放入了一个左侧元素进入新数组
//所以标识应该指向下一个
leftIndex++;
}
else
{
array[i] = right[rightIndex];
//已经放入了一个右侧元素进入新数组
//所以标识应该指向下一个
rightIndex++;
}
}
//3.得到了新数组直接返回出去
return array;
}
//第二步:
//递归平分数组
//结束条件为长度小于2
public static int[ ] Merge(int[ ] array)
{
//递归结束条件
if (array . Length < 2)
return array;
//1.数组分两段,得到一个中间索引
int mid = array . Length / 2;
//2.初始化左右数组
//左数组
int[] left = new int[mid];
//右数组
int[] right = new int[array. Length - mid];
//左右初始化内容
for (int i = 0; i <array. Length; i++)
{
if (i < mid)
left[i] = array[i];
else
right[i - mid] = array[i];
}
//3.递归再分再排序
return Sort(Merge(left), Merge(right));
//实际逻辑中,多次平分,之后再调用了sort
}
int[] arr = new int[] { 8,7,1,5,4,2,6,3,9 };
//排序使用merge函数(merge函数中调用了sort函数)
arr=Merge(arr);
- 递归逻辑:
一开始不会执行sort函数的
要先找到最小容量数组时
才会回头递归调用sort进行排序
- 基本原理:
归并=递归+合并
数组分左右,左右元素相比较,一侧用完放对面,不停放入新数组
递归不停分,分完再排序,排序结束往上走,边走边合并,走到头顶出结果
- 套路写法:两个函数
一个基本排序规则
一个递归平分数组
快速排序
堆排序
控制台小游戏——
C#入门——回合制战斗
C#基础——飞行棋
C#核心——贪食蛇
C#进阶——俄罗斯方块
Console.SetCursorPosition(0, 0);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("");
unity
:先进后出,弹栈)(语句块结束就会释放)
引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾
想要不被回收或者不变垃圾,必须将其记录下来:
在更高层级记录,或者使用静态全局变量记录
结构体中的值和引用
结构体本身是值类型
前提:该结构体没有做为其它类的成员
-
在结构体中的值,栈中存储值具体的内容
-
在结构体中的引用,堆中存储引用具体的内容
引用类型始终存储在堆中
真正通过结构体,使用其中引用类型时,只是顺藤摸瓜
类中的值和引用
类本身是引用类型
-
在类中的值,堆中存储具体的值
-
在类中的引用,堆中存储具体的值
值类型跟着大哥走好,引用类型一根筋
数组中的存储规则
数组本身是引用类型
-
值类型数组,堆中房间存具体内容
-
引用类型数组,堆中房间存地址(引用包裹引用)
结构体继承接口
利用里氏替换原则,用接口容器装载结构体,存在装箱拆箱
interface ITest
{
int value
{
get;
set;
}
}
struct TestStruct : ITest //结构体继承接口
{
int value;
public int Value
{
get
{
return value;
}
set
{
this.value = value;
}
}
}
Teststruct obj1 =new Teststruct();//结构体:值类型
obj1.value = 1;
Teststruct obj2 = obj1;
obj2.Value = 2;//值类型 存在栈中 不影响
Console.WriteLine(obj1.Value);
Console.writeLine(obj2.Value);
ITest iobj1 = obj1;//用接口容器装载结构体(父类装子类) 装箱value 1 值变成引用类型
ITest iobj2 = iobj1;
iobj2.Value = 99;//引用类型 改2会影响1
Console.writeLine(iobj1.value);
Console.writeLine(iobj2.value);
Teststruct obj3 = (Teststruct)iobj1;//拆箱
排序进阶
插入排序
两个区域,排序区,未排序区
用一个索引值做分水岭
未排序区元素,与排序区元素比较,插入到合适位置,直到未排序区清空
排序开始前,首先认为第一个元素在排序区中,其它所有元素在未排序区中
排序开始后,每次将未排序区第一个元素取出,用于和排序区中元素比较(从后往前)
满足条件(较大或者较小),则排序区中元素往后移动一个位置。
所有数字都在一个数组中
所谓的两个区域,是一个分水岭索引
//第1步
//能取出未排序区的所有元素进行比较
//i=1的原因:默认第一个元素就在排序区
for (int i = 1; i < arr.Length; i++)
{
//第2步
//每一轮
//1.取出排序区的最后一个元素索引
int sortIndex = i - 1;
//2.取出未排序区的第一个元素
int noSortNum = arr[i];
//第3步
//在未排序区进行比较
//移动位置
//确定插入索引
//循环停止的条件:
//1.发现排序区中所有元素都已经比较完
//2.发现排序区中的元素不满足比较条件了
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
//只要进了这个while循环证明满足条件
//排序区中的元素就应该往后退一格
arr[sortIndex + 1] = arr[sortIndex];
//移动到排序区的前一个位置准备继续比较
--sortIndex;
}
//最终插入数字
//循环中知识在确定位置 和找最终的插入位置
//最终插入对应位置应该循环结束后
arr[ sortIndex + 1] = noSortNum;
}
- 为什么有两层循环
第一层循环:一次取出未排序区的元素进行排序
第二层循环:找到想要插入的位置
- 为什么第一层循环从1开始遍历
插入排序的关键是分两个区域,已排序区和未排序区
默认第一个元素在已排序区
- 为什么使用while循环
满足条件才比较
否则证明插入位置已确定,不需要继续循环
- 为什么可以直接往后移位置
每轮未排序数已记录
最后一个位置不怕丢
- 为什么确定位置后,是放在sortIndex +1的位置
当循环停止时,插入位置应该是,停止循环的索引加1处
- 基本原理:
两个区域
用索引值来区分未排序区与排序区元素
不停比较找到合适位置插入当前元素
- 套路写法:
两层循环
一层获取未排序区元素
一层找到合适插入位置
默认开头已排序
第二层循环外插入
希尔排序
插入排序的升级版
将整个待排序序列
分割成为若干子序列
分别进行插入排序
希尔排序对插入排序的升级:主要就是加入了一个步长的概念
通过步长,每次可以把原序列分为多个子序列
对子序列进行插入排序
每次希尔排序轮次,步长除以2,直到1
在极限情况下,可以有效降低普通插入排序的时间复杂度,提升算法效率
//第1步:确定步长
//基本规则:每次步长变化都是/2
//一开始步长就是数组的长度/2
//之后每一次都是在上一次的步长基础上/2
//结束条件是步长<=日
//1.第一次的步长是数组长度/2所以: int step = arr.length/2
//2.之后每一次步长变化都是/2索引:step /= 2
//3.最小步长是1所以:step > 6
for (int step = arr.Length / 2; step > 0; step/=2)
{
//第2步:执行插入排序
//注释的代码是普通插入排序的代码(大部分1改为步长step)
//i=1代码相当于代表取出来的排序区的第一个元素
//for (int i = 1; i < arr.Length; i++)
//i=step相当于代表取出来的排序区的第一个元素
for (int i = step; i < arr.Length; i++)
{
//得出未排序区的元素
int noSortNum = arr[i];
//得出排序区中最后一个元素索引
//int sortIndex = i - 1;
//i-step代表和子序列中已排序区元素一一比较
int sortIndex = i - step;
//进入条件
//首先排序区中还有可以比较的>=0
//排序区中元素满足交换条件,升序就是排序区中元素
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
//arr[sortIndex +1]= arr[sortIndex];
//代表移步长个位置代表子序列中的下一个位”
arr[sortIndex + step] = arr[sortIndex];
//--sortIndex;
//一个步长单位之间的比较
sortIndex -= step;
}
//找到位置过后真正的插入值
// arr[sortIndex + 1]= noSortNum;
//现在是加步长个单位
arr[sortIndex + step]= noSortNum;
}
//第1步:确定步长
for (int step = arr.Length / 2; step > 0; step/=2)
{
//第2步:执行插入排序
for (int i = step; i < arr.Length; i++)
{
int noSortNum = arr[i];
int sortIndex = i - step;
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum)
{
arr[sortIndex + step] = arr[sortIndex];
sortIndex -= step;
}
arr[sortIndex + step]= noSortNum;
}
- 基本原理:
设置步长
步长不停缩小
到1排序后结束
具体排序方式:插入排序原理
- 套路写法:三层循环
一层获取步长
—层获取未排序区元素
一层找到合适位置插入
归并排序
归并=递归+合并
数组分左右,左右元素相比较,满足条件放入新数组,一侧用完放对面
递归不停分,分完再排序,排序结束往上走,边走边合并,走到头顶出结果
归并排序分成两部分:
1.基本排序规则:
左右元素进行比较,依次放入新数组中,—侧没有了,另一侧直接放入新数组
2.递归平分数组:
不停进行分割,长度小于2停止,开始比较,一层一层向上比
//第一步:
//基本排序规则
//左右元素相比较
//满足条件放进去
//—侧用完直接放
public static int[]Sort(int[] left,int[] right)
{
//1.先准备一个新数组
int[]array = new int[ left.Length + right.Length];
int leftIndex = o;//左数组索引
int rightIndex = ;//右数组索引
//2.最终目的是要填满这个新数组
//不会出现两侧都放完还在进循环
//因为这个新数组的长度是根据左右两个数组长度计算出来的
for (int i = 0; i < array. Length; i++)
{
//左侧放完了 直接放对面右侧
if( leftIndex >= left.Length )
{
array[i] = right[rightIndex];
//已经放入了一个右侧元素进入新数组
//所以标识应该指向下一个
rightIndex++;
}
//右侧放完了直接放对面左侧
else if( rightIndex >= right.Length )
{
array[i] = left[ leftIndex];
//已经放入了一个左侧元素进入新数组
//所以标识应该指向下一个
leftIndex++;
}
else if( left[leftIndex] < right[rightIndex] )
{
array[i] = left[ leftIndex];
//已经放入了一个左侧元素进入新数组
//所以标识应该指向下一个
leftIndex++;
}
else
{
array[i] = right[rightIndex];
//已经放入了一个右侧元素进入新数组
//所以标识应该指向下一个
rightIndex++;
}
}
//3.得到了新数组直接返回出去
return array;
}
//第二步:
//递归平分数组
//结束条件为长度小于2
public static int[ ] Merge(int[ ] array)
{
//递归结束条件
if (array . Length < 2)
return array;
//1.数组分两段,得到一个中间索引
int mid = array . Length / 2;
//2.初始化左右数组
//左数组
int[] left = new int[mid];
//右数组
int[] right = new int[array. Length - mid];
//左右初始化内容
for (int i = 0; i <array. Length; i++)
{
if (i < mid)
left[i] = array[i];
else
right[i - mid] = array[i];
}
//3.递归再分再排序
return Sort(Merge(left), Merge(right));
//实际逻辑中,多次平分,之后再调用了sort
}
int[] arr = new int[] { 8,7,1,5,4,2,6,3,9 };
//排序使用merge函数(merge函数中调用了sort函数)
arr=Merge(arr);
- 递归逻辑:
一开始不会执行sort函数的
要先找到最小容量数组时
才会回头递归调用sort进行排序
- 基本原理:
归并=递归+合并
数组分左右,左右元素相比较,一侧用完放对面,不停放入新数组
递归不停分,分完再排序,排序结束往上走,边走边合并,走到头顶出结果
- 套路写法:两个函数
一个基本排序规则
一个递归平分数组
快速排序
堆排序
控制台小游戏——
C#入门——回合制战斗
C#基础——飞行棋
C#核心——贪食蛇
C#进阶——俄罗斯方块
Console.SetCursorPosition(0, 0);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("");