C#语言进阶

news2024/11/23 15:50:10

一、简单数据结构类

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 泛型字母:(约束类型)

泛型约束一共有六种:

  1. 值类型                                                           where 泛型字母:struct
  2. 引用类型                                                       where 泛型字母:class
  3. 存在无参公共构造函数                                  where 泛型字母:new()
  4. 某个类本身或者其派生类                              where 泛型字母:类名
  5. 某个接口的派生类型                                     where 泛型字母:接口名
  6. 另一个泛型类型本身或者派生类型               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;
}

委托常用在:

  1. 作为类的成员
  2. 作为函数的参数
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. 事件

事件是一种使对象或类具备通知能力的成员。事件的功能就是通知。

事件的应用符合订阅者模式,其事件模型结构包含五部分,即:发布者、事件成员、订阅者、事件处理程序(即事件触发时会被调用的方法)、事件订阅(或叫触发事件,指当事件被触发时,所有订阅的事件处理程序都会被依次调用)。

​编辑

有关事件的私有委托需要了解的事项如下:

  1. 事件提供了对他的私有控制委托的结构化访问,无法直接访问委托,是私有控制委托的进一步封装。
  2. 事件可用的操作比委托要少。对于事件,在类内部可以赋值和调用;在类外部不能赋值、也不能调用(但是可以通过封装方法的方式在外部调用),外部我们只可以添加(+=)、删除(-=)
  3. 事件被触发时,他会依次调用订阅的方法。

事件中使用的代码分为五部分:

  1. 委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,他们通过委托类型进行描述
  2. 事件声明:发布者类必须声明一个订阅者类可以注册的事件成员
  3. 事件处理程序声明:订阅者类中会在事件触发时执行的方法声明
  4. 事件注册:订阅者必须注册事件,才能在事件被触发时得到通知
  5. 触发事件的代码:发布者类中触发事件的代码

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 订阅事件
  1. 使用+=运算符来为事件添加事件处理程序,-=运算符来取消事件处理程序对事件的订阅。
  2. 事件处理程序的规范可以是以下任意一种:实例方法的名称,静态方法的名称,匿名方法,Lambda表达式

2.1.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委托类型。声明需要注意以下几点:

  1. 第一个参数用来保存触发事件的对象的引用,由于它是 object 类型的,所以可以匹配任何类型的实例。
  2. 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。此时EventArgs不能传递任何数据,它用于不需要传递数据的事件处理程序,如果希望传递数据必须声明一个派生自EventArgs的类,并用适合的字段来保存需要传递的数据。
  3. 返回类型是 void 的
  4. 规定如果是声明事件的委托类型,名字后面要用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 为什么有事件

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

3. 匿名函数

匿名函数顾名思义就是没有名字的函数,匿名函数的使用主要是配合委托和事件进行使用,脱离委托和事件是不会使用匿名函数的

3.1 基本语法

delegate (参数列表)
{
    //函数逻辑
}

3.2 使用

何时使用?

  1. 函数中传递委托参数时
  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. 程序运行时得到的所有元数据,包括元数据的特性
  2. 程序运行时,实例化对象,操作对象
  3. 程序运行时,创建新对象,用这些对象执行任务
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),new 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是一种特殊的变量类型,它可以用来表示任意类型的变量。一般在不确定类型时使用

注意:

  1. var不能作为类的成员,只能用于临时变量声明时。也就是一般写在函数语句块中
  2. 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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1672460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ--死信队列

目录 一、死信队列介绍 1.死信 2.死信的来源 2.1 TTL 2.2 死信的来源 3.死信队列 4.死信队列的用途 二、死信队列的实现 1.导入依赖 pom.xml 2.application.properties 3.配置类 4.生产者 5.业务消费者&#xff08;正常消费者&#xff09; 6.死信队列消费者 一、…

STM32-LCD液晶屏(ILI9341)

MCU&#xff1a;STM32F103VET6 开发环境&#xff1a;STM32CubeMXMDK5 目录 STM32液晶屏LCD&#xff08;ILI9341&#xff09; LCD液晶显示 液晶控制原理 ILI9341液晶控制器简介 8080写时序 8080读时序 FSMC模拟8080时序 液晶屏的信号线 STM32CubeMX配置FSMC 测试部分 …

工作玩手机监测识别摄像机

工作场所的员工玩手机已经成为了一种常见的现象&#xff0c;特别是在办公室、生产车间等地方。而这种现象不仅仅影响了员工的工作效率&#xff0c;还可能会对工作安全造成一定的隐患。为了监测和识别员工玩手机的情况&#xff0c;工作玩手机监测识别摄像机应运而生。工作玩手机…

05 | 如何确保消息不会丢失?

检测消息丢失的方法 我们可以利用消息队列的有序性来验证是否有消息丢失。在 Producer 端,我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的连续性。 如果没有消息丢失,Consumer 收到消息的序号必然是连续递增的,或者说收到的消息,其中的…

物联网实战--平台篇之(六)应用管理后台

目录 一、应用数据库 二、登录记忆 三、新建应用 四、获取应用列表 五、重命名应用 本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_126313…

2024统计建模成品论文39页(附带完整数据集和代码)

2024统计建模成品论文完整版一等奖论文【1.5w字全网最佳】2024统计建模大赛高质量成品论文39页配套完整代码运行全套数据集https://www.jdmm.cc/file/2710661/

华为配置带反射器的iNOF功能实验

配置带反射器的iNOF功能示例 适用产品和版本 安装了SAN系列单板的CE16800系列交换机V300R020C10或更高版本。 安装了P系列单板的CE16800系列交换机V300R021C00或更高版本。 CE6866、CE6866K、CE8851-32CQ8DQ-P、CE8851K系列交换机V300R020C00或更高版本。 CE6860-SAN、CE8850-S…

【全开源】商会招商项目系统基于FastAdmin+ThinkPHP+Uniapp(源码搭建/上线/运营/售后/维护更新)

一款基于FastAdminThinkPHPUniapp开发的商会招商项目系统&#xff0c;是一个集PC和移动端功能于一体的解决方案&#xff0c;线上线下进行服务&#xff0c;围绕 活动报名、在线课程、项目大厅、线下签到、会员系统等。为商会提供了更加便捷高效的管理方式&#xff0c;提升了商会…

python数据分析——seaborn绘图1

参考资料&#xff1a;活用pandas库 matplotlib库是python的和兴绘图工具&#xff0c;而seaborn基于matplotlib创建&#xff0c;它为绘制统计图提供了更高级的接口&#xff0c;使得只用少量代码就能生成更美观、更复杂的可视化效果。 seaborn库和pandas以及其他pydata库&#xf…

括号匹配(栈)

20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; c有栈 但是C语言没有 到那时我们可以自己造 这里的代码是直接调用栈&#xff0c;然后调用 等于三个左括号的任意一个 我们就入栈 左括号&#xff08;入栈&#xff09; 右括号 取出栈顶数据&#xff0c;出栈并且进行匹配…

用Transformers实现简单的大模型文本生成

根据输入的prompt&#xff0c;生成一段指定长度的文字。Llama跑起来太慢了&#xff0c;这里用GPT-2作为列子。 from transformers import GPT2LMHeadModel, GPT2Tokenizer import torchtokenizer GPT2Tokenizer.from_pretrained("gpt2") model GPT2LMHeadModel.fr…

Java 实现Mybatis plus 批量删除

数据库实体字段并不映射的情况&#xff0c;直接请求体集合接收。 PostMapping("/removeIdsInfo")public R<Void> removeIdsInfo(RequestBody List<Integer> ids) {return exStudentService.removeIdsInfo(ids);} /**** 学生模块根据集合id 批量删除数据*…

BGP基础配置实验

BGP基础配置实验 一、实验拓扑 初始拓扑&#xff1a; 最终拓扑&#xff1a; 二、实验要求及分析 实验要求&#xff1a; 1&#xff0c;R1为AS 100区域&#xff1b;R2、R3、R4为AS 200区域且属于OSPF协议&#xff1b;R5为AS 300区域&#xff1b; 2&#xff0c;每个设备上都有…

AIM可以像 LLM 一样进行扩展的自回归图像模型

0.引言 AIM&#xff08;Autoregressive Image Model&#xff09;是一种自回归学习图像模型&#xff0c;它是对语言模型的图像版本进行了推广。该模型的预训练图像特征质量会随着模型大小和数据质量的提高而提高&#xff0c;从而带来更好的性能。同时&#xff0c;下游任务的性能…

Linux 中 alarm 函数详解

目录 简介函数原型函数参数返回值使用示例设置 3 秒闹钟修改闹钟与取消闹钟设置 1 秒周期定时器 更多内容 简介 alarm 函数的功能是设置一个闹钟&#xff08;定时器&#xff09;&#xff0c;当闹钟时间到时&#xff0c;内核会向当前进程发送一个 SIGALRM 信号。 打开 Linux 终…

AI智能体|手把手教你申请一个Kimi(Moonshot)的API KEY

大家好&#xff0c;我是无界生长。 今天分享一下如何申请一个Kimi(Moonshot)的API KEY&#xff0c;为后面Kimi(Moonshot)接入微信机器人做铺垫。学会了的话&#xff0c;欢迎分享转发&#xff01; 前提 拥有一个Kimi(Moonshot)账号 使用手机号注册即可&#xff0c;新用户可免费…

五、Linux二进制安装MariaDB 六、MariaDB主从复制

目录 五、Linux二进制安装MariaDB1 卸载mariadb1.1 卸载相关的服务(mysql和mariadb都查询一下)1.2 查找MySQL和mariadb相关的文件目录 2 安装mariadb2.1 mariadb下载地址2.2 将安装包放入到服务器中并解压 (我放到opt下)2.3 将解压后的目录移动到安装目录下2.4 创建数据目录(根…

【iOS】架构模式

文章目录 前言一、MVC二、MVP三、MVVM 前言 之前写项目一直用的是MVC架构&#xff0c;现在来学一下MVP与MVVM两种架构&#xff0c;当然还有VIPER架构&#xff0c;如果有时间后面会单独学习 一、MVC MVC架构先前已经详细讲述&#xff0c;这里不再赘述&#xff0c;我们主要讲一…

YOLOv9改进策略目录 | 包含卷积、主干、检测头、注意力机制、Neck上百种创新机制

&#x1f451; YOLOv9有效涨点专栏目录 &#x1f451; 专栏视频介绍&#xff1a;包括专栏介绍、得到的项目文件、模型二次创新、权重文件的使用问题&#xff0c;点击即可跳转。 前言 Hello&#xff0c;各位读者们好 本专栏自开设两个月以来已经更新改进教程50余篇其中包含Re…

持续集成-Git

重要步骤命令 git init (初始化一个仓库) git add [文件名] (添加新的文件) git commit -m [关于本次提交的相关说明] (提交) git status (查看文件状态) git diff (如果文件改变&#xff0c;比较两个文件内容) git add[文件名] || git commit -a -m [关于本次提交的相关说…