简单数据结构类
一.ArrayList
1.ArrayList的本质
ArrayList是一个C#为我们封装好的类,它的本质是一个object类型的数组,所以可以在其中存储各式各样任意类型的数据
ArrayList类帮我们实现了很多方法,比如数组的增删查改
2.申明
需要引用命名空间Using System.Collections;
ArrarryList array=new ArrayList();
3.增删查改
增:
array.add(object);
范围增加(批量增加,把另一个list容器里面的内容加到后面):
AddRange(array2);
array.Insert(index,object);
删:
移除指定元素,从头找,找到删:
array.Remove(object);
移除指定位置的元素:
array.RemoveAt(index);
清空:
array.Clear();
查
得到指定位置的元素:
Conssole.writeline(array[index]);
查看元素是否存在:
if(array.Contains(object))
正向查找元素位置:
找到的返回值是位置,找不到返回值是-1
int index=array.IndexOf(object);
方向查找元素位置:
++返回的是从头开始的索引数++
int index=array.LastIndexOf(object);
改
array[index]=new object;
4.遍历
长度(具体存了多少个):
Console.writeLie(array.Count);
容量(避免产生过多的垃圾):
Console.WriteLine(array.Capacity);
for(int i=0;i<array.count,i++)
{
console.WriteLine(array[i]);
}
迭代器遍历:
foreach(object item(随便取) in array)
{
Console.WriteLine(item);
}
5.装箱拆箱
ArrayList本质上是一个可以自动扩容的object数组
由于用万物之父来存储数据,自然存在装箱拆箱
当往其中进行值类型的存储时就是在转向,当将值类型对象取出来转换使用时,就是在拆箱
所以,ArrayList尽量少用,之后我们会学习更好的数据容器
二.Stack(栈)
1.Stack的本质
Stack(栈)是一个C#为我们封装好的类
它的本质也是Object[]数组,只是封装了特殊的存储规则
Stack是栈存储容器,栈是一种先进后出的数据结构
先存入的数据后获取,后存入的数据先获取
栈是先进后出
2.申明
需要引用命名空间 System.Collections
3.增取查改
增
压栈
statck.Push(object);
取
栈中不存在删除的概念
只有取的概念
弹栈
object v=statck.Pop();
查
1.栈无法查看指定内容的元素
只能查看栈顶的内容(但不会移除)
v=stack.Peek();
2.查看元素是否存在于栈中
if(stack.Contains(object))
改
栈无法改变其中的元素 只能压(存)和弹(取)
实在要改 只有清空
4.遍历
由于没有索引器,无法用for循环遍历
1.长度:
stack.Count
2.用foreach遍历
而且遍历出来的顺序,也是从栈顶到栈低
foreach(object item in stack)
{
Console.writeLine(item);
}
3.还有一种遍历方式
将栈转换为object数组(stack自带了一个ToArray方法)
遍历出来的顺序,也是从栈顶到栈低
object[] array=stack.ToArray();
for(int i=0;i<array.length;i++)
{
Console.WriteLine(array[i]);
}
4.循环弹栈
一直将栈里元素弹出直到变空
while(stack.Count>0)
{
object o=stacck.pop();
console.writeLine(stack.Count)
}
5.装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱
当往里面进行值类型存储就是在装箱
当将值类型对象取出来转换使用时,就存在拆箱
三.Queue(队列)
1.Queue本质
Queue是一个C#为我们封装好的类
它的本质也是object[]数组,只是封装了特殊的存储规则
queue是队列存储容器
队列是一种先进先出的数据结构
先存入的数据先存取,后存入的数据后获取
先进先出
2.申明
需要引用命名空间 System.collections
3.增取查改
增
queue.Enqueue(object);
取
队列中不存在删除的概念
只有取的概念,取出先加入的对象
object v=queue.Dequeue();
查
1.查看队列头部元素,但不会移除
object v=queue.Peek();
2.查看元素是否存在于队列中
if(queue.Contains(object))
改
队列无法改变其中的元素 只能进出队列
实在要改,只有清除
4.遍历
1.长度:
Console.WriteLine(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(qieue.Count>0)
{
object o=queue.Dequeue();
Console.WriteLine(o.Couunt);
}
5.装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱
当往里面进行值类型存储就是在装箱
当将值类型对象取出来转换使用时,就存在拆箱
四.Hashtable(散列表 哈希表)
1.Hashtable的本质
Hashtable(又称散列表) 是基于键的哈希代码组织起来的 键/值对
它的主要作用是提高数据查询的效率
使用键来访问集合中的元素
适合拿来装载游戏中的管理物品的容器
2.申明
需要引用命名空间 System.Collections
3.增删查改
增
键和值都是object类型,参数中,前为键,后为值
hashtable.Add(object,object);
注意:不能出现相同键
删
1.只能通过键去删除
Hashtable.Remove(object);
2.删除不存在的键,没反应
3.或者直接清空
hashtable.Clear();
查
1.通过键查看值
找不到会返回空
Console.WriteLine(hashtableobject);
2.查看是否存在
根据键检测
if(hashtable.Contains(object(键)))
if(hashtable.ContainsKey(object(键)))
根据值检测
if(hashtable.ContainsValue(object(值)))
改
只能改键对应的值内容 无法修改键
hashtable[object(键)]=new object
4.遍历
得到键值对对数:
console.WriteLine(hashtable.Count)
1.遍历所有键
foreach(object item in ++hashtable.Keys++)
{
Console.WriteLine(“键”+item);
Console.WriteLine(“值”+hashtable[item])
}
2.遍历所有值
foreach(object item in ++hashtab.Values)++
{
Console.WriteLine(“值”+item)
}
注意:无法通过值去找到键
3.键值一起去遍历
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)
flage= myEnumerator.MoveNext();
}
5.装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱
当往里面进行值类型存储就是在装箱
当将值类型对象取出来转换使用时,就存在拆箱
泛型
一.泛型
1.泛型是什么
泛型实现了类型参数化,达到代码重用的目的
通过类型参数化来实现同一份代码上操作多种类型
泛型相当于类型占位符
定义类或方法时使用替代符代表变量类型
当真正使用类或方法的时候再具体指定类型
2.泛型分类
泛型类和泛型接口
基本语法:
class 类名<泛型占位字母>
interface 接口名<泛型占位字母>
泛型函数
基本语法:
函数名:函数名<泛型占位字母>(参数列表)
注意:泛型占位字母可以有多个,用逗号分割开来
3.泛型的使用
可以在普通类中放泛型方法
而泛型类中引用了泛型参数的方法不是泛型方法,而是普通方法,因为调用的泛型已经在实例化的时候就确定了,只有在泛型方法中依然定义了泛型方法才是真正的泛型方法
4.泛型的作用
1.不同类型对象的相同逻辑处理就可以选择泛型
2.使用泛型可以一定程度避免装箱拆箱
举例:可以用来优化ArrayList,不过会导致会成为只能装一种类型的容器
二.泛型约束
1.什么是泛型约束
让泛型的类型有一定的限制
关键字:where
泛型约束一共有6种
1.泛型参数是值类型 where 泛型字母:struct
2.泛型参数是引用类型 where 泛型字母:class
3.泛型参数存在非抽象无参公共构造函数 where 泛型字母:new()
4.泛型参数是某个类本身或者其派生类 where 泛型字母:类名
5.泛型参数是某个接口的派生类型 where 泛型字母:接口名
6.泛型参数是 另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母
语法:where 泛型字母:(约束的类型)
2.约束可以组合使用
约束可以组合使用,即:后面可以有多个约束
3.多个泛型有约束
即可以使用多个where对不同泛型参数进行约束
4.总结
泛型约束:让类型有一定限制
注意:
1.可以组合使用
2.多个泛型约束,用where连接即可
常用泛型数据结构类
一.List
1.List的本质
List是一个C#为我们封装好的类
它的本质是一个可变类型的泛型数组
List类帮助我们实现了很多方法
比如泛型数组的增删查改
和arrayList不同的是,可以放置的类型在创建List对象的时候已经确定了(但是实际上你可以把泛型确定为object,那几乎就一模一样),但是方法和arraylist非常类似
2.申明
需要引用命名空间
using System.Collections.Generic
3.增删改查
增
list.add();
list.addRange(object) (范围增加)
list.Inser(index,object);
删
1.移除指定元素
list.remove();
2.移除指定位置的元素
list.removeat(index);
3.清空
list.Clear();
查
1.得到指定位置的元素
list[index]
2.查看元素是否存在
if(list.Contains(object))
3.正向查找元素位置
找到返回位置 找不到返回-1
int index=list.IndexOf(object);
4.方向查找元素位置
找到返回位置 找不到返回-1
int index=list.LastIndexOf(object);
改
list[index]=new object;
5.遍历
长度:
console.writeLine(list.Count);
容量:
Console.WriteLine(list.Capacity)
for(int i=0;i<list.Count;i++
{
Console.WriteLine(list[index]);
}
foreach(int item in list)
{
Console.WriteLine(item);
}
二.Dictionary(字典)
1.Dictionary的本质
可以将Dictionary理解为拥有泛型的hashtable
它也是基于键的哈希代码组织起来的键/值对
键值对类型从Hasgtable的object变为了可以自己制定的泛型
2.申明
需要引用命名空间 using System.Collections.Generic
3.增删查改
增
注意:不能出现相同键
dictionary.Add(键,值);
dictionary[键]=object;
删
只能通过键去删除
删除不存在键,没反应
dictionary.Remove(键);
清空
dictionary.Clear();
查
通过键查看值
找不到不会返回空,而是会报错(和hashtable的区别)
Console.WriteLine(dictionary[键]);
查看是否存在
根据键检测
if(dictionary.ContainsKey(键))
根据值检测
if(dictionary.ContainsValue(值))
改
dictionary[键]=new object;
4.遍历
长度:
dictionary.Count
遍历所有键
foreach(int item in dictionary.Keys)
{
Console.Writeline(item);//键
Console.WriteLine(dictionary[item])//值
}
遍历所有值
foreach(String item in dictionary.Values)
{
Console.WriteLine(item);//值,但是无法通过值得到键
}
键值对一起遍历
foreach(keyvaluePair<int,string> item in dictionary)
{
Console.writeLine(item.key+item.value);
}
三.顺序存储和链式存储
1.数据结构
数据结构是计算机存储,组织数据的方式(规则)
数据结构是指相互之间存在一种或多种特定关系的数据元素的集合
比如自定义的一个类也可以称为一种数据结构,自己定义的数据组合规则
不要把数据结构想的太复杂
简单点理解,就是人定义的存储数据和表示数据之间关系的规则而已
常用的数据结构(前辈总结和指定的一些经典规则)
数组,栈,队列,链表,树,图,堆,散列表
顺序存储和链式存储是数据结构中的两种存储结构
2.线性表
线性表是一种数据结构,是由n个具有相同特性的数据元素的有限序列
比如数组,ArrayList,Stack,Queue,链表等等
3.顺序存储
数组,Stack,Queue,List,ArrayList 皆为顺序存储
只是 数组,Stack,Queue的组织规则不同而已
本质是用一组地址连续的存储单元依次存储线性表的各个数据元素
4.链式存储
单向列表,双向列表,循环列表皆为链式存储
本质是用一组任意的存储单元存储线性表中的各个数据元素
注意,不可达对象会被回收,所以当head无法到达某个对象后,即使该对象的next指向了一个可达对象,此对象依然是不可达,所以最后会被回收掉,可以用这个原理来删除某些链表元素
5.顺序存储和链式存储的优缺点
增:链式存储 计算上 由于顺序存储(中间插入时链式不用像顺序一样去移动位置)
删:链式存储 计算上 由于顺序存储(中间删除时链式不用像顺序一样去移动位置)
查:顺序存储 使用上 优于链式存储(数组可以直接通过下标得到元素,链式需要遍历)
改:顺序存储 使用上 优于链式存储(数组可以直接通过下标获得元素,链式需要遍历)
四.LinkedList
1.LinkedList
LinkedList是一个C#为我们封装好的类
它的本质是一个可变类型的泛型双向列表
2.申明
需要引用命名空间
using System.Collections.Generic
链表对象 需要掌握两个类
一个是链表本身 一个是链表节点类LinkedListNode
3.增删查改
增
1.在链表尾部添加元素
linkedList.AddLast(object);
2.在链表头部添加元素
linkedList.AddFirst(object);
3.在某一个节点之后添加一个节点
要指定节点,先得得到一个节点(查)
4.在某一个节点之前添加一个节点
要指定节点,先得得到一个节点(查)
删
1.移除头结点
linkedList.RemoveFirst();
2.移除尾节点
linkedList.ReoveLast();
3.移除指定节点
无法通过位置直接移除,只能通过值
linkedList.remove(object);
4.清空
linkedList.Clear();
查
改
要先得再改 得到节点再改变其中的值
Console.WriteLine(linkedList.First.Value);
linkedList.First.Value=10;
Console.WriteLine(linkedList.First.Value);
4.遍历
1.foreach遍历
foreach(int item in linkedList)
{
Console.WriteLine(item);
}
五.泛型栈和队列
命名空间:using System.Collections.Generic;
使用上 和之前的stack和Queue一模一样
六.对于各大类型数据结构的使用总结
数组、List、Dictionary、Stack、Queue、LinkedList
这些存储容器,对于我们来说应该如何选择他们来使用
普通线性表:
数组,List,LinkedList
数组:固定的不变的一组数据
List: 经常改变,经常通过下标查找
LinkedList:不确定长度的,经常临时插入改变,查找不多
先进后出:
Stack
对于一些可以利用先进后出存储特点的逻辑
比如:UI面板显隐规则
先进先出:
Queue
对于一些可以利用先进先出存储特点的逻辑
比如:消息队列,有了就往里放,然后慢慢依次处理
键值对:
Dictionary
需要频繁查找的,有对应关系的数据
比如一些数据存储 id对应数据内容
道具ID ——> 道具信息
怪物ID ——> 怪物对象
委托和事件
一.委托
1.委托是什么
委托是函数(方法)的容器
可以理解为表示函数(方法)的变量类型
用来存储,传递函数(方法)
委托的本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)
不同的函数(方法)必须对应和各自“格式”一致的委托
2.基本语法
关键字:delegate
语法:访问修饰符 delegate 返回值 委托名(参数列表);
写在哪里?:
可以申明在namespace和class语句块中,更多的写在namespace中
简单记忆委托语法 就是 函数申明语法前面加一个delegate关键字
3.定义自定义委托
++访问修饰符默认不写,为public,在别的命名空间也能使用++
private 其他命名空间就不能用了
一般使用public
申明了一个可以用来存储无参无返回值函数的容器
这里只是定义了规则 并没有使用
delegate void MyFun();
委托规则的申明,是不能重名的(同一语句块中)
表示用来装载或传递返回值为int 有一个int参数的函数的委托容器规则
public delegate int MyFun2(int a);
泛型委托定义
4.使用定义好的委托
委托变量是函数的容器
委托常用在:
1.作为类的成员
2.作为函数的参数(在函数中可以先处理一些别的逻辑,处理完了再执行传入的委托,即延迟调用)
使用例(注意,委托的参数和目标函数一一对应):
MyFun f = new MyFun(Fun);
f.Invoke();
MyFun f2 = Fun;
f2();
MyFun2 f3 = Fun2;
Console.WriteLine(f3(1));
MyFun2 f4 = new MyFun2(Fun2);
Console.WriteLine(f4.Invoke(3));
5.委托变量可以存储多个函数(多播委托)
增
MyFun ff=Fun;
ff+=Fun3;
ff();
便会在调用委托ff时执行Fun和Fun3方法(按照添加的顺序来执行)
或者在开头把委托设置为null,便可以+=函数,但是不可以初始化的时候就直接+=函数
删
ff-= fun;
上面为从容器中移除指定函数
多减不会报错,无非是不处理而已
ff=null;
清空容器,如果清空后调用的话会报错
6.系统定义好的委托
使用系统自带委托,需要引用using System;
省去了构造委托的麻烦,而且更容易让别人意识到这是个委托,毕竟委托很多地方和普通方法很像
无参无返回值:
Action action=Fun
action+=Fun3;
action();
可以传n个参数(系统提供了1到16个参数的泛型委托):
可以传n个参数,并且有返回值的(系统也提供了16个委托
可以指定返回值类型的,泛型委托:
总结:
简单理解 委托就是装载,传递函数的容器而已
可以用委托变量 来存储函数或者传递函数的
系统其实已经提供了很多委托给我们用
Action:没有返回值,参数提供了0-16个委托给我们用
Func:有返回值,参数提供了0-16个委托给我们用
++一般情况下,委托关联的函数,有加就有减++(或直接清空)
二.事件
1.事件是什么
事件是基于委托的存在
事件是委托的安全包裹
让委托的使用更具有安全性
事件是一种特殊的变量类型
2.事件的使用
申明语法:
访问修饰符 event 委托类型 事件名; (在委托的基础上加工)
事件的使用:
1.事件是作为 成员变量存在于类中
2.委托怎么用 事件就怎么用
事件相对于委托的区别:
1.不能在类外部 赋值
2.不能在类外部 调用
但是可以在类外面加减
注意:
它只能作为成员存在于类和接口以及结构体中
3.为什么有事件
1.防止外部随意置空委托
2.防止外部随意调用委托
3.事件相当于对委托进行了一次封装,让其更加安全
总结:
事件和委托的区别
事件和委托的使用基本是一模一样的
事件就是特殊的委托
主要区别:
1.事件不能在外部使用赋值=符号,只能用+ - 配合=。委托哪里都能用
2.事件不能在外部执行,委托哪里都能执行
3.事件不能作为函数中的临时变量,委托可以
三.匿名函数
1.什么是匿名函数
顾名思义,就是没有名字的函数
匿名函数主要是配合委托和事件进行使用
++脱离委托和事件,是不会使用匿名函数的++
2.基本语法
delegate(参数列表)
{
函数逻辑
}
何时使用?
1.函数中传递委托参数时
2,委托或事件赋值的时候
3.使用
1.无参无返回:
这样申明匿名函数,只是在申明函数而已,还没有调用
真正调用它的时候,是这个委托容器啥时候调用,就申明时候调用这个匿名函数
Action a=delegate()
{
console.WriteLine(“匿名函数逻辑”);
}
a(); //执行匿名函数
2.有参:
Action<int,String> b=delegate(int a,String b)
{
Console.WriteLine(a);
Console.WriteLine(b);
}
b(100,“123”); //执行匿名函数
3.有返回值:
Fun<String,> c=delegate()
{
return “123123”;
}
console.WriteLine(c());
Fun中,最后一个参数为返回值(在超过1个参数泛型的情况下,如果只有一个参数那那个个参数就是返回值,同时没有说明匿名函数没有参数)
4.一般情况会作为函数参数传递 或者 作为函数返回值
Text t=new Text();
参数传递:
t.Dosomething(100,delegate()
{
逻辑
}
);
上面自动把匿名函数识别为了需要的参数 ,Action
也可以先给匿名函数一个容器Action,然后再当参数传进去(比较美观)
总的来说 匿名函数讲究当需要委托和事件相关的函数的时候,可以一步到位
返回值
Action ac2=t.GetFun();
ac2();
一部到位 直接调用返回的委托函数:
t.GetFun()();
4.匿名函数的缺点
添加到委托或事件容器中后 不记录 无法单独移除(可以添加多个匿名函数,但是移除不了):
Action ac3=delegate()
{
console.WriteLine(“无法移除”);
}
因为匿名函数没有名字,所以没有办法指定移除某一个匿名函数
即使你-=了一个结构内容一模一样的匿名函数,即使代码允许,也没有任何影响和作用
总结:
匿名函数,就是没有名字的函数
固定写法:
delegate(参数列表){}
主要是在委托传递和存储时,为了方便可以直接使用匿名函数
缺点是没有办法指定移除匿名函数。
四.lambad表达式
1.什么是lambad表达式
可以将lambad表达式,理解为匿名函数的简写
它除了写法不同外
使用上和匿名函数一模一样
都是和委托或者事件配合使用的
2.lambad表达式语法
(参数列表)=>
{
函数体
}
3.使用
1.无参无返回值
Action a=()=>
{
console.WriteLine(“无参无返回值的lambad表达式”);
};
a();
2.有参:
Action a2=(int value)=>
{
Console.WriteLine(“有参数lambad表达式{0}”,value);
};
a2(100);
3.甚至参数类型都可以省略,参数类型和委托或事件容器一致(前面已经写出参数)
Action<Int,> a3=(value)=>
{
console.writeLine(“省略参数类型的写法{0}”,value)
};
4.有返回值
Func<String,int>a4=(value)=>
{
Console.WriteLine(“有返回值有参数的lambad表达式{0}”,value);
return 1;
};
Console.WriteLine(a4(“123123”));
其他传参使用等和匿名函数一样
缺点也是和匿名函数一样(不能移除)
4.闭包(重点)
注意:++闭包并不是和匿名函数绑定,而是和内函数绑定,当然,如果没有委托机制,你很难在外面调用一个内函数++
内层函数可以引用包含在外层的函数的变量
即使外层函数的执行已经终止
class Text
{
private event Action action;
public Text()
{
int value=10;
//这里就形成了闭包,因为当构造函数执行完成时,其中申明的临时变量value的生命周期被改变了
action=()=>
{
Console.WriteLine(Value);
};
}
}
如上,value被匿名函数占用,生命周期无限拉长,触发把Action置空。
++闭包的概念非常重要,建议重点研究++
5.闭包陷阱
注意:
该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值
如:
for(int i=0 ; i《10 ;i++)
{
action+=()=>
{
Console.WriteLine(i);
};
}
调用上面的匿名函数上面输出将不是1 2 3.。。9 而是10个10!!!即最终结果
除非你在前面再次申明一个int index=i ,然后让输出为index,才会输出0到9,即后申明的index会不断改变,因为这个新建的index会在每次For循环进来重新声明一个t的内存地址,最后让匿名函数得以改变
总的来说,闭包是一个函数的内函数把原来属于外部函数的变量给抓住了为自己所用,强行增加了生命周期,变成了自由变量(NTR?)
闭包陷阱例子(编译器在lambda背后的行为):
class Program
{
static void Main(string[] args)
{
Action[] actions = new Action[5];
var innerClass = new InnerClass();//闭包陷阱的关键在这里,它居然只new了一次在外部,而不是在for循环里多次new!!!!!!导致5个Action共用一个innerClass(因为指向的地址相同),而innerClass只保留了最后一个值,4....
int i;//for循环中定义的局部变量是被当作外部变量来使用的,这是在C#中的实现。
for (i = 0; i 《 actions.Length; i++)
{
innerClass.i = i;
actions[i] = innerClass.DoIt;
}
foreach (var item in actions)
{
item();
}
Console.ReadKey();
}
public class InnerClass//这里是模拟编译器为lambda表达式生成的类,我暂时命名为InnerClass,实际上编译器生成的这个内部类有自己的命名规则。
{
public int i;//这个是捕获的for循环中的那个变量。
public void DoIt()
{
Console.WriteLine(i);
}
}
}
闭包陷阱的完整内容,可参考:https://www.cnblogs.com/pangjianxin/p/8400155.html
知乎: https://zhuanlan.zhihu.com/p/31616347
总结:
匿名函数的特殊写法,就是lambad表达式
固定写法就是(参数列表)=》{}
参数列表可以直接省略参数类型
主要在委托传递和存储时,为了方便可以直接使用匿名函数或者lambad表达式
缺点:无法移除指定匿名函数
补充知识点 有返回值的委托存储多个函数 调用时如何获取多个返回值?
问题:
当用有返回值的委托容器存储多个函数时
.cn/item/649e7f991ddac507cc32a721/C#进阶12.png)
解决问题:
如果想要获取到每一个函数执行后的返回值
知识点:委托容器中存在方法GetInvocationList()可以返回一个委托数组
List排序
1.List提供了排序方法
list.Sort();
ArrayList也用于此方法
实际上,int可以排序是因为int继承和实现了排序的接口
2.自定义类排序
sort无法直接对自定义类排序
但是类可以通过继承和实现IComparable《item》()接口来获得排序能力。
IComparable的实现如下:
public int CompareTo(Item other)
{
//返回值的含义
//小于0:
//放在传入对象的前面
//等于0:
//保持当前的位置不变
//大于0:
//放在传入对象的后面
//可以简单理解 传入对象的位置 就是0
//如果你的返回值为负数,就放在它的左边,也就是前面
//如果你返回正数,就放在它的右边,也就是后面
if(this.money>other.money)
{
return 1;
}
else
{
return -1;
}
//如上,就是让类根据类中的money变量降序排列。
}
本质上sort方法是通过将排序对象as为 IComparable来进行算法(子类允许转接口)
3.通过委托函数进行排序
通过委托设置规则,然后放在sort参数里,可以不需要实现接口就设定类的排序规则,甚至可以匿名函数直接塞sort参数里
一下为利用委托激活Sort排序的例子
//委托放参数里
shopItems.Sort(SortShopItem);
//匿名函数
//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;});
static int SortShopItem( ShopItem a, ShopItem b )
{
//传入的两个对象 为列表中的两个对象
//进行两两的比较 用左边的和右边的条件 比较
//返回值规则 和之前一样 0做标准 负数在左(前) 正数在右(后)
if (a.id > b.id)
{
return -1;
}
else
{
return 1;
}
}
}
总结
系统自带的变量(int,float,double…)一般都可以直接sort
自定义类Sort有两种方式
1.继承接口 IComparable
2.在Sort中传入委托函数
例题:
//题目:写一个物品类(类型,名字,品质),创建10个物品,添加到List中
// 同时使用类型,品质,名字长度进行比较
//裴谞的权重是:类型>品质>名字长度
namespace 自定义类排序
{
class Program
{
static void Main(string[] args)
{
//自己指定新的排序规则,并用于下文的sort(先按类型降序排序,类型相同按品质降序排序,都相同则按照名字长度降序排序)
int text(wuping a, wuping b)
{
if (a.leixing != b.leixing)
{
return a.leixing > b.leixing ? -1 : 1;
}
else if (a.ping != b.ping)
{
return a.ping > b.ping ? -1 : 1;
}
else
{
return a.name.Length> b.name.Length ? -1 : 1;
}
}
List<wuping> list = new List<wuping>();
Random r = new Random();
list.Add(new wuping(r.Next(1,6), "item"+r.Next(1,300), r.Next(1,6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Add(new wuping(r.Next(1, 6), "item" + r.Next(1, 300), r.Next(1, 6)));
list.Sort(text);
foreach (wuping item in list)
{
Console.WriteLine("类型:"+item.leixing+",名字:"+item.name+",品质:"+item.ping);
}
}
}
class wuping
{
public int leixing;
public String name;
public int ping;
public wuping(int leixing, String name, int ping)
{
this.leixing = leixing;
this.name = name;
this.ping = ping;
}
}
}
协变逆变
1.什么是协变逆变
协变:
和谐的变化,自然的变化
因为 里氏替换原则 父类可以装子类
所以 子类变父类
所以 string 变成 object
感觉是和谐的
逆变:
逆常规的变化,不支持的变化
因为 里氏替换原则 父类可以装子类 但是子类不能装父类
所以父类变子类
比如 objec 变成 String
感觉是不和谐的
协变和逆变是用来修饰泛型的
协变:out
逆变:in
用于在泛型中 修饰 泛型字母的
++只有泛型接口和泛型委托能用++
2.作用
1.返回值和参数
用out修饰的泛型,只能作为返回值(不能作为参数)
用in修饰的泛型,只能作为参数(不能作为返回值)
2.结合里氏替换原则理解
//1.返回值 和 参数
//用out修饰的泛型 只能作为返回值
delegate T TestOut<out T>();
//用in修饰的泛型 只能作为参数
delegate void TestIn<in T>(T t);
//2.结合里氏替换原则理解
class Father
{
}
class Son:Father
{
}
//协变 父类总是能被子类替换
// 看起来 就是 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
总结
协变 out
逆变 in
用来修饰 泛型替代符的 只能修饰接口和委托中的泛型
作用两点
1.out修饰的泛型类型 只能作为返回值类型 in修饰的泛型类型 只能作为 参数类型
2.遵循里氏替换原则的 用out和in修饰的 泛型委托 可以相互装载(有父子关系的泛型)
协变 父类泛型委托装子类泛型委托 逆变 子类泛型委托装父类泛型委托
多线程
1.了解线程前先了解进程
进程(process)是计算机中的程序关于某数据集合上的一次允许活动
是系统进行资源分配和调度的基本单位,是操作系统结构的基础
说人话:打开一个应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立允许,互不干扰
进程之间也可以相互访问,操作
2.什么是线程
操作系统能够进行运算调度的最小单位
它被包含在进程之中,是进程中的实际运作单位
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
我们目前写的程序 都在主线程中
简单理解线程
就是代码从上到下运行的一条“管道”
3.什么是多线程
我们可以通过代码 开启新的线程
可以通过运行代码的多条“管道” 就叫多线程
4.语法相关
线程类 Thread
需要引用命名空间 using System.Threading
1.申明一个新的线程
注意 线程执行的代码 需要封装到一个函数中
新线程 将要执行的代码逻辑 被封装到了一个函数语句块中
Therad t =new Thread(函数);
2.启动线程
t.Start();
3.设置为后台线程
当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
后台线程不会发防止引用程序的进程被终止掉
如果不设置为后台线程 ++可能导致进程无法正常关闭++
t.IsBackground=true;
4.关闭释放一个线程
如果开启的线程不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它
如果是死循环 想要中止这个线程 有两种方式
4.1-死循环中bool标识
4.2-通过线程提供的方法(注意在.New core版本中无法中止 会报错)
//中止线程方法:
t.Abort()
t=null;
5.线程休眠
让线程休眠多少毫秒 1s=1000毫秒
在哪个线程里执行,就休眠哪个线程
Thread.Sleep(int);
5.线程之间共享数据
多个线程使用的内存是共享的,都属于该应用程序(进程)
所以要注意 当多线程 同时操作同一片内存区域时可能会出问题
lock:
当我们再多个线程当中想要访问同样的东西 进行逻辑处理时
为了避免不必要的逻辑顺序执行的差错
lock(引用类型对象)
{
执行内容
}
多线程对我们的意义
可以用多线程专门处理一些复杂耗时的逻辑
比如 寻路,网络通信等等
总结
多线程是多个可以同时执行代码逻辑的管道
可以通过代码开启多线程,用多线程处理一些复杂的可能影响主线程流畅度的逻辑
关键字Thread
预处理器指令
1.什么是编译器
编译器是一种翻译程序
它用于将源语言程序翻译为目标语言程序
源语言程序:某种程序设计语言写成的,比如C#,c,c++,java等语言写的程序
目标语言程序:二进制数表示的伪机器代码写的程序
2.什么是预处理器指令
预处理器指令 指导编译器 在实际编译开始之前对信息进行预处理
预处理器指令 都是以#开始
预处理器指令不是语句,所以它们不以分号;结束
目前我们经常用到的 折叠代码块 就是预处理器指令
3.常见的预处理器指令
#define
定义一个符号,类似一个没有值的变量
#under
取消define定义的符号,让其失效
两者都是写在脚本文件的最前面
一般配合if指令使用或配合特性
#if
#elif
#else
#endif
和if语句规则一样,一般配合#define定义的符号使用
用于告诉编译器进行编译代码的流程控制
例子:如果发现有unity4这个符号,那么其中包含的代码,就会被编译器编译
可以通过 逻辑或 和 逻辑与 进行多种符号的组合判断
#if unity || IOS
Console.WriteLine(“unity4”);
#endif
#warning
#error
告诉编译器
是报警告还是报错误(用来提示其他程序员)
一般还是配合if使用
总结
预处理器指令
可以让代码还没有编译之前就可以进行一些预处理判断
在Unity中会用来进行一些平台或者版本的判断
决定不同版本或者不同平台使用不同的代码逻辑
反射和特性
一.反射
1.程序集
程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物
在WINDOWS系统中,它一般表现为后缀为.dll(库文件)或者是.exe(可执行文件)的格式
说人话:
程序集就是我们写的一个代码集合,我们现在写的所有代码
最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件(dll)或者一个可执行文件(exe)
2.元数据
元数据就是用来描述数据的数据
这个概念不仅仅用于程序上,在别的领域也有元数据
说人话:
程序中的类,类中的函数,变量等等信息就是 程序的 元数据
有关程序以及类型的数据被称为元数据,他们保存在程序集中
3.反射的概念
程序运行时,通过反射可以得到其他程序集或者自己程序集代码的各种信息
类,函数,变量,对象等等,实例化它们,执行它们,操作它们
4.反射的作用
因为反射可以在程序编译后获得信息,所以它提高了程序的扩展性和灵活性
1.程序运行时得到所有元数据,包括元数据的特性
2.程序运行时,实例化对象,操作对象
3.程序运行时创建新对象,用这些对象执行任务
5.语法相关
Type(类的信息类)
它是反射功能的基础!
它是访问元数据的主要方式
使用Type的成员获取有关类型申明的信息
有关类型的成员(如构造函数,方法,字段,属性和类的事件)
1.万物之父object中的GetType()可以获取对象的Type
2.通过typeof关键字 传入类名 也可以获得对象的Type
3.通过类的名字 也可以获取类型
注意 类名必须包含命名空间 不然找不到
Type type=Type.GetType(“System.Int32”);
相同类的type所指向的内存空间都是完全一样的,即每一个类的Type都是唯一的
得到类的程序集信息(不怎么重要,重点是后面)
Console.WriteLine(type.Assembly);
获取类中的所有公共成员
首先得到Type
Type t=typeof(Text);
然后得到所有公共成员
需要引用命名空间 using System.Reflection;
MemberInfo[] infos=t.GetMembers();
for(int i=0;i《infos.Length;i++)
{
Console.WriteLine(infos[i]);
}
获取类的公共构造函数并调用
1.获取所有构造函数
ConstructorInfo[] ctors=t.GetConstructors();
for(int i=0;i《ctors.Length;i++)
{
Console.WriteLine(ctors[i]);
}
2.获取其中一个构造函数 并执行
得构造函数传入 Type数组 数组中内容按顺序是参数类型
执行构造函数传入 object数组 表示按顺序传入的参数
通过信息对象的构造函数来实例化
2-1得到无参构造函数
ContructorInfo info=t.GetConstructor(new Type[0]);
执行无参构造 无参构造 没有参数 传null
Text obj=info.Invoke(null) as Text;
2.2得到有参构造
ConstructorInfo info2=t.GetConstructor(new type[] {typeof(int),…}); //得到构造函数参数类型,存到type数组中,然后在寻找int类型的构造函数
obj=info2.Invoke(new object[]{2}) as Text;
获取类的公共成员变量
1.得到所有成员变量
FieldInfo[] fieldInfos=t.GetFields();
for(int i=0;i>fieldInfos.Length;i++)
{
Console.WriteLine(fieldInfos[i]);
}
2.得到指定名称的公共成员变量
FieldInfo infoJ=T.GetField(“j”);
3.通过反射获取和设置对象的值
Text text=new Text();
text.j=99;
text.str=“2222”;
3-1通过反射 获取对象的某个变量的值
Console.WriteLine(infoJ.GetValue(test));
3-2通过反射 设置指定对象的某个变量的值
infoJ.SetValue(text,100);
4.获取类的公共成员方法
通过Type类中的GetMethod方法,得到类中的方法
MehodInfo是方法的反射信息
Type strType=typeof(Stinrg);
1.如果存在方法重载 用Type数组表示参数类型
MethodInfo[] method=strType.GetMethods();
for(int i=0;i《methods.Length;i++)
{
Console.WriteLine(metho[i]);
}
MethondInfo 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})
其他
得枚举
GetEnumNmae
GetEnumNames
得事件
GetEvent
GetEvents
得接口
GetInterface
GetInterfaces
得属性
GetProperty
GetPrropertys
等等
Activator
用于快速实例化对象的类
用于将Type对象快捷实例化为对象
想得到Type
然后快速实例化一个对象
Type textType=typeof(Text);
1.无参构造
Text textObj=Activator.CreateInstance(testType) as Text;
2.有参数构造
textObj=Activator.CreateInstacne(textType,99)as Text;
textObj=Activator.CreateInstance(textType,55,“11222”)as Text;
Assembly
程序集
主要用来加载其他程序集,加载后
才能用Type来使用其他程序集中的信息
如果想要使用的不是自己程序集中的内容 需要先加载程序集
比如 dll文件(库文件)
简单的把库文件看成一种diamante仓库,它提供给使用者一些可以直接拿来用的变量,函数或类
三种加载程序集的函数
一般用来加载在同一文件下的其他程序集
Assembly asembly2=Assembly.Load(“程序集名称”);
一般用来加载不再同一文件下的其他程序集
Assembly asembly=Assembly.loadForm(“包含程序集清单的文件的名称或路径”);
Assembly asembly3=Assembly.LoadFile(“要加载的文件的完全限定路径”);
1.先加载一个指定程序集
Assembly asembly=Assembly.LoadFrom(“地址”); (如果有\,无法直接使用,可以使用\或者在整个句子前面加个@)
2.再加载程序集中的一个类对象,之后才能使用反射
Type icon=asembly.GetType(“Lesson18_练习题.ICON”);
MemberInfo[] menbers=icon.GetMembers();
通过反射 实例化一个 icon对象
首先得到枚举Type 来得到可以传入的参数
Type moveDir=asembly.GetType(“Lesson18_练习题.E_MoveDir”);
FieldInfo right=moveDir.GetField(“Right”);
//直接实例化对象
object iconObj= Activator.CreateInstance(icon,10,5,right.GetValue(null)); //由于枚举没对象,直接给个null即可
得到对象中的方法 通过反射
MethodInfo move=icon.GetMethond(“move”);
6.总结
反射
在程序运行时,通过反射可以得到其他程序集或者自己的程序集代码的各种信息
类,函数,变量,对象等等,实例化他们,执行他们,操作他们
关键类
Type
Assembly
Activator
二.特性
####1.特性是什么
特性是一种允许我们向程序的程序集添加元数据的语言结构
它是用于保存程序结构信息的某种特殊类型的类
特性提供功能强大的方法以将申明信息与C#代码(类型。方法,属性等)相关联
特性与程序实体关联后,即可在运行时使用反射查询特性信息
特性是目的是告诉编译器把程序结构的某组元数据嵌入程序集中
它可以放置在几乎所有的申明中(类,变量,函数等等声明)
说人话:
特性本质是个类
我们可以利用特性类为元数据添加额外信息
比如一个类,成员变量,成员方法等等为他们添加更多的额外信息
之后可以通过反射来获取这些额外信息
2.自定义特性
继承特性基类 Atrribute
class myCostomAttribute: Attribute
{
特性中的成员 一般根据需求来写
public string info;
}
3.特性的使用
基本语法:
[特姓名(参数列表)]
本质上 就是在调用特性类的构造函数
写在哪里?
类,函数,变量上一行,表示他们具有该特性信息
判断是否使用了某个特性
参数一:特性的类型
参数二:代表是否搜索继承链(属性和事件忽略此参数)
if(t.IsDefined(typeof(特性名)),false)
{
Console.writeLine(“该类型引用了此特性”)
}
获得Type元数据中的所有特性,参数代表是否搜索继承链。
Object[] array=t.GetCustomAttributes(true);
for(int i=0;i《array.Length;i++)
{
if(array[i] is MyCustomAttribute)
{
Console.WriteLine((array[i] as MyCustomAtrribute).info);
}
}
4.限制自定义特性的使用范围
通过为特性类 加特性 限制其使用范围
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
//参数一:AttributeTargets —— 特性能够用在哪些地方
//参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
//参数三:Inherited —— 特性是否能被派生类和重写成员继承
public class MyCustom2Attribute : Attribute
{
]
4.限制自定义特性的使用范围
通过为特性类 加特性 限制其使用范围
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
//参数一:AttributeTargets —— 特性能够用在哪些地方
//参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
//参数三:Inherited —— 特性是否能被派生类和重写成员继承
public class MyCustom2Attribute : Attribute
{
}
5.系统自带特性——过时特性
//过时特性
//Obsolete
//用于提示用户 使用的方法等成员已经过时 建议使用新方法
//一般加在函数前的特性
class TestClass
{
参数一:调用过时方法时 提示的内容
参数二:true-使用该方法时会报错 false-使用该方法时直接警告
[Obsolete(“OldSpeak方法已经过时了,请使用Speak方法”, false)]
public void OldSpeak(string str)
{
Console.WriteLine(str);
}
public void Speak()
{
}
public void SpeakCaller(string str, [CallerFilePath]string fileName = “”,
[CallerLineNumber]int line = 0, [CallerMemberName]string target = “”)
{
Console.WriteLine(str);
Console.WriteLine(fileName);
Console.WriteLine(line);
Console.WriteLine(target);
}
}
6.系统自带特性——调用者信息特性
//哪个文件调用?
//CallerFilePath特性
//哪一行调用?
//CallerLineNumber特性
//哪个函数调用?
//CallerMemberName特性
//需要引用命名空间 using System.Runtime.CompilerServices;
//一般作为函数参数的特性
7.系统自带特性——条件编译特性
//条件编译特性
//Conditional
//它会和预处理指令 #define配合使用
//需要引用命名空间using System.Diagnostics;
//主要可以用在一些调试代码上
//有时想执行有时不想执行的代码
8.系统自带特性——外部Dll包函数特性
//DllImport
//用来标记非 Net(C#)的函数,表明该函数在一个外部的DLL中定义。
//一般用来调用 C或者C++的Dll包写好的方法
//需要引用命名空间 using System.Runtime.InteropServices
总结:
//特性是用于 为元数据再添加更多的额外信息(变量、方法等等)
//我们可以通过反射获取这些额外的数据 来进行一些特殊的处理
//自定义特性——继承Attribute类
// 系统自带特性:过时特性
// 为什么要学习特性
// Unity引擎中很多地方都用到了特性来进行一些特殊处理
迭代器
1.迭代器是什么
迭代器有时又称光标
是程序设计的软件设计模式
迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素
而又不暴露其内部的表示
在表现效果上看
是可以在容器对象(例如链表或数组)上遍历访问的接口
设计人员无需关心容器对象的内存分配的实现细节
可以用foreach遍历的类,都是实现了迭代器的
2.标准迭代器的实现方法
关键接口:IEnumerator,IEnumerable
命名空间:using System.Collections;
可以通过同时继承IEnumerable和IEnumerator实现其中的方法(类似sort排序重载)
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 };
}
#region IEnumerable
public IEnumerator GetEnumerator()
{
Reset();
return this;
}
#endregion
public object Current
{
get
{
return list[position];
}
}
public bool MoveNext()
{
//移动光标
++position;
//是否溢出 溢出就不合法
return position 《 list.Length;
}
//reset是重置光标位置 一般写在获取 IEnumerator对象这个函数中
//用于第一次重置光标位置
public void Reset()
{
position = -1;
}
}
#endregion
3.用yield return 语法糖实现迭代器(较为简单)
yield return 是C#提供给我们的语法糖
所谓语法糖,也称糖衣语法
主要作用是将复杂逻辑简单化,可以增加程序的可读性
从而减少程序代码出错的机会
关键接口:IEnumerable
命名空间:using System.Collections;
让想要通过foreach遍历的自定义类实现接口中的方法GetEnumerator即可
例子:
class CustomList2 : IEnumerable
{
private int[] list;
public CustomList2()
{
list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i 《 list.Length; i++)
{
//yield关键字 配合迭代器使用
//可以理解为 暂时返回 保留当前的状态
//本质上还是动用了之前3个方法,不过C#官方动了手脚,运行的时候会帮你写
//C#的语法糖
yield return list[i];
}
//yield return list[0];
//yield return list[1];
//yield return list[2];
//yield return list[3];
//yield return list[4];
//yield return list[5];
//yield return list[6];
//yield return list[7];
}
}
4.用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];
}
}
}
总结
//迭代器就是可以让我们在外部直接通过foreach遍历对象中元素而不需要了解其结构
//主要的两种方式
1.传统方式: 继承两个接口 实现里面的方法
2.用语法糖: yield return 去返回内容 只需要继承一个接口即可
特殊语法
1.var隐式类型
var是一种特殊的变量类型
它可以用来表示任意类型的变量
注意:
1.var不能作为类的成员,只能用于临时变量申明时
也就是 一般写在函数语句块中
2.var必须初始化
2.设置对象初始值
申明对象时
可以通过直接写大括号的形式初始化公共成员变量和属性(语法糖)
Person p=new Person(100){sex=true;age=18;name=zhang};
3.设置集合初始值(其实之前经常用)
申明集合对象时
也可以通过大括号 直接初始化内部属性
int[] array2=new 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=“zhang”}
};
4.匿名类型
var 类型可以申明为自定义的匿名类型
var v=new {age=10,money=11,name=“小明”}
Console.WriteLine(v.age);
Console.WriteLine(v.name);
5.可空类型
1.值类型是不能赋值为空的
int c=null; (报错)
2.申明时 在值类型后面加? 可以赋值为空
int? c=null;
float? f=null;
double? d=null;
3.判断是否为空
if(c.HasValue)
{
}
4.安全获取可空类型值(为了安全起见,使用时最好保证不为空)
int? value=null;
4-1.如果为空 默认返回值类型的默认值
value.GetValueOrDefault();
4-2.也可以指定一个默认值
value.GetValueOrDafult(100);
5.?语法糖
//相当于一种语法糖 能够帮助我们自动去判断o是否为空,如果是null就不会执行tostring也不会报错
o?.tostring;
console.writeline(ArrayInt?[0]);
action?.Invoke();
上面即使为空,也不会报错,只会不打印或者调用方法
6.空合并操作符
空合并操作符 ??
左边值 ?? 右边值
如果左边值为null 就返回右边值 否则返回左边值
只要是可以为null的类型都能用
有一种三位运算符的感觉
int? intV=null;
int I=intv??100;
string a=a??100; //实际上??拿去给String比较合适,毕竟string可以置空,int却得申明时加?
7.内插字符串
关键符号:$
用来构造字符串,让字符串可以拼接变量
String name="张";
int age=18;
Console.WriteLine("好好学习,{name},年龄:{age}");
8.单句逻辑简略写法
//当循环或if语句中只有一句代码时 大括号可以省略
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
委托
值类型和引用类型的本质区别
值的++具体内容++在栈内存上
引用的++具体内容++在堆内存上(引用地址还是在栈内存上)
1.如何判定 值类型和引用类型
F12进到类型的内部去查看
是class就是引用
是struct就是值
2.语句块
命名空间
↓
类、接口、结构体
↓
函数、属性、索引器、运算符重载等(类、接口、结构体)
↓
条件分支、循环
上层语句块:类、结构体
中层语句块:函数
底层的语句块: 条件分支 循环等
我们的逻辑代码写在哪里?
函数、条件分支、循环-中底层语句块中
我们的变量可以申明在哪里?
上、中、底都能申明变量
上层语句块中:成员变量
中、底层语句块中:临时变量
3.变量的生命周期
编程时大部分都是 临时变量
在中底层申明的临时变量(函数、条件分支、循环语句块等)
语句块执行结束
没有被记录的对象将被回收或变成垃圾
值类型:被系统自动回收
引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收
int i2 = 1;
string str2 = "123";
//{
// int b = 1;
//}
//Console.WriteLine(b);
//while(true)
//{
// int index = 1;
//}
//想要不被回收或者不变垃圾
//必须将其记录下来
//如何记录?
//在更高层级记录或者
//使用静态全局变量记录
b = 0;
if(true)
{
b = 1;
}
int c = 10;
Test.TestI = c;
//Game g = new Game();
//while(true)
//{
//}
4.结构体中的值和引用
结构体本身是值类型
前提:该结构体没有做为其它类的成员
在结构体中的值,栈中存储值具体的内容
在结构体中的引用,堆中存储引用具体的内容
引用类型始终存储在堆中
真正通过结构体使用其中引用类型时只是顺藤摸瓜
即结构体中的引用类型的地址会和其他的值类型一起存在栈内存里(其实原来引用类型的地址就是存在栈内存里的。。。),但是引用类型依然还是指向堆内存
即只是拿了一个引用对象的拷贝出来调用,因此,C#也不允许你对结构体里面的引用类型进行修改。
5.类中的值和引用
类本身是引用类型
在类中的值,堆中存储具体的值
在类中的引用,堆中存储具体的值
值类型跟着大哥走,引用类型一根筋
即引用类型里面的引用,在堆内存里面会有一个引用地址,然后再指向另一个堆内存里面的实际内容
6.数组中的存储规则
数组本身是引用类型
值类型数组,堆中房间存具体内容
引用类型数组,堆中房间存地址
7.结构体继承接口
利用里氏替换原则,用接口容器装载结构体存在装箱拆箱
//可以对比下下面两个经过了装箱和未装箱的结果(本质上是值类型和引用类型的不同,这点之前的结构体相关内容有提过)
TestStruct obj1 = new TestStruct();
obj1.Value = 1;
Console.WriteLine(obj1.Value);
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;
Console.WriteLine(iObj1.Value);
Console.WriteLine(iObj2.Value);
TestStruct obj3 = (TestStruct)iObj1;//拆箱
排序进阶
一.插入排序
1.插入排序的基本原理
8 7 1 5 4 2 6 3 9
两个区域
排序区
未排序区
用一个索引值做分水岭
未排序区元素
与排序区元素比较
插入到合适位置
直到未排序区清空
2.代码实现
//实现升序 把 大的 放在最后面
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
//前提规则
//排序开始前
//首先认为第一个元素在排序区中
//其它所有元素在未排序区中
//排序开始后
//每次将未排序区第一个元素取出用于和
//排序区中元素比较(从后往前)
//满足条件(较大或者较小)
//则排序区中元素往后移动一个位置。
//注意
//所有数字都在一个数组中
//所谓的两个区域是一个分水岭索引
//第一步
//能取出未排序区的所有元素进行比较
//i=1的原因:默认第一个元素就在排序区
for (int i = 1; i < arr.Length; i++)
{
//第二步
//每一轮
//1.取出排序区的最后一个元素索引
int sortIndex = i - 1;
//2.取出未排序区的第一个元素
int noSortNum = arr[i];
//第三步
//在未排序区进行比较
//移动位置
//确定插入索引
//循环停止的条件
//1.发现排序区中所有元素都已经比较完
//2.发现排序区中的元素不满足比较条件了
while (sortIndex >= 0 &&
arr[sortIndex] > noSortNum) //注意,这里的条件顺序不可调换,否则会导致index越界bug(因为index有可能是-1)
{
//只要进了这个while循环 证明满足条件
//排序区中的元素 就应该往后退一格
arr[sortIndex + 1] = arr[sortIndex];
//移动到排序区的前一个位置 准备继续比较
--sortIndex;
}
//最终插入数字
//循环中知识在确定位置 和找最终的插入位置
//最终插入对应位置 应该循环结束后
arr[sortIndex + 1] = noSortNum;
}
for (int i = 0; i 《 arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
3.总结
为什么有两层循环
第一层循环:一次取出未排序区的元素进行排序
第二层循环:找到想要插入的位置
为什么第一层循环从1开始遍历
插入排序的关键是分两个区域
已排序区 和 未排序区
默认第一个元素在已排序区
为什么使用while循环
满足条件才比较
否则证明插入位置已确定
不需要继续循环
为什么可以直接往后移位置
每轮未排序数已记录
最后一个位置不怕丢
为什么确定位置后,是放在sortIndex + 1的位置
当循环停止时,插入位置应该是停止循环的索引加1处
基本原理
两个区域
用索引值来区分
未排序区与排序区
元素不停比较
找到合适位置
插入当前元素
套路写法
两层循环
一层获取未排序区元素
一层找到合适插入位置
注意事项
默认开头已排序
第二层循环外插入
二.希尔排序
1.希尔排序的基本原理
希尔排序是
插入排序的升级版
必须先掌握插入排序
希尔排序的原理
将整个待排序序列
分割成为若干子序列
分别进行插入排序
总而言之
希尔排序对插入排序的升级主要就是加入了一个步长的概念
通过步长每次可以把原序列分为多个子序列
对子序列进行插入排序
在极限情况下可以有效降低普通插入排序的时间复杂度
提升算法效率
2.代码实现
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
//学习希尔排序的前提条件
//先掌握插入排序
//第一步:实现插入排序
//第一层循环 是用来取出未排序区中的元素的
//for (int i = 1; i < arr.Length; i++)
//{
// //得出未排序区的元素
// int noSortNum = arr[i];
// //得出排序区中最后一个元素索引
// int sortIndex = i - 1;
// //进入条件
// //首先排序区中还有可以比较的 >=0
// //排序区中元素 满足交换条件 升序就是排序区中元素要大于未排序区中元素
// while (sortIndex >= 0 &&
// arr[sortIndex] > noSortNum)
// {
// arr[sortIndex + 1] = arr[sortIndex];
// --sortIndex;
// }
// //找到位置过后 真正的插入 值
// arr[sortIndex + 1] = noSortNum;
//}
//for (int i = 0; i < arr.Length; i++)
//{
// Console.WriteLine(arr[i]);
//}
//第二步:确定步长
//基本规则:每次步长变化都是/2
//一开始步长 就是数组的长度/2
//之后每一次 都是在上一次的步长基础上/2
//结束条件是 步长 <=0
//1.第一次的步长是数组长度/2 所以:int step = arr.length/2
//2.之后每一次步长变化都是/2 索引:step /= 2
//3.最小步长是1 所以:step > 0
for (int step = arr.Length / 2; step > 0; step/=2)
{
//注意:
//每次得到步长后 会把该步长下所有序列都进行插入排序
//第三步:执行插入排序
//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;
}
}
for (int i = 0; i 《 arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
3.总结
基本原理
设置步长
步长不停缩小
到1排序后结束
具体排序方式
插入排序原理
套路写法
三层循环
一层获取步长
一层获取未排序区元素
一层找到合适位置插入
注意事项
步长确定后
会将所有子序列进行插入排序
三.归并排序
1.归并排序基本原理
归并 = 递归 + 合并
数组分左右
左右元素相比较
满足条件放入新数组
一侧用完放对面
递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果
归并排序分成两部分
1.基本排序规则
2.递归平分数组
递归平分数组:
不停进行分割
长度小于2停止
开始比较
一层一层向上比
基本排序规则:
左右元素进行比较
依次放入新数组中
一侧没有了另一侧直接放入新数组
2.代码实现
//第一步:
//基本排序规则
//左右元素相比较
//满足条件放进去
//一侧用完直接放
public static int[] Sort(int[] left, int[] right)
{
//先准备一个新数组
int[] array = new int[left.Length + right.Length];
int leftIndex = 0;//左数组索引
int rightIndex = 0;//右数组索引
//最终目的是要填满这个新数组
//不会出现两侧都放完还在进循环
//因为这个新数组的长度 是根据左右两个数组长度计算出来的
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++;
}
}
//得到了新数组 直接返回出去
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));
}
3.总结
理解递归逻辑
一开始不会执行Sort函数的
要先找到最小容量数组时
才会回头递归调用Sort进行排序
基本原理
归并 = 递归 + 合并
数组分左右
左右元素相比较
一侧用完放对面
不停放入新数组
递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果
套路写法
两个函数
一个基本排序规则
一个递归平分数组
注意事项
排序规则函数 在 平分数组函数
内部 return调用
四.快速排序(背下来)
1.快速排序基本原理
选取基准
产生左右标识
左右比基准
满足则换位
排完一次
基准定位
左右递归
直到有序
2.代码实现
//第一步:
//申明用于快速排序的函数
public static void QuickSort(int[] array, int left, int right)
{
//第七步:
//递归函数结束条件
if (left >= right)
return;
//第二步:
//记录基准值
//左游标
//右游标
int tempLeft, tempRight, temp;
temp = array[left];
tempLeft = left;
tempRight = right;
//第三步:
//核心交换逻辑
//左右游标会不同变化 要不相同时才能继续变化
while(tempLeft != tempRight)
{
//第四步:比较位置交换
//首先从右边开始 比较 看值有没有资格放到表示的右侧
while (tempLeft < tempRight &&
array[tempRight] > temp)
{
tempRight--;
}
//移动结束证明可以换位置
array[tempLeft] = array[tempRight];
//上面是移动右侧游标
//接着移动完右侧游标 就要来移动左侧游标
while (tempLeft < tempRight &&
array[tempLeft] < temp)
{
tempLeft++;
}
//移动结束证明可以换位置
array[tempRight] = array[tempLeft];
}
//第五步:放置基准值
//跳出循环后 把基准值放在中间位置
//此时tempRight和tempLeft一定是相等的
array[tempRight] = temp;
//第六步:
//递归继续
QuickSort(array, left, tempRight - 1);
QuickSort(array, tempLeft + 1, right);
}
3.总结
归并排序和快速排序都会用到递归
两者的区别
相同点:
1.他们都会用到递归
2.都会把数组分成几部分
不同点:
1.归并排序递归过程中会不停产生新数组用于合并;快速排序不会产生新数组
2.归并排序是拆分数组完毕后再进行排序;快速排序是边排序边拆分
基本原理
选取基准
产生左右标识
左右比基准
满足则换位
排完一次 基准定位
基准左右递归
直到有序
套路写法
基准值变量
左右游标记录
3层while循环
游标不停左右移动
重合则结束
结束定基准
递归排左右
错位则结束
注意事项
左右互放
while循环外定基准
注意,上面的代码可能无法应付有重复数字的队列,如果需要预防这种情况需要给两个while循环里面的temp判定加个等号
五.堆排序(背下来)
1.堆排序基本原理
构建二叉树
大堆顶调整
堆顶往后方
不停变堆顶
关键规则
最大非叶子节点:
数组长度/2 - 1
父节点和叶子节点:
父节点为i
左节点2i+1
右节点2i+2
2.代码实现
//第一步:实现父节点和左右节点比较
/// <summary>
///
/// </summary>
/// <param name="array">需要排序的数组</param>
/// <param name="nowIndex">当前作为根节点的索引</param>
/// <param name="arrayLength">哪些位置没有确定</param>
static void HeapCompare(int[] array, int nowIndex, int arrayLength)
{
//通过传入的索引 得到它对应的左右叶子节点的索引
//可能算出来的会溢出数组的索引 我们一会再判断
int left = 2 * nowIndex + 1;
int right = 2 * nowIndex + 2;
//用于记录较大数的索引
int biggerIndex = nowIndex;
//先比左 再比右
//不能溢出
if( left < arrayLength && array[left] > array[biggerIndex])
{
//认为目前最大的是左节点 记录索引
biggerIndex = left;
}
//比较右节点
if( right < arrayLength && array[right] > array[biggerIndex] )
{
biggerIndex = right;
}
//如果比较过后 发现最大索引发生变化了 那就以为这要换位置了
if( biggerIndex != nowIndex )
{
int temp = array[nowIndex];
array[nowIndex] = array[biggerIndex];
array[biggerIndex] = temp;
//通过递归 看是否影响了叶子节点他们的三角关系
HeapCompare(array, biggerIndex, arrayLength);
}
}
//第二步:构建大堆顶
static void BuildBigHeap(int[] array)
{
//从最大的非叶子节点索引 开始 不停的往前 去构建大堆顶
for (int i = array.Length / 2 - 1; i >= 0 ; i--)
{
HeapCompare(array, i, array.Length);
}
}
//第三步:结合大堆顶和节点比较 实现堆排序 把堆顶不停往后移动
static void HeapSort(int[] array)
{
//构建大堆顶
BuildBigHeap(array);
//执行过后
//最大的数肯定就在最上层
//往屁股后面放 得到 屁股后面最后一个索引
for (int i = array.Length - 1; i > 0; i--)
{
//直接把 堆顶端的数 放到最后一个位置即可
int temp = array[0];
array[0] = array[i];
array[i] = temp;
//重新进行大堆顶调整
HeapCompare(array, 0, i);
}
}
3.总结
基本原理
构建二叉树
大堆顶调整
堆顶往后方
不停变堆顶
套路写法
3个函数
1个堆顶比较
1个构建大堆顶
1个堆排序
重要规则
最大非叶子节点索引:
数组长度/2 - 1
父节点和叶子节点索引:
父节点为i
左节点2i+1
右节点2i-1
注意:
堆是一类特殊的树
堆的通用特点就是父节点会大于或小于所有子节点
我们并没有真正的把数组变成堆
只是利用了堆的特点来解决排序问题