基本结构

using System; // 引入其它程序集
​
namespace CSharpLearn // 命名空间,通常为当前程序集名称
{
    class Program // 类
    {
        /// <summary>
        /// 函数注释
        /// </summary>
        /// <param name="变量名称">变量解释</param>
        static void Main(string[] args) // 入口函数
        {
            #region VarDefine // #region 折叠代码块名称
            string outStr = "Hello C#!"; // 变量定义
            const int id = Int32.MaxValue; // 常量定义
            #endregion // #endregion 结束折叠代码块
                
            #region MainFunc
            try // 异常捕获
            {
                Console.WriteLine(outStr);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw; // 抛出异常
            }
            finally
            {
            }
            #endregion
        }
    }
}

常用用法

1.字符串拼接

string str = string.Format("内容{0}内容{1}内容{2}内容{...}",0位置的值,1位置的值,2位置的值,...);
string str = $"内容{0位置的值}内容{1位置的值}内容{2位置的值}内容{...}";
-----------------------------------------------
// 需要频繁操作字符串,可以使用StringBuilder
using System.Text;
// 初始化
StringBuilder stringBuilder = new StringBuilder("1233bcfh7ds");
stringBuilder.Append(" abc"); // 附加
stringBuilder.Insert(3, "VVV"); // 插入
stringBuilder.Remove(6, 3); // 移除
stringBuilder.Replace("VVV", "AAA"); // 替换
stringBuilder[0] = 'Q'; // 访问/修改
Console.WriteLine(stringBuilder); // Q23AAAfh7ds abc
stringBuilder.Clear(); // 清空

2. 枚举

enum EMyEnum // 枚举名称
{
    Name0, // 0
    Name1 = 5,
    Name2, // 6
    Name3 = 100,
    Name4, // 101
    Name5 // 102
}
string str = myEnum.ToString(); // 枚举转字符串
myEnum = (EMyEnum)Enum.Parse(typeof(EMyEnum), "Name5"); // 字符串转枚举

3.数组

// 一维数组声明
int[] arr;
// 一维数组初始化
int[] arr = new arr[5];
int[] arr = new arr[5]{1,2,3,4,5};
int[] arr = new arr[]{1,2,3,4,5,6,7,8};
int[] arr = {1,2,3,4,5,6};
---------------------------------------------------------
// 二维数组声明
int[,] arr;
// 二维数组初始化
int[,] arr = new arr[3,3];
int[,] arr = new arr[3,3]{{1,2,3},
                         {4,5,6},
                         {7,8,9}};
int[,] arr = new arr[,]{{1,2,3},
                        {4,5,6},
                        {7,8,9}};
int[,] arr = {{1,2,3},
              {4,5,6},
              {7,8,9}};

交错数组

基本语法
// 交错数组声明
// 变量类型[][] 变量名称 = new 变量类型[行数][];
int[][] arr;
// 交错数组初始化
// 变量类型[][] 变量名称 = new 变量类型[行数][]{一维数组1,一维数组2,一维数组3,...};
int[][] arr = new int[6][];
//      一维数组的类型必须与交错数组定义的一致
int[][] arr = new int[3][]{new int[] { 1, 2, 3 }, 
                           new int[] { 4, 5 },
                           new int[] { 7, 8, 9, 10 }};
int[][] arr = new int[][]{new int[] { 1, 2, 3 },
                          new int[] { 4, 5 },
                          new int[] { 7, 8, 9, 10 }};
int[][] arr = {new int[] { 1, 2, 3 },
               new int[] { 4, 5 },
               new int[] { 7, 8, 9, 10 }};
用法
1. 数组长度
// 获取交错数组行数
arr.GetLength(行数);
// 获取某一行一维数组的列数
arr[行数].Length;
2. 获取元素
// 获取一维数组
arr[行数];
// 获取一维数组中的元素
arr[行数][索引值];

4. 值和引用类型

值类型

不同的值类型变量拥有独立的内存空间,互不干扰(栈空间)

数值类型、结构体、bool型、枚举、可空类型

int a = 10;
int b = a; // b=10,a=10
b = 30; // b=30,a=10
// b并不是指向a,而是将a指向的值拷贝到b指向的内存空间中
// 所以修改b,a没有变化

引用类型

将b变量指向a变量的地址,通过b改变值,a指向的值也会改变,相当于b是a的别名(堆空间)

数组、委托、接口、object、string(特殊引用类型)、类

int[] a = {1,2,3,4};
int[] b = a;
b[0] = 33; // a{33,2,3,4} b{33,2,3,4}
// b指向a,a又指向值
// 所以修改b,a会变化

特殊string引用类型

// 当string重新赋值时,会重新分配内存空间
string str = "123";
string str_c = str;
str_c = "666"; // 相当于:str_c = new string("666");

函数修饰符ref和out

共同点:都是修饰传入的参数为引用类型,使得函数内部改变值,实际的值也会改变

不同点:ref传入的变量必须初始化out传入的变量必须在函数内部赋值

int a = 20;
​
// ref
static void ChangeValue(ref int num)
{
    num = 333;
}
ChangeValue(ref a);
-----------------------------------------
// out
static void UnchangeValue(out int num)
{
    num = 666;
}
UnchangeValue(out a);

5. 变长参数和参数默认值

变长参数只能为参数列表的最后一个

// params 参数类型[] 参数名
static int SumNum(int a, params int[] arr)
{
    return a + arr.Sum();
}
SumNum(66, 1, 2, 3, 4, 5, 6);

参数默认值只能在普通参数的后面

static void Say(double price, string str="book", int num=99)
{
}
Say(.45, str:"card"); // 可以不传入已经有默认值的参数

面向对象(核心)

1. 三大特性

封装

用程序语言形容对象

class MyClass
{
}
构造函数

默认有无参构造。如果写了有参构造,没写无参构造,那默认的无参构造就会消失

class MyClass
{
    private int id;
    private string name;
        
    public MyClass()
    {
        id = 0;
    }
​
    public MyClass(string name):this() // this(参数) 调用其它构造函数
    {
        this.name = name;
    }
​
    public MyClass(int id):this("oop")
    {
        this.id = id;
    }
}
内存回收(GC)

一般在游戏进度条加载时候进行,强制回收会消耗性能,可能会有卡顿

// 强制执行内存回收
GC.Collect();
成员属性

get/set访问器,二者可选其一或都用,可以使成员变量只能读/只能写/能读能写

可以在set中写加密逻辑,get中写解密逻辑,用以保护成员变量

public class Person
{
    private int id;
    private string name;
    private bool sex;
​
    public string Name
    {
        get // 可以加访问修饰符,默认是属性的修饰符
        {
            string newName = name + id;
            return newName;
        }
​
        set
        {
            string newName = value + "ok";
            name = newName;
        }
    }
    
    // 自动属性,不用进行特殊处理可以这样写
    public int Id
    {
        get;
        set;
    }
}
-----------------------------------------
OOP.Person person = new OOP.Person();
person.Name = "none";
person.Id = 223;
Console.WriteLine($"{person.Name} --- {person.Id}");
索引器

让类对象可以像数组一样通过索引访问其中的元素

//访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名,...]
//{
//      get{}
//      set{}
//}
public class Person
{
    private int id;
    private string name;
    private bool sex;
    private Person[] friends;
    
    public Person this[int index]
    {
        get
        {
            return _friends[index];
        }
        set
        {
            _friends[index] = value;
        }
    }
}
------------------------------------
Person person = new OOP.Person();
Person p = person[0]; // 通过索引器访问成员变量
静态构造函数

1.只会自动调用一次;

2.常用来初始化静态变量;

3.可以写在静态和普通函数中;

public class Person
{
    static Person()
    {
    }
}
扩展方法

可以为现有非静态变量类型添加新方法

只能在非嵌套静态类中定义

如果定义的扩展方法及参数类型与原有方法一致,则默认调用原方法

// 访问修饰符 static 返回值 函数名(this 被扩展的类名 参数名,参数类型 参数名,参数类型 参数名,...)
static class Tool
{
    public static void PrintInt(this int value)
    {
        Console.WriteLine("Int value = " + value);
    }
}
-----------------
114.PrintInt(); // Int value = 114
运算符重载

使自定义类结构体对象进行运算

1.不能使用refout

2.条件运算符需要成对重载。如重载了<,则必须重载>

3.&&||[]().=?: 不能重载

访问修饰符 static 返回值类型 operator 运算符(参数列表){}
public class Point
{
    private int _x;
    private int _y;
​
    public Point()
    {
    }
​
    public Point(int x, int y)
    {
        this._x = x;
        this._y = y;
    }
​
    // 重写输出方法
    public override string ToString()
    {
        return $"({_x},{_y})";
    }
    
    // 重载加法+运算符
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1._x + p2._x, p1._y + p2._y);
    }
}
---------------------------------
Point p1 = new Point(1, 2);
Point p2 = new Point(5, 3);
Console.WriteLine(p1 + p2); // (6,5)

继承

复用封装对象的代码(子类继承父类)

1.单根性:子类只能有一个父类

2.传递性:子类可以间接继承父类的父类

class 子类名 : 父类名 {}
class Teacher
{
}
​
class TeachTeacher : Teacher
{
}
构造函数

1.子类创建时,先执行父类的构造函数,再执行子类的构造函数

2.父类的无参构造消失后,子类要用base显示调用指定父类构造函数

class Father
{
    public Father(int num)
    {
    }
}
​
class Son : Father
{
    // 子类要用base显示调用指定父类构造函数
    public Son(int id) : base(id)
    {
    }
}
装箱和拆箱

装箱:值类型用引用类型存储。内存 -> 堆内存

object v = 3;

拆箱:将值类型从引用类型取出来。内存 -> 栈内存

int value = (int)v;

示例:

static void Func(params object[] arr) {}
Func("123",1,24,.45f,.88877,new Son());
密封类(sealed)

sealed修饰的类不能再被继承

sealed class MyClass {}

多态

同样行为的不同表现(子类继承父类,子类有父类没有的行为,相当于扩展父类)

为了使同一个对象有唯一的行为

问题描述
class Father
{
    public void Speak()
    {
        Console.WriteLine("Father");
    }
}
​
class Son : Father
{
    public new void Speak()
    {
        Console.WriteLine("Son");
    }
}
--------------------------------------
Father son = new Son();
son.Speak(); // 实例化是用子类实例化,但是直接调用确实父类的函数
(son as Son)?.Speak(); // 需要转换才能调用子类的函数

可以看到,son可以调用有2种相同的函数,Father的Speak和Son的Speak

Father son = new Son();
son.Speak();
(son as Son)?.Speak();

为了使son只能调用自己的Speak函数,就要用到多态(即对象new的是什么类,就只能执行什么类里的函数

需要用到重载,virtual(虚函数),override(重写),base(父类)

以下是解决方法:

解决问题
class GameObject
{
    protected string Name;
​
    public GameObject(string name)
    {
        Name = name;
    }
​
    // 父类定义虚函数,子类可以重写这个函数
    public virtual void Atk()
    {
        Console.WriteLine($"GameObject \"{Name}\" attack.");
    }
}
​
class Player : GameObject
{
    public Player(string name) : base(name)
    {
    }
​
    // 子类可以重写virtual虚函数
    public override void Atk()
    {
        // 可以通过base.调用父类的函数
        // base.Atk();
        Console.WriteLine($"Player \"{Name}\" attack.");
    }
}
--------------------------------
GameObject player = new Player("fu");
player.Atk(); // 父类对象装载子类的实例,调用子类重写的函数
抽象函数

1.子类直接继承必须实现

2.必须定义在抽象类中;

3.修饰符只能是publicprotected

// 定义
abstract class Thing
{
    public abstract void Say(); // 又叫 纯虚方法
}
​
// 继承
class Apple : Thing
{
    public override void Say()
    {
        Console.WriteLine("Apple.");
    }
}
接口

接口是行为的抽象规范(相当于将某些特定行为抽象出来,需要用到的类继承接口后,就有了相应的行为

1.只能包含方法属性索引器事件

2.类继承接口后必须实现接口中的所有成员;

3.类可以继承多个接口;

4.接口不能继承类,可以继承另一个接口;

5.类继承接口后,实现的成员必须是public

6.接口也遵循里氏替换原则;

语法
访问修饰符 interface I接口名 {}
定义
public interface IFly
{
    // 方法
    void Fly();
​
    // 属性
    string Name
    {
        get;
        set;
    }
​
    // 索引器
    string this[int index]
    {
        get;
        set;
    }
    
    // 事件
    event Action DoSomething;
}
使用
class Person : Animal, IFly
{
    // 类继承接口后,实现的成员必须是public
    public virtual void Fly() // 加上virtual后,可以让继承这个类的子类去重写
    {
    }
​
    public string Name { get; set; }
​
    public string this[int index]
    {
        get
        {
            return "";
        }
        set
        {
        }
    }
​
    public event Action? DoSomething;
}
------------------------
IFly f = new Person(); // f只能调用Person已经实现IFly的成员,不能调用Person里的其它成员
显示实现接口

当一个类同时继承2个接口,且2个接口有同种方法时使用

1.不能使用修饰符;

返回值类型 接口名.方法名() {}
interface IAtk
{
    public void Atk();
}
​
interface ISuperAtk
{
    public void Atk();
}
​
class MyPlayer : IAtk, ISuperAtk
{
    // 显示实现接口
    void IAtk.Atk()
    {
    }
​
    void ISuperAtk.Atk()
    {
    }
}
---------------------------
IAtk ia = new MyPlayer();
ISuperAtk isa = new MyPlayer();
ia.Atk(); // 调用IAtk接口的Atk方法(Atk方法由MyPlayer实现)
isa.Atk(); // 调用ISuperAtk接口的Atk方法(Atk方法由MyPlayer实现)

但是显示实现后,MyPlayer的对象不能直接调用Atk方法,必须要先转换成对应的接口

MyPlayer player = new MyPlayer();
(player as IAtk).Atk();
(player as ISuperAtk).Atk();
密封函数

使函数不能再被重写

public sealed override void Atk() {} // 后续继承该类的类不能再重写此Atk函数 

2.七大原则

详细可以看我写的https://loii.cc/kRMtu

里氏替换原则

概念

任何父类出现的地方,子类都可以替代

语法表现

父类的容器可以装载子类对象,因为子类对象包含了父类所有内容

 public static void Run()
 {
     // 使用 父类对象 装载 子类对象
     GameObject player = new Player();
     GameObject monster = new Monster();
     GameObject boss = new Boss();
 }
作用

方便进行对象存储和管理

代码定义
class GameObject
{
}
​
class Player : GameObject
{
    public void PlayerAtk(double damage)
    {
        Console.WriteLine($"Player attack {damage} damage.");
    }
}
​
class Monster : GameObject
{
    public void MonsterAtk(double damage)
    {
        Console.WriteLine($"Monster attack {damage} damage.");
    }
}
​
class Boss : GameObject
{
    public void BossAtk(double damage)
    {
        Console.WriteLine($"Boss attack {damage} damage.");
    }
}
is和as

is:用来判断一个对象是否为指定对象

as:将一个对象转换成指定对象的类

GameObject player = new Player();
​
// 判断player是否为Player类的对象
if (player is Player)
{
    // 将GameObject类的变量player转换为Player类的变量
    (player as Player)?.PlayerAtk(.114); // ?代表如果转换结果为null,则不执行.后面的代码
}

开闭原则

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化

即,对系统进行抽象约束


依赖倒置原则

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象


单一职责原则

一个类只负责一项职责。换种说法,就一个类而言,应该只有一个引起它变化的原因


接口隔离原则

类的依赖关系应建立在最小接口上,不要都塞在一起。即类不应该依赖它不需要的接口


合成复用原则

合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用


迪米特原则

一个对象应尽可能少的了解其它对象,尽量降低类与类之间的耦合


数据集合

ArrayList

本质是object的数组

using System.Collections;
​
ArrayList list = new ArrayList();

可以添加任意的类型

list.Add("ad");
list.Add(null);
list.Add(9);
​
// 将一个数组添加到另一个最后
list.AddRange(new ArrayList { 123, "fu" });
list.AddRange(new int[]{1,2,3});

删改查:

list.Remove(1);
list.RemoveAt(2);
list.Clear();
list[0];

装箱和拆箱:

// 装箱
int i = 1;
list[0] = i;
​
// 拆箱
i = (int)list[0];

Stack

本质为object数组

Stack存储容器,遵循先进后出原则

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

using System.Collections;
​
Stack stack = new Stack();
stack.Push("777"); // 增加(压栈)
stack.Push(0x56);
​
stack.Pop(); // 取(弹栈),取出的值已经不在里面了
Console.WriteLine(stack.Pop());
​
stack.Peek(); // 仅查看,不删除
    
stack.Contains("777"); // 是否包含在栈中
​
stack.Count(); // 长度
​
stack.ToArray(); // 转化为object数组

Queue

本质为object数组

Queue队列存储容器,遵循先出后进原则

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

using System.Collections;
​
Queue queue = new Queue();
​
// 新增元素
queue.Enqueue(122);
queue.Enqueue("opp");
queue.Enqueue(.78f);
​
queue.Dequeue(); // 取出元素
queue.Peek(); // 查看元素
​
queue.Contains(.78f); // 是否包含
​
queue.Clear(); // 清空
​
queue.Count(); // 长度
​
queue.ToArray(); // 转化为object数组

Hashtable

又称散列表,是由键的哈希代码组织起来的键值对

主要用于提高数据查询效率

使用访问元素

不能出现相同键;不能修改键,只能修改键对应的值

using System.Collections;
​
Hashtable hashtable = new Hashtable();
        
hashtable.Add(1, "abc"); // 增加键值对
hashtable.Add("good", .345);
​
hashtable.Remove(1); // 通过键删除键值对
hashtable.Clear(); // 清空
​
var num = hashtable["good"]; // 通过键查找对应值,找不到返回null
​
// 根据键检测是否包含
hashtable.Contains(1);
hashtable.ContainsKey(1);
​
// 根据值检测是否包含
hashtable.ContainsValue(.345);
​
// 通过键修改值,不能修改键
hashtable["good"] = "ikun";

泛型

多个泛型用逗号分隔

class 类名<泛型占位符>
interface 接口名<泛型占位符>
    
// 泛型函数
修饰符 返回值类型 函数名<泛型占位符>(参数列表) {}
class TestClass<T>
{
    public T t;
}
​
interface ITest<T>
{
    T Num
    {
        get;
        set;
    }
}
​
void Func<T>()
{
    T t;
}

泛型约束

where 泛型字母:值类型              // interface ITest<T> where T:struct {}
where 泛型字母:引用类型            // void Func<T>() where T:class {}
where 泛型字母:无参公共构造函数     // void Func<T>() where T:new() {}
where 泛型字母:接口名              // void Func<T>() where T:ITest {}
where 泛型字母:另一个泛型字母       // void Func<T,K>() where T:K {}

多约束类型

void Func<T>() where T:class, new() {}

多泛型字母的约束

void Func<T,K>() where T:class, new() where K:struct {}

常用泛型数据结构

// 列表
List<int> list = new List<int> { 1, 2, 3, 4 };
-------------------------
// 字典
Dictionary<int, string> dictionary = new Dictionary<int, string>
{
    { 1, "app" },
    { 2, "cpp" },
    { 1114, "nuk" }
};

List排序

Sort()

List<int> list = new List<int> { 1, 5, 67, 9, 0, 3, 4, 54, 3, 0 };
list.Sort();

自定义类排序

继承接口IComparable即可进行排序和比较

class Item : IComparable<Item>
{
    public int Money { get; set; }
​
    public Item(int money)
    {
        Money = money;
    }
​
    // 重写排序比较逻辑
    public int CompareTo(Item? other)
    {
        // 返回值小于0:this放在传入对象other的前面
        // 返回值等于0:this保持当前位置不变
        // 返回值大于0:this放在传入对象other的后面
        return Money - (other?.Money ?? 0); // 升序
        return (other?.Money ?? 0) - Money; // 降序
    }
}
--------------------------------------------
List<Item> items = new List<Item>
{
    new Item(100), 
    new Item(223), 
    new Item(0), 
    new Item(9999), 
    new Item(150),
    new Item(223)
};
items.Sort(); // 排序
foreach (var item in items)
{
    Console.WriteLine(item.Money);
}
重写排序函数返回值

返回值小于0:this放在传入对象other的前面 返回值等于0:this保持当前位置不变 返回值大于0:this放在传入对象other的后面

相当于一个数轴

升序
0
100
150
223
223
9999
降序
9999
223
223
150
100
0

委托排序

使用comparison委托进行排序

函数返回值与自定义类排序相同,只不过第一个参数itemthis第二个参数shopItemother

就是第一个参数与第二个参数比较,第一个参数放在第二个参数的前面,后面还是不变

class ShopItem
{
    public int Id { get; set; }
​
    public ShopItem(int id)
    {
        Id = id;
    }
}
--------------------------------
List<ShopItem> shopItems = new List<ShopItem>
{
    new ShopItem(100), 
    new ShopItem(223), 
    new ShopItem(0), 
    new ShopItem(9999), 
    new ShopItem(150),
    new ShopItem(223)
};
​
// 使用comparison委托进行排序
// 第一个参数与第二个参数比较,第一个参数放在第二个参数的前面,后面还是不变
shopItems.Sort((item, shopItem) => item.Id - shopItem.Id); // 升序
shopItems.Sort((item, shopItem) => shopItem.Id - item.Id); // 降序
​
foreach (var item in shopItems)
{
    Console.WriteLine(item.Id);
}

可变类型的泛型双向链表

// 创建链表
LinkedList<int> linkedList = new LinkedList<int>();
​
------------------------------增-------------------------
// 在链表尾部加节点,数据为10
linkedList.AddLast(10);
// 在链表头部加节点,数据为20
linkedList.AddFirst(20);
// 在某一个节点之后添加节点
LinkedListNode<int> n1 = linkedList.Find(20);
linkedList.AddAfter(n1, 30);
// 在某一个节点之前添加节点
LinkedListNode<int> n2 = linkedList.Find(10);
linkedList.AddBefore(n2, -5);
​
------------------------------删-------------------------
// 移除头节点
linkedList.RemoveFirst();
// 移除尾结点
linkedList.RemoveLast();
// 移除指定值所在的节点
linkedList.Remove(20);
// 清空
linkedList.Clear();
​
------------------------------查-------------------------
// 获取头节点
LinkedListNode<int> first = linkedList.First;
// 获取尾节点
LinkedListNode<int> last = linkedList.Last;
// 获取指定值的节点(找不到返回null)
LinkedListNode<int> node = linkedList.Find(10);
// 查询是否存在
linkedList.Contains(-5);
​
------------------------------改-------------------------
// 得到节点,改变节点的值
node.Value = 100;
​
------------------------------遍历-------------------------
// 遍历
foreach (var item in linkedList)
{
    Console.WriteLine(item);
}
​
// 从头到尾
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;
}

委托

概念

委托函数的容器,用来表示函数的变量类型

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

不同的函数必须对应和各自“格式”一致的委托

语法

访问修饰符 delegate 返回值类型 委托名(参数列表);

访问修饰符默认public

声明位置

namespace(常写)和class语句块中

定义自定义委托(单播委托)

一个函数变量只存储了一个函数

namespace abc
{
    // 声明了一个可以用来存储无参无返回值的函数容器(相当于定义了一个函数变量类)
    public delegate void MyFuncVoid();
    // 声明了一个可以用来存储int参数int返回值的函数容器
    public delegate int MyFuncInt(int num);
    
    public class DelegateLearn
    {
        static void FuncVoid()
        {
            Console.WriteLine("FuncVoid");
        }
        
        static int FuncInt(int num)
        {
            Console.WriteLine(num);
            return ++num;
        }
​
        public static void Run()
        {
            // 实例化函数变量类,赋值(将函数装入变量中)
            MyFuncVoid myFuncVoid = FuncVoid;
            // 不同的函数必须对应和各自“格式”一致的委托,“格式”又叫函数签名
            MyFuncInt myFuncInt = FuncInt;
​
            // 通过函数变量调用存储的函数
            myFuncVoid.Invoke(); // 相当于 FuncVoid();
            myFuncVoid(); // 另一种调用方法
            
            int retValue = myFuncInt.Invoke(114);
            int retValue2 = myFuncInt(38);
        }
    }
}

定义自定义委托(多播委托)

一个函数变量存储了多个函数

有返回值的,默认返回最后一个函数执行的返回值

MyFuncVoid myFuncVoid = FuncVoid; // 初始化赋值
myFuncVoid += FuncVoid; // 添加委托
myFuncVoid -= FuncVoid; // 删除委托
// myFuncVoid = null; // 清空委托
myFuncVoid?.Invoke(); // 该函数变量存储了2个相同的函数FuncVoid,调用时会执行2次
​
MyFuncInt myFuncInt = FuncInt;
myFuncInt += FuncInt;
Console.WriteLine(myFuncInt(112)); // 有返回值的,默认返回最后一个函数执行的返回值

作用

委托当做函数参数传递,在函数内部,先处理一些逻辑,当这些逻辑处理完后,再执行传入的函数。

系统提供的委托

无参无返回值

Action action = FuncVoid;

无参自定义返回值

static string FuncString()
{
    return "OK";
}
​
Func<string> func = FuncString;

有参无返回值(最多16个参数)

static void FuncIntNo(int a, string b) {}
​
Action<int, string> action = FuncIntNo;

有参有返回值(最多16个参数)

static bool FuncFull(float f, int i, string s)
{
    return true;
}
​
Func<float, int, string, bool> fun = FuncFull;

事件

1.事件基于委托存在;

2.事件是委托的安全包裹,让委托的使用更安全;

3.事件是一种特殊的变量类型;

4.只能做为成员变量存在于接口结构体中;

5.不能在类外部赋值,但是可以增加和删除

6.不能在类外部调用;

class Test
{
    public event Action? MyEvent; // ?表示对象可以为null
​
    void TestFunc()
    {
        Console.WriteLine("In");
    }
​
    public void TestRun()
    {
        MyEvent += TestFunc;
        MyEvent += TestFunc;
        MyEvent -= TestFunc;
​
        MyEvent?.Invoke();
    }
}
​
public static void Main(string args)
{
    Test test = new Test();
    // 不能在类外部赋值,但是可以增加和删除
    test.MyEvent += () => { Console.WriteLine("Out"); };
    // 不能在外部调用事件
    test.TestRun();
}

匿名函数

主要配合委托和事件使用

语法

delegate (参数列表)
{
    ...
};

用法

public event Action? MyEvent;
​
public void TestRun()
{
    // 无参无返回值
    MyEvent += delegate ()
    {
        Console.WriteLine("匿名函数");
    };
​
    // 有参无返回值
    Action<int, string> action = delegate(int a, string s)
    {
        Console.WriteLine($"{a},{s}");
    };
​
    // 有参有返回值
    Func<int, bool> func = delegate(int a)
    {
        return a > 20;
    };
​
    MyEvent?.Invoke();
    action.Invoke(100, "ok");
    Console.WriteLine(func.Invoke(23));
}

缺点

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


lambda表达式

使用方法与匿名函数相同

语法

(参数列表) => { ... }

用法

// 无参无返回
MyEvent += () => { Console.WriteLine("none"); };
// 有参无返回
Action<int, string> action = (a, s) => { Console.WriteLine($"{a},{s}"); };
// 有参有返回
Func<int, bool> func = a => a > 20;
​
MyEvent?.Invoke();
action.Invoke(100, "ok");
Console.WriteLine(func.Invoke(23));

闭包

内层的函数可以引用包含在它外层的函数的变量,即使外层函数执行已经终止。

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

public event Action? MyEvent;
​
public void TestRun() // 外层函数
{
    int value = 10; // 外层函数变量
​
    // 此处就形成了闭包
    MyEvent = () =>
    { // 内层函数
        Console.WriteLine(value); // 引用外层函数的变量value
    };
}

当内层函数结束执行后,改变了外层函数的值(value)的生命周期,使其并不会被自动释放,而是一直存在。


协变(out)和逆变(in)

概念

协变:里氏替换原则,父类可以装子类,所以子类变为父类,如:string变为object感受是和谐的

逆变:子类不能装父类,所以父类变成子类,如:object变成string感受是不和谐的

协变和逆变用来修饰泛型字母,只有泛型接口泛型委托能用

“逆变”的“逆”读音为“ni”,所以可以跟in相互记忆。

作用

out:只能作为返回值

delegate T TestOut<out T>();

in:只能用在参数

delegate void TestIn<in T>(T value);

综合:

interface ITest<in T, out K>
{
    public K Test(T v);
}

协变:父类泛型委托装子类泛型委托

逆变:子类泛型委托装父类泛型委托


多线程

概念

进程(process):操作系统(OS)里启动的程序,每一个程序就是一个进程

线程(thread):运行在进程里,是进程中的实际运作单位,是OS能调度的最小单位

示例

IsBackground是否为后台线程

如果为false主线程结束,需要等待子线程结束才能退出程序;

如果为true主线程结束,子线程也跟着结束

private static bool _isRunning = true; // 线程运行标识符,控制线程是否运行
    
public static void Main(string[] args) // 主线程
{
    Thread thread = new Thread(Start) // 创建子线程
    {
        IsBackground = true // 设置为后台线程
    };
    thread.Start(); // 启动子线程
​
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(i);
    }
​
    _isRunning = false;
}
​
private static void Start() // 子线程
{
    while (_isRunning) // 线程运行标识符为false,结束此线程运行
    {
        Console.WriteLine("New thread logic.");
        Thread.Sleep(10); // 当前线程休眠10ms
    }
}

数据共享

问题

多个线程同时访问/修改同一个变量会出现意想不到的问题

解决问题

通过对要访问的引用变量进行加锁

lock(引用类型对象) {}

示例

private static object _obj = new();
private static int _num = 100;
​
public static void Main(string[] args) // 主线程
{
    while(true)
    {
        lock(_obj)
        {
            _num += 100;
        }
    }
}
private static void Start() // 子线程
{
    while (true)
    {
        lock(_obj)
        {
            _num -= 100;
        }
    }
}

反射

元数据

用来描述数据的数据

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

反射概念

程序在运行的时候,可以查看其它或自身程序集的元数据,这个查看的行为叫做反射

即,通过反射可以在程序运行的时候,对自身或其它程序集中的类、函数、对象等等进行实例化、执行等操作

语法

Type

是反射功能的基础,是访问元数据的主要方式

使用Type获取有关类声明的信息,成员(构造函数、方法、字段、属性和类的事件)

获取Type

object.GetType()

int a = 33;
Type type = a.GetType();
Console.WriteLine(type); // System.Int32

typeof(类型)

Type type = typeof(int);
Console.WriteLine(type); // System.Int32

Type.GetType("类名") 类名必须包含命名空间

Type type = Type.GetType("System.Int32");
Console.WriteLine(type); // System.Int32
获取类的使用公共成员
MemberInfo[] infos = Type对象.GetMembers();
类(10-Type)
class Test
{
    private int _i = 1;
    public int J = 2;
    public string Str = "reflect";
​
    public Test()
    {
    }
​
    public Test(int i)
    {
        _i = i;
    }
​
    public Test(int i, string str) : this(i)
    {
        Str = str;
    }
​
    public void Speak()
    {
        Console.WriteLine("Speak.");
    }
}
主函数
using System.Reflection;
​
public static void Main(string[] args)
{
    Type type = typeof(Test);
    MemberInfo[] infos = type.GetMembers();
    foreach (var info in infos)
    {
        Console.WriteLine(info);
    }
}
输出
Void Speak()
System.Type GetType()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
Void .ctor() // 类的构造函数
Void .ctor(Int32)
Void .ctor(Int32, System.String)
Int32 J
System.String Str
获取类的构造函数并调用

具体类定义

Type type = typeof(Test);
// 获取所有构造函数
ConstructorInfo[] ctorInfos = type.GetConstructors();
​
// 获取无参构造函数
ConstructorInfo ctorInfo = type.GetConstructor(new Type[0]);
// 获取有参构造函数,按顺序传入构造函数的参数
ConstructorInfo ctorInfo2 = type.GetConstructor(new Type[] { typeof(int), typeof(string) });
​
// 执行构造函数,按顺序往object数组传入值,没有用null
object obj = ctorInfo.Invoke(null);
object obj2 = ctorInfo2.Invoke(new Object[] { 233, "OKK" });
​
// 转化为对应类
Test test = obj as Test;
Test test2 = obj2 as Test;
​
// 调用类对象的函数
test.Speak();
test2.Speak();
Speak 1 ---> reflect
Speak 233 ---> OKK
获取类的公共成员变量

具体类定义

Type type = typeof(Test);
// 获取所有成员变量
FieldInfo[] fieldInfos = type.GetFields();
// 获取指定名称的公共成员变量
FieldInfo field = type.GetField("Str");
​
Test test = new Test(100, "Jok");
// 获取对象的值
Console.WriteLine(field.GetValue(test));
// 设置对象的值
field.SetValue(test, "OvO");
Console.WriteLine(test.Str);
Jok
OvO
获取类的公共成员方法

获取方法:名称获取,如果有重载,第二个参数需要按顺序传入参数类型

调用方法:传入执行对象参数数组,如果为静态方法,则第一个参数null

Type strType = typeof(string);
// 获取所有公共成员方法
MethodInfo[] methodInfos = strType.GetMethods();
// 获取公共成员方法(如果有重载,需要按顺序传入参数类型)
MethodInfo subStr = strType.GetMethod("Substring", new[] { typeof(int), typeof(int) });
​
string str = "Hello fu";
// 调用方法,传入 执行对象 和 参数数组,如果为静态方法,则第一个参数为null
object result = subStr.Invoke(str, new object[] { 1, 3 });
Console.WriteLine(result);
ell

Activator

用于快速实例化Type对象的类

具体类定义

无参构造
// 获取类型
Type type = typeof(Test);
// 快速实例化对象,默认无参构造
object obj = Activator.CreateInstance(type);
// 转换类
Test test = obj as Test;
// 调用类方法
test.Speak();
Speak 1 ---> reflect
有参构造
// 获取类型
Type type = typeof(Test);
// 快速实例化对象,第二个参数为变长参数,直接传入值
object obj = Activator.CreateInstance(type, 250, "olmn");
// 转换类
Test test = obj as Test;
// 调用类方法
test.Speak();
Speak 250 ---> olmn

Assembly

程序集类

主要用来加载其它程序集(不是自己的程序集),比如dll文件

加载程序集函数

加载同一文件下的其它程序集:

Assembly.Load("程序集名称")

加载不在同一文件下的其它程序集:

Assembly.LoadFrom("包含程序集清单的文件的名称或路径")
Assembly.LoadFile("要加载的文件的完全限定路径")
示例

具体类定义

// 加载指定程序集
Assembly assembly = Assembly.LoadFrom(@"D:\Debug\net7.0\CSharpLearn.dll");
// 获取类对象
Type testClassType = assembly.GetType("CSharpLearn.Test");
// 构造类
object testClass = Activator.CreateInstance(testClassType);
// 通过反射获取方法
MethodInfo speak = testClassType.GetMethod("Speak");
// 调用类方法
speak.Invoke(testClass, null);
Speak 1 ---> reflect

特性

本质是个,可以利用这种特性类为元数据添加额外信息,之后可以通过反射获取这些额外信息

语法

class 类名称Attribute : Attribute
{
}

示例

定义特性类

class MyCustomAttribute : Attribute
{
    public string Info;
​
    public MyCustomAttribute(string info)
    {
        Info = info;
    }
}

特性名称为MyCustom

使用特性类

[MyCustom("My Class")]
public class ReflectLearn
{
    [MyCustom("My Id")]
    private int _id = 14;
    
    [MyCustom("My Run")]
    public static void Run([MyCustom("My param")]float mul)
    {
    }
}

为类、成员变量、成员方法等等添加额外信息

反射获取特性

Type type = typeof(ReflectLearn);
        
// 判断是否使用了某个特性 IsDefined(特性类类型,是否搜索继承链)
Console.WriteLine(type.IsDefined(typeof(MyCustomAttribute), false));
​
// 获取type类使用的特性类对象
object[] attributes = type.GetCustomAttributes(true);
foreach (var attribute in attributes)
{
    if (attribute is MyCustomAttribute)
    {
        // 获取特性的成员对象
        Console.WriteLine((attribute as MyCustomAttribute).Info);
        // 调用特性的方法
        (attribute as MyCustomAttribute).Print();
    }
}
True
My Class
Hello attribute.

限定自定义特性的使用范围

可以在自定义特性类上面使用AttributeUsage修饰使用范围

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
class MyCustomAttribute : Attribute
{
    public string Info;
​
    public MyCustomAttribute(string info)
    {
        Info = info;
    }
​
    public void Print()
    {
        Console.WriteLine("Hello attribute.");
    }
}

AttributeTargets:特性能用在哪里

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

Inherited:特性能否被派生类和重写成员继承

过时特性

[Obsolete(过时提示内容, 是否报错)]
[Obsolete("This method was obsolete.", false)]
public static void Run(float mul)
{
}
-----------------------
ReflectLearn.Run(.34f);

条件编译特性

一般和#define配合使用

using System.Diagnostics;
[Conditional("标识符")]
#define USING
using System.Diagnostics;
​
[Conditional("USING")] // 有了USING标识符才会编译此函数
static void Func()
{
    Console.WriteLine("Func");
}
    
static void Main(string[] args)
{
    Func(); // 如果没有USING标识符,就不会调用
}

外部DLL函数特性

用来定义该函数是由非.Net(C#)定义的函数,一般用来调用C或C++的DLL中的方法

using System.Runtime.InteropServices;
​
[DllImport("dll")]
public static extern 返回值类型 dll中的函数名(dll中的参数列表);
[DllImport("Test.dll")]
public static extern void Func();
​
static void Main(string[] args)
{
    Func();
}

迭代器

概念

iterator又称光标(cursor),是一种程序设计的软件设计模式,迭代器模式提供了一个方法顺序访问一个聚合对象中的各个元素

是容器对象用来遍历的接口,使用foreach之前必须先实现迭代器

标准迭代器的实现方法

IEnumerable:是否可以迭代接口

IEnumerator:迭代器接口

foreach执行顺序:

  1. 自动调用GetEnumerator()

  2. 光标移动到下一个位置MoveNext()

  3. 如果为true,返回Current;否则跳出循环

class CustomList : IEnumerable, IEnumerator
{
    public int[] List { get; set; }
​
    public CustomList()
    {
        List = new[] { 1, 2, 3, 4, 5, 6 };
    }
    
    // 声明光标
    private int cursor = -1;
​
    // 当前光标所在位置的值
    public object Current => List[cursor];
​
    // 1.foreach会自动调用这个方法
    public IEnumerator GetEnumerator()
    {
        // 重置光标位置,用以下次循环使用
        Reset();
        return this;
    }
​
    // 2.光标移动到下一个位置
    // 3.如果为true,返回Current;否则跳出循环
    public bool MoveNext()
    {
        // 移动光标
        ++cursor;
        // 如果光标表示的索引超出List的长度,则跳出循环
        return cursor < List.Length;
    }
​
    // 重置光标位置至初始位置
    public void Reset()
    {
        cursor = -1;
    }
}

主函数

public static void Main(string args)
{
    CustomList customList = new CustomList();
    foreach (int cl in customList)
    {
        Console.WriteLine(cl);
    }
​
    foreach (int cl in customList)
    {
        Console.WriteLine(cl);
    }
}

输出

1
2
3
4
5
6
1
2
3
4
5
6

语法糖实现迭代器

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

使用yield return返回就可以

class CustomList : IEnumerable
{
    public int[] List { get; set; }
​
    public CustomList()
    {
        List = new[] { 1, 2, 3, 4, 5, 6 };
    }
    
    // 只要实现此方法即可
    public IEnumerator GetEnumerator()
    {
        for (int i = 0; i < List.Length; i++)
        {
            // C#语法糖,系统自动生成IEnumerator的函数
            yield return List[i];
        }
    }
}

语法糖实现泛型类迭代器

class CustomList<T> : IEnumerable
{
    private T[] _array;
    public IEnumerator GetEnumerator()
    {
        foreach (var t in _array)
        {
            yield return t;
        }
    }
}

特殊语法

var隐式类型

表示任意类型

  1. 不能做为类成员,只能用于临时变量,一般写在函数中;

  2. 必须初始化

var a = 99;

大括号设置对象初始值

类名 变量名 = new 类名(参数值){ 成员变量1 = 值1, 成员变量2 = 值2, ... };
Person p = new Person(200, "ok"){ age = 11, idCard = 10235345, name = "lok" };

匿名类型

var 变量名 = new { 变量1 = 值1, 变量2 = 值2, ... }
var v = new { age = 23, ok = "kok", cou = .34f };

可空类型(?)

声明时,在值类型后写上?就可以为null

变量类型? 变量名 = 值;
int? val = null;

判断是否为空

变量名.HasValue

安全获取可空类型的值

为空默认返回默认值
int? value = null;
value.GetValueOrDefault();
指定为空时返回的默认值
int? value = null;
value.GetValueOrDefault(38);

自动判断引用类型是否为空

如果为空则不执行

变量名?.方法名(值)
变量名?[值]
object obj = null;
obj?.ToString(); // 相当于:if (obj != null) { obj.ToString(); }
int[] array = null;
arr?[0];

空合并操作符(??)

如果左边为null,就返回右边,否则左边

可以为null的类型都可以使用

左值 ?? 右值

相当于:

if (左值 == null)
{
    return 右值;
}
else
{
    return 左值;
}
int? v = null;
Console.WriteLine(v ?? 0); // 0

规则,就是用来打破的( ̄へ ̄)!