ArrayList 知识点

知识点一 ArrayList 的本质

ArrayList 是一个 C#为我们封装好的类,它的本质是一个 object 类型的数组。

ArrayList 类帮助我们实现了很多方法,比如数组的增删查改。

知识点二 声明

需要引用命名空间 using System.Collections;

1
ArrayList array = new ArrayList();

知识点三 增删查改

1
2
3
4
public virtual int Add(object? value);	//添加一个元素
public virtual void AddRange(ICollection c); //添加一个范围内的元素
public virtual void Insert(int index, object? value); //在index处插入指定元素
public virtual void InsertRange(int index, ICollection c); //在index处插入指定范围元素

1
2
3
4
public virtual void Remove(object? obj);	//删除指定元素
public virtual void RemoveAt(int index); //删除指定位置index的元素
public virtual void RemoveRange(int index, int count); //删除指定位置index开始的count个元素
public virtual void Clear(); //清空容器

1
2
3
4
public virtual object? this[int index] { get; set; }	//索引器
public virtual bool Contains(object? item); //查找item是否存在于容器中
public virtual int IndexOf(object? value); //正向查找value的位置
public virtual int LastIndexOf(object? value); //反向查找value的位置

1
array[0] = "999";	//直接使用索引器进行修改

遍历

1
2
3
4
5
6
7
public virtual int Count { get; }	//属性,返回元素个数
public virtual int Capacity { get; set; } //属性,返回容量
//迭代器遍历
foreach (object item in array)
{
Console.WriteLine(item);
}

知识点四 装箱拆箱

ArrayList 本质上是一个可以自动扩容的 object 数组,由于用万物之父来存储数据,自然存在装箱拆箱

当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时,就存在拆箱

所以 ArrayList 尽量少用,之后我们会学习更好的数据容器。

1
2
3
int k = 1;
array[0] = k;//装箱
k = (int)array[0];//拆箱

练习题一:请简述 ArrayList 和数组的区别

ArrayList 本质上是一个 object 数组的封装

  1. ArrayList 可以不用一开始就定长,单独使用数组是定长的
  2. 数组可以指定存储类型,ArrayList 默认为 object 类型
  3. 数组的增删需要我们自己去实现,ArrayList 帮我们封装了方便的 API 来使用
  4. ArrayList 使用时可能存在装箱拆箱,数组使用时只要不是 object 数组那就不存在这个问题
  5. 数组长度为 Length, ArrayList 长度为 Count

Stack 知识点

知识点一 Stack 的本质

Stack(栈)是一个 C#为我们封装好的类

它的本质也是 object[]数组,只是封装了特殊的存储规则

Stack 是栈存储容器,栈是一种先进后出的数据结构

先存入的数据后获取,后存入的数据先获取

栈是先进后出

知识点二 声明

需要引用命名空间 System.Collections

1
Stack stack = new Stack();

知识点三 增取查改

1
public virtual void Push(object obj);	//入栈

栈中不存在删除的概念,只有取出栈的概念

1
public virtual object Pop();	//出栈,删除栈顶元素,并返回这个元素

1
2
3
//栈无法查看指定位置的 元素,只能查看栈顶的内容
public virtual object Peek(); //返回栈顶元素,但是不删除
public virtual bool Contains(object obj); //查看元素是否存在于栈中

栈无法改变其中的元素 只能压(存)和弹(取),实在要改 只有清空

1
public virtual void Clear();	//清空栈

知识点四 遍历

长度

1
public virtual int Count { get; }	//属性,返回栈中元素个数

用 foreach 遍历,遍历出来的顺序 也是从栈顶到栈底

1
2
3
4
foreach(object item in stack)
{
Console.WriteLine(item);
}

还有一种遍历方式,将栈转换为 object 数组,遍历出来的顺序 也是从栈顶到栈底

1
2
3
4
5
object[] array = stack.ToArray();
for (int i = 0; i < array.Length; i++)
{
Console.WriteLine(array[i]);
}

循环弹栈遍历

1
2
3
4
5
6
while( stack.Count > 0 )
{
object o = stack.Pop();
Console.WriteLine(o);
}
Console.WriteLine(stack.Count);

知识点五 装箱拆箱

由于用万物之父来存储数据,自然存在装箱拆箱。

当往其中进行值类型存储时就是在装箱。

当将值类型对象取出来转换使用时,就存在拆箱。

Queue 知识点

知识点一 Queue 本质

Queue 是一个 C#为我们封装好的类,它的本质也是 object[]数组,只是封装了特殊的存储规则

Queue 是队列存储容器,队列是一种先进先出的数据结构,先存入的数据先获取,后存入的数据后获取。

先进先出

知识点二 声明

需要引用命名空间 System.Collections

1
Queue queue = new Queue();

知识点三 增取查改

1
public virtual void Enqueue(object obj);	//入队

队列中不存在删除的概念,只有取的概念 取出先加入的对象

1
public virtual object Dequeue();	//取出队头元素并删除,然后返回

1
2
3
//查看队列头部元素但不会移除
public virtual object Peek(); //取出队头元素但不删除,然后返回
public virtual bool Contains(object obj); //查看元素是否存在于队列中

队列无法改变其中的元素 只能进出队列,实在要改 只有清空

1
public virtual void Clear();

知识点四 遍历

长度

1
public virtual int Count { get; }

用 foreach 遍历

1
2
3
4
foreach (object item in queue)
{
Console.WriteLine(item);
}

将队列转换为 object 数组遍历

1
2
3
4
5
object[] array = queue.ToArray();
for (int i = 0; i < array.Length; i++)
{
Console.WriteLine(array[i]);
}

循环出队遍历

1
2
3
4
5
6
while(queue.Count>0)
{
object o = queue.Dequeue();
Console.WriteLine(o);
}
Console.WriteLine(queue.Count);

知识点五 装箱拆箱

由于用万物之父来存储数据,自然存在装箱拆箱。

当往其中进行值类型存储时就是在装箱。

当将值类型对象取出来转换使用时,就存在拆箱。

Hashtable 知识点

知识点一 Hashtalbe 的本质

Hashtable(又称散列表) 是基于键的哈希代码组织起来的 键/值对

它的主要作用是提高数据查询的效率

使用键来访问集合中的元素

知识点二 声明

需要引用命名空间 System.Collections

1
Hashtable hashtable = new Hashtable();

知识点三 增删查改

注意:不能出现相同键

1
public virtual void Add(object key, object? value);	//添加一组键值对

删:只能通过键去删除;删除不存在的键 没反应;或者直接清空

1
2
public virtual void Remove(object key);
public virtual void Clear();

1
2
3
4
5
6
public virtual object? this[object key] { get; set; }	//索引器,通过键查找值
//查看是否存在,根据键检测,找不开返回null
public virtual bool Contains(object key);
public virtual bool ContainsKey(object key);
//根据值检测
public virtual bool ContainsValue(object? value);

改:只能改 键对应的值内容 无法修改键

1
hashtable[1] = 100.5f;	//通过索引器修改

知识点四 遍历

得到键值对 对数

1
public virtual int Count { get; }	//属性,返回键值对的个数

遍历所有键

1
2
3
4
5
foreach (object item in hashtable.Keys)
{
Console.WriteLine("键:"+item);
Console.WriteLine("值:"+hashtable[item]);
}

遍历所有值

1
2
3
4
foreach (object item in hashtable.Values)
{
Console.WriteLine("值:" + item);
}

键值对一起遍历

1
2
3
4
foreach (DictionaryEntry item in hashtable)
{
Console.WriteLine("键:" + item.Key + "值:" + item.Value);
}

迭代器遍历法

1
2
3
4
5
6
7
IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator();
bool flag = myEnumerator.MoveNext();
while (flag)
{
Console.WriteLine("键:" + myEnumerator.Key + "值:" + myEnumerator.Value);
flag = myEnumerator.MoveNext();
}

知识点五 装箱拆箱

由于用万物之父来存储数据,自然存在装箱拆箱

当往其中进行值类型存储时就是在装箱

当将值类型对象取出来转换使用时,就存在拆箱

泛型 知识点

知识点一 泛型是什么

泛型实现了类型参数化,达到代码重用目的

通过类型参数化来实现同一份代码上操作多种类型

泛型相当于类型占位符

定义类或方法时使用替代符代表变量类型

当真正使用类或者方法时再具体指定类型

知识点二 泛型分类

泛型类和泛型接口

基本语法:

1
2
class 类名<泛型占位字母>
interface 接口名<泛型占位字母>

泛型函数

1
//基本语法:函数名<泛型占位字母>(参数列表)

注意:泛型占位字母可以有多个,用逗号分开

知识点三 泛型类和接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TestClass<T>
{
public T value;
}

class TestClass2<T1,T2,K,M,LL,Key,Value>
{
public T1 value1;
public T2 value2;
public K value3;
public M value4;
public LL value5;
public Key value6;
public Value value7;
}

interface TestInterface<T>
{
T Value
{
get;
set;
}
}

//注意,继承泛型接口,必须指定类型
class Test : TestInterface<int>
{
public int Value { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
}
1
2
3
4
5
6
7
8
9
10
TestClass<int> t = new TestClass<int>();
t.value = 10;
Console.WriteLine(t.value);

TestClass<string> t2 = new TestClass<string>();
t2.value = "123123";
Console.WriteLine(t2.value);

TestClass2<int, string, float, double, TestClass<int>, uint, short> t3 = new TestClass2<int, string, float, double, TestClass<int>, uint, short>();

知识点四 泛型方法

普通类中的泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test2
{
public void TestFun<T>( T value)
{
Console.WriteLine(value);
}

public void TestFun<T>()
{
//用泛型类型 在里面做一些逻辑处理
T t = default(T);
}

public T TestFun<T>(string v)
{
return default(T);
}

public void TestFun<T,K,M>(T t, K k, M m)
{

}
}

泛型类中的泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test2<T>
{
public T value;

public void TestFun<K>(K k)
{
Console.WriteLine(k);
}

//这个不叫泛型方法 因为 T是泛型类申明的时候 就指定 在使用这个函数的时候
//我们不能再去动态的变化了
public void TestFun(T t)
{

}
}
1
2
3
4
5
6
7
8
Test2 tt = new Test2();
tt.TestFun<string>("123123");

Test2<int> tt2 = new Test2<int>();
tt2.TestFun(10);
tt2.TestFun<string>("123");
tt2.TestFun<float>(1.2f);
tt2.TestFun(20);

知识点五 泛型的作用

  1. 不同类型对象的相同逻辑处理就可以选择泛型
  2. 使用泛型可以一定程度避免装箱拆箱

举例:优化 ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ArrayList<T>
{
private T[] array;

public void Add(T value)
{

}

public void Remove( T value)
{

}
}

总结

  1. 声明泛型时 它只是一个类型的占位符
  2. 泛型真正起作用的时候 是在使用它的时候
  3. 泛型占位字母可以有 n 个用逗号分开
  4. 泛型占位字母一般是大写字母
  5. 不确定泛型类型时 获取默认值 可以使用 default(占位字符)
  6. 看到<>包裹的字母 那肯定是泛型

泛型约束 知识点

知识点一 什么是泛型约束

让泛型的类型有一定的限制,关键字:where

泛型约束一共有 6 种:

约束 关键字
值类型 where 泛型字母:struct
引用类型 where 泛型字母:class
存在无参公共构造函数 where 泛型字母:new()
某个类本身或者其派生类 where 泛型字母:类名
某个接口的派生类型 where 泛型字母:接口名
另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母

知识点二 各泛型约束讲解

值类型约束

约束类型 T 必须是值类型

1
2
3
4
5
6
7
8
9
class Test1<T> where T:struct
{
public T value;

public void TestFun<K>(K v) where K:struct
{

}
}

引用类型约束

约束类型 T 必须是引用类型

1
2
3
4
5
6
7
8
9
class Test2<T> where T:class
{
public T value;

public void TestFun<K>(K k) where K:class
{

}
}

公共无参构造约束

约束类型 T 必须拥有公告无参构造函数(即可实例化,可 new)

1
2
3
4
5
6
7
8
9
class Test3<T> where T:new()
{
public T value;

public void TestFun<K>(K k) where K : new()
{

}
}

类约束

约束类型 T 必须是某个类或者这个类的子类

1
2
3
4
5
6
7
8
9
class Test4<T> where T : Test1
{
public T value;

public void TestFun<K>(K k) where K : Test1
{

}
}

接口约束

约束类型必须是这个接口的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface IFly
{

}

interface IMove:IFly
{

}

class Test4:IFly
{

}

class Test5<T> where T : IFly
{
public T value;

public void TestFun<K>(K k) where K : IFly
{

}
}

另一个泛型约束

约束类型 T 是 U 本身或者 U 的子类

1
2
3
4
5
6
7
8
9
class Test6<T,U> where T : U
{
public T value;

public void TestFun<K,V>(K k) where K : V
{

}
}

知识点三 约束的组合使用

约束可以组合使用,但有时不能同时存在两个约束,或者两个约束的顺序有次序

比如,new()约束一般写在其他约束前面。

1
2
3
4
class Test7<T> where T: class,new()
{

}

知识点四 多个泛型有约束

1
2
3
4
class Test8<T,K> where T:class,new() where K:struct
{

}

总结

泛型约束:让类型有一定限制

六种约束:class,struct,new(),类名,接口名,另一个泛型字母

注意:

  1. 可以组合使用
  2. 多个泛型约束 用 where 连接即可

List 知识点

知识点一 List 的本质

List 是一个 C#为我们封装好的类,它的本质是一个可变类型的泛型数组。

List 类帮助我们实现了很多方法,比如泛型数组的增删查改。

知识点二 声明

需要引用命名空间

1
using System.Collections.Generic;
1
2
3
List<int> list = new List<int>();
List<string> list2 = new List<string>();
List<bool> list3 = new List<bool>();

知识点三 增删查改

1
2
3
public void Add(T item);	//添加一个元素
public void AddRange(IEnumerable<T> collection); //添加一个范围内的元素
public void Insert(int index, T item); //在index处插入item

1
2
3
public bool Remove(T item);
public void RemoveAt(int index);
public void Clear();

1
2
3
4
public T this[int index] { get; set; }	//索引器
public bool Contains(T item);
public int IndexOf(T item);
public int LastIndexOf(T item);

改:使用索引器修改

知识点四 遍历

长度和容量

1
2
public int Count { get; }
public int Capacity { get; set; }

直接遍历

1
2
3
4
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}

foreach 遍历

1
2
3
4
foreach (int item in list)
{
Console.WriteLine(item);
}

Dictionary 知识点

知识点一 Dictionary 的本质

可以将 Dictionary 理解为 拥有泛型的 Hashtable;

它也是基于键的哈希代码组织起来的 键/值对;

键值对类型从 Hashtable 的 object 变为了可以自己制定的泛型。

知识点二 声明

需要引用命名空间 using System.Collections.Generic

1
Dictionary<int, string> dictionary = new Dictionary<int, string>();

知识点三 增删查改

注意:不能出现相同键

1
public void Add(TKey key, TValue value);

删:只能通过键去删除,删除不存在键 没反应。

1
2
public bool Remove(TKey key);
public void Clear(); //清空

1
2
3
4
5
6
//通过键查看值,找不到直接报错,和HashTable不一样,不会返回null了
public TValue this[TKey key] { get; set; } //索引器
//查看是否存在,根据键检测
public bool ContainsKey(TKey key);
//根据值检测
public bool ContainsValue(TValue value);

改:通过索引器修改

知识点四 遍历

遍历所有键

1
2
3
4
5
foreach (int item in dictionary.Keys)
{
Console.WriteLine(item);
Console.WriteLine(dictionary[item]);
}

遍历所有值

1
2
3
4
foreach (string item in dictionary.Values)
{
Console.WriteLine(item);
}

键值对一起遍历

1
2
3
4
foreach (KeyValuePair<int,string> item in dictionary)
{
Console.WriteLine("键:" + item.Key + "值:" + item.Value);
}

顺序存储和链式存储 知识点

知识点一 数据结构

数据结构:

数据结构是计算机存储、组织数据的方式(规则);

数据结构是指相互之间存在一种或多种特定关系的数据元素的集合;

比如自定义的一个 类 也可以称为一种数据结构 自己定义的数据组合规则

不要把数据结构想的太复杂,简单点理解,就是人定义的 存储数据 和 表示数据之间关系 的规则而已

常用的数据结构(前辈总结和制定的一些经典规则)

数组、栈、队列、链表、树、图、堆、散列表

知识点二 线性表

线性表是一种数据结构,是由 n 个具有相同特性的数据元素的有限序列

比如数组、ArrayList、Stack、Queue、链表等等

顺序存储和链式存储 是数据结构中两种 存储结构

知识点三 顺序存储

数组、Stack、Queue、List、ArrayList —— 顺序存储

只是 数组、Stack、Queue 的 组织规则不同而已

顺序存储:用一组地址连续的存储单元依次存储线性表的各个数据元素

知识点四 链式存储

单向链表、双向链表、循环链表 —— 链式存储

链式存储(链接存储):用一组任意的存储单元存储线性表中的各个数据元素

知识点五 自己实现一个最简单的单向链表

这里实现的链表有 bug,没有处理移除尾结点的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/// <summary>
/// 单向链表节点
/// </summary>
/// <typeparam name="T"></typeparam>
class LinkedNode<T>
{
public T value;
//这个存储下一个元素是谁 相当于钩子
public LinkedNode<T> nextNode;

public LinkedNode(T value)
{
this.value = value;
}
}

/// <summary>
/// 单向链表类 管理 节点 管理 添加等等
/// </summary>
/// <typeparam name="T"></typeparam>
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;
}
}
}
}

知识点六 顺序存储和链式存储的优缺点

从增删查改的角度去思考:

增:链式存储 计算上 优于顺序存储 (中间插入时链式不用像顺序一样去移动位置)

删:链式存储 计算上 优于顺序存储 (中间删除时链式不用像顺序一样去移动位置)

查:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)

改:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)

练习题三-实现双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class LinkedNode<T>
{
public T value;
public LinkedNode<T> frontNode;
public LinkedNode<T> nextNode;

public LinkedNode(T value)
{
this.value = value;
}
}

class LinkedList<T>
{
private int count = 0;
private LinkedNode<T> head;
private LinkedNode<T> last;

public int Count
{
get
{
return count;
}
}
public LinkedNode<T> Head
{
get
{
return head;
}
}
public LinkedNode<T> Last
{
get
{
return last;
}
}

public void Add(T value)
{
//新加节点
LinkedNode<T> node = new LinkedNode<T>(value);
if( head == null )
{
head = node;
last = node;
}
else
{
//添加到尾部
last.nextNode = node;
//尾部添加的节点 记录自己的上一个节点是谁
node.frontNode = last;
//让当前新加的变成最后一个节点
last = node;
}
//加了一个节点
++count;
}

public void RemoveAt(int index)
{
//首先判断 有没有越界
if( index >= count || index < 0)
{
Console.WriteLine("只有{0}个节点,请输入合法位置", count);
return;
}
int tempCount = 0;
LinkedNode<T> tempNode = head;
while (true)
{
//找到了对应位置的节点 然后移除即可
if ( tempCount == index )
{
//当前要移除的节点的上一个节点 指向自己的下一个节点
if( tempNode.frontNode != null )
{
tempNode.frontNode.nextNode = tempNode.nextNode;
}
if(tempNode.nextNode != null)
{
tempNode.nextNode.frontNode = tempNode.frontNode;
}
//如果是头节点 那需要改变头节点的指向
if (index == 0)
{
//如果头节点被移除 那头节点就变成了头节点的下一个
head = head.nextNode;
}
else if( index == count - 1 )
{
//如果尾节点被移除了 那尾结点就变成了尾结点的上一个
last = last.frontNode;
}
//移除了一个元素 就应该短一截
--count;
break;
}
//每次循环完过后 要让当前临时节点 等于下一个节点
tempNode = tempNode.nextNode;
++tempCount;
}
}
}

LinkedList 知识点

知识点一 LinkedList

LinkedList 是一个 C#为我们封装好的类,它的本质是一个可变类型的泛型双向链表

知识点二 声明

需要引用命名空间

1
using System.Collections.Generic;
1
2
LinkedList<int> linkedList = new LinkedList<int>();
LinkedList<string> linkedList2 = new LinkedList<string>();

链表对象 需要掌握两个类

一个是链表本身 一个是链表节点类 LinkedListNode

知识点三 增删查改

1
2
3
4
public LinkedListNode<T> AddLast(T value);	//尾部添加元素
public LinkedListNode<T> AddFirst(T value); //头部添加元素
public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value); //在某个节点后添加元素
public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value);//在某个节点前添加元素

1
2
3
4
5
public void RemoveFirst();	//移除头结点
public void RemoveLast(); //移除尾结点
public bool Remove(T value); //移除指定元素
public void Remove(LinkedListNode<T> node); //移除指定节点
public void Clear(); //清空元素

1
2
3
4
5
6
7
public LinkedListNode<T>? First { get; }	//头结点
public LinkedListNode<T>? Last { get; } //尾结点
//找到第一个指定值的节点,无法直接通过下标获取中间元素,只有遍历查找指定位置元素
public LinkedListNode<T>? Find(T value);
//找到最后一个指定值的节点
public LinkedListNode<T>? FindLast(T value);
public bool Contains(T value); //判断是否存在

改:要先得再改 得到节点 再改变其中的值

1
2
3
Console.WriteLine(linkedList.First.Value);
linkedList.First.Value = 10;
Console.WriteLine(linkedList.First.Value);

知识点四 遍历

LinkedListNode

1
2
public LinkedListNode<T>? Next { get; }	//属性,上一个节点
public LinkedListNode<T>? Previous { get; } //属性,下一个节点

foreach 遍历

1
2
3
4
foreach (int item in linkedList)
{
Console.WriteLine(item);
}

通过节点遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//从头到尾
LinkedListNode<int> nowNode = linkedList.First;
while (nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Next;
}
//从尾到头
nowNode = linkedList.Last;
while (nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Previous;
}

泛型栈和队列 知识点

知识点一 回顾数据容器

变量

1
2
3
4
5
6
7
8
//无符号
//byte ushort uint ulong
//有符号
//sbyte short int long
//浮点数
//float double decimal
//特殊
//char bool string

复杂数据容器

1
2
3
4
//枚举 enum
//结构体 struct
//数组(一维、二维、交错) [] [,] [][]
//类

数据集合(容器)

1
2
3
4
5
6
//using System.Collections;

//ArrayList object数据列表
//Stack 栈 先进后出
//Queue 队列 先进先出
//Hashtable 哈希表 键值对

泛型数据集合

1
2
3
4
5
6
7
//using System.Collections.Generic;

//List 列表 泛型列表
//Dictionary 字典 泛型哈希表
//LinkedList 双向链表
//Statck 泛型栈
//Queue 泛型队列

知识点二 泛型栈和队列

命名空间:using System.Collections.Generic;

使用上 和之前的 Stack 和 Queue 一模一样

1
2
Stack<int> stack = new Stack<int>();
Queue<object> queue = new Queue<object>();

练习题

自己总结一下,数组、List、Dictionary、Stack、Queue、LinkedList

这些存储容器,对于我们来说应该如何选择他们来使用

普通线性表:
数组,List,LinkedList
数组:固定的不变的一组数据
List: 经常改变,经常通过下标查找
LinkedList:不确定长度的,经常临时插入改变,查找不多

先进后出:
Stack
对于一些可以利用先进后出存储特点的逻辑
比如:UI 面板显隐规则

先进先出:
Queue
对于一些可以利用先进先出存储特点的逻辑
比如:消息队列,有了就往里放,然后慢慢依次处理

键值对:
Dictionary
需要频繁查找的,有对应关系的数据
比如一些数据存储 id 对应数据内容
道具 ID ——> 道具信息
怪物 ID ——> 怪物对象
等等

委托 知识点

C# 的委托与事件具体是怎么一回事_哔哩哔哩_bilibili

知识点一 委托是什么

委托是 函数(方法)的容器 ,可以理解为表示函数(方法)的变量类型

用来 存储、传递函数(方法)

委托的本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)

不同的 函数(方法)必须对应和各自”格式”一致的委托

知识点二 基本语法

关键字 : delegate
语法:访问修饰符 delegate 返回值 委托名(参数列表);

写在哪里?

可以声明在 namespace 和 class 语句块中

更多的写在 namespace 中

简单记忆委托语法 就是 函数申明语法前面加一个 delegate 关键字

知识点三 定义自定义委托

访问修饰默认不写 为 public 在别的命名空间中也能使用

private 其它命名空间就不能用了,一般使用 public

1
2
3
4
5
6
7
8
9
10
//申明了一个可以用来存储无参无返回值函数的容器
//这里只是定义了规则 并没有使用
delegate void MyFun();

//委托规则的申明 是不能重名(同一语句块中)
//表示用来装载或传递 返回值为int 有一个int参数的函数的 委托 容器规则
public delegate int MyFun2(int a);

//委托是支持 泛型的 可以让返回值和参数 可变 更方便我们的使用
delegate T MyFun3<T, K>(T v, K k);

知识点四 使用定义好的委托

记住,委托变量是函数的容器

注意:委托变量只能装载与本委托声明时,格式一致的函数。这里的格式是指返回值与参数

委托常用在:

  1. 作为类的成员
  2. 作为函数的参数

直接使用委托变量:

注意:使用委托,会将容器中的所有函数全部都调用,调用的顺序是添加的顺序

1
2
3
4
//定义了一个委托变量f,它的格式是无参无返回值
MyFun f = new MyFun(Fun); //Fun是一个无参无返回值的函数名
f.Invoke(); //调用了Fun
f(); //调用了Fun

知识点五 委托变量可以存储多个函数(多播委托)

增加委托:

1
2
3
4
5
6
public void AddFun(MyFun fun, MyFun2 fun2)
{
//fun是一个委托变量
this.fun += fun; //直接加
this.fun2 += fun2;
}

注意:也可以直接加一个函数

1
2
3
4
5
6
MyFun ff = null;
//ff = ff + Fun;
ff += Fun;
ff += Fun3;
//注意,不能在初始化时直接+
//MyFun ff += Fun; //error

删除委托:

同样的,可以直接减一个函数,指定删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void RemoveFun(MyFun fun, MyFun2 fun2)
{
//this.fun = this.fun - fun;
this.fun -= fun; //直接减
this.fun2 -= fun2;
}

//从容器中移除指定的函数
ff -= Fun;
//多减 不会报错 无非就是不处理而已
ff -= Fun;
ff();
//清空容器
ff = null;

知识点六 系统定义好的委托

使用系统自带委托 需要引用 using System;

参数修饰符:in 协变为参数,out 逆变为返回值。

Action:无返回值

1
2
3
4
5
6
Action action = Fun;
action += Fun3;
action();
//可以传n个参数的 系统提供了 1到16个参数的委托 直接用就行了
//定义了一个Action委托变量,这个委托的参数有两个
Action<int, string> action2 = Fun6;

Func:有返回值,最后一个类型为返回值。

1
2
3
//定义了一个Func委托变量,这个委托的参数有两个
Func<int, string> funcString = Fun4;
Func<int> funcInt = Fun5;

总结

简单理解 委托 就是装载、传递函数的容器而已

可以用委托变量 来存储函数或者传递函数的

系统其实已经提供了很多委托给我们用

Action:没有返回值,参数提供了 0~16 个委托给我们用

Func:有返回值,参数提供了 0~16 个委托给我们用

事件 知识点

知识点一 事件是什么

事件是基于委托的存在,事件是委托的安全包裹;

让委托的使用更具有安全性,事件 是一种特殊的变量类型。

可以说,事件,就是类中的委托。

知识点二 事件的使用

声明语法:

1
访问修饰符 event 委托类型 事件名;

事件的使用:

  1. 事件是作为 成员变量存在于类中
  2. 委托怎么用 事件就怎么用

事件相对于委托的区别:

  1. 不能在类外部 赋值,即不能使用=,但是可以在类外部+=,-=
  2. 不能再类外部 调用,但是可以通过封装再调用
  3. 事件 是不能作为临时变量在函数中使用的!

注意:它只能作为成员存在于类和接口以及结构体中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Test
{
//委托成员变量 用于存储 函数的
public Action myFun;
//事件成员变量 用于存储 函数的
public event Action myEvent;

public Test()
{
//事件的使用和委托 一模一样 只是有些 细微的区别
myFun = TestFun;
myFun += TestFun;
myFun -= TestFun;
myFun();
myFun.Invoke();
myFun = null;

myEvent = TestFun;
myEvent += TestFun;
myEvent -= TestFun;
myEvent();
myEvent.Invoke();
myEvent = null;
}

public void DoEvent()
{
if(myEvent != null)
{
myEvent();
}
}

public void TestFun()
{
Console.WriteLine("123");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//事件是不能再外部赋值的
//t.myEvent = null;
//t.myEvent = TestFun;
//t.myEvent = t.myEvent + TestFun; //注意,这样也不行
//虽然不能直接赋值 但是可以 加减 去添加移除记录的函数
t.myEvent += TestFun;
t.myEvent -= TestFun;

//事件不能在外部调用
//t.myEvent();
//只能在类的内部去封装 调用
t.DoEvent();

//事件 是不能作为临时变量在函数中使用的
//event Action ae = TestFun;

知识点三 为什么有事件

  1. 防止外部随意置空委托
  2. 防止外部随意调用委托
  3. 事件相当于对委托进行了一次封装 让其更加安全

总结

事件和委托的区别:事件和委托的使用基本是一模一样的,事件就是特殊的委托(类中的委托)

主要区别:

  1. 事件不能再外部使用赋值=符号,只能使用+ - 委托 哪里都能用
  2. 事件 不能再外部执行 委托哪里都能执行
  3. 事件 不能作为 函数中的临时变量的 委托可以

匿名函数 知识点

知识点一 什么是匿名函数

顾名思义,就是没有名字的函数

匿名函数的使用主要是配合委托和事件进行使用

脱离委托和事件 是不会使用匿名函数的

知识点二 基本语法

1
2
3
4
delegate (参数列表)
{
//函数逻辑
};

何时使用?

  1. 函数中传递委托参数时
  2. 委托或事件赋值时

知识点三 使用

它的使用就是委托或者事件的使用,区别是,一声明匿名函数就必须保存到委托或者事件中。

无参无返回

注意:由于匿名函数是匿名的,所以,要使用它,必须保存到委托或者事件中!

这样声明匿名函数 只是在声明函数而已 还没有调用

真正调用它的时候 是这个委托容器啥时候调用 就什么时候调用这个匿名函数

1
2
3
4
5
6
Action A = delegate ()
{
Console.WriteLine("匿名函数逻辑");
};

A();

有参

1
2
3
4
5
6
7
Action<int, string> b = delegate (int a, string b)
{
Console.WriteLine(a);
Console.WriteLine(b);
};

b(100, "123");

有返回值

注意,这个返回值是通过委托 Func 控制的

1
2
3
4
5
6
Func<string> c = delegate ()
{
return "123123";
};

Console.WriteLine(c());

一般使用情况

一般情况会作为函数参数传递 或者 作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Test
{
public Action action;

//作为参数传递时
public void Dosomthing(int a, Action fun)
{
Console.WriteLine(a);
fun();
}

//作为返回值
public Action GetFun()
{
return delegate() {
Console.WriteLine("函数内部返回的一个匿名函数逻辑");
};
}

public void TestTTTT()
{

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//作函数参数
Test t = new Test();
Action ac = delegate ()
{
Console.WriteLine("随参数传入的匿名函数逻辑");
};
t.Dosomthing(50, ac);

// 返回值
Action ac2 = t.GetFun();
ac2();
//一步到位 直接调用返回的 委托函数
t.GetFun()(); //这里调用GetFun返回了一个匿名函数,然后调用了他

知识点四 匿名函数的缺点

添加到委托或事件容器中后 不记录 无法单独移除

因为匿名函数没有名字 所以没有办法指定移除某一个匿名函数

注意:同样逻辑的匿名函数,不是同一个函数。

1
2
3
4
5
//此匿名函数 非彼匿名函数 不能通过看逻辑是否一样就证明是一个
//ac3 -= delegate ()
//{
// Console.WriteLine("匿名函数一");
//};

总结

匿名函数 就是没有名字的函数

固定写法:

1
delegate(参数列表){}

使用:主要是在 委托传递和存储时 为了方便可以直接使用该匿名函数

缺点是 没有办法指定移除

lambda 表达式 知识点

知识点一 什么是 lambda 表达式

可以将 lambad 表达式 理解为匿名函数的简写。

它除了写法不同外,使用上和匿名函数一模一样,都是和委托或者事件 配合使用的。

知识点二 lambda 表达式语法

1
2
3
4
5
6
7
8
9
10
11
//匿名函数
//delegate (参数列表)
//{

//};

//lambad表达式
//(参数列表) =>
//{
// //函数体
//};

知识点三 使用

无参无返回

1
2
3
4
5
Action a = () =>
{
Console.WriteLine("无参无返回值的lambda表达式");
};
a();

有参

1
2
3
4
5
Action<int> a2 = (int value) =>
{
Console.WriteLine("有参数lambda表达式{0}", value);
};
a2(100);

甚至参数类型都可以省略 参数类型和委托或事件容器一致

1
2
3
4
5
Action<int> a3 = (value) =>
{
Console.WriteLine("省略参数类型的写法{0}", value);
};
a3(200);

有返回值

1
2
3
4
5
6
Func<string, int> a4 = (value) =>
{
Console.WriteLine("有返回值有参数的lambda表达式{0}", value);
return 1;
};
Console.WriteLine(a4("123123"));

其它传参使用等和匿名函数一样
缺点也是和匿名函数一样的

知识点四 闭包

内层的函数可以引用包含在它外层的函数的变量;

即使外层函数的执行已经终止

注意:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Test
{
public event Action action;

public Test()
{
int value = 10;
//这里就形成了闭包
//因为 当构造函数执行完毕时 其中申明的临时变量value的声明周期被改变了
action = () =>
{
Console.WriteLine(value);
};

for (int i = 0; i < 10; i++)
{
//此index 非彼index
int index = i;
action += () =>
{
//注意,这里的index值是10,因为i的最终值是10
Console.WriteLine(index);
};
}
}

public void DoSomthing()
{
action();
}
}

注意,上方程序中的 index 值是 10,因为 i 的最终值是 10

委托 补充知识点

有返回值的委托存储多个函数 调用时如何获取多个返回值?

问题:当有返回值的委托容器,存储多个函数时,我们想要获取所有的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Func<string> funTest = () => {
Console.WriteLine("第一个函数");
return "1";
};

funTest += () => {
Console.WriteLine("第二个函数");
return "2";
};

funTest += () => {
Console.WriteLine("第三个函数");
return "3";
};
Console.WriteLine(funTest()); //3

上述程序如果直接调用,只会输出一个 3,因为第三个函数是最后执行的,也就只返回最后一个函数的返回值。

为了解决这个问题,我们可以遍历委托容器中的每个函数

使用**Func.GetInvocationList()**函数获取容器中的每个函数,然后使用 foreach 遍历就可以了。

1
2
3
foreach (Func<string> func in funTest.GetInvocationList()) {
Console.WriteLine(func());
}

List 排序 知识点

知识点一 List 自带排序方法

list 提供了排序方法

1
2
public void Sort();	//默认升序
//ArrayList中也有Sort排序方法

知识点二 自定义类的排序

如果需要自定义类支持排序,类需要继承接口 IComparable<ClassName>

并且实现接口

1
public int CompareTo(ClassName other);

CompareTo 函数

在这个函数当中,我们需要处理排序逻辑,通过这个函数的返回值,决定排序的规则。

返回值的含义:

小于 0:放在传入对象的前面

等于 0:保持当前的位置不变

大于 0:放在传入对象的后面

可以简单理解 传入对象的位置 就是 0;

如果你的返回为负数 就放在它的左边 也就前面;

如果你返回正数 就放在它的右边 也就是后面;

注意,这里作为参照的对象是传入的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public int CompareTo(Item other)
{
//降序排序
//如果本身的钱比另一个大,放到另一个的前面
if( this.money > other.money )
{
return -1;
}
else
{
return 1;
}
}

知识点三 通过委托函数进行排序

我们可以通过在排序函数传入委托,也就是函数,来指定排序规则。

1
2
public delegate int Comparison<in T>(T x, T y);
public void Sort(Comparison<T> comparison);

comparison 是一个委托变量,所以,我们需要传入一个委托或者一个函数,用来指定排序规则。

其中,这个函数或委托,是一个有两个 T 类型的参数,返回 int。

1
2
3
4
5
6
7
//传入一个委托(也可以说是匿名函数)
shopItems.Sort(delegate (ShopItem a, ShopItem b)
{
return a.id > b.id ? 1 : -1;
});
//传入一个lambda表达式(其实就是函数或者说委托)
shopItems.Sort((a, b) =>{ return a.id > b.id ? 1 : -1;});

总结

系统自带的变量(int,float,double…..) 一般都可以直接 Sort

自定义类 SOrt 有两种方式

  1. 继承接口 IComparable
  2. 在 Sort 中传入委托函数

协变逆变 知识点

要理解协变逆变,其实就是要遵循里氏替换原则

知识点一 什么是协变逆变

协变:和谐的变化,自然的变化;和谐的变化,自然的变化。

所以 子类变父类,比如 string 变成 object,感受是和谐的。

逆变:逆常规的变化,不正常的变化;因为 里氏替换原则 父类可以装子类 但是子类不能装父类。

所以 父类变子类,比如 object 变成 string,感受是不和谐的。

协变和逆变是用来修饰泛型的

协变:out

逆变:in

用于在泛型中 修饰 泛型字母的;只有泛型接口和泛型委托能使用

知识点二 作用

返回值 和 参数

用 out 修饰的泛型 只能作为返回值

1
delegate T TestOut<out T>();

用 in 修饰的泛型 只能作为参数

1
delegate void TestIn<in T>(T t);

知识点二 作用(结合里氏替换原则理解)

协变 out:父类总是能被子类替换,允许子类返回值委托赋值给父类返回值委托。

out 是修饰返回值的。

out 修饰返回值类型,可以使得系统帮我们判断out 修饰的这个类型的委托返回值是否可以存储到父类委托当中。

如果不添加 out 修饰符,是不允许子类返回值委托赋值给父类返回值委托。的。

1
2
3
4
5
6
7
8
9
10
11
//这里声明了一个Son委托变量os,委托函数返回Son对象
TestOut<Son> os = () =>
{
return new Son();
};
//然后声明了一个Father委托变量of = os;
//由于是out协变,允许子类委托赋值给父类委托
//因为里氏替换原则,父类可以装子类。
TestOut<Father> of = os;
//实际上 返回的 是os里面装的函数 返回的是Son
Father f = of();

逆变 int:父类总是能被子类替换,允许父类参数委托赋值给子类参数委托

in 是修饰参数的。

in 修饰返回值类型,可以使得系统帮我们判断in 修饰的这个类型的委托参数是否可以存储到子类委托当中。

如果不添加 in 修饰符,是不允许父类参数委托赋值给子类参数委托

1
2
3
4
5
6
7
8
9
10
//这里声明了一个Father委托变量if,参数类型为Father
TestIn<Father> iF = (value) =>
{

};
//然后声明了一个Son委托变量is,参数类型为Son
//由于是in逆变,允许父类委托赋值给子类委托
//因为里氏替换原则,父类可以装子类
TestIn<Son> iS = iF;
iS(new Son());//实际上 调用的是 iF

总结

协变 out

逆变 in

用来修饰 泛型替代符的 只能修饰接口和委托中的泛型

作用两点

  1. out 修饰的泛型类型 只能作为返回值类型 in 修饰的泛型类型 只能作为 参数类型
  2. 遵循里氏替换原则的 用 out 和 in 修饰的 泛型委托 可以相互装载(有父子关系的泛型)
  3. 协变 父类参数泛型委托装子类参数泛型委托 逆变 子类泛型返回值委托装父类泛型返回值委托

多线程 知识点

C# 多线程的使用 - 简书 (jianshu.com)

多线程篇-线程安全-原子性、可见性、有序性解析 - 知乎 (zhihu.com)

知识点一 了解线程前先了解进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动

系统进行资源分配和调度的基本单位,是操作系统结构的基础

说人话:打开一个应用程序就是在操作系统上开启了一个进程

进程之间可以相互独立运行,互不干扰

进程之间也可以相互访问、操作

知识点二 什么是线程

操作系统能够进行运算调度的最小单位。

它被包含在进程之中,是进程中的实际运作单位

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程

我们目前写的程序 都在主线程中

简单理解线程:就是代码从上到下运行的一条“管道”

知识点三 什么是多线程

我们可以通过代码 开启新的线程

可以同时运行代码的多条“管道” 就叫多线程

知识点四 语法相关

线程类 Thread

需要引用命名空间 using System.Threading;

1.声明一个新的线程

注意:线程执行的代码 需要封装到一个函数中

新线程 将要执行的代码逻辑 被封装到了一个函数语句块中

1
Thread t = new Thread(NewThreadLogic);	//这里的参数是一个函数

2.启动线程

1
t.Start();

3.设置为后台线程

当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行

后台线程不会防止应用程序的进程被终止掉

如果不设置为后台线程 可能导致进程无法正常关闭

1
t.IsBackground = true;

4.关闭释放一个线程

如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它

如果是死循环 想要中止这个线程 有两种方式

4.1-死循环中 bool 标识

定义一个全局标记(isRuning),在主线程控制线程运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void NewThreadLogic()
{
//新开线程 执行的代码逻辑 在该函数语句块中
while(isRuning)
{
//Thread.Sleep(1000);
//Console.WriteLine("新开线程代码逻辑");
lock(obj)
{
Console.SetCursorPosition(10, 5);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("■");
}
}
}

4.2-通过线程提供的方法

Thread.Abort 方法 (System.Threading) | Microsoft Learn

注意:在.Net core 版本中无法中止 会报错

此方法已过时。 在 .NET 5 及更高版本上,调用此方法会生成编译时警告。 此方法在 .NET 5 及更高版本和 .NET Core 的运行时引发 PlatformNotSupportedException

1
2
3
4
5
6
7
8
9
10
//终止线程
try
{
t.Abort();
t = null;
}
catch
{

}

5.线程休眠

让线程休眠多少毫秒 1s = 1000 毫秒

在哪个线程里执行 就休眠哪个线程

1
Thread.Sleep(1000);

知识点五 线程之间共享数据

多个线程使用的内存是共享的,都属于该应用程序(进程)

所以要注意 当多线程 同时操作同一片内存区域时可能会出问题

可以通过加锁的形式避免问题

lock:对于对象,加互斥锁。

当我们在多个线程当中想要访问同样的东西 进行逻辑处理时

为了避免不必要的逻辑顺序执行的差错

1
lock(引用类型对象)
1
2
3
4
5
6
7
8
9
while (true)
{
lock(obj)
{
Console.SetCursorPosition(0, 0);
Console.ForegroundColor = ConsoleColor.Red;
Console.Write("●");
}
}

知识点六 多线程对于我们的意义

可以用多线程专门处理一些复杂耗时的逻辑

比如 寻路、网络通信等等

总结

多线程是多个可以同时执行代码逻辑的“管道”

可以通过代码开启多线程,用多线程处理一些复杂的可能影响主线程流畅度的逻辑

关键字 Thread

预处理器指令 知识点

知识点一 什么是编译器

编译器是一种翻译程序

它用于将源语言程序翻译为目标语言程序

源语言程序:某种程序设计语言写成的,比如 C#、C、C++、Java 等语言写的程序

目标语言程序:二进制数表示的伪机器代码写的程序

知识点二 什么是预处理器指令

预处理器指令 指导编译器 在实际编译开始之前对信息进行预处理

预处理器指令 都是以#开始

预处理器指令不是语句,所以它们不以分号;结束

目前我们经常用到的 折叠代码块 就是预处理器指令

知识点三 常见的预处理器指令

#define,#undef

#define 定义一个符号,类似一个没有值的变量

#undef 取消 define 定义的符号,让其失效

两者都是写在脚本文件最前面

一般配合 if 指令使用 或配合特性

1
2
3
4
5
6
7
8
9
10
11
//定义一个符号
#define Unity4
#define Unity5
#define Unity2017
#define Unity2019
//取消定义一个符号
#undef Unity4

#define IOS
#define Android
#define PC

#if,#elif,#else,#endif

和 if 语句规则一样,一般配合#define 定义的符号使用

用于告诉编译器进行编译代码的流程控制

1
2
3
4
5
6
7
8
9
10
11
            //如果发现有Unity4这个符号 那么其中包含的代码 就会被编译器翻译
//可以通过 逻辑或 和 逻辑与 进行多种符号的组合判断
#if Unity4
Console.WriteLine("版本为Unity4");
#elif Unity2017 && IOS
Console.WriteLine("版本为Unity2017");
//#warning 这个版本 不合法
//#error 这个版本不准执行
#else
Console.WriteLine("其它版本");
#endif

#warning,#error

告诉编译器,是报警告还是报错误,一般还是配合 if 使用

总结

预处理器指令

可以让代码还没有编译之前就可以进行一些预处理判断

在 Unity 中会用来进行一些平台或者版本的判断

决定不同的版本或者不同的平台使用不同的代码逻辑

反射概念和关键类 Type 知识点

反射是 Unity 工作的基本原理之一

知识点一 什么是程序集

程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物

在 WINDOWS 系统中,它一般表现为后缀为·dll(库文件)或者是·exe(可执行文件)的格式

说人话:

程序集就是我们写的一个代码集合,我们现在写的所有代码

最终都会被编译器翻译为一个程序集供别人使用

比如一个代码库文件(dll)或者一个可执行文件(exe)

知识点二 元数据

元数据就是用来描述数据的数据

这个概念不仅仅用于程序上,在别的领域也有元数据

说人话:

程序中的类,类中的函数、变量等等信息就是 程序的 元数据

有关程序以及类型的数据被称为 元数据,它们保存在程序集中

知识点三 反射的概念

程序正在运行时,可以查看其它程序集或者自身的元数据。

一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射

说人话:

在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息

类,函数,变量,对象等等,实例化它们,执行它们,操作它们

知识点四 反射的作用

因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性

  1. 程序运行时得到所有元数据,包括元数据的特性
  2. 程序运行时,实例化对象,操作对象
  3. 程序运行时创建新对象,用这些对象执行任务

知识点五 语法相关

Type

1
Type(类的信息类)

它是反射功能的基础!

它是访问元数据的主要方式。

使用 Type 的成员获取有关类型声明的信息

有关类型的成员(如构造函数、方法、字段、属性和类的事件)

获取 Type

注意:对于同一个类型,获取的 Type(类型)是唯一的!

也就是说,无论你通过什么方式获取 Type,获取到的 Type 变量都是同一个。

1.物之父 object 中的 **GetType()**可以获取对象的 Type

1
2
3
int a = 42;
Type type = a.GetType();
Console.WriteLine(type);

2.通过typeof(关键字) 传入类名 也可以得到对象的 Type

1
2
Type type2 = typeof(int);
Console.WriteLine(type2);

3.通过类的名字 也可以获取类型

注意:类名必须包含命名空间 不然找不到

1
2
Type type3 = Type.GetType("System.Int32");
Console.WriteLine(type3);

获取程序集

type.Assembly

1
2
3
4
//可以通过Type可以得到类型所在程序集信息
Console.WriteLine(type.Assembly);
Console.WriteLine(type2.Assembly);
Console.WriteLine(type3.Assembly);

获取类中的所有公共成员

1
2
//获取类中的所有公共成员
public MemberInfo[] GetMembers();
1
MemberInfo[] infos = t.GetMembers();

需要引用命名空间 using System.Reflection;

1
2
3
4
5
6
7
8
9
//首先得到Type
Type t = typeof(Test);
//然后得到所有公共成员
//需要引用命名空间 using System.Reflection;
MemberInfo[] infos = t.GetMembers();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine(infos[i]);
}

获取类的公共构造函数并调用

1.获取所有构造函数

1
2
3
4
5
ConstructorInfo[] ctors = t.GetConstructors();
for (int i = 0; i < ctors.Length; i++)
{
Console.WriteLine(ctors[i]);
}

2.获取其中一个构造函数 并执行

1
2
//获取构造函数,types表示参数
public ConstructorInfo? GetConstructor(Type[] types);

2-1 得到无参构造

1
2
3
4
5
6
7
8
//得构造函数传入 Type数组 数组中内容按顺序是参数类型
//执行构造函数传入 object数组 表示按顺序传入的参数
public object Invoke(object?[]? parameters); //这是ConstructorInfo类的一个成员
// 2-1得到无参构造
ConstructorInfo info = t.GetConstructor(new Type[0]);
//执行无参构造 无参构造 没有参数 传null
Test obj = info.Invoke(null) as Test;
Console.WriteLine(obj.j);

注意,此时调用的 Invoke,返回一个 Object,所以需要 as。

2-2 得到有参构造

1
2
3
4
5
6
7
8
9
ConstructorInfo info2 = t.GetConstructor(new Type[] { typeof(int) });
//注意:Invoke函数需要传入的是Object类型的参数
obj = info2.Invoke(new object[] { 2 }) as Test;
Console.WriteLine(obj.str);

ConstructorInfo info3 = t.GetConstructor(new Type[] { typeof(int), typeof(string) });
//注意:Invoke函数需要传入的是Object类型的参数
obj = info3.Invoke(new object[] { 4, "444444" }) as Test;
Console.WriteLine(obj.str);

获取类的公共成员变量

1.得到所有成员变量

1
2
3
4
5
FieldInfo[] fieldInfos = t.GetFields();
for (int i = 0; i < fieldInfos.Length; i++)
{
Console.WriteLine(fieldInfos[i]);
}

2.得到指定名称的公共成员变量

1
2
FieldInfo infoJ = t.GetField("j");
Console.WriteLine(infoJ);

3.通过反射获取和设置对象的值

1
2
3
Test test = new Test();
test.j = 99;
test.str = "2222";

3-1 通过反射 获取对象的某个变量的值

1
Console.WriteLine(infoJ.GetValue(test));

3-2 通过反射 设置指定对象的某个变量的值

1
2
infoJ.SetValue(test, 100);
Console.WriteLine(infoJ.GetValue(test));

获取类的公共成员方法

通过 Type 类中的 GetMethod 方法 得到类中的方法

MethodInfo 是方法的反射信息

1
2
3
4
5
6
Type strType = typeof(string);
MethodInfo[] methods = strType.GetMethods();
for (int i = 0; i < methods.Length; i++)
{
Console.WriteLine(methods[i]);
}

1.如果存在方法重载 用 Type 数组表示参数类型

1
2
MethodInfo subStr = strType.GetMethod("Substring",
new Type[] { typeof(int), typeof(int) });

2.调用该方法

注意:如果是静态方法 Invoke 中的第一个参数传 null 即可

1
2
3
4
5
6
7
8
9
10
string str = "Hello,World!";
//第一个参数 相当于 是哪个对象要执行这个成员方法
object result = subStr.Invoke(str, new object[] { 7, 5 });
Console.WriteLine(result);

object flag = methodEquals.Invoke(null, new Object[] {
"11", "11"
});
Console.WriteLine((bool)flag);
//输出True

其它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Type;
//得枚举
//GetEnumName
//GetEnumNames

//得事件
//GetEvent
//GetEvents

//得接口
//GetInterface
//GetInterfaces

//得属性
//GetProperty
//GetPropertys
//等等

关键类 Assembly 和 Activator 知识点

Activator

用于快速实例化对象的类

用于将 Type 对象快捷实例化为对象

先得到 Type

然后 快速实例化一个对象

1
2
3
4
5
6
7
8
9
//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);

主要是通过 CreateInstance 方法,快捷创建一个对象的实例。

1
2
public static object? CreateInstance(Type type, params object?[]? args);
//注意,我们必须在使用时使得构造函数的参数匹配,不然会报错

Assembly

程序集类

主要用来加载其它程序集,加载后,才能用 Type 来使用其它程序集中的信息。

如果想要使用不是自己程序集中的内容 需要先加载程序集,比如 dll 文件(库文件)。

dll:简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类

三种加载程序集的函数

一般用来加载在同一文件下的其它程序集

1
Assembly asembly2 = Assembly.Load("程序集名称");

一般用来加载不在同一文件下的其它程序集

1
2
Assembly asembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
Assembly asembly3 = Assembly.LoadFile("要加载的文件的完全限定路径");

加载过程

1.先加载一个指定程序集

1
2
3
4
5
6
Assembly asembly = Assembly.LoadFrom(@"C:\Users\MECHREVO\Desktop\CSharp进阶教学\Lesson18_练习题\bin\Debug\netcoreapp3.1\Lesson18_练习题");
Type[] types = asembly.GetTypes();
for (int i = 0; i < types.Length; i++)
{
Console.WriteLine(types[i]);
}

2.在加载程序集中的一个类对象 之后才能使用反射

1
2
3
4
5
6
Type icon = asembly.GetType("Lesson18_练习题.Icon");
MemberInfo[] members = icon.GetMembers();
for (int i = 0; i < members.Length; i++)
{
Console.WriteLine(members[i]);
}

通过反射 实例化一个 icon 对象

1
2
Type moveDir = asembly.GetType("Lesson18_练习题.E_MoveDir");
FieldInfo right = moveDir.GetField("Right");

直接实例化对象

1
object iconObj = Activator.CreateInstance(icon, 10, 5, right.GetValue(null));

得到对象中的方法 通过反射

1
2
3
MethodInfo move = icon.GetMethod("Move");
MethodInfo draw = icon.GetMethod("Draw");
MethodInfo clear = icon.GetMethod("Clear");

类库工程创建

我们可以自己创建类库,编译后,将生成 dll 文件。

我们可以直接引用 dll 文件,或者通过反射获取。

特性 知识点

知识点一 特性是什么

特性是一种允许我们向程序的程序集添加元数据的语言结构

它是用于保存程序结构信息的某种特殊类型的类

特性提供功能强大的方法以将声明信息与 C# 代码(类型、方法、属性等)相关联。

特性与程序实体关联后,即可在运行时使用反射查询特性信息

特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集中

它可以放置在几乎所有的声明中(类、变量、函数等等申明)

说人话:特性本质是个类,我们可以利用特性类为元数据添加额外信息

比如一个类、成员变量、成员方法等等为他们添加更多的额外信息

之后可以通过反射来获取这些额外信息

知识点二 自定义特性

特性实质就是一个类,所以,只需要再声明类时继承 Attribute 基类,那么这个类就是一个特性。

特性命名规范:特性名 Attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
//注意,类名中的Attribute在使用时可以省略,但是typeof时不能省略
//继承特性基类 Attribute
class MyCustomAttribute : Attribute
{
//特性中的成员 一般根据需求来写
public string info;

public MyCustomAttribute(string info)
{
this.info = info;
}

public void TestFun()
{
Console.WriteLine("特性的方法");
}
}

知识点三 特性的使用

基本语法

1
2
//[特性名(参数列表)]
//本质上 就是在调用特性类的构造函数

写在哪里?

类、函数、变量上一行,表示他们具有该特性信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[MyCustom("这个是我自己写的一个用于计算的类")]
[MyCustom("这个是我自己写的一个用于计算的类")]
class MyClass
{
[MyCustom("这是一个成员变量")]
public int value;

//[MyCustom("这是一个用于计算加法的函数")]
//public void TestFun( [MyCustom("函数参数")]int a )
//{

//}
public void TestFun(int a)
{

}
}

判断是否使用了某个特性

参数一:特性的类型

参数二:代表是否搜索继承链(属性和事件忽略此参数)

1
2
3
4
if( t.IsDefined(typeof(MyCustomAttribute), false) )
{
Console.WriteLine("该类型应用了MyCustom特性");
}

获取 Type 元数据中的所有特性

1
2
3
4
5
6
7
8
9
object[] array = t.GetCustomAttributes(true);
for (int i = 0; i < array.Length; i++)
{
if( array[i] is MyCustomAttribute )
{
Console.WriteLine((array[i] as MyCustomAttribute).info);
(array[i] as MyCustomAttribute).TestFun();
}
}

知识点四 限制自定义特性的使用范围

通过为特性类 加特性 限制其使用范围

1
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]

参数一:AttributeTargets —— 特性能够用在哪些地方

参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上

参数三:Inherited —— 特性是否能被派生类和重写成员继承

知识点五 系统自带特性——过时特性

过时特性:Obsolete

用于提示用户 使用的方法等成员已经过时 建议使用新方法

一般加在函数前的特性

1
public ObsoleteAttribute(string? message, bool error);
1
2
3
4
5
6
7
//参数一:调用过时方法时 提示的内容
//参数二:true-使用该方法时会报错 false-使用该方法时直接警告
[Obsolete("OldSpeak方法已经过时了,请使用Speak方法", false)]
public void OldSpeak(string str)
{
Console.WriteLine(str);
}

知识点六 系统自带特性——调用者信息特性

需要引用命名空间 using System.Runtime.CompilerServices;

哪个文件调用?

CallerFilePath 特性

哪一行调用?

CallerLineNumber 特性

哪个函数调用?

CallerMemberName 特性

一般作为函数参数的特性

1
2
3
4
5
6
7
8
9
10
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);
}

知识点七 系统自带特性——条件编译特性

条件编译特性:Conditional

它会和预处理指令 #define 配合使用

需要引用命名空间 using System.Diagnostics;

主要可以用在一些调试代码上

有时想执行有时不想执行的代码

只有 define 了 conditionString,被特性修饰的东西才能执行

1
public ConditionalAttribute(string conditionString);

知识点八 系统自带特性——外部 Dll 包函数特性

DllImport

用来标记非.Net(C#)的函数,表明该函数在一个外部的 DLL 中定义。

一般用来调用 C 或者 C++的 Dll 包写好的方法

需要引用命名空间 using System.Runtime.InteropServices

1
public DllImportAttribute(string dllName);

总结

特性是用于 为元数据再添加更多的额外信息(变量、方法等等)

我们可以通过反射获取这些额外的数据 来进行一些特殊的处理

自定义特性——继承Attribute

系统自带特性:过时特性等

为什么要学习特性

Unity 引擎中很多地方都用到了特性来进行一些特殊处理

迭代器 知识点

迭代器一定是可迭代对象。迭代器要求实现 movenext 方法。可迭代对象要求实现 getIEnumerator 方法,getIEnumerator 方法返回的是该对象的迭代器。迭代器不负责维护可迭代对象的数据信息,只负责维护读取数据的位置。所以就是工厂和螺丝钉的关系

知识点一 迭代器是什么

迭代器(iterator)有时又称光标(cursor)

是程序设计的软件设计模式

迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素

而又不暴露其内部的标识

在表现效果上看

是可以在容器对象(例如链表或数组)上遍历访问的接口

设计人员无需关心容器对象的内存分配的实现细节

可以用 foreach 遍历的类,都是实现了迭代器的

知识点二 标准迭代器的实现方法

关键接口:IEnumerator,IEnumerable

命名空间:using System.Collections;

可以通过同时继承 IEnumerable 和 IEnumerator 实现其中的方法

IEnumerator

IEnumerator 就是迭代器接口,拥有三个函数,用来实现遍历操作。

1
2
3
4
5
6
7
8
9
public interface IEnumerator
{
//获取当前元素
object? Current { get; }
//检测下一个元素是否存在,如果不存在则不可继续遍历
bool MoveNext();
//重置下标
void Reset();
}

IEnumerable

IEnumerable 的作用是获取 IEnumerator,并重置下标。

一般 Reset 函数在 GetEnumerator()中调用。

1
2
3
4
public interface IEnumerable {
//获取迭代器,并且重置迭代器
IEnumerator GetEnumerator();
}

foreach 本质

1
foreach (var item in list) { }
  1. 先获取 in 后面这个对象的 IEnumerator,会调用对象其中的 GetEnumerator 方法 来获取
  2. 执行得到这个 IEnumerator 对象中的 MoveNext 方法
  3. 只要 MoveNext 方法的返回值时 true 就会去得到 Current,然后复制给 item

标准迭代器的实现方法

标准迭代器的实现方法,其实就是实现 IEnumerator,IEnumerable 这两个接口。

注意:下标最开始应该是-1,因为开始遍历时,就会调用一次 MoveNext();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
}
}

知识点三 用 yield return 语法糖实现迭代器

yield return

yield return 是 C#提供给我们的语法糖

所谓语法糖,也称糖衣语法

主要作用就是将复杂逻辑简单化,可以增加程序的可读性

从而减少程序代码出错的机会

在执行

1
yield return item;

之后,程序会返回之前调用的函数,当需要再次获取 item 时,又会回到 yield return 然后返回下一个元素。

用 yield return 语法糖实现迭代器

关键接口:IEnumerable

命名空间:using System.Collections;

让想要通过 foreach 遍历的自定义类只需要实现接口中的方法 GetEnumerator 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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关键字 配合迭代器使用
//可以理解为 暂时返回 保留当前的状态
//一会还会在回来
//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];
}
}

知识点四 用 yield return 语法糖为泛型类实现迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 去返回内容 只需要继承一个接口即可

特殊语法 知识点

知识点一 var 隐式类型

其实就是 Cpp 的 auto

var 是一种特殊的变量类型

它可以用来表示任意类型的变量

注意:

  1. var 不能作为类的成员 只能用于临时变量声明时,也就是 一般写在函数语句块中
  2. var 必须初始化(因为需要类型推导)
1
2
3
4
var i = 5;
var s = "123";
var array = new int[] { 1, 2, 3, 4 };
var list = new List<int>();

知识点二 设置对象初始值,大括号初始化法

声明对象时,可以通过直接写大括号的形式初始化公共成员变量和属性

1
2
Person p = new Person(100) { sex = true, Age = 18, Name = "唐老狮" };
Person p2 = new Person(200) { Age = 18 };

知识点三 设置集合初始值

声明集合对象时,也可以通过大括号 直接初始化内部属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int[] array2 = new int[] { 1, 2, 3, 4, 5 };

List<int> listInt = new List<int>() { 1, 2, 3, 4, 5, 6 };

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"}
};

知识点四 匿名类型

var 变量可以申明为自定义的匿名类型

1
2
3
var v = new { age = 10, money = 11, name = "小明" };
Console.WriteLine(v.age);
Console.WriteLine(v.name);

知识点五 可空类型

[C#中 ??、 ?、 ?: 、?.、? ] 问号 - 幽冥狂_七 - 博客园 (cnblogs.com)

1.值类型是不能赋值为 空的

1
int c = null;	//error

2.申明时 在值类型后面加? 可以赋值为空

1
int? c = 3;

3.判断是否为空

1
2
3
4
5
if( c.HasValue )
{
Console.WriteLine(c);
Console.WriteLine(c.Value);
}

4.安全获取可空类型值

4-1.如果为空 默认返回值类型的默认值

1
Console.WriteLine(value.GetValueOrDefault());

4-2.也可以指定一个默认值

1
Console.WriteLine(value.GetValueOrDefault(100));

?.,?[]

先检测对象是否为空,再执行方法

相当于是一种语法糖 能够帮助我们自动去判断 o 是否为空

1
2
3
4
//如果obj不为空,就执行ToString()
obj?.ToString();
//如果obj不为空,就执行[]
obj?[0];

知识点六 空合并操作符

空合并操作符 ??

语法:左边值 ?? 右边值

语义:如果左边值为 null 就返回右边值 否则返回左边值

只要是可以为 null 的类型都能用

是三目运算符的一种特殊缩略写法。

1
2
3
4
5
6
7
8
int? intV = null;
//int intI = intV == null ? 100 : intV.Value;
int intI = intV ?? 100;
Console.WriteLine(intI);

string str = null;
str = str ?? "hahah";
Console.WriteLine(str);

知识点七 内插字符串

关键符号:$

用$来构造字符串,让字符串中可以拼接变量

1
2
3
string name = "唐老狮";
int age = 18;
Console.WriteLine($"好好学习,{name},年龄:{age}");

知识点八 单句逻辑简略写法

当循环或者 if 语句中只有 一句代码时 大括号可以省略

1
2
3
4
5
6
7
8
if (true)
Console.WriteLine("123123");

for (int j = 0; j < 10; j++)
Console.WriteLine(j);

while (true)
Console.WriteLine("123123");

值和引用类型补充

知识回顾

值类型
无符号:byte,ushort,uint,ulong
有符号:sbyte,short,int,long
浮点数:float,double,decimal
特殊:char,bool
枚举:enum
结构体 :struct

引用类型:string,数组,class,interface,委托

值类型和引用类型的本质区别:值的具体内容存在栈内存上;引用的具体内容存在堆内存上

问题一 如何判断 值类型和引用类型

F12 进到类型的内部去查看

是 class 就是引用

是 struct 就是值

问题二 语句块

语句块一共有以下几种:

命名空间

类、接口、结构体

函数、属性、索引器、运算符重载等(类、接口、结构体)

条件分支、循环

上层语句块:类、结构体
中层语句块:函数
底层的语句块: 条件分支 循环等

我们的逻辑代码写在哪里?
函数、条件分支、循环 - 中底层语句块中

我们的变量可以申明在哪里?
上、中、底都能申明变量
上层语句块中:成员变量
中、底层语句块中:临时变量

问题三 变量的生命周期

编程时大部分都是 临时变量,在中底层申明的临时变量(函数、条件分支、循环语句块等)

语句块执行结束,没有被记录的对象将被回收或变成垃圾

值类型:被系统自动回收

引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次 GC 回收

想要不被回收或者不变垃圾,则必须将其记录下来

如何记录?

在更高层级记录或者,使用静态全局变量记录

问题四 结构体中的值和引用

结构体本身是值类型
前提:该结构体没有做为其它类的成员
在结构体中的值,栈中存储值具体的内容
在结构体中的引用,堆中存储引用具体的内容

引用类型始终存储在堆中,真正通过结构体使用其中引用类型时只是顺藤摸瓜

问题五 类中的值和引用

类本身是引用类型
在类中的值,堆中存储具体的值(托管值类型)
在类中的引用,堆中存储具体的值

值类型跟着大哥走,引用类型一根筋

问题六 数组中的存储规则

数组本身是引用类型

值类型数组,堆中房间存具体内容

引用类型数组,堆中房间存地址

问题七 结构体继承接口

利用里氏替换原则,用接口容器装载结构体存在装箱拆箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TestStruct obj1 = new TestStruct();
obj1.Value = 1;
Console.WriteLine(obj1.Value);
TestStruct obj2 = obj1;
obj2.Value = 2;
//由于是值类型,obj1.value不会改变
Console.WriteLine(obj1.Value);
Console.WriteLine(obj2.Value);

ITest iObj1 = obj1;//装箱 value 1
ITest iObj2 = iObj1;
iObj2.Value = 99;
//由于是引用类型,iobj1.value会变成99
Console.WriteLine(iObj1.Value);
Console.WriteLine(iObj2.Value);

TestStruct obj3 = (TestStruct)iObj1;//拆箱

插入排序

知识点一 插入排序的基本原理

8 7 1 5 4 2 6 3 9
两个区域
排序区
未排序区
用一个索引值做分水岭

未排序区元素
与排序区元素比较
插入到合适位置
直到未排序区清空

知识点二 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//实现升序 把 大的 放在最后面
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)
{
//只要进了这个while循环 证明满足条件
//排序区中的元素 就应该往后退一格
arr[sortIndex + 1] = arr[sortIndex];
//移动到排序区的前一个位置 准备继续比较
--sortIndex;
}
//最终插入数字
//循环中知识在确定位置 和找最终的插入位置
//最终插入对应位置 应该循环结束后
arr[sortIndex + 1] = noSortNum;
}

for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int[] nums = new int[] { 3, 5, 6, 9, 0, 1, 2, 4, 6, 8 };
for (int i = 1; i < nums.Length; ++i)
{
int sortedIndex = i - 1;
int nowSortNum = nums[i];
while (sortedIndex >= 0 && nums[sortedIndex] > nowSortNum)
{
nums[sortedIndex + 1] = nums[sortedIndex];
--sortedIndex;
}
nums[sortedIndex + 1] = nowSortNum;
}
foreach (var item in nums)
{
Console.Write(item + " ");

}

知识点三 总结

为什么有两层循环?

第一层循环:一次取出未排序区的元素进行排序
第二层循环:找到想要插入的位置

为什么第一层循环从 1 开始遍历?

插入排序的关键是分两个区域,已排序区 和 未排序区,默认第一个元素在已排序区

为什么使用 while 循环?

满足条件才比较
否则证明插入位置已确定
不需要继续循环

为什么可以直接往后移位置

每轮未排序数已记录

最后一个位置不怕丢

为什么确定位置后,是放在 sortIndex + 1 的位置

当循环停止时,插入位置应该是停止循环的索引加 1 处

基本原理

两个区域
用索引值来区分
未排序区与排序区
元素不停比较
找到合适位置
插入当前元素

套路写法

两层循环
一层获取未排序区元素
一层找到合适插入位置

注意事项

默认(假设)开头已排序,其实一个元素,就是有序的。
第二层循环外插入

希尔排序

知识点一 希尔排序的基本原理

希尔排序是,插入排序的升级版,必须先掌握插入排序

希尔排序的原理:将整个待排序序列,分割成为若干子序列,分别进行插入排序

总而言之:

希尔排序对插入排序的升级主要就是加入了一个步长的概念,通过步长每次可以把原序列分为多个子序列;对子序列进行插入排序,在极限情况下可以有效降低普通插入排序的时间复杂度,提升算法效率。

知识点二 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int[] nums = new int[] { 3, 5, 6, 9, 0, 1, 2, 4, 6, 8 };
for (int step = nums.Length / 2; step > 0; step /= 2)
{
for (int i = step; i < nums.Length; i += step)
{
int sortedIndex = i - step;
int nowSortNum = nums[i];
while (sortedIndex >= 0 && nowSortNum > nums[sortedIndex])
{
nums[sortedIndex + step] = nums[sortedIndex];
sortedIndex -= step;
}
nums[sortedIndex + step] = nowSortNum;
}
}

知识点三 总结

基本原理

设置步长
步长不停缩小
到 1 排序后结束

具体排序方式

插入排序原理

套路写法

三层循环
一层获取步长
一层获取未排序区元素
一层找到合适位置插入

注意事项

步长确定后
会将所有子序列进行插入排序

归并排序

知识点一 归并排序基本原理

归并 = 递归 + 合并

数组分左右
左右元素相比较
满足条件放入新数组
一侧用完放对面

递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果

归并排序分成两部分 1.基本排序规则 2.递归平分数组

递归平分数组:
不停进行分割
长度小于 2 停止
开始比较
一层一层向上比

基本排序规则:
左右元素进行比较
依次放入新数组中
一侧没有了另一侧直接放入新数组

知识点二 代码实现

唐老狮做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//第一步:
//基本排序规则
//左右元素相比较
//满足条件放进去
//一侧用完直接放
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));
}

更简洁的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static void MergeSort(int[] nums, int left, int right)
{
if (left < right)
{
int center = left + (right - left) / 2;
MergeSort(nums, left, center);
MergeSort(nums, center + 1, right);
Merge(nums, left, right);
}
}

public static void Merge(int[] nums, int left, int right)
{
int center = left + (right - left) / 2;
int leftIndex = left;
int rightIndex = center + 1;
int index = 0;
int[] tempNums = new int[right - left + 1];
while (leftIndex <= center && rightIndex <= right)
{
if (nums[leftIndex] < nums[rightIndex])
{
tempNums[index++] = nums[leftIndex++];
}
else
{
tempNums[index++] = nums[rightIndex++];
}
}
while (leftIndex <= center)
{
tempNums[index++] = nums[leftIndex++];
}
while (rightIndex <= right)
{
tempNums[index++] = nums[rightIndex++];
}
for (int i = 0; i < tempNums.Length; ++i)
{
nums[left++] = tempNums[i];
}
}

知识点三 总结

理解递归逻辑
一开始不会执行 Sort 函数的
要先找到最小容量数组时
才会回头递归调用 Sort 进行排序

基本原理
归并 = 递归 + 合并
数组分左右
左右元素相比较
一侧用完放对面
不停放入新数组

递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果

套路写法
两个函数
一个基本排序规则
一个递归平分数组

注意事项
排序规则函数 在 平分数组函数
内部 return 调用

快速排序

知识点一 快速排序基本原理

选取基准
产生左右标识
左右比基准
满足则换位

排完一次
基准定位

左右递归
直到有序

知识点二 代码实现

快速排序是不稳定排序

为什么快速排序是不稳定的d4shman 的博客-CSDN 博客快排是稳定排序吗

唐老狮写法

有 bug,数组有相同元素的情况下,会死循环。

在查找时加<=解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//第一步:
//申明用于快速排序的函数
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);
}

优化写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void QuickSort(int[] nums)
{
RandonPatition(nums, 0, nums.Length - 1);
}

public static void RandonPatition(int[] nums, int left, int right)
{
Random r = new Random();
if (left < right)
{
int mark = r.Next(left, right + 1);
int temp = nums[left];
nums[left] = nums[mark];
nums[mark] = temp;
int index = Patition(nums, left, right);
RandonPatition(nums, left, index - 1);
RandonPatition(nums, index + 1, right);
}
}

public static int Patition(int[] nums, int left, int right)
{
int benchMark = nums[left];
while (left != right)
{
while (left < right && nums[right] <= benchMark)
{
--right;
}
nums[left] = nums[right];
while (left < right && nums[left] > benchMark)
{
++left;
}
nums[right] = nums[left];
}
nums[right] = benchMark;
return right;
}

堆排序

知识点一 堆排序基本原理

构建二叉树,大堆顶调整,堆顶往后方,不停变堆顶

关键规则:最大非叶子节点是 数组长度 / 2 - 1

父节点和叶子节点:父节点为 i,左节点 2i+1,右节点 2i+2

知识点二 代码实现

堆排序的精髓在于,利用数组长度这个参数,每次对其他元素进行 HeapCompare

使得每次排序,忽略最大/最小的元素,已达到排序的效果

唐老狮写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//第一步:实现父节点和左右节点比较
/// <summary>
///
/// </summary>
/// <param name="array">需要排序的数组</param>
/// <param name="nowIndex">当前作为根节点的索引</param>
/// <param name="arrayLength">固定一个位置,那么长度就会减去1,是没有确定位置的数组元素长度</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);
}
}

重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void HeapCompare(int[] nums, int nowIndex, int len)
{
int biggestIndex = nowIndex;
int left = nowIndex * 2 + 1;
int right = nowIndex * 2 + 2;
if (left < len && nums[left] > nums[biggestIndex])
{
biggestIndex = left;
}
if (right < len && nums[right] > nums[biggestIndex])
{
biggestIndex = right;
}
if (biggestIndex != nowIndex)
{
int temp = nums[nowIndex];
nums[nowIndex] = nums[biggestIndex];
nums[biggestIndex] = temp;
HeapCompare(nums, biggestIndex, len);
}
}

public static void BuildHeap(int[] nums)
{
for (int i = nums.Length / 2 - 1; i >= 0; --i)
{
HeapCompare(nums, i, nums.Length);
}
}

public static void HeapSort(int[] nums)
{
BuildHeap(nums);
for (int i = nums.Length - 1; i >= 0; --i)
{
int temp = nums[0];
nums[0] = nums[i];
nums[i] = temp;
//堆排序的精髓就在这里,利用数组长度这个参数
//使得每次排序,忽略最大/最小的元素,已达到排序的效果
HeapCompare(nums, 0, i);
}
}

知识点三 总结

基本原理
构建二叉树
大堆顶调整
堆顶往后方
不停变堆顶

套路写法
3 个函数
1 个堆顶比较
1 个构建大堆顶
1 个堆排序

重要规则
最大非叶子节点索引:数组长度/2 - 1
父节点和叶子节点索引:父节点为 i,左节点 2i+1,右节点 2i-1

注意:
堆是一类特殊的树,堆的通用特点就是父节点会大于或小于所有子节点;

我们并没有真正的把数组变成堆,只是利用了堆的特点来解决排序问题