一、简单数据结构类
1. ArrayList
ArrayList是一个 C# 为我们封装好的类,它的本质是一个 object 类型的数组。ArrayList类帮助我们实现了很多方法,比如数组的增删查改
1.1 声明
using System.Collections; ArrayList array = new ArrayList();
讯享网
1.2 增删查改
讯享网ArrayList array = new ArrayList(); //增 array.Add(1); array.Add("123"); array.Add(true); array.Add(new object()); ArrayList array2 = new ArrayList() { 1, 2, 3, "123" }; array.AddRange(array2); //范围增加,把另一个 list 容器里面的内容加到后面 array.Insert(0, 123); //在指定位置插入元素 //删 array.Remove(1); //移除指定元素,从头找,删掉第一个1 array.RemoveAt(1); //移除指定位置的元素 array.Clear(); //清空 //查 object o = array[0]; //获取指定位置的元素 if (array.Contains("123")) //查看元素是否存在 { } int i=array.IndexOf("123"); //正向查找元素位置,找到返回位置,找不到返回-1 i = array.LastIndexOf("123"); //反向查找元素位置,找到返回位置,找不到返回-1 //改 array[0] = 999;
1.3 遍历
//长度 int len = array.Count; //容量 int cap = array.Capacity; //同StringBuilder,自动扩容,避免产生过多的垃圾 //for foreach遍历
1.4 拆箱装箱
ArrayList本质上是一个可以自动扩容的 object 数组,由于用万物之父来存储数据,自然存在装箱拆箱。当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时就存在拆箱。所以ArrayList尽量少用。
2. Stack
Stack是一个 C# 为我们封装好的类,它的本质也是一个 object 类型的数组,只是封装了特殊的存储规则。Stack是栈存储容器,栈是一种先进后出的数据结构。先存入的数据后获取,后存入的数据先获取,栈是先进后出
2.1 声明
讯享网using System.Collections; Stack stack = new Stack();
2.2 增取查改
//增(压栈) stack.Push(1); stack.Push("123"); stack.Push(true); stack.Push(new object()); //取(弹栈) object v= stack.Pop(); //查 //栈无法查看指定位置的元素,只能查看栈顶位置元素,且只会查看,不会弹出 object p=stack.Peek(); //查看元素是否存在于栈中 if (stack.Contains("123")) { } //改 //栈无法改变其中的元素,只能压栈弹栈,或者清空 stack.Clear();
2.3 遍历
讯享网//长度 len = stack.Count; //不能用for遍历,要通过foreach遍历,而且遍历出来的顺序也是从栈顶到栈底 foreach (object item in stack) { } //还有一种遍历方式,将栈转换为 object 数组,遍历出来的顺序也是从栈顶到栈底 object[] array1=stack.ToArray(); for (int i1 = 0; i1 < array1.Length; i1++) { } //循环弹栈 while (stack.Count > 0) { object o1=stack.Pop(); Console.WriteLine(o1); }
2.4 拆箱装箱
由于用万物之父来存储数据,自然存在装箱拆箱。当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时就存在拆箱。
3. Queue
Queue是一个 C# 为我们封装好的类,它的本质也是一个 object 类型的数组,只是封装了特殊的存储规则。Queue是队列存储容器,队列是一种先进先出的数据结构,先存入的数据先获取,后存入的数据后获取,先进先出
3.1 声明
using System.Collections; Queue queue = new Queue();
3.2 增取查改
讯享网//增 queue.Enqueue(1); queue.Enqueue("123"); queue.Enqueue(new object()); //取 //队列中不存在删除的概念,只有取的概念。取出先加入的对象 o = queue.Dequeue(); //查 //队列无法查看指定位置的元素,只能查看队列头部元素,且只会查看,不会弹出 p = queue.Peek(); //查看元素是否存在于队列中 if (queue.Contains("123")) { } //改 //队列无法改变其中的元素,只能进出队列,或者清空 queue.Clear();
3.3 遍历
//长度 len = queue.Count; //不能用for遍历,要通过foreach遍历,而且遍历出来的顺序也是从队头到队尾 foreach (object item in queue) { } //还有一种遍历方式,将队列转换为 object 数组,遍历出来的顺序也是从队头到队尾 array1 = queue.ToArray(); for (int i1 = 0; i1 < array1.Length; i1++) { } //循环出队 while (queue.Count > 0) { object o1 = queue.Dequeue(); Console.WriteLine(o1); }
3.4 拆箱装箱
由于用万物之父来存储数据,自然存在装箱拆箱。当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时就存在拆箱。
4. Hashtable
又称散列表,是基于键的哈希代码组织起来的键值对。它的主要作用是提高数据查询的效率,使用键来访问集合中的元素
4.1 声明
讯享网using System.Collections; Hashtable hashtable = new Hashtable();
4.2 增删查改
//增 //注意不能出现相同键 hashtable.Add(1, 1); hashtable.Add("12", "123"); hashtable.Add(new Hashtable(), new object()); //删 //直接填写要删除的键 //删除不存在的键没反应 hashtable.Remove(1); hashtable.Remove(6); //清空 hashtable.Clear(); //查 //找不到会返回空 p = hashtable[1]; p = hashtable["123"]; //查看是否存在 //根据键检测 if (hashtable.Contains("123")) { } //根据值检测 if (hashtable.ContainsValue("123")) { } //改 hashtable[1] = 0;
4.3 遍历
讯享网//长度 len = hashtable.Count; //遍历所有键(顺序不一定) foreach (object item in hashtable.Keys) { Console.WriteLine(item); Console.WriteLine(hashtable[item]); } //遍历所有值(顺序不一定) foreach (object item in hashtable.Values) { Console.WriteLine(item); } //键值对一起遍历 foreach (DictionaryEntry item in hashtable) { Console.WriteLine("键:" + item.Key + "值:" + item.Value); } //迭代器遍历法 IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator(); bool flag = myEnumerator.MoveNext(); while (flag) { Console.WriteLine("键:" + myEnumerator.Key + "值:" + myEnumerator.Value); flag = myEnumerator.MoveNext(); }
4.4 拆箱装箱
由于用万物之父来存储数据,自然存在装箱拆箱。当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时就存在拆箱。
二、泛型
1. 泛型
泛型实现了类型参数化,达到代码重用目的。通过类型参数化来实现同一份代码上操作多种类型。
泛型相当于类型占位符,定义类或方法时使用替换符代表变量类型,当真正使用类或者方法时,再具体指定类型
1.1 泛型分类
泛型类和泛型接口基本语法: class 类名<泛型占位字母> interface 接口名<泛型占位字母> 泛型函数基本语法: 函数名<泛型占位字母>(参数列表) 注意,泛型占位字母可以有多个,用逗号分开
1.2 泛型类和接口
泛型类
讯享网class TestClass<T> { public T value; //尚未指明的类型 } internal class Program { static void Main(string[] args) { TestClass<int> t=new TestClass<int>(); //此次声明说明T指的是int类型,成员变量value也是int类型 t.value = 1; TestClass<string> t1=new TestClass<string>(); //此次声明说明T指的是string类型 t1.value = "1"; } }
在使用的时候要说明T所指的类型,引用类型(包括自定义类)值类型均可以填。
class TestClass1<T,K,M,LL> //使用多个 { public T value; public K value1; public M value2; public LL value3; }
泛型接口
讯享网interface ITest<T> { T value { get; set; } } class Test : ITest<int> { public int value { get; set; } }
1.3 泛型方法
普通类中的泛型方法
public void TestFun<T>(T value) { Console.WriteLine(value); } public void TestFun<T>() { T value= default(T); } public T TestFun<T>(string str) { return default(T); } public void TestFun<T,K,M>(T value,K value1,M value2) { Console.WriteLine(value); }
泛型类中的泛型方法
讯享网class Test2<T> { public T value; public void TestFun(T t) //这个不算作是泛型方法,因为在声明泛型类变量的时候就会指定T的类型,在调用函数的时候不能去改变函数中T的类型 { } public void TestFun1<K>(K k) //声明泛型方法时注意不要使用与泛型类相同的字符 { } }
1.4 泛型的作用
不同类型对象的相同逻辑处理,就可以选择泛型
使用泛型可以一定程度避免拆箱装箱,如:ArrayList
2. 泛型约束
让泛型的类型有一定的限制。关键字 where。
where 泛型字母:(约束类型)
泛型约束一共有六种:
- 值类型 where 泛型字母:struct
- 引用类型 where 泛型字母:class
- 存在无参公共构造函数 where 泛型字母:new()
- 某个类本身或者其派生类 where 泛型字母:类名
- 某个接口的派生类型 where 泛型字母:接口名
- 另一个泛型类型本身或者派生类型 where 泛型字母:另一个泛型字母
2.1 各泛型约束讲解
值类型
class Test<T> where T : struct //约束只能是值类型 { public T value; public void TestFun<K>(K k) where K : struct { } } internal class Program { static void Main(string[] args) { Test<int> test = new Test<int>(); test.value = 42; test.TestFun<float>(1.2f); } }
引用类型
讯享网class Test<T> where T : class { public T value; public void TestFun<K>(K k) where K : class { } } internal class Program { static void Main(string[] args) { Test<Random> test = new Test<Random>(); test.value = new Random(); test.TestFun<Random>(new Random()); } }
存在无参公共构造函数
class Test<T> where T : new() //要求指定的类型必须有公共且无参的构造函数,并且是非抽象类型 { public T value; public void TestFun<K>(K k) where K : struct { } } class Test1 { private Test1() { } } class Test2 { public Test2(int x) { } } internal class Program { static void Main(string[] args) { //两个均会报错 Test<Test1> text1 = new Test<Test1>(); Test<Test2> text2 = new Test<Test2>(); //第三个不会报错 Test<Test3> text3 = new Test<Test3>(); } }
某个类本身或者其派生类
讯享网 class Test<T> where T : Test1 { public T value; public void TestFun<K>(K k) where K : Test1 { } } class Test1 { } class Test2:Test1 { public Test2(int x):base() { } } internal class Program { static void Main(string[] args) { Test<Test1> text1 = new Test<Test1>(); Test<Test2> text2 = new Test<Test2>(); } }
某个接口的派生类型
class Test<T> where T : IFly { public T value; public void TestFun<K>(K k) where K : IFly { } } interface IFly { } class Test1:IFly { } internal class Program { static void Main(string[] args) { Test<IFly> test1 = new Test<IFly>(); Test<Test1> test = new Test<Test1>(); } }
另一个泛型类型本身或者派生类型
讯享网class Test<T,U> where T : U { public T value; public void TestFun<K,V>(K k) where K : V { } } interface IFly { } abstract class Test1 : IFly { } internal class Program { static void Main(string[] args) { Test<Test1,IFly> test = new Test<Test1, IFly>(); Test<Test1,Test1> test1 = new Test<Test1, Test1>(); } }
2.2 约束的组合使用
class Test<T> where T : class, new() //添加两种以上的约束,中间用逗号隔开 { public T value; }
2.3 多个泛型有约束
每个泛型约束都和一个where配对,中间不加其他标点符号
讯享网class Test<T,K> where T : class, new() where K : class { }
三、常用泛型数据结构类
1. List
List是一个C#为我们封装好的类,它的本质是一个可变类型的泛型数组,List类帮助我们实现了很多方法,比如泛型数组的增删查改
1.1 声明
using System.Collections.Generic List<int> list = new List<int>(); List<string> listStr = new List<string>();
1.2 增删查改
讯享网//增 list.Add(1); list.Add(2); list.Add(3); List<int> list2 = new List<int>{ 1, 2, 3 }; list.AddRange(list2); //范围增加,把另一个 list 容器里面的内容加到后面 list.Insert(0, 123); //在指定位置插入元素 //删 list.Remove(1); //移除指定元素,从头找,删掉第一个1 list.RemoveAt(1); //移除指定位置的元素 list.Clear(); //清空 //查 int i = list[0]; //获取指定位置的元素 if (list.Contains(1)) //查看元素是否存在 { } int i1 = list.IndexOf(2); //正向查找元素位置,找到返回位置,找不到返回-1 i1 = list.LastIndexOf(2); //反向查找元素位置,找到返回位置,找不到返回-1 //改 list[0] = 999;
1.3 遍历
int len = list.Count; //容量 int cap = list.Capacity; //for foreach遍历
2. Dictionary
可以将Dictionary理解为拥有泛型的Hashtable,它也是基于键的哈希代码组织起来的键值对。
键值对类型从Hashtable的object变为了可以自己指定的泛型
2.1 声明
讯享网using System.Collections.Generic Dictionary<int,string> dictionary = new Dictionary<int,string>();
2.2 增删查改
//增 //注意不能出现相同键 dictionary.Add(1, "123"); //删 //直接填写要删除的键 //删除不存在的键没反应 dictionary.Remove(1); dictionary.Remove(6); //清空 dictionary.Clear(); //查 //找不到会返回空 string p = dictionary[1]; //查看是否存在 //根据键检测 if (dictionary.ContainsKey(1)) { } //根据值检测 if (dictionary.ContainsValue("123")) { } //改 dictionary[1] = "12";
2.3 遍历
讯享网//长度 int len = dictionary.Count; //遍历所有键(顺序不一定) foreach (int item in dictionary.Keys) { Console.WriteLine(item); Console.WriteLine(dictionary[item]); } //遍历所有值(顺序不一定) foreach (string item in dictionary.Values) { Console.WriteLine(item); } //键值对一起遍历 foreach (KeyValuePair<int,string> item in dictionary) { Console.WriteLine("键:" + item.Key + "值:" + item.Value); }
3. 顺序存储和链式存储
3.1 数据结构
数据结构是计算机存储、组织数据的方式,数据结构是指相互之间存在一种或多种特定关系的数据元素的集合,比如自定义一个类,也可以称为一种数据结构,自己定义的数据组合规则
常用的数据结构:数组、栈、队列、链表、树、图、堆、散列表
3.2 线性表
线性表是一种数据结构,是由 n 个具有相同特性的数据元素的有限序列。
比如数组、ArrayList、Stack、Queue、链表等等
3.3 顺序存储
数组、Stack、Queue、List、ArrayList——顺序存储
只是数组、Stack、Queue的组织规则不同而已
顺序存储:用一组地址连续的存储单元,依次存储线性表的各个数组元素
3.4 链式存储
单向链表、双向链表、循环链表——链式存储
链式存储:用一组任意的存储单元存储线性表中的各个数据元素
3.5 自己实现一个最简单的单向链表
/// <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 LinkedList<T> { public LinkedNode<T> head; public LinkedNode<T> tail; public void Add(T value) { LinkedNode<T> linkNode = new LinkedNode<T>(value); if (head == null) { head = linkNode; tail=linkNode; } else { tail.nextNode = linkNode; tail = linkNode; } } public void Remove(T value) { if (head == null) { return; } if (head.value.Equals(value)) { head = head.nextNode; if (head == null) { tail = null; } return; } LinkedNode<T> node = head; while (node.nextNode != null) { if (node.nextNode.value.Equals(value)) { //让当前找到的这个元素上的节点指向自己的下一个节点 node.nextNode=node.nextNode.nextNode; break; } node = node.nextNode; } } } internal class Program { static void Main(string[] args) { LinkedList<int> link= new LinkedList<int>(); link.Add(1); link.Add(2); link.Add(3); LinkedNode<int> node = link.head; while (node != null) { Console.WriteLine(node.value); node = node.nextNode; } }
3.6 顺序存储和链式存储的优缺点
增:链式存储计算上优于顺序存储(中间插入时,链式不用像顺序一样去移动位置)
删:链式存储计算上优于顺序存储(中间删除时,链式不用像顺序一样去移动位置)
查:顺序存储计算上优于链式存储(数组可以直接通过下标得到元素,链式需要遍历)
改:顺序存储计算上优于劣势存储(数组可以直接通过下标得到元素,链式需要遍历)
4. LinkedList
LinkedList是一个C#为我们封装好的类,它的本质是一个可变类型的泛型双向链表
4.1 声明
讯享网using System.Collections.Generic LinkedList<int> linkList = new LinkedList<int>();
4.2 增删查改
//增 //在链表尾部添加元素 linkList.AddLast(10); //在链表头部添加元素 linkList.AddFirst(20); //在某一个节点之后添加一个节点 LinkedListNode<int> n = linkList.Find(20); linkList.AddAfter(n, 15); //在某一个节点之前添加一个节点 linkList.AddBefore(n, 13); //删 //删除头节点 linkList.RemoveFirst(); //删除尾节点 linkList.RemoveLast(); //移除指定值节点(输入节点的值) linkList.Remove(20); //清空 linkList.Clear(); //查 //头节点 LinkedListNode<int> first = linkList.First; //尾节点 LinkedListNode<int> last = linkList.Last; //找到指定值的节点,无法通过下标获取中间元素,只有遍历查找指定位置元素 LinkedListNode<int> node = linkList.Find(20); //判断是否存在 if (linkList.Contains(20)) { } //改 linkList.First.Value = 17;
4.3 遍历
讯享网//foreach遍历 foreach (int i in linkList) { } //通过节点遍历 //从头到尾 LinkedListNode<int> nowNode = linkList.First; while (nowNode != null) { Console.WriteLine(nowNode.Value); nowNode = nowNode.Next; } //从尾到头 nowNode = linkList.Last; while (nowNode != null) { Console.WriteLine(nowNode.Value); nowNode = nowNode.Previous; }
5. 泛型栈和队列
5.1 声明
using System.Collections.Generic; Stack<int> s = new Stack<int>();
四、委托和事件
1. 委托
1.1 委托是什么
委托是函数(方法)的容器,可以理解为表示函数(方法)的变量类型,用来存储、传递函数(方法)。委托的本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)。不同的函数(方法)必须对应和各自”格式“一致的委托
1.2 基本语法
关键字 delegate
讯享网访问修饰符 delegate 返回值 委托名(参数列表);
写在哪里?可以声明在 namespace 和 class 语句块中,更多的写在 namespace 中
1.3 定义自定义委托
访问修饰符默认不写为 public,在别的命名空间中也能使用。若是private,其他命名空间就不能用的。一般使用 public
注意委托没有函数的重载,声明是不能重名的(同一语句块中)。
delegate void MyFun(); //声明了一个可以用来存储无参无返回值函数的容器,这里只是定义了规则,并没有使用 delegate int MyFun1(int a); //表示用来装载或传递 返回值为 int、有一个 int 参数的函数的委托
1.4 使用定义好的委托
委托变量是函数的容器
讯享网static void Main(string[] args) { //方式一 MyFun f = new MyFun(Fun); //这里只是存储方法,而不是调用方法,所以Fun方法名后不要括号 f.Invoke(); //方式二 MyFun f1 = Fun; f1(); MyFun1 f2 = Fun; //由于Fun方法重载,调用的是返回值和参数都为 int 的方法 f2(1); MyFun1 f3 = new MyFun1(Fun); f3.Invoke(1); } public static void Fun() { Console.WriteLine("委托方法"); } public static int Fun(int a) { return 1; }
委托常用在:
- 作为类的成员
- 作为函数的参数
delegate void MyFun(); delegate int MyFun1(int a); class Test { public MyFun fun; public MyFun1 fun1; public void TestFun(MyFun fun, MyFun1 fun1) { //先处理一些别的逻辑 当这些逻辑处理完了 再执行传入的函数 } } internal class Program { static void Main(string[] args) { MyFun f1 = Fun; f1(); MyFun1 f2 = Fun1; f2(1); Test t= new Test(); t.TestFun(Fun, Fun1); t.TestFun(f1, f2); } public static void Fun() { Console.WriteLine("委托方法"); } public static int Fun1(int a) { return 1; } }
1.5 委托变量可以存储多个函数(多播委托)
一个委托可以存储多个方法,在调用委托的时候会将存储的方法一起调用
讯享网delegate void MyFun(); internal class Program { static void Main(string[] args) { //增 MyFun myFun = Fun; //初始化的时候不可以用+= myFun = myFun + Fun1; myFun += Fun2; //后续存储的时候用+=或者直接+存多个,也可以同一个方法重复添加 //减 myFun = myFun - Fun1; myFun -= Fun2; //后续删除的时候用-=或者直接-,多减不会报错 //清空容器 myFun = null; if(myFun != null) myFun(); //委托为空时调用会报错 } public static void Fun() { Console.WriteLine("Fun"); } public static void Fun1() { Console.WriteLine("Fun1"); } public static void Fun2() { Console.WriteLine("Fun2"); } }
1.6 系统定义好的委托
无参无返回值委托:Action
可以指定返回值类型的泛型委托:Func<string>
可以传 n 个参数,但是无返回值的委托:Action<int,int>
可以传 n 个参数,并且有返回值的委托:Func<string,int,int>
2. 事件
事件是一种使对象或类具备通知能力的成员。事件的功能就是通知。
事件的应用符合订阅者模式,其事件模型结构包含五部分,即:发布者、事件成员、订阅者、事件处理程序(即事件触发时会被调用的方法)、事件订阅(或叫触发事件,指当事件被触发时,所有订阅的事件处理程序都会被依次调用)。

讯享网
编辑
有关事件的私有委托需要了解的事项如下:
- 事件提供了对他的私有控制委托的结构化访问,无法直接访问委托,是私有控制委托的进一步封装。
- 事件可用的操作比委托要少。对于事件,在类内部可以赋值和调用;在类外部不能赋值、也不能调用(但是可以通过封装方法的方式在外部调用),外部我们只可以添加(+=)、删除(-=)
- 事件被触发时,他会依次调用订阅的方法。
事件中使用的代码分为五部分:
- 委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,他们通过委托类型进行描述
- 事件声明:发布者类必须声明一个订阅者类可以注册的事件成员
- 事件处理程序声明:订阅者类中会在事件触发时执行的方法声明
- 事件注册:订阅者必须注册事件,才能在事件被触发时得到通知
- 触发事件的代码:发布者类中触发事件的代码
2.1 使用已有的事件
以timer.Elapsed事件为例
using System.Timers; class Program { static void Main(string[] args) { Timer timer = new Timer(); timer.Interval = 1000; //每秒会触发一次事件 Boy boy = new Boy(); timer.Elapsed += boy.Action; //通过+=的方式对事件进行订阅 timer.Start(); //开始计时 } } class Boy { public void Action(object sender,ElapsedEventArgs e) { Console.WriteLine("Jump"); } }
2.1.1 订阅事件
- 使用+=运算符来为事件添加事件处理程序,-=运算符来取消事件处理程序对事件的订阅。
- 事件处理程序的规范可以是以下任意一种:实例方法的名称,静态方法的名称,匿名方法,Lambda表达式
2.1.2 订阅事件:
- 使用+=运算符来为事件添加事件处理程序。
- 事件处理程序的规范可以是以下任意一种:实例方法的名称,静态方法的名称,匿名方法,Lambda表达式
2.1.3 触发事件:
事件成员本身只是保存了需要被调用的事件处理程序,如果事件没有被触发,什么都不会发生。
触发事件之前和null比较,从而查看事件是否包含事件处理程序。
事件在类外部不能赋值、也不能调用(但是可以通过封装方法的方式在外部调用),外部我们只可以添加(+=)、删除(-=)
讯享网if(myEvent != null) { myEvent(); }
2.2 标准事件用法(自定义事件)
事件需要委托类型来做一个约束,这个约束规定了事件能发送什么消息给响应者,也规定了事件的响应者能收到什么类型的消息
委托与事件的关系和字段与属性的关系相似,下面会进行对比讲解
2.2.1 完整版事件声明
属性:
//字段与属性,属性是对字段的保护,防止外界随意更改 class Test { private int _id; public int Id { get { return this._id; } set { this._id = value; } } }
事件:
讯享网class Test { private Action action; //声明一个私有委托 public event Action ActionEvent //声明对应的公共事件,并进行封装 { add { this.action += value; } remove { this.action -= value; } } public void Happened() //事件的触发 { if(this.action != null) { this.action(); } } }
若需要自定义声明一个EventHandler委托类型。声明需要注意以下几点:
- 第一个参数用来保存触发事件的对象的引用,由于它是 object 类型的,所以可以匹配任何类型的实例。
- 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。此时EventArgs不能传递任何数据,它用于不需要传递数据的事件处理程序,如果希望传递数据必须声明一个派生自EventArgs的类,并用适合的字段来保存需要传递的数据。
- 返回类型是 void 的
- 规定如果是声明事件的委托类型,名字后面要用EventHandler做后缀
public delegate void OrderEventHandler(object source,EventArgs e);
讯享网public class OrderEventArgs:EventArgs //声明传递信息的类,继承EventArgs { public int id; } public delegate void OrderEventHandler(object source,OrderEventArgs e); class Test //发布者 { private OrderEventHandler orderEventHandler; //声明一个私有委托 public event OrderEventHandler OrderEvent //声明对应的公共事件,并进行封装 { add { this.orderEventHandler += value; } remove { this.orderEventHandler -= value; } } public void Happened() { if(orderEventHandler != null) { OrderEventArgs e = new OrderEventArgs(); e.id = 001; this.orderEventHandler(this,e); } } } public class Waiter //订阅者 { public void Action(object source,OrderEventArgs e) //事件处理器 { Console.WriteLine(id); } } internal class Program { static void Main(string[] args) { Test t=new Test(); Waiter waiter = new Waiter(); t.OrderEvent += waiter.Action; //事件订阅 t.Happened(); } }
2.2.2 简略事件声明
class Test //发布者 { public event OrderEventHandler orderEvent; //声明事件 public void Happened() { if(orderEvent != null) { OrderEventArgs e = new OrderEventArgs(); e.id = 001; this.orderEvent(this,e); } } }
2.3 为什么有事件
- 防止外部随意置空委托
- 防止外部随意调用委托
- 事件相当于委托进行了一次封装,让其更加安全
3. 匿名函数
匿名函数顾名思义就是没有名字的函数,匿名函数的使用主要是配合委托和事件进行使用,脱离委托和事件是不会使用匿名函数的
3.1 基本语法
讯享网delegate (参数列表) { //函数逻辑 }
3.2 使用
何时使用?
- 函数中传递委托参数时
- 委托或事件赋值时
无参无返回 这样声明匿名函数只是在声明函数而已,还没有调用 Action a = delegate () { Console.WriteLine("匿名函数逻辑"); }; a();
讯享网有参无返回 Action<string,int> b = delegate (string str,int i) { Console.WriteLine(str+i); }; b("123",5);
有返回值 Func<int> a = delegate () { return 1; }; a();
讯享网一般情况会作为函数参数传递或者作为函数返回值 static Action TestAction(int i,Action fun) //作为返回值 { return delegate(){Console.WriteLine("随参数传入的匿名函数");}; } static void Main(string[] args) { TestAction(1, delegate () //作为参数 { Console.WriteLine("随参数传入的匿名函数") }); }
事件赋值 class Test { public event Action ActionEvent; } static void Main(string[] args) { Test test = new Test(); test.ActionEvent += delegate () { }; }
3.3 匿名函数的缺点
添加到委托或事件容器中后,不记录,无法单独移除,只能清空
4. Lambda表达式
可以将Lambda表达式理解为匿名函数的简写。它除了写法不同外,使用上和匿名函数一模一样,都是和委托或者事件配合使用的
4.1 Lambda表达式语法
讯享网匿名函数 delegate (参数列表) { //函数体 } Lambda表达式 (参数列表)=> { //函数体 }
4.2 使用
参照匿名函数
甚至参数类型都可以省略,参数类型和委托或事件容器一致 Action<int> a=(value)=> {}; a(100);
4.3 闭包
内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经结束,改变了它的生命周期
注意:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值
讯享网Action action = new Action(); for(int i = 0;i<10;i++) { action += ()=> {Console.WriteLine(i);}; } action(); //最终会打印十个十,因为i最终变化成为10 Action action = new Action(); for(int i = 0;i<10;i++) { int index=i; action += ()=> {Console.WriteLine(index);}; } action(); //最终会打印0~9,每次会记录虽同名但是是不同变量的index
五、List排序
1. List自带的排序方法
List<int> list=new List<int>(){3,2,6,1,4,5}; list.Sort(); //list自己提供的默认升序的排列方法,ArrayList也有相同的方法
2. 自定义类的排序
list之所以能够进行排序,是因为int继承了IComparable<>接口,其中有CompareTo方法。要想自定义类也能够进行list排序,就需要继承IComparable<>接口并自己实现CompareTo方法
讯享网class Item : IComparable<Item> { public int money; public Item(int money) { this.money = money; } public int CompareTo(Item? other) { //返回值的含义 //按照数轴排列 这里的传入对象是other //负数会放在传入对象的前面 //0会保持当前位置不变 //正数会放在传入对象的后面 if (this.money > other.money) { return 1; //大的放在右面就传入正数 } else { return -1; } } } static void Main(string[] args) { List<Item> list = new List<Item>(); list.Add(new Item(10)); list.Add(new Item(8)); list.Add(new Item(9)); list.Add(new Item(2)); list.Sort(); }
3. 通过委托方法进行排序
class Item { public int money; public Item(int money) { this.money = money; } } static void Main(string[] args) { List<Item> list = new List<Item>(); list.Add(new Item(10)); list.Add(new Item(8)); list.Add(new Item(9)); list.Add(new Item(2)); list.Sort(delegate(Item a,Item b) //sort方法接收传入一个泛型委托,public delegate int Comparison<in T>(T x, T y); 返回负数,第一个会排在左,返回正数,第一个会排在右。传入委托后list会自己依次进行比较 { if(a.money>b.money) { return 1; } else { return -1; } }); /*list.Sort((a, b)=> { if (a.money > b.money) { return 1; } else { return -1; } });*/ }
六、协变逆变
协变:和谐的变换、自然的变换。因为里式替换原则,父类可以装子类,所以子类变父类,如string变object,感觉是和谐的
逆变:逆常规的变化,不正常的变化。因为里式替换原则,父类可以装子类,但是子类不能装父类,所以父类变子类,如object变string,感觉是不和谐的
协变和逆变是用来修饰泛型的,协变:out,逆变:in。用于在泛型中修饰泛型字母的,只有泛型接口和泛型委托能使用
1. 作用
1.1 返回值和参数
用 out 修饰的泛型只能作为返回值
讯享网public delegate T TestOut<out T>();
用 in 修饰的泛型只能作为参数

public delegate void TestOut<in T>(T t);
1.2 结合里式替换原则理解
协变 父类泛型委托装子类泛型委托
逆变 子类泛型委托装父类泛型委托
讯享网delegate T TestOut<out T>(); delegate void TestIn<in T>(T t); class Father { } class Son : Father { } static void Main(string[] args) { //协变 父类总是能被子类替换 //看起来就是son装入father TestOut<Son> outS = () => { return new Son(); }; TestOut<Father> outF = outS; //最终的返回的是Son,但是被装在Father中 Father f = outF(); //逆变 父类总是能被子类替换 //看起来像是father装入son TestIn<Father> inF = delegate (Father value) { }; TestIn<Son> inS = inF; inS(new Son()); //实际上调用的是iF }
协变直觉符合里氏替换原则,返回的Son直接装入Father中。
逆变实际上是逆向将inF委托装入inS中,传入的参数是Son。实际上调用时等同于将Son作为参数传入inF中,也是符合里氏替换原则。
七、多线程
1. 了解线程前先了解进程
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。打开一个应用程序就是在操作系统上开启了一个进程,进程之间可以相互独立、运行,互不干扰。进程之间也可以相互访问操作
2. 什么是线程
操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,我们目前写的程序都在主线程中。简单理解:线程就是代码从上到下运行的一条“管道”
3. 什么是多线程
我们可以通过代码开启新的线程,可以同时运行代码的多个“管道”就叫多线程
4. 语法相关
线程类 Thread
需要引用命名空间 using System.Threading;
4.1 声明一个新的线程
注意:线程执行的代码需要封装到一个函数中。新线程将要执行的代码逻辑被封装到了一个函数语句块中
static void Main(string[] args) { Thread t = new Thread(NewThreadLogic); //参数为一个委托 } static void NewThreadLogic() { //新开线程,执行的代码逻辑在该函数语句块中 }
4.2 启动线程
讯享网static void Main(string[] args) { Thread t = new Thread(NewThreadLogic); //参数为一个委托 t.Start(); //启动线程 } static void NewThreadLogic() { //新开线程,执行的代码逻辑在该函数语句块中 }
4.3 设置为后台线程
线程默认为前台线程。当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行,后台线程不会防止应用程序的进程被终止掉。如果不设置为后台线程,可能导致进程无法正常关闭。
static void Main(string[] args) { Thread t = new Thread(NewThreadLogic); //参数为一个委托 t.Start(); //启动线程 t.IsBackground = true; //设置为后台线程 } static void NewThreadLogic() { //新开线程,执行的代码逻辑在该函数语句块中 }
4.4 关闭释放一个线程
如果开启的线程中不是死循环,是能够结束的逻辑,那么不用刻意的去关闭它。如果是死循环,想要终止这个线程,有两种方式
4.4.1 死循环中bool标识
讯享网internal class Program { static bool IsRunning = true; static void Main(string[] args) { Thread t = new Thread(NewThreadLogic); t.Start(); t.IsBackground = true; Console.ReadKey(); IsRunning = false; Console.ReadKey(); } static void NewThreadLogic() { while (IsRunning) { Console.WriteLine("新开线程代码逻辑"); } } }
4.4.2 通过线程提供的方法(注意在.Net core 版本中无法终止,会报错)
try { t.Abort(); t=null; } catch {}
4.5 线程休眠
让线程休眠多少毫秒,在哪个线程里执行,就休眠哪个线程
讯享网Thread.Sleep(1000); //1s=1000毫秒
5. 线程之间共享数据
多个线程使用的内存是共享的,都属于该应用程序(进程)。所以要注意,当多线程同时操作同一片内存区域时,可能会出现问题,可以通过枷锁的形式避免问题
lock 当我们在多个线程当中,想要访问同样的东西进行逻辑处理时,为了避免不必要的逻辑顺序执行的差错 lock(引用类型对象) static bool IsRunning = true; static void Main(string[] args) { Thread t=new Thread() t.IsBackground = true; t.Start(); while(true) { lock(key) //括号中添加的一定是一个引用类型 { Console.SetCursorPosition(0,0); Console.ForegroundColor = ConsoleColor.Red; Console.Write("!"); } } } static void NewThreadLogic() { while (IsRunning) { lock(key) { Console.SetCursorPosition(10,5); Console.ForegroundColor = ConsoleColor.White; Console.Write("?"); } } }
6. 多线程对于我们的意义
可以用多线程专门处理一些复杂耗时的逻辑,比如寻路、网络通信等等
八、预处理器指令
1. 什么是编译器
编译器是一种翻译程序,它用于将源语言程序翻译为目标语言程序
源语言程序:某种程序设计语言写成的,如:C#、C、C++、Java等语言写的程序
目标语言程序:二进制数表示的伪机器代码写的程序
2. 什么是预处理器指令
预处理器指令:指导编译器 在实际编译开始之前 对信息进行预处理
预处理器指令都是以#开始
预处理器指令不是语句,所以它不以分号;结束
目前我们经常用到的 折叠代码块 就是预处理指令
3. 常见的预处理器指令
举例unity中会有不同平台
可以通过此方法来对不同平台进行不同操作
讯享网#define 定义一个符号,类似一个没有值的变量 #undef 取消 define 定义的符号,让其失效 两者都是写在脚本文件最前面(using前), 一般配合 if 指令使用 或 配合特性
#if #elif #else #endif 和 if 语句规则一样,一般配合 #define 定义的符号使用 用于告诉编译器进行编译代码的流程控制
讯享网#warning #error 告诉编译器 是报警告还是报错误 一般还是配合 if 使用
//定义一个符号 #define Unity4 #define Unity2017 #define IOS //取消定义一个符号 #undef Unity4 static void Main(string[] args) { #if Unity4 Console.WriteLine("版本为Unity4"); #elif Unity2017 && IOS Console.WriteLine("版本为Unity2017"); #else Console.WriteLine("其他版本"); #warning 这个版本不合法 #endif }
九、反射和特性
1. 反射
1.1 概念和关键类Type
1.1.1 什么是程序集
程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物,在 Windows 系统中,它一般表现为后缀为.dll (库文件)或是.exe (可执行文件)的格式
程序集就是我们写的一个代码集合,我们现在写的所有代码,最终都会被编译器翻译为一个程序集供别人使用。比如一个代码库文件(dll)或者一个可执行文件(exe)
1.1.2 元数据
元数据就是用来描述数据的数据,这个概念不仅仅用于程序上,在别的领域也有元数据
程序中的类、类中的函数、变量等等信息就是程序的元数据。有关程序以及类型的数据被称为元数据,他们保存在程序集中
1.1.3 反射的概念
程序正在运行时,可以查看其他程序集或者自身的元数据。一个运行的程序,查看本身或者其他程序的元数据的行为就叫反射。
在程序运行时,通过反射,可以得到其他程序集或者自己程序集代码的各种信息,类、函数、变量对象等等,实例化它们,执行他们,操作它们
1.1.4 反射的作用
因为反射可以在程序变异后获得信息,所以它提高了程序的拓展性和灵活性。
- 程序运行时得到的所有元数据,包括元数据的特性
- 程序运行时,实例化对象,操作对象
- 程序运行时,创建新对象,用这些对象执行任务
1.1.5 语法
1.1.5.1 Type
Type(类的信息类)
它是反射功能的基础。它是访问元数据的主要方式。
使用 Type 的成员获取有关类型声明的信息
有关类型的成员(如构造函数、方法、字段、属性和类的事件)
1.1.5.2 获取Type
每一个类型的type都是唯一的,不管得到多少次,相同类型的type在堆中只会有一个
1.万物之父 object 中的 GetType() 可以获取对象的 Type
讯享网int i=0; Type type = i.GetType(); //会把类型中的各种成员都存储到type中
2.通过typeof关键字,传入类名,也可以得到对象的Type
Type type = typeof(int); //参数传入类名或者变量名都可以
3.通过类的名字也可以获取类型,注意:类名必须包含命名空间,不然找不到
讯享网Type type = Type.GetType("System.Int32");
1.1.5.3 得到类的程序集信息
type.Assembly
1.1.5.4 获取类中的所有公共成员
讯享网//需要引用命名空间 using System.Reflection; using System.Reflection; Type type = typeof(Test); MemberInfo[] infos = type.GetMembers(); for(int i = 0;i < infos.Length;i++) { Console.WriteLine(infos[i]); //可以得到所有公共成员,私有成员不可以 }
1.1.5.5 获取类的公共构造函数并调用
1.获取所有构造函数
ConstructorInfo[] ctors = type.GetConstructors(); for(int i = 0;i < ctors.Length;i++) { Console.WriteLine(ctors[i]); }
2.获取其中一个构造函数并执行
得到构造函数传入Type数组,数组中内容按顺序是参数类型。执行构造函数传入 object数组 表示按顺序传入的参数
讯享网得到无参构造 ConstructorInfo info = type.GetConstructor(new Type[0]); 执行无参构造 Test obj = info.Invoke(null) as Test;
得到有参构造 ConstructorInfo info = type.GetConstructor(new Type[]{typeof(int)}); 执行有参构造 Test obj = info.Invoke(new object[]{1}) as Test;
1.1.5.6 获取类的公共成员变量
1.得到所有成员变量
讯享网FieldInfo[] fieldInfos = type.GetFields(); for(int i = 0;i < fieldInfos.Length;i++) { Console.WriteLine(fieldInfos[i]); }
2.得到指定名称的公共成员变量
FieldInfo fieldInfo = type.GetField("j"); //直接传字段名称 Console.WriteLine(fieldInfo );
3.通过反射获取和设置对象的值
讯享网通过反射 获取对象的某个变量的值 Console.WriteLine(fieldInfo.GetValue(test)); //括号中的参数是想得到哪个对象的值
通过反射 设置指定对象的某个变量的值 fieldInfo.SetValue(test,100) //第一个参数是,想要设置哪个对象的值;第2个参数是,设置为多少
1.1.5.7 获取类的公共成员方法
通过Type类中的 GetMethod方法 得到类中的方法
MethodInfo是方法的反射信息
1.得到所有成员方法
讯享网Type strType = typeof(string); MethodInfo[] methods = strType.GetMethods(); for(int i = 0;i<methods.Length;i++) { Console.WriteLine(methods[i]); }
2.获取其中一个构造函数并执行
MethodInfo subStr = strType.GetMethod("Substring",new Type[]{typeof(int),typeof(int)}); string str = "HelloWorld"; string odj = subStr.Invoke(str,new object[]{7,5}) as string; Console.WriteLine(obj);
1.2 关键类 Assembly 和 Activator
1.2.1 Activator
用于快速实例化对象的类,用于将 Type 对象快捷实例化为对象
讯享网Type testType = typeof(Test); 1. 无参构造 Test testObj = Activator.CreateInstance(testType) as Test; //无参只传类型就可以 2.有参构造 Test testObj = Activator.CreateInstance(testType,11) as Test; //有参在传类型后再传对对应的构造函数参数类型就可以
1.2.2 Assembly
程序集类
主要用来加载其他程序集,加载后才能用 Type 来使用其他程序集中的信息。如果想要使用不是自己程序集中的内容,需要先加载程序集。比如 dll文件(库文件)。简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类
三种加载程序集的函数:
一般用来加载在同一文件下的其他程序集 Assembly assembly = Assembly.Load("程序集名称");
讯享网一般用来加载不在同一文件下的其他程序集 Assembly assembly1 = Assembly.LoadFrom("包含程序集清单的文件的名称或路径"); Assembly assembly2 = Assembly.LoadFile("要加载的文件的完全限定路径");
//先加载一个指定程序集 Assembly assembly = Assembly.LoadFrom(@"G:\C# program\2024c-_code\test_5_8\test_5_8\bin\Debug\net8.0\test_5_8"); Type[] types = assembly.GetTypes(); for(int i = 0;i < types.Length;i++) { Console,WriteLine(types[i]); } //再加载程序集中的一个类对象之后,才能使用反射 Type icon = assembly.GetType("test_5_8.Icon"); MemberInfo[] members = icon.GetMembers(); for(int i = 0;i < members.Length;i++) { Console,WriteLine(members[i]); } //通过反射实例化一个对象,构造函数中一个参数为枚举类型,首先通过反射得到枚举 Type moveDir = assembly.GetType("test_5_8.E_MoveDir"); FieldInfo right = moveDir.GetField("Right"); object iconObj = Activator.CreateInstance(icon,10,5,right.GetValue(null)); //这里不能用 as 去转换,因为这里不能够直接得到 Icon 类,所以只能用 object 类型装载 MethodInfo move = icon.GetMethod("Move"); MethodInfo draw = icon.GetMethod("draw");
2. 特性
特性是一种允许我们向程序的程序集添加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类。特性提供功能强大的方法以将声明信息与C#代码(类型、方法、属性等)相关联,特性与程序实体关联后,即可在运行时使用反射查询特性信息。特性的目的是告诉编译器,把程序结构的某组元数据嵌入程序集中,它可以放置在几乎所有的声明中(类、变量、函数等等声明)。
特性本质是个类,我们可以利用特性类为元数据添加额外信息。比如一个类、成员变量、成员方法等等,为他们添加更多的额外信息之后,可以通过反射来获取这些额外信息。
2.1 自定义特性
继承特性基类 Attribute
讯享网class MyCustomAttribute : Attribute { public string info; public MyCustomAttribute(string info) { this.info = info; } public void TestFun1() { Console.WriteLine("特性的方法"); } }
2.2 特性的使用
基本声明语法:
[特性名(参数列表)]
本质上就是在调用特性类的构造函数,写在类、函数、变量上一行,表示它们具有该特性信息
[MyCustom("这是一个用于计算的类")] class MyClass { [MyCustom("这是一个成员变量")] public int value; public void TestFun([MyCustom("函数参数")] int a) {} }
通过反射使用:
讯享网Type t = typeof(MyClass); //判断是否使用了某个特性 //参数一:特性的类型 //参数二:代表是否搜索继承链,属性和事件忽略此参数 if(t.IsDefined(typeof(MyCustomAttribute),false) { Console.WriteLine("该类型应用了MyCustom特性"); } //获取Type元数据中的所有特性 object[] array = t.GetCustomAttributes(true); for(int i = 0;i<array.Length;i++) { if(array[i] is MyCuetomAttribute) { Console.WriteLine((array[i] as MyCustomAttribute).info); (array[i] as MyCustomAttribute).TestFun1(); //调用特性的方法 } }
2.3 限制自定义特性的使用范围
为特性类 加特性,限制其使用范围
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)] 参数一:AttributeTargets——特性能够用在哪些地方 参数二:AllowMultiple——是否允许多个特性实例用在同一个目标上 参数三:Inherited——特性是否能被派生类和重写成员继承 public class MyCustomAttribute : Attribute { }
2.4 系统自带特性——过时特性
过时特性:Obsolete
用于提示用户使用的方法等成员已经过时,建议使用新方法,一般加在函数前的特性
讯享网//参数一:调用过时方法时提示的内容 //参数二:true——使用该方法时会报错;false——使用该方法时会警告 [Obsolete("方法已经过时",true)]
2.5 系统自带特性——调用者信息特性
需要引用命名空间 using System.Runtime.CompilerServices; 一般作为函数参数的特性
//CallerFilePath:哪个文件调用? //CallerLineNumber:哪一行调用? //CallerMemberName:哪个函数调用? public void SpeakCaller(string str,[CallerFilePath]string fileName="",[CallerLineNumber]int line=0,[CallerMemberName]string target="") {}
2.6 系统自带特性——条件编译特性
条件编译特性:Conditional 他会和预处理指令 #define 配合使用
需要引用命名空间 using System.Diagnostics; 主要可以用在一些调试代码上,有时想执行,有时不想执行的代码
讯享网#define Fun class Program { [Conditional("Fun")] static void Fun() { Console.WriteLine("Fun执行"); } } static void Main(string[] args) { //在预处理指令定义Fun之后才会被调用 Fun(); }
2.7 系统自带特性——外部dll包函数特性
DllImport 用来标记非.Net(C#)的函数,表明该函数在一个外部的 dll 中定义。一般用来调用C或C++的 dll 包写好的方法。
需要引用命名空间 using System.Runtime.InteropServices
class Program { [DllImport("Test.dll")] //括号中为同文件夹下C的dll文件包 public static extern int Add(int a,int b); //引用了C中的方法 }
十、迭代器
迭代器(iterator)有时又称光标(cursor),是程序设计的软件设计模式,迭代器模式提供一个方法,顺序访问一个集合对象中的各个元素,而又不暴露其内部的标识。
在表现效果上看,是可以在容器对象(例如链表或数组)上遍历访问的接口,设计人员无需关心容器对象的内存分配的实现细节,可以用 foreach 遍历的类,都是实现了迭代器的。
1. 标准迭代器的实现方法
关键接口:IEnumerator,IEnumerable
命名空间:using System.Collections;
可以通过同时继承IEnumerator和IEnumerable实现其中的方法
讯享网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, 0 }; } public IEnumerator GetEnumerator() { Reset(); return this; } public object Current { get { return list[position]; } set { } } public bool MoveNext() { ++position; return position<list.Length?true:false; } public void Reset() { this.position = -1; } } internal class Program { static void Main(string[] args) { CustomList list = new CustomList(); //foreach的本质 //1. 先获取 in 后面这个对象的IEnumerator //会调用这个对象其中的GetEnumerator方法 同时里面有Reset复原光标的方法 //2. 执行得到这个IEnumerator对象中的 MoveNext 方法 //3. 只要MoveNext方法的返回值是true就会去得到Current //然后复制给item //4. 因为会先执行MoveNext方法,所以光标位置先从-1开始 foreach (int item in list) { Console.WriteLine(item); } } }
2. 用yield return 语法糖实现迭代器
yield return是C#提供给我们的语法糖。
所谓语法糖,也称糖衣语法,主要作用就是将复杂逻辑简单化,可以增加程序的可读性,从而减少程序代码出错的机会。
关键接口:IEnumerable
命名空间:using System.Collections;
让想要通过 foreach 遍历的自定义类实现接口中的方法GetEnumerator即可
class CustomList : IEnumerable { private int[] list; public CustomList() { list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 0 }; } public IEnumerator GetEnumerator() { for(int i = 0;i<list.Length;i++) { //yield关键字 配合迭代器使用 //可以理解为暂时返回保留当前的状态,一会还会再回来 yield return list[i]; } } } internal class Program { static void Main(string[] args) { CustomList list = new CustomList(); //foreach的本质 //1. 先获取 in 后面这个对象的IEnumerator //会调用这个对象其中的GetEnumerator方法 同时里面有Reset复原光标的方法 //2. 执行得到这个IEnumerator对象中的 MoveNext 方法 //3. 只要MoveNext方法的返回值是true就会去得到Current //然后复制给item //4. 因为会先执行MoveNext方法,所以光标位置先从-1开始 foreach (int item in list) { Console.WriteLine(item); } } }
3. 用yield return 语法糖为泛型类实现迭代器
讯享网class CustomList<T> : IEnumerable { private T[] array; public CustomList(params T[] array) { this.array = array; } public IEnumerator GetEnumerator() { for(int i = 0;i<array.Length;i++) { //yield关键字 配合迭代器使用 //可以理解为暂时返回保留当前的状态,一会还会再回来 yield return array[i]; } } } internal class Program { static void Main(string[] args) { CustomList<string> array = new CustomList<string>("123","321","221",""345); //foreach的本质 //1. 先获取 in 后面这个对象的IEnumerator //会调用这个对象其中的GetEnumerator方法 同时里面有Reset复原光标的方法 //2. 执行得到这个IEnumerator对象中的 MoveNext 方法 //3. 只要MoveNext方法的返回值是true就会去得到Current //然后复制给item //4. 因为会先执行MoveNext方法,所以光标位置先从-1开始 foreach (string item in array ) { Console.WriteLine(item); } } }
十一、特殊语法
1. var隐式类型
var是一种特殊的变量类型,它可以用来表示任意类型的变量。一般在不确定类型时使用
注意:
- var不能作为类的成员,只能用于临时变量声明时。也就是一般写在函数语句块中
- var必须初始化
2. 设置对象初始值
声明对象时,可以通过直接写大括号的形式,初始化公共成员变量和属性
Person p = new Person(){age = 10, sex = true, Name = "zhangsan"}; Person p1 = new Person(){age=10};
3. 设置集合初始值
声明集合对象时,也可以通过大括号直接初始化内部属性
讯享网int[] array = new int[]{1,2,3,4}; List<int> array1 = new List<int>(){ 1, 2, 3, 4, 5 }; Dictionary<int,string> dic = new Dictionary<int,string>() { {1,"123"}, {2, "234"}, {3, "244"} };
4. 匿名类型
var变量可以声明为自定义的匿名类型
var v = new {age = 10 ,money = 11}; //里面只能写成员变量,不能有函数相关内容
5. 可空类型
讯享网//值类型是不能赋值为空的 int i = null;//会报错 //声明时在值类型后面加?可以赋值为空(此时的i的类型不是 int 类型,而是一个Nullable<int>结构体类型) int? i = null; //使用前需要判断是否为空 if(i.HasValue) { Console.WriteLine(c); Console.WriteLine(c.Value); } //安全获取可空类型值 //1.如果为空 默认返回值类型的默认值 Console.WriteLine(i.GetValueOrDefault()); //2.如果为空 返回一个指定值(并没有给i赋值) Console.WriteLine(i.GetValueOrDefault(10)); //也可以用在引用类型上 obj?.ToString(); //若没有问号,obj为空时会报错;若有问号,obj为空时不会执行后续方法,相当于有了if(obj!=null)判断
6. 空合并操作符
空合并操作符??
左边值 ?? 右边值 如果左边值为null就返回右边值,否则返回左边值 只要是可以为null的类型都能用 int? intV = null; int intI = intV ?? 10;
7. 内插字符串
讯享网关键符号:$ 用$来构造字符串,让字符串中可以拼接变量
8. 单句逻辑简略写法
条件语句和循环语句后,如果只有一句代码,可以省略大括号
if (obj==null)Console.WriteLine("null");
属性和方法后,如果只有一句代码,可以省略大括号,用=>衔接
讯享网public string Name { get=>"zhangsan"; set=>name=value; } public void Add(int x,int y) => x+y;
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/64332.html