C#进阶
C#进阶
简单数据结构类
Arraylist
本质:Arraylist是一个C#为我们封装好的类,它的本质是一个object类型的数组,Arraylist类帮助我们实现很多方法:数组的增删查改等等。
//申明
using System.Collections;
//需要引用命名空间using System.Collections;
ArrayList array = new ArrayList();
//1.增(因为是object类型,所以可以存任何类型的数据)
array.Add(1);
array.Add("123");
array.Add(true);
array.Add(new object());
array.Add(new Test());
//范围增加(批量增加,把另一个list容器里面的内容加到后面)
ArrayList array2 = new ArrayList();
array2.Add(123);
array.AddRange(array2);
//增加到指定位置
array2.Insert(1,"123456");
Console.WriteLine(array[1]);
//2.删
//移除指定元素 从头找 找到删
array.Remove(1);
//移除指定位置的元素
array.Remove(2); //删object
//清空
array.Clear();
//3.查
//得到指定位置的元素
Console.WriteLine(array[0]);
//查看元素是否存在
if(array.Contains("123"))
{
Console.WriteLine("存在123");
}
//正向查找元素位置 找到返回值是位置 找不到返回-1
int index = array.IndexOf(true);
Console.WriteLine(index); //1
Console.WriteLine(array.IndexOf(false)); //-1
//反向查找LastIndexOf
//4.改
Console.WriteLine(array[0]);
array[0] = "999";
Console.WriteLine(array[0]);
//遍历
//长度
Console.WriteLine(array.Count); //7
//容量
Console.WriteLine(array.Capacity); //16
for(int i = 0; i < array.Count; i++)
{
Console.WriteLine(array[i]);
}
//迭代器遍历
foreach (object item in array)
{
Console.WriteLine(item);
}
装箱拆箱
-
ArrayList本质上是一个可以自动扩容的object数组,由于用万物之父来储存数据,自然存在装箱拆箱。
-
当其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时,就存在拆箱。
int i = 1; array[0] = i; //装箱 i = (int)array[0]; //拆箱
所以ArrayList尽量少用,之后会学习更好的数据容器。
但也不是不能用,ArrayList的优点是object可以存储任何类型的数据。
Stack
Stack也是一个c#为我们封装好的类,它的本质也是object[ ]数组,只是封装了特殊的存储规则。
Stack是栈储存容器,栈是一种先进后出的数据结构。
//申明
using System.Collections;
//需要引用命名空间using System.Collections;
Stack stack = new Stack();
//增删查改
//压栈
stack.Push(1);
stack.Push(true);
stack.Push(1.2f);
stack.Push(new Test());
//栈中不存在删除的概念,只有取
//弹栈(取)
object v = stack.Pop();
Console.WriteLine(v); //Test里的东西
object v = stack.Pop();
Console.WriteLine(v); //1.2f
//查
//栈无法查看指定元素的内容,只能看栈顶的内容
object v = stack.Peek();
Console.WriteLine(v); //true
object v = stack.Peek();
Console.WriteLine(v); //true
//查看元素是否存在于栈中
if(stack.Contains("123"))
{
Console.WriteLine("存在123");
}
//改
//栈无法改变其中的元素 只能压栈(存) 弹栈(取)
//实在要改 只有清空
stack.Clear();
stack.Push("1");
stack.Push(2);
stack.Push("哈哈哈");
//遍历
//长度
Console.WriteLine(stack.Count);
//用foreach遍历,而且遍历出来的顺序也是从栈顶到栈底
foreach (object item in stack)
{
Console.WriteLine(item);
}
//不可以直接用for循环遍历,需要将栈转化为object数组
//遍历出来的顺序也是从栈顶到栈底
object[] array = stack.ToArray();
for(int i = 0;i < array.Length;i++)
{
Console.WriteLine(array[i]);
}
//循环弹栈 即边取边用
Console.WriteLine(stack.Count); //3
while(stack.Count > 0)
{
object o = stack.Pop();
Console.WriteLine(o);
}
Console.WriteLine(stack.Count); //0
装箱拆箱
-
ArrayList本质上是一个可以自动扩容的object数组,由于用万物之父来储存数据,自然存在装箱拆箱。
-
当其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时,就存在拆箱。
Queue
-
Queue也是一个c#为我们封装好的类,它的本质也是object[ ]数组,只是封装了特殊的存储规则。
-
Queue是队列储存容器,栈是一种先进先出的数据结构。
//和栈基本一样
Queue queue = new Queue();
queue.Enqueue(1);
Queue.Enqueue("123");
Queue.Enqueue(1.4f);
Queue.Enqueue(new Test());
object v = queue.Dequeue();
Console.WriteLine(v);
v = queue.Peek();
Console.WriteLine(v);
queue.Clear();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Console.WriteLine(queue.Count);
foreach (object item in queue)
{
Console.WriteLine(item);
}
object[] array = queue.ToArray();
for(int i = 0;i < array.Length;i++)
{
Console.WriteLine(array[i]);
}
Console.WriteLine(queue.Count); //3
while(queue.Count > 0)
{
object o = queue.Pop();
Console.WriteLine(o);
}
Console.WriteLine(queue.Count); //0
装箱拆箱
-
ArrayList本质上是一个可以自动扩容的object数组,由于用万物之父来储存数据,自然存在装箱拆箱。
-
当其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时,就存在拆箱。
Hashtable
- Hashtable是基于键的哈希代码组织起来的键、值对
- 它的在就主要作用是提高数据查询的效率
- 使用键来访问集合中的元素
//申明
//需要引用命名空间using System.Collections;
Hashtable hashtable = new Hashtable();
//增删查改
//增 注意不能出现相同的键
hashtable.Add(1,"123");
hashtable.Add("123",2);
hashtable.Add(true,false);
hashtable.Add(false,true);
//删 只能通过键去删除
hashtable.Remove(1);
//删除不存在的键没反应
hashtable.Remove(2);
//直接清空
hashtable.Clear();
hashtable.Add(1,"123");
hashtable.Add(2,"1234");
hashtable.Add(3,"123");
hashtable.Add("123",12);
//查
//1.通过键值查找 找不到会返回空
Console.WriteLine(hashtable[1]);
Console.WriteLine(hashtable[2]); //null
Console.WriteLine(hashtable["123"]);
//2.查看是否存在
//根据键检测
if(hashtable.Contains(2))
{
Console.WriteLine("存在键为2的键值对");
}
if(hashtable.ContainsKey(2))
{
Console.WriteLine("存在键为2的键值对");
}
//根据值检测
if(hashtable.ContainsValue(12))
{
Console.WriteLine("存在值为12的键值对");
}
//改 只能修改键对应值的内容,无法修改键
Console.WriteLine(hashtable[1]);
hashtable[1] = 100.5f;
Console.WriteLine(hashtable[1]);
//遍历
//得到键值对 对数
Console.WriteLine(hashtable.Count);
//1.遍历所有键
foreach (object item in hashtable.Key)
{
Console.WriteLine("键:" + item);
Console.WriteLine("值:" + hashtable[item]);
}
//2.遍历所有值
foreach (object item in hashtable.Values)
{
Console.WriteLine("值:" + item);
}
//3.键值对一起遍历
foreach (DictionaryEntry item in hashtable.Key)
{
Console.WriteLine("键:" + item.Key);
Console.WriteLine("值:" + item.Value);
}
//4.迭代器遍历法
IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator();
bool flag = myEnumerator.MoveNext();
while(flag)
{
Console.WriteLine("键:" + myEnumerator.Key + "值:" + myEnumerator.Value);
flag = myEnumerator.MoveNext();
}
泛型
泛型
概念:
- 泛型实现了类型参数化,达到代码重用的目的,通过类型参数化来实现同一份代码上操作多种类型
- 泛型相当于类型占位符,定义类或方法时使用替代符代表变量类型,当真正使用类或者方法时再具体指定类型
//泛型分类:
//1.泛型类和泛型接口
//class 类名<泛型占位字母>
class TestClass<T>
{
public T value;
}
//泛型占位符可以有多个
class TestClass2<T1,T2,K,LL,M>
{
public T1 value1;
public T3 value2;
public K value3;
public LL value4;
public M value5;
}
//interface 接口名<泛型占位字母>
interface TestInterface<T>
{
T value
{
get;
set;
}
}
class Test:TestInterface<int>
{
public int value
{
get;
set;
}
}
//2.泛型函数
//函数名<泛型占位字母>(参数列表)
//2.1普通类中的泛型方法
class Test2
{
public void Testfun<T>(T value)
{
Console.WriteLine(value);
}
public void Testfun<T>()
{
//用泛型在里面做一些逻辑处理
T t = default(T); //不可以赋值0或null之类,用default能获得类型默认值
}
public void Testfun<T>(string v)
{
return default(T);
}
public void Testfun<T,K,M>(T t,K k,M m)
{
}
}
//2.2泛型类中的泛型方法
class Test2<T>
{
//不是泛型方法 T是类中定义的
/*public void Testfun<T>(T value)
{
Console.WriteLine(value);
}*/
public void Testfun<K>(K k)
{
Console.WriteLine(k);
}
}
TestClass<int> t = new TestClass<int>();
t.value = 10;
Console.WriteLine(t.value);
TestClass<string> t = new TestClass<string>();
t.value = "123123";
Console.WriteLine(t.value);
class TestClass2<int,string,float,TestClass<int>,uint> = new TestClass2<int,string,float,TestClass<int>,uint>();
Test2 tt = new Test2();
tt.TestFun<string> ("123123"); //输出123123
泛型约束
概念:让泛型的类型有一定的限制
关键字:where
泛型约束一共有六种:
- 1.值类型 where 泛型字母:struct
- 2.引用类型 where 泛型字母:class
- 3.存在无参公共构造函数 where 泛型字母:new()
- 4.某个类本身或者其派生类 where 泛型字母:类名
- 5.某个接口的派生类型 where 泛型字母:接口名
- 6.另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母
where 泛型字母 : (约束的类型)
//各种泛型约束讲解
//1.值类型约束
class Test1<T> where T:struct
{
public T value;
public void TestFun<K>() where K:struct
{
}
}
//引用是不可用null值的数据类型,必须是值类型
Test1<int> t1 = new Test1<int>();
t1.TestFun<float>(1.3f);
//2.引用类型约束
class Test2<T> where T:class
{
public T value;
public void TestFun<K>() where K:class
{}
}
Test2<Random> t2 = new Test2<Random>();
t2.value = new Ramdom();
t2.TestFun<object>(new object());
//3.公共无参构造约束
class Test3<T> where T:new()
{
public T value;
public void TestFun<K>(K k) where K:new()
{
}
}
class Test1
{
//默认有无参构造函数
}
class Test2
{
public Test2(int a);
}
class Test3
{
private Test3();
}
Test3<Test1> t3 = new Test3<Test1>();
//Test3<Test2> t3 = new Test3<Test2>(); //会报错 有参构造函数会把无参构造函数顶掉
//Test3<Test3> t3 = new Test3<Test3>(); //会报错 必须公共无参构造函数
//还需要非抽象类 因为抽象类无法被new
//4.类约束
class Test4<T> where T:Test1
{
public T value;
public void TestFun<K>(K k) where K:Test1
{
}
}
class Test3 : Test1
{
}
Test4<Test1> t4 = new Test3<Test1>();
Test4<Test3> t4 = new Test3<Test3>();
//Test4<Test2> t4 = new Test3<Test2>(); //会报错 不是Test1或其派生类 父类也不行
//5.接口约束
interface IFly
{
}
interface Test5 : IFly
{
}
class Test5<T> where T : IFly
{
public T value;
public void TestFun<K>(K k) where K : IFly
{
}
}
Test5<IFly> t5 = new Test5<IFly>();
//不能new IFly 但可以用里氏替换原则
t5.value = new Test5();
//也可以直接Test5<Test5> t5 = new Test5<Test5>();
//6.另一个泛型约束
class Test6<T,U> where T : U
{
public T value;
public void TestFun<K>(K k) where K : U
{
}
}
Test6<Test5,IFly> t6 = new Test6<Test5,IFly>();
Test6<Test5,Test5> t6 = new Test6<Test5,Test5>();
//约束的组合使用
class Test7<T> where T: class,IFly
{
}
//多个泛型有约束
class Test8<T,K> where T:class,new() where K:struct
{
}
常用泛型数据结构类
List
概念:List是一个C#为我们封装好的类,它的本质是一个可变类型的泛型数组,List类实现了很多方法如:泛型数组的增删查改。
//申明
//using System.Collections.Generic
List<int> list = new List<int>();
List<string> list2 = new List<string>();
List<bool> list3 = new List<bool>();
//增删查改
//增
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list2.Add("123");
List<string> listStr = new List<string>();
ListStr.Add("123");
list.AddRange(listStr);
//删
//1.移除指定元素
list.Remove(1);
//2.移除指定位置的元素
list.RemoveAt(0);
//3.清空
list.clear();
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
//查
//1.得到指定位置的元素
Console.WriteLine(list[0]);
//2.查看元素是否存在
if(list.Contains(1))
{
Console.WriteLine(1);
}
//3.正向查找元素位置 找到返回值是位置 找不到返回-1
int index = list.IndexOf(1);
Console.WriteLine(index); //0
Console.WriteLine(array.IndexOf(5)); //-1
//4.反向查找LastIndexOf
int index = list.IndexOf(2);
Console.WriteLine(index); //1
//4.改
Console.WriteLine(list[0]); //1
list[0] = 99;
Console.WriteLine(list[0]); //99
//遍历
//长度
Console.WriteLine(list.Count); //4
//容量
Console.WriteLine(array.Capacity); //8
for(int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
//迭代器遍历
foreach (object item in list)
{
Console.WriteLine(item);
}
Dictionary
和hashtable基本一样,可以理解为拥有泛型的hashtable,它也是基于键的哈希代码组织起来的,键、值对。
键值对类型从Hashtable的object变为了可以自己制定类型的泛型。
//申明
//using System.Collection.Generic
Dictionart<int,string> dictionary = new Dictionary<int,string>();
//增删查改
//增
dictionary.Add(1,"123");
dictionary.Add(2,"222");
dictionary.Add(3,"222");
//1.删 只能通过键去删除
dictionary.Remove(1);
//删除不存在的键没反应
dictionary.Remove(4);
//2.直接清空
dictionary.Clear();
dictionary.Add(1,"123");
dictionary.Add(2,"222");
dictionary.Add(3,"222");
//查
//1.通过键值查找 找不到会直接报错
Console.WriteLine(dictionary[1]);
//Console.WriteLine(dictionary[4]); //会报错
//2.查看是否存在 找不到就返回false
//根据键检测 和哈希表不一样只有一个函数
if(dictionary.ContainsKey(2))
{
Console.WriteLine("存在键为2的键值对");
}
//根据值检测
if(dictionary.ContainsValue(123))
{
Console.WriteLine("存在值为123的键值对");
}
//改 只能修改键对应值的内容,无法修改键
Console.WriteLine(dictionary[1]);
dictionary[1] = "555";
Console.WriteLine(dictionary[1]);
//遍历
//得到键值对 对数
Console.WriteLine(dictionary.Count);
//1.遍历所有键
foreach (int item in dictionary.Key)
{
Console.WriteLine("键:" + item);
Console.WriteLine("值:" + dictionary[item]);
}
//2.遍历所有值
foreach (string item in dictionary.Values)
{
Console.WriteLine("值:" + item);
}
//3.键值对一起遍历
foreach (KeyValue<int,string> item in dictionary)
{
Console.WriteLine("键:" + item.Key);
Console.WriteLine("值:" + item.Value);
}
顺序储存和链式储存
顺序存储:用一组地址连续的存储单元一次存储线性表的各个数据元素。
链式存储:用一组任意的存储单元存储线性表的各个数据元素。
从增删查改的角度思考顺序存储和链式存储的优缺点:
-
增:链式存储 计算上 优于顺序存储 (中间插入时链式不用像顺序一样去移动位置)
-
删:链式存储 计算上 优于顺序存储 (中间删除时链式不用像顺序一样去移动位置)
-
查:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)
-
改:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)
Linkedlist
Linkedlist是一个c#为哦我们封装好的类,它的本质是一个可变类型的双向链表。
//申明
//using System.Collection.Generic
LinkedList<int> linkedList = new LinkListed<int>();
//链表对象需要掌握两个类:LinkedList和LinkedListNode
//增删查改
//增
//1.在链表尾部添加元素
linkedList.AddLast(10);
//2.在链表头部添加元素
linkedList.AddFirst(20);
//3.在某一个节点之后添加一个节点 要在指定节点 先得得到一个节点
linkedListNode<int> n = LinkedList.Find(20);
linkedList.addAfter(n,15);
//4.在某一个节点之前添加一个节点 要在指定节点 先得得到一个节点
linkedList.addBefore(n,11);
//删
//1.移除头节点
linkedList.RemoveFirst();
//2.移除尾结点
linkedList.RemoveLast();
//3.移除指定节点 无法通过指定位置直接移除
linkedList.Remove(20);
//4.清空
linkedList.Clear();
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
linkedList.AddLast(4);
//查
//1.头节点
linkedListNode<int> first = LinkedList.First; //1
//2.尾结点
linkedListNode<int> last = LinkedList.Last; //4
//3.找到指定值的节点 无法通过下标 只有遍历查找指定位置的元素
linkedListNode<int> node = LinkedList.Find(3); //去找值为3的节点
Console.WriteLine(node.Value);
node = linkedList.Find(5); //找不到会返回空
//4.判断是否存在
if(linkedLine.Contains(1))
{
Console.WriteLine("链表中存在1");
}
//改
//要先得到节点 再改变其中的值
Console.WriteLine(linkedList.First.Value);
linkedList.First.Value = 10;
Console.WriteLine(linkedList.First.Value);
//遍历
//1.foreach遍历
foreach (int item in linkedList)
{
Console.WriteLine(item);
}
//2.通过节点遍历
//从头到尾
LinkedListNode<int> nowNode = linkedList.First;
while(nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Next;
}
//从尾到头
nowNode = linkedList.Last;
while(nowNode != null)
{
Console.WriteLine(nowNode.Value);
nowNode = nowNode.Previous;
}
泛型栈和队列
使用上和之前的Stack和Queue一模一样
//using System.Collection.Generic
Stack<int> stack = new Stack<int>();
Queue<int> queue = new Queue<int>()
委托和事件
委托
概念
委托是函数(方法)的容器,可以理解为表示函数的变量类型。用来存储,传递函数(方法)。
委托的本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)。
不同的 函数(方法)必须对应各自“格式”一直的委托。
基本语法
关键字:delegate
语法:访问修饰符 delegate 返回值 委托名(参数列表)
写在哪里?
可以申明再namespace和class语句块中 更多的写在namespace中
简单记忆委托语法 就是函数申明语法前面加一个delegate关键字
定义自定义委托:
访问修饰符默认不写为public 在别的命名空间中也能使用
private 其他命名空间就不能用了
一般使用public。
//申明了一个可以用来储存无参无返回值函数的容器
//这里只是定义了规则 并没有使用
delegate void MyFun();
//委托规则的申明是不能重名(同一语句块中)
//表示用来装载或传递 返回值是int 有一个int参数的函数的 委托 容器规则
delegate int MyFun2(int a);
使用定义好的委托
委托变量是函数的容器
static void Main(string[] args)
{
MyFun f = new MyFun(Fun);
Console.WriteLine("1");
Console.WriteLine("2");
Console.WriteLine("3");
Console.WriteLine("4");
Console.WriteLine("5");
f.Invoke(); //123123
MyFun f2 = Fun; //与MyFun f2 = new MyFun(Fun)一样
Console.WriteLine("1");
Console.WriteLine("2");
Console.WriteLine("3");
Console.WriteLine("4");
Console.WriteLine("5");
f2(); //123123
MyFun2 f3 = Fun2;
Console.WriteLine(f3.(1));
MyFun2 f4 = new MyFun2(Fun2);
Console.WriteLine(f4.Invoke(3));
}
static void Fun()
{
Console.WriteLine("123123");
}
static int Fun2(int value)
{
return value;
}
static void Fun3()
{
Console.WriteLine("李四在做什么");
}
static string Fun4()
{
return "";
}
static int Fun5()
{
return 1;
}
委托常用在:
1.作为类的成员
2.作为函数的参数
class Test
{
public MyFun fun;
public MyFun2 fun2;
public MyFun(MyFun fun,MyFun2 fun2)
{
//先处理一些别的逻辑 当这些逻辑处理完了 再执行传入的函数
int i = 1;
i *= 2;
i += 2;
this.fun = fun;
this.fun2 = fun2;
}
}
Test t = new Test();
t.TestFun(Fun,Fun2);
委托变量可以存储多个函数(多播委托)
MyFun ff = Fun;
ff += Fun;
ff(); //会输出两次123123
使用系统自带的委托
//using System;就可以使用Action 无参无返回值的委托
Action action = Fun;
action += Fun;
action();
//系统给我们自带的返回值为<>里面的类型的委托
Func<string> funcString = Fun4;
Func<int> funcInt = Fun5;
//可以传n个参数的 系统提供了1到16个参数的委托 直接用就行
Action<int,string> = action2 Fun6;
//可以传n个参数的 并且有返回值的 系统也提供了16个委托
Func<int,int> func2 = Fun2;
//自己写一个泛型委托
delegate T MyFun3<T,K>(T t,K k);
事件
概念
事件是基于委托的存在,事件是委托的安全包裹。让委托的使用更具安全性,事件是一种特殊的变量类型。
语法
访问修饰符 event 委托类型 事件名
事件的使用:
- 1.事件是作为成员变量存在于类中
- 2.委托怎么用 事件就怎么用
事件相对于委托的区别:
- 1.不能再类外部赋值
- 2.不能再类外部调用
注意:它只能作为成员存在于类和接口以及结构体中
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("123123");
}
}
Test t = new Test();
//委托可以再外部赋值
t.myFun = null;
t.myFun = TestFun2;
//事件是不能再外部赋值的
//t.myEvent = null;
//t.myEvent = TestFun2;
//虽然不能直接赋值,但是可以+- 去添加移除记录的函数
t.myEvent += TestFun2;
t.myEvent -= TestFun2;
//事件不可以直接赋值
//t.myEvent = t.myEvent + TestFun2;
//委托是可以再外部调用的
t.myFun.Invoke();
t.myFun();
//事件是不能再外部调用的
//t.myEvent();
//t.myEvent.Invoke();
t.DoEvent();
Action a = TestFun2;
//事件是不能作为临时变量在函数中使用的
//event Action ae = TestFun2;
static void TestFun2()
{
}
为什么有事件?
- 1.防止外部随意置空委托
- 2.防止外部随意调用委托
- 3.事件相当于对委托进行了一次封装 让其更安全
匿名函数
概念
顾名思义,就是没有名字的函数。
匿名函数的使用主要是配合委托和事件进行使用。
脱离委托和事件是不会使用匿名函数的。
基本语法
delegate (参数列表)
{
函数逻辑
}
何时使用?
- 函数中传递委托参数时
- 委托或事件赋值时
匿名函数的使用
//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.有返回值
Fanc<string> c = delegate()
{
return "123";
};
c();
//4.一般情况会作为函数参数传递或者作为函数返回值
Test t = new Test();
//参数传递
Action ac = delegate()
{
Console.WriteLine("随参数传入的匿名函数逻辑");
};
t.Dosomething(100,ac);
t.Dosomething(100, delegate()
{
Console.WriteLine("随参数传入的匿名函数逻辑");
});
//返回值
Action ac2 = t.GetFun();
ac2();
//一步到位
t.GetFun()();
class Test
{
public Action action;
//作为参数传递时
public void DOsomething(int a,Action fun)
{
Console.WriteLine(a);
fun();
}
//作为返回值
public Action GetFun()
{
return delegate()
{
Console.WriteLine("函数内部返回的一个匿名函数的逻辑");
};
}
}
匿名函数的缺点
- 添加到委托或事件容器中后 不记录 无法单独移除
Action ac3 = delegate()
{
Console.WriteLine("匿名函数一");
};
ac3 += delegate()
{
Console.WriteLine("匿名函数二");
};
ac3();
//会输出匿名函数一 匿名函数二
//因为匿名函数没有名字 所以没有办法指定移除某一个匿名函数
Lambad表达式
可以将Lambad表达式理解为匿名函数的简写。
它除了写法不同外,使用上几乎和匿名函数一模一样,都是和委托或者事件配合使用的、
//lambad表达式
//(参数列表)=>
//{ 函数体 };
//1.无参无返回值
Action a = ()=>
{
Console.WriteLine("无参无返回值的lambad表达式");
};
a();
//2.有参
Action<int> b = (int value) =>
{
Console.WriteLine("有参数的lambad表达式{0}",value);
};
b(100);
//3.甚至参数类型可以省略 参数类型和委托或事件容器一致
Action<int> c = (value) =>
{
Console.WriteLine("省略参数类型的lambad表达式{0}",value);
};
c(200);
//4.有返回值
//Func<>最后一个类型是返回类型,前面都是参数
Func<string,int> d = (value)=>
{
Console.WriteLine("有返回值的lambad表达式{0}",value);
return 1;
}
d("123123");
//其它传参使用等和匿名函数一样
//缺点也和匿名函数一样的
闭包
内层函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。
注意:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
class Test
{
public event Action action;
public Test()
{
int value = 10;
//这里就形成了闭包
//因为当构造函数执行完毕时 其中申明的临时变量value的声明周期被改变了
action = ()=>
{
Console.WriteLine(value)
};
for(int i=0;i<10;i++)
{
action += ()=>
{
Console.WriteLine(i);
}
}
for(int i=0;i<10;i++)
{
int index = i;
action += ()=>
{
Console.WriteLine(index);
}
}
}
public void DoSomething()
{
action();
}
}
Test t = new Test();
t.DoSomething();
//打印出来11个10
//再y打印1-9
List排序
List自带的排序方法
List<int> list = new List<int>();
List.Add(3);
List.Add(2);
List.Add(6);
List.Add(1);
List.Add(4);
List.Add(5);
for(int i = 0;i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
//list提供了排序方法 升序排列
list.Sort();
for(int i = 0;i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
//123456
//ArrayList也自带Sort方法
自定义类的排序
class Item : ICpmparable<Item>
{
public int money;
public Item(int money)
{
this.money = momey;
}
public int CompareTo(Item other)
{
//返回值的含义
//小于0:放在传入对象的前面
//等于0:保持当前的位置不变
//大于0:放在传入对象的后面
//可以简单理解传入对象的位置 就是0
//如果你的返回为负数,就放在它的左边 也就是前面
//如果你返回正数 就放在它的右边 也就是后面
//升序排列
if(this.money>other.money)
{
return 1;
}
else
{
return -1;
}
}
}
List<Item> itemList = new List<Item>();
itemList.Add(new Item(45));
itemList.Add(new Item(10));
itemList.Add(new Item(99));
itemList.Add(new Item(24));
itemList.Add(new Item(100));
itemList.Add(new Item(12));
//排序方法
itemList.Sort();
for(int i = 0;i < itemList.Count; i++)
{
Console.WriteLine(itemList[i].money);
}
通过委托函数排序
class ShopItem
{
public int id;
public ShopItem(int id)
{
this.id = id;
}
}
List<ShopItem> shopItems = new List<ShopItem>();
shopItems.Add(new ShopItem(2));
shopItems.Add(new ShopItem(1));
shopItems.Add(new ShopItem(4));
shopItems.Add(new ShopItem(3));
shopItems.Add(new ShopItem(5));
shopItems.Add(new ShopItem(6));
shopItems.Sort(SortShopItem);
for(int i = 0;i < shopItems.Count; i++)
{
Console.WriteLine(shopItems[i].id);
}
static int SortShopItem( ShopItem a,ShopItem b)
{
//传入的两个对象为列表中的两个对象
//进行两两的比较 用左边的和右边的条件比较
//返回值规则和之前的一样 0做标准 负数在左(前) 正数在右
if(a.ad>b.id)
{
return 1;
}
else
{
return -1;
}
}
还可以直接用匿名函数传入
shopItems.Sort(delegate (ShopItem a,ShopItem b)
{
if(a.ad>b.id)
{
return 1;
}
else
{
return -1;
}
});
//lambad配合三目运算符 完美呈现
shopItems.Sort((a,b) =>
{
return a.ad>b.id?1:-1;
});
协变逆变
协变:和谐的变化,自然的变化。因为里氏替换原则,父类可以装子类,所以 子类变父类,比如string变成object。
感受是和谐的
逆变:逆常规的变化,不正常的变化。因为里氏替换原则,父类可以装子类,但是子类不能装父类。所以 父类变子类,
比如object变成string感受是不和谐的。
协变和逆变是用来修饰泛型的
协变:out
逆变:in
用于泛型中 修饰 泛型字母的
只有泛型接口和泛型委托能使用的
//作用
//1、返回值和参数
//用out修饰的泛型 只能作为返回值
delegate T TestOut<out T>(); //括号里面不能写T类型参数了
//用in修饰的泛型 只能作为参数
delegate void TestIn<in T>(T t);
interface Tesr<out T>
{
T TestFun();
}
//2.结合里氏替换原则理解
class Father
{
}
class Son:Father
{
}
//协变 父类总是能被子类替换
//看起来 就是son ->father
TestOut<Son> os = ()=>
{
return new Son();
};
TestOut<Father> of = os;
Father f= of(); //实际上返回的是 os里面装的函数返回的是Son
//逆变 父类总能被子类替换
TestIn<Father> iF = (value) =>
{
};
TestIn<Son> iS = iF;
iS(new Son()); //实际调用的是iF
多线程
了解线程前先了解进程
- 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。打开一个应用程序就是在操作系统上开启了一个进程。
进程之间可以相互独立运行,互不干扰。
进程之间也可以相互访问、操作。
什么是线程?
- 操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
- 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程。
简单理解线程: 就是代码从上到下运行的一条管道。
什么是多线程?
- 我们可以通过代码 开启新的线程。可以同时运行代码的多条“管道”就叫做多线程
语法相关
线程类 Thread
需要引用命名空间 using System.Threading;
static bool isRuning = true;
//1.申明一个新的线程
// 注意 线程执行的代码 需要封装到一个函数中
// 新线程 将要执行的代码逻辑 被封装到了一个函数语句块中
Thread t = new Thread(NewThreadLogic);
//2.启动线程
t.Start();
//3.设置为后台线程
//当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
//后台线程不会防止应用程序的进程被终止掉
//如果不设置为后台线程 可能导致进程无法正常关闭
t.IsBackground = true; //设置为后台线程
//4.关闭释放一个线程
//如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意地去关闭它
//如果是死循环 想要终止这个线程 有两种方式
//4.1 死循环中的bool标识
Console.ReadKey();
isRuning = false;
Console.ReadKey();
//4.2 通过线程提供的方法(注意在.Net core 版本中无法终止 会报错)
try
{
t.Abort();
t = null;
}
catch
{
}
//5.线程休眠
//让线程休眠多少毫秒 1s=1000ms
//在哪个线程里执行 就休眠哪个线程
//Thread.Sleep(1000);
static void NewThreadLogic()
{
//新开线程 执行代码的逻辑 在该函数语句块中
while(isRuning)
{
Thread.Sleep(1000);
Console.WriteLine("新开线程代码逻辑");
}
}
线程之间共享数据
- 多个线程使用的内存是共享的,都属于该应用程序(进程)
- 所以要注意,当多线程同时操作同一片内存区域时可能会出问题
- 可以通过加锁的形式避免问题。
static object obj = new object();
//lock 当我们在多个线程当中想要访问同样的东西 进行逻辑处理时
//为了避免不必要的逻辑顺序执行的查错
//lock(引用对象)
while(true)
{
lock(obj)
{
Console.SetCursorPosition(0,0);
Console.ForegroundColor = ConsoleColor.Red;
Console.Write("⚪");
}
}
static void NewThreadLogic()
{
lock(obj)
{
Console.SetCursorPosition(0,0);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("方形");
}
}
多线程对于我们意义
- 可以用多线程专门处理一些复杂耗时的逻辑
- 比如寻路、网络通信等等
预处理器指令
什么是编译器?
- 编译器是一种翻译程序,它用于将源语言程序翻译为目标语言程序
- 源语言程序:某种程序设计语言写成的,比如C#、C++、JAVA等语言写的程序
- 目标语言程序:二进制数表示的伪辑器代码写的程序。
什么是预处理指令?
- 预处理指令 指导编译器 在实际编译开始之前对信息进行预处理
- 预处理指令 都是以#开始
- 预处理指令不是语句,所以它们不以分号;结束
- 目前我们经常用到的 折叠代码块 就是预处理器指令
常见的预处理指令
1.#define 定义一个符号,类似一个没有值的变量
undef 取消define定义的符号,让其失效。
两者一般都写在脚本文件最前面。一般配合if指令使用,或配合特性
//定义一个符号
#define Unity4
#define IOS
#define Unity2021
//取消定义一个符号
#undef Unity4
2.#if #elif #else #endif 和用if语句一样,一般配合# define定义的符号使用,用于告诉编译器进行编译代码的流程控制。
如果发现有Unity4这个符号 那么其中包含的代码就会被编译器编译
可以通过 逻辑或 和 逻辑与 进行多种符号的组合判断
//如果发现有Unity4的符号 那么其中包含的代码 就会被编译器翻译
#if Unity4
Console.WriteLine("版本为Unity4"); //不会打印 因为前面把unity4取消了
#elif Unity2021 && IOS
Console.WriteLine("版本为Unity2021");
//#warning 这个版本不合法
//#error z
#else
Console.WriteLine("其他版本");
#endif
3.#warning #error 告诉编译器,是报警告还是报错误 一般还是配合if使用
反射和特性
反射
什么是程序集?
程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物
在WINDOWS系统中,它一般表现为后缀为.dll(库文件)或者.exe(可执行文件)的格式
说人话:
程序集就是我们写的一个代码集合,我们现在写的所有代码,最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件(dll)或者一个可执行文件(exe)。
元数据
元数据就是用来描述数据的数据。
这个概念不仅仅用于程序上,在别的领域也有元数据
说人话:
程序中的类,类中的函数、变量等等信息就是 程序的 元数据
有关程序以及类型的数据被称为 元数据,它们保存在程序集中
反射的概念
程序正在运行时,可以查看其他程序集或者自身的元数据。
一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射。
说人话:
程序在运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
类、函数、变量、对象等等,实例化它们,执行它们,操作它们。
反射的作用
因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性。
- 1.程序运行时得到所有元数据,包括元数据的特性
- 2.程序运行时实例化对象,操作对象
- 3.程序运行时创建新对象,用这些对象执行任务
class Test
{
private int i = 1;
public int j = 1;
public string str = "123";
public Test()
{
}
public Test(int i)
{
this.i = i;
}
public Test(int i,string str):str(i)
{
this.str = str;
}
public void Speak()
{
Console.WriteLine(i);
}
}
Type
获取Type
//Type(类下信息类)
//它是反射功能的基础
//它是访问元数据的主要方式
//使用Type的成员获取有关类型声明的信息
获取Type
//1.万物之父object中的GetType()可以获取对象的Type
int a = 42;
Type type = a.GeyType();
Console.WriteLine(type);
//输出System.Int32
//2.通过typeof关键字 传入类名 也可以得到对象的Type
Type type2 = typeof(int);
Console.WriteLine(type2);
//输出System.Int32
//3.通过类的名字 也可以获取类型
// 注意:类名必须包括命名空间
Type type3 = Type.GeyType("System.Int32");
Console.WriteLine(type3);
//输出System.Int32
//type123它们的值和在堆里的地址都是一样的
得到类的程序集信息
//可以通过Type得到类型所在程序集信息
Console.WriteLine(type.Assembly);
Console.WriteLine(type2.Assembly);
Console.WriteLine(type3.Assembly);
//输出三次:System.Private.CoreLib, Version=4.0.0.0, Culture-neutral, PublicKeyToken=7cec85d7ba7798e
//以上是版本信息
获取类中的公共成员
//首先得到Type
Type t = typeof(Test);
//然后得到所有公共成员
//需要引用命名空间 using System.Reflection;
MemberInfo[] infos = t.GetMembers();
for(int i = 0;i < infos.Length; i++)
{
Console.WriteLine(infos[i]);
}
//会输出所有公共成员Void Speak()等等
获取类的公共构造函数并调用
//1.获取所有构造函数
ConstructorInfo[] ctors = t.GetConstructors();
for(int i = 0;i < ctors.Length; i++)
{
Console.WriteLine(ctors[i]);
}
//输出 Void .ctor()\n Void .ctors(Int32) \n Void .ctors(Int32,System String)
//2.获取其中一个构造函数传入Type数组 数组中内容按顺序是参数类型
//得构造函数传入Type数组 数组中内容按顺序是参数类型
//执行构造函数传入 object数组 表示按顺序传入的参数
// 2-1得到无参构造
ConstructorInfo info = t.GetConstructor(new Type[0]);
//执行无参构造 无参构造 没有参数 传null
Test obj = info.Invoke(null) as Test;
Console.WriteLine(obj.j);
// 2-2得到有参构造
ConstructorInfo info2 = t.GetConstructor(new Type[]{typeof(int)});
obj = info2.Invoke(new object[]{ 2 }) as Test;
Console.WriteLine(obj.str);
ConstructorInfo info3 = t.GetConstructor(new Type[]{typeof(int),typeof(string)});
obj = info3.Invoke(new object[]{ 4,"44444" }) as Test;
Console.WriteLine(obj.str);
获取类的公共成员变量
//1.得到所有成员变量
FieldInfo[] fieldInfos = t.GetFields();
for(int i = 0; i < fieldfos.Length; i++)
{
Console.WriteLine(fieldInfos[i]);
}
//输出 Int32 j System String str
//2.得到指定名称的公共成员变量
FieldInof infoJ = t.GetField("j");
Console.WriteLine(infoJ);
//输出 Int32 j
//3.通过反射来获取和设置对象的值
Test test = new Test();
test.j = 99;
test.str = "999";
// 3-1 通过反射 获取对象的某个变量的值
Console.WriteLine(infoJ.GetValue(test));
// 3-2 通过反射 设置对象的某个变量的值
infoJ.SetValue(test,100);
Console.WriteLine(infoJ.GetValue(test));
获取类的公共成员方法
//通过Type类中的GetMethod公共 得到类中的方法
//MethodInfo 是方法的反射信息
Type strType = typeof(string);
//1.如果存在方法重载 用Type数组表示参数类型
MethodInfo[] methods = strType.GetMethods();
for(int i = 0; i < methods.Length; i++)
{
Console.WriteLine(methods[i]);
}
MethodInfo subStr = strType.GetMethod("Substring",new Type[] {typeof(int),typeof(int)});
//2.调用该方法
string str = "Hello,World!";
//注意:如果是静态方法 Invoke中的第一个参数传null即可
object result = subStr.Invoke(str,new object[]{7,5});
Console.WriteLine(result);
//输出orld!
Actovator
- 用于快速实例化对象的类
- 用于将Type对象快捷实例化为对象
//先得到type
//然后快速实例化一个对象
Test testType = typeof(Test);
//1.无参构造
Test testObj = Activator.CreateInstance(testType) as Test;
Console.WriteLine(testObj.str);
//2.有参数构造
testObj = Activator.CreateInstance(testType,99) as Test;
Console.WriteLine(testObj.j); //i是私有的,没办法访问,但是可以通过断点看到i是99
testObj = Activator.CreateInstance(testType,99,"111222") as Test;
Console.WriteLine(testObj.j);
Assembly
程序集类:主要用来加载其它程序集。加载后才能用Type来使用其他程序集中的信息,比如,dll文件(库文件),简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
//三种加载程序集的函数
//一般用来加载同一文件下的其他程序集
//sembly assembly2 = Assembly.Load("程序集名称");
//一般用来加载不在同一文件下的其他程序集
//sembly assembly = Assembly.LoadFrom("")
//一般用来加载不在同一文件下的其他程序集
//Assembly assembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
//Assembly assembly2 = Assembly.LoadFile("要加载的文件完全限定路径");
//1.先加载一个程序集
//加一个@能够取消转义字符 找到dll文件复制路径+dll文件名
Assembly assembly = Assembly.LoadFrom(@"E:\C#\ArrayList\ArrayList\bin\Debug\netcoreapp3.1\Lesson_18练习题");
//得到元数据中所有的类型
Type[] types = assembly.GetTypes();
for(int i = 0; i < types.length; i++)
{
Console.WriteLine(types[i]);
}
//2.再加载程序集中的一个类对象 之后才能用反射
Type icon = assembly.GetType("Lession_18练习题.Icon");
MemberInfo[] members = icon.GetMembers();
for(int i = 0; i < members.Length; i++)
{
Console.WriteLine(members[i]);
}
//通过反射 实例化一个icon
//首先得到枚举Type 来得到可以传入的参数
Type moveDir = assembly.GetType("Lession_18练习题.E_MoveDir");
FieldInfo right = moveDir.GetField("Right");
//直接实例化对象
object iconObj = Activator.CreateInstance(icon,10,5,right.GetValue(null));
//得到对象中的方法
MethodInfo move = icon.GetMethod("Move");
MethodInfo draw = icon.GetMethod("Draw");
MethodInfo clear = icon.GetMethod("Clear");
while(true)
{
Thread.Sleep(1000); //休眠1s
clear.Invoke(iocnObj,null);
move.Invoke(iocnObj,null);
draw.Invoke(iocnObj,null);
}
//3.类库工程创建
为什么要学反射?
为了之后学习Unity引擎的基本工作原理做铺垫
Unity引擎的基本工作机制 就是建立在反射的基础上
特性
特性是什么?
- 1.特性是一种允许我们向 程序的程序集 添加 元数据 的语言结构
- 它是用于保存程序结构信息的某种特殊类型的类
- 2.特性提供功能强大的方法以将声明信息与 C# 代码(类型、方法、属性等)相关联。
- 特性与程序实体关联后,即可在运行是使用反射查询特性信息。
- 3.特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集中。
- 它可以放置在几乎所有的声明中(类、变量、函数等等申明)
说人话:
- 特性的本质是个类
- 我们可以利用特性类为元数据添加额外信息
- 比如一个类、成员变量、成员方法等等为他们添加更多额外信息
- 之后可以通过反射来获取这些额外信息
自定义特性
//继承特性基类 Attribute
class MyCustomAttribute : Attribute
{
//特性中的成员 一般根据需求来写
public string info;
public MyCustomAttribute(string info)
{
this.info = info;
}
public void TestFun()
{
Console.WriteLine("特性的方法");
}
}
特性的使用
基本语法:[特性名(参数列表)]
本质上 就是在调用特性类的构造函数
写在哪?类、函数、变量上一行,表示他们具有该特性信息
[MyCustom("这是一个我自己写的用于计算的类")]
class MyClass
{
[MyCustom("这是一个成员变量")]
public int value;
[MyCustom("这是一个用于计算加法的函数")]
public void TestFun([MyCustom("函数参数")]int a)
{
}
}
//特性的使用
MyClass mc = new MyClass();
//复习下多种得到type方法
Type t = mc.GetType();
//t = typeof(MyClass);
//t = Type.GetType("Lession21_特性.MyClass");
//判断是否使用了某个特性
//参数一:特性的类型
//参数二:代表是否继承链(属性和事件忽略此参数)
if(t.IsDefined(typeof(MyCustomAttribute),false))
{
Console.WriteKLine("该类型应用了特性");
}
//获取Type元数据中的所有特性
object[] array = t.GetCustomAttributes(true);
for(int i = 0; i < array.Length; i++)
{
if(array[i] is MyCustomAttribute)
{
Console.WriteLine((array[i] as MyCustomAttribute).info);
(array[i] as MyCustomAttribute).TestFun();
}
}
限制自定义特性的使用范围
通过为特性类 加特性 限制其使用范围
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,AllowMultiple = true,Inherited = true)]
- 参数一:AttributeTargets —— 特性能够用在哪些地方
参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
参数三:Inherited —— 特性是否能被派生类和重写成员基础
系统自带特性——过时特性
过时特性 Obsolete
- 用于提示用户 使用的方法等成员以及过时 建议使用新方法
一般加在函数前的特性
class TestClass
{
//参数一:调用过时方法时 提示的内容
//参数二:true——使用该方法时会报错 false——使用该方法时直接警告
[Obsolete("OldSpeak方法已经过时了,请使用Speak方法"),false]
public void OldSpeak(string str)
{
}
public void Speak()
{
}
//系统自带特性——调用者信息特性
//系统会通过特性自动传入需要的信息
public void SpeakCaller(string str,[CallerFilePath]string fileName = "",
[CallerLineNumber]int line = 0,[CallerMemberName]string target = "")
}
系统自带特性——调用者信息特性
哪个文件调用? CallerFilePath特性
哪一行调用? CallerLineNumber特性
哪个函数调用?CallerMemberName特性
需要引用命名空间 using System.Runtime.CompilerServices;
一般作为函数参数的特性
系统自带特性——条件编译特性
条件编译特性 Conditional
它会和预处理指令 #define 配合使用
需要引用命名空间using System.Diagnostics;
主要可以用在一些调试代码上
有时想执行有时不想执行的代码
#define Fun
[Conditional("Fun")]
static void Fun()
{
Console.WriteLine("Fun执行");
}
Fun(); //无法调用 只有加上#define Fun才能打印
系统自带特性——外部Dll包函数特性
DllImport
用来标记非.Net(C#)的函数,表明该函数在一个外部的DLL中定义。
一般用来调用C或者C++的DLL包写好的方法
需要引用命名空间 using System.Runtime.InteropServices
[DllImport("Test.dll")]
public static extern int Add(int a,int b);
迭代器
迭代器是什么?
- 迭代器有时又称光标,是程序设计的软件设计模式。
- 迭代器提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的标识。
- 在表现效果上看:
- 是可以在容器对象(例如链表或数组)上遍历访问的接口
- 设计人员无需关心容器对象的内存分配的实现细节
- 可以用foreach遍历的类,都是实现了迭代器的
标准迭代器的实现方法
关键接口:IEnumerable,Enumerator
命名空间:using System.Collections;
可以通过同时继承IEnumerable和Enumerator实现其中的方法
using System;
using System.Collections;
namespace ConsoleApp1
{
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 };
}
public object Current
{
get
{
return list[position];
}
}
#region IEnumerable
public IEnumerator GetEnumerator()
{
Reset();
return this;
}
#endregion
public bool MoveNext()
{
//移动光标
++position;
//是否溢出 溢出就不合法
return position < list.Length;
}
//areset是重置光标位置 一般写在获取 IEnumeraror对象这个函数中
//用于第一次重置光标位置
public void Reset()
{
position = -1;
}
}
class Program
{
static void Main(string[] args)
{
//foreach本质
//1.先获取in后面这个对象的 IEnumerator
// 会调用对象其中的 GetEnumerator 方法 来获取
//2.执行得到这个IEnumerator对象中的MoveNext方法
//3.只要MoveNext方法的返回值时true,就会得到Current
// 然后复制给 item
CustomList list = new CustomList();
foreach(int item in list)
{
Console.WriteLine(item);
}
}
}
}
用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关键字 配合迭代器使用
//可以理解为 暂时返回 保留当前的状态
//一会儿还会再回来
//C#的语法糖
yield return list[i];
}
}
}
用yield return 语法糖为泛型类实现迭代器
class CustomList<T> : IEnumerable
{
private T[] array;
public CustomList2(params T[] array)
{
this.array = array;
}
public IEnumerator GetEnumerator()
{
for(int i=0;i<array.Length;i++)
{
yield return array[i];
}
}
}
特殊语法
var隐式类型
- var是一种特殊的变量类型
- 它可以用来表示任意类型的变量
- 注意:
- 1.var不能作为类的成员 只能用于临时变量申明时
- 也就是 一般写在函数语句块中
- 2.var必须初始化
var i = 5;
var s = "123";
var array = new int[] { 1, 2, 3, 4 };
var list = new List<int>();
设置对象初始值
- 申明对象时,可以通过直接写大括号的形式初始化公共成员变量和属性
class Person
{
private int money;
public bool sex;
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public Person(int money)
{
this.money = money;
}
}
Person p = new Person(100) { sex = true, Age = 18, Name = "唐老狮" };
Person p2 = new Person(200) { Age = 18 };
设置集合初始值
- 申明集合对象时,也可以通过大括号 直接初始化内部属性
int[] array = new int[]{1,2,3,4,5};
List<int> listInt = new List<(){1,2,3,4,5,6};
List<Person> listPerson = new List<Persion>(){
new Person(200),
new Person(100){Age = 10},
new Person(1){sex = true,Name = "唐老狮"},
};
Dictionart<int,string> dictionary = new Dictionary<int,string>()
{
{1,"123"},{2,"222"}
};
匿名类型
- var 变量可以申明为自定义的匿名类型
var v = new{age = 10,money = 11,name = "小明"};
Console.WriteLine(v.age);
Console.WriteLine(v.name);
可空类型
//1.值类型是不能赋值为空的
//int c = null; //会报错,值类型不能赋值为 空的
//2.申明时 在值类型后面加?可以赋值为空
int? c = null;
//3.判断是否为空
if(c.HasValue)
{
//打印值有这两种方法,一样的
Console.WriteLine(c);
Console.WriteLine(c.Value);
}
//4.安全获取可空类型值
int? value = null;
// 4-1.如果为空 默认返回值类型的默认值
Console.WriteLine(value.GetValueOrDefault()); //输出0
// 4-2.也可以指定一个默认值
Console.WriteLine(value.GetValueOrDefault(100)); //输出100,并没有给value赋值
Console.WriteLine(value); //无输出
float? f = null;
double? d = null;
//引用类型
object o = null;
if(o! = null)
{
Console.WriteLine(o.ToString());
}
//相当于是一种语法糖 能够帮助我们自动去判断o是否为空
//如果是null就不会执行tostring,也不会报错
Console.WriteLine(o?.ToString());
int[] arrayInt = null;
Console.WriteLine(array?[]); //不会报错 只是返回空而已
Action action = null;
if(action != null)
{
action();
}
action?.Invoke(); //和上面那段相同
空合并操作符
//空合并操作符??
//左边值??右边值
//如果左边值为null 就返回右边值 否则返回左边值
//只要是可以为null的类型都能用
//相当于一个三目运算符
int ? intV = null;
int intI = intV == null?100 : intV.value;
intI = intV ?? 100; //和上一句一样
Console.WriteLine(intI);
string str = null;
str = str ?? "hahah";
Console.WriteLine(str);
内插字符串
- 关键符号:$
- 用$来构造字符串,让字符串中可以拼接变量
string name = "唐老狮";
int age = 18;
Console.WriteLine($"好好学习,{name},年龄:{age}");
单逻辑简略写法
//当循环或者if语句中只有一句代码时,可以省略大括号
if(true) Console.WriteLine("123123");//不用大括号
for(int i = 0;i < 10; i++)
Console.WriteLine(i);
class Person
{
public bool sex;
public string Name
{
//简略写法
get => "唐老狮";
set => set = true;
}
public int Add(int x,int y) => x+y;
public void Speak(string str) => Console.WriteLine(str);
}
值类型和引用类型
知识回顾:
值类型
- 无符号:byte,ushort,uint,ulong
- 有符号:sbyte,short,int,long
- 浮点数:float,double,decimal
- 特殊:char,bool
- 枚举:enum
- 结构体:struct
引用类型
- string
- 数组
- class
- interface
- 委托
值类型和引用类型的本质区别:
- 值的具体内容存在栈内存上
- 引用的具体内容存在堆内存上
如何判断值类型和引用类型?
F12进到类型的内部去查看,是class就是引用,是struct就是值。
语句块
命名空间 -> 类、接口、结构体 -> 函数、属性、索引器、运算符重载等 -> 条件分支、循环
- 上层语句块:类、结构体
- 中层语句块:函数
- 底层语句块:条件分支、循环等
我们的逻辑代码写在哪里?
- 函数、条件分支、循环-中底层语句块中
我们的变量可以申明在哪里?
- 上、中、底都能申明变量
- 上层语句块中:成员变量
- 中、底层语句块中:临时变量
变量的生命周期
-
编程时,大部分都是临时变量
-
在中底层申明的临时变量(函数、条件分支、循环语句块等)
-
语句块执行结束
-
没有被记录的对象将被回收或变成垃圾
-
值类型:被系统自动回收
-
引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收
int i = 1; string str = "123";
-
想要不被回收或者不变垃圾
-
必须将其记录下来
-
如何记录?
-
在更高层级记录或者使用静态全局变量记录
int b = 0;
while(1)
{
b=1;
}
结构体中的值和引用
-
结构体本身时值类型
-
前提:该结构体没有做为其他类的成员
-
在结构体中的值,栈中存储值具体的内容
-
在结构体中的引用,堆中存储引用具体的内容
-
引用类型始终存储在堆中
-
真正通过结构体使用其中引用类型时知识顺藤摸瓜
struct TestStruct
{
public Test t;
public int i;
}
TestStruct td = new TestStrict();
类中的值和引用
-
类本身是引用类型
-
在类中的值,堆中存储具体的值
-
在类中的引用,堆中存储具体的值
-
值类型跟大哥走,引用类型一根筋
数组中的存储规则
- 数组本身是引用类型
- 值类型数组,堆中房间存具体内容
- 引用类型数组,堆中房间存地址
结构体继承接口
- 利用里氏替换原则,用接口容器装载结构体存在装箱拆箱
interface ITest
{
int Value
{
get;
set;
}
}
struct TestStruct : ITest
{
int value;
public int Value
{
get
{
return value;
}
set
{
this.value = value;
}
}
}
TestStruct obj1 = new TestStruct();
obj1.Value = 1;
Console.WriteLine(obj1.Value); //1
TestStruct obj2 = obj1;
obj2.Value = 2;
Console.WriteLine(obj1.Value); //1
Console.WriteLine(obj2.Value); //2
ITest iObj1 = obj1; //里氏替换原则 父类装子类 装箱 value=1
ITest iObj2 = iObj1; //没有在堆里新开房间
iObj2.Value = 99;
Console.WriteLine(iObj1.Value); //99
Console.WriteLine(iObj2.Value); //99
TestStruct obj3 = (TesrStruct)iObj1; //拆箱