视频学习笔记 --C#基础其他知识点(一些高级用法)

哔哩哔哩视频:【C#筑基教程】

委托,事件,Action,Func

进程单例模式:

控制进程只能单开,通常情况创建对象是在进程内部,但是创建一个Mutex对象在操作系统中(在进程外部),所以在其它的地方也能访问,有唯一的标识,创建之后成功的话操作系统会返回true给创建的进程A,另外的一个进程B去创建的时候会发现操作系统中有该标识的文件,那么就不会再重复创建,而是直接指向它,操作系统返回false给进程B(因为不是创建者),直接关闭进程B,这样就实现了进程的单例模式。

关键字:Mutex(线程同步锁)
启动的代码

//第一个参数表示是控制者,直接获得mutex的控制权
//第二个参数为名称,【采用工具-》创建GUID的方式进行创建一个长字符串】
//名字前可添加前缀"Global\"实现多用户系统单例
//第三个参数:为true则表示为对象的创建者,为false则代表不是对象的创建者,仅仅是获得引用
Mutex mutex = new Mutex(true, "{D52AEA01-5548-4555-B5CA-D51BF0B39322}",out bool createNew);
if (!createNew)
{
    //如果不是创建者,则结束程序的执行(不创建新的实例)
    MessageBox.Show($"{Application.Current} 已经在运行.","提示");
    return;
}

条件编译【预处理编译命令】

根据不同的需求,编译生成不同的程序版本
快捷键:ctrl + k,S 再按Tab键 ,则自动生成条件编译代码
#if … #endif …
#if … #else …#endif …
#if … #elif … #else …#endif …

可以使用系统自带的名字(DEBUG/RELEASE)或者自己定义名称
可以在代码头部使用#define 当不需要时,可以注释掉定义的#define或者#undef表示限制代码不再编译。

还可以使用条件编译特性

#define 高级玩法			//在头部添加对应的define,则可以

//预处理编译命令:高级玩法
Play();
Console.WriteLine("Hello !");

//......

 [Conditional("高级版")]
 static void Play()
 {
     Console.WriteLine("高级玩法");
 }

顶级语句:编写简单程序(写一行语句即可运行)

系统会自动将顶级语句放在main方法【程序的入口方法】
顶级语句可以增加方法,但是这个方法时局部方法(main方法内部定义的方法),隐式位于全局命名空间中

扩展方法:

可以为现有的非静态类型添加新方法,扩展方法的本质是静态方法,扩展方法需要放在静态类中,但是调用它的时候通过对象来调用。第一个参数类型即扩展的类型,必须用this关键字修饰。
以下举例两种方法:使用对象的方式调用,和使用静态的方式调用,对于string和int的扩展方法

            string s = "你好,世界!你好,中国!";
            s.Print_str();      //对象方式调用
            MyExtensions.Print_str(s);  //静态方法调用

            int numb = 0;
            numb.Print_int();
            MyExtensions.Print_int(numb);
public static class MyExtensions
{
    //扩展方法第一个参数是要扩展的类,必须用this修饰
    public static void Print_str(this string str)
    {
        System.Console.WriteLine(str);
    }

    public static void Print_int(this int str)
    {
        System.Console.WriteLine(str);
    }
}

用例:linq查询方法基本都是扩展方法:Enumerable类为IEnumerable接口扩展(系统中的扩展方法,可以直接使用)

Console.WriteLine(s.Count(r => r == '你'));

注意:取好名<建议加上Ex作为后缀>不要滥用扩展方法

匿名类型:

不用写class 直接可以new对象,组合数据,传递数据
好处:避免编写不必要的类,提高编码效率,逻辑更清晰
缺点:超出当前作用域,需使用dynamic或反射等方式来获取数据项
主要应用场景:Linq语句的select子语句及多返回值场景

匿名类型不是没有类型,编译器会自动生成一个类型
使用var

var person = new { Name = "张三", Age = 30 };
Console.WriteLine($"Name:{person.Name},Age:{person.Age}");

不在同个作用域,如何使用:
1.动态类型处理匿名类型对象

//匿名对象
dynamic GetStudentInfo()
{
    return new { Name = "张三", Age = 22 };
}
dynamic student = GetStudentInfo();
Console.WriteLine($"Name:{student.Name},Age:{student.Age}");		//在这里的属性无法自动生成,因为是动态属性《类型不够安全》

2.指定类型接收匿名类型对象

//2.指定类型接收匿名类型对象
//这个技巧只有在同一个程序集中的代码才有效
var student_2 = new {Name = "",Age = 0};   
student_2 = GetStudentInfo();
Console.WriteLine($"Name:{student_2.Name},Age:{student_2.Age}");

3.通过反射获取匿名类型数据项(测试匿名类型作参数)
反射是在运行时获取类型信息

//使用反射获取匿名类型数据项(测试匿名类型作为参数)
void Test_3(object obj)
{
    Type t = obj.GetType();     //首先获得类型
    PropertyInfo? pName = t.GetProperty("Name");    //或者类型指定名称的属性
    PropertyInfo? pAge = t.GetProperty("Age");

    Console.WriteLine($"匿名类型:{t.Name}");        //通过value来获得该属性的值
    Console.WriteLine($"Name:{pName?.GetValue(obj)},Age:{pAge?.GetValue(obj)}");
}

//使用:
//使用反射获取匿名类型数据项(测试匿名类型作为参数)
Test_3(new { Name = "张三",Age=22});

LINQ语句应用:
a.先创建最底层的class类和student类

//LINQ语句应用:
//a.新的类:Student【学生类】和Class【班级类】
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Gender {  get; set; }     //性别
    public int ClassId {  get; set; }   //班级编号
}

public class Class
{
    public int Id { get; set; }
    public string ClassName { get; set; }
    public string Teacher { get; set; }
}

b.实例化列表

LINQ语句应用:
///b.实例化班级信息和学生信息的列表
var classList = new List<Class>
{
    new Class{Id = 1,ClassName = "一年级一班",Teacher = "张老师"},
    new Class{Id = 2,ClassName = "一年级二班",Teacher = "李老师"},
    new Class{Id = 3,ClassName = "二年级一班",Teacher = "王老师"},
    new Class{Id = 4,ClassName = "二年级二班",Teacher = "赵老师"},
};


var studentList = new List<Student>
{
    new Student{Id = 1,Name = "张三",Age = 8,Gender = "男",ClassId = 1},
    new Student{Id = 2,Name = "李四",Age = 8,Gender = "男",ClassId = 1},
    new Student{Id = 3,Name = "王五",Age = 8,Gender = "女",ClassId = 2},
    new Student{Id = 4,Name = "赵六",Age = 8,Gender = "男",ClassId = 2},
    new Student{Id = 5,Name = "张三",Age = 8,Gender = "男",ClassId = 1},
};

C.使用LINQ语句进行联合查询

//使用LINQ语句进行联合查询,查询学生信息以及对应的班级信息:
var result = from stu in studentList
             join cla in classList on stu.ClassId equals cla.Id
             select new             //select new { }重新组合成一个新的类《匿名类型》
             {
                 学生姓名 = stu.Name,
                 年龄 = stu.Age,
                 性别 = stu.Gender,
                 班级名称 = cla.ClassName,
                 班级教师 = cla.Teacher,
             };

//输出查询结果
foreach(var item in result)
{
    Console.WriteLine($"学生姓名:{item.学生姓名},年龄:{item.年龄},"+
        $"性别:{item.性别},班级名称:{item.班级名称},班级教师:{item.班级教师}");
}

D.使用LINQ语句进行联合查询,查询学生信息(简写版)

//4.使用LINQ语句进行联合查询,查询学生信息(简写版)
var result2 = from stu in studentList
              select new
              {
                  stu.Id,   //此处简写,没写属性名,自动以赋值属性为名称
                  stu.Name,
              };

foreach( var item in result2)
{
    Console.WriteLine($"学生ID:{item.Id},学生姓名:{item.Name}");
}

元组:

//元组:
string x = "长江";
string y = "黄河";
(x,y) = (y,x);  //利用元组进行交换

详解:valueTuple元组和匿名类型
同:都不用写class,new对象
同:都创建格式简洁,匿名类型:new{} ,元组:{} 【元组更简洁】
不同:匿名类型:属性名 = 属性值 ,元组:字段名:字段值
元组可以很方便地作为参数和返回值,匿名类型需要使用dynamic或反射
同:匿名类型在linq中使用方便,元组也是【元组可以基于匿名类型(删掉new,将属性的‘.’改变为‘:’)进行修改】
不同:元组中的字段值可以修改,而匿名类型中的字段值不可修改

新版本元组创建:
(string, int) x_tuple = ("鲁班七号", 12); 
var x_tuple_2 = ("鲁班七号", 12);	//再次简化版本
Console.WriteLine($"姓名={x_tuple_2.Item1},年龄={x_tuple_2.Item2}");    //元组的使用
//命名字段  方式1
var x_tuple_3 = (name: "鲁班七号", Age : 12);
Console.WriteLine($"姓名={x_tuple_3.name},年龄={x_tuple_3.Age}");    
//命名字段  方式2
(string name, int age) x_tuple_4 = ("鲁班七号", 12);
Console.WriteLine($"姓名={x_tuple_4.name},年龄={x_tuple_4.age}");

//元组的解构
//(string name, int age) = ("妲己", 18);  //第一种解构
var(name, age) = ("妲己", 18);        //第二种解构
Console.WriteLine($"姓名:{name},年龄{age}");

//元组的弃元 即放弃不需要的元素
var tuple = (1, "a", 3.14);
var (_,letter,_) = tuple;
Console.WriteLine(letter);

//元组作为参数
void processTuple((string Name,int Age))
{
    Console.WriteLine($"Name:{person.Name},Age:{person.Age}");
}
//调用时:processTuple(("Alice", 25));

//元组作为返回类型(比作为变量更加常用),可用于在方法中返回多个数据
(int Sum, int Product) GetSumAndProduct(int a,int b)
{ 
    return (a + b, a * b); 
}
//调用:
var result_tuple = GetSumAndProduct(3, 4);
Console.WriteLine($"和:{result_tuple.Sum},乘积:{result_tuple.Product}");

//元组作为返回值——2
(string name, int age) GetPerson()
{
    return ("Bob",30);
}
//调用
(string name_tuple2, int age_tuple2) = GetPerson();
Console.WriteLine($"Name:{name_tuple2},Age:{age_tuple2}");

//元组可以直接交换,也可以直接输出(tostring会自动输出元组中的全部内容)

数据基本的统计方法:Lambda表达式

语法:(参数列表)=> {//函数体};
注:通常参数可以省略类型说明;如只有一个参数,可以省略();如方法体只一句,可省略{};如方法体只一句,可省略return

Lambda表达式无法单独使用,需要与委托或事件配合:

Action action = () => Console.WriteLine("无参无返Lambda表达式");
action;

Lambda表达式实现闭包
闭包是一种语言特性,它允许在函数内部定义的函数访问外部函数的局部变量。即使外层函数执行已终止,在C#中我们可以使用Lambda来实现闭包。【会存储上一次的状态】
【与委托或事件相结合】

public Action<string?> CreateWrite()
{
	string mag = "";
	return (string? info) =>
	{
		mag = info ?? msg;
		Console.WriteLine(msg);
	}
}
EventHandler btn2Click()
{
	double sum = 0;
	int count = 0;
	return (sender,e) =>
	{
		sum += double.Parse(textBox.Text);
		count++;
		label1.Text = $"....";
	} ;
}

一行代码完成求和求平均最大值最小值
借用之前的studentList

Lambda表达式
//例子1
Console.WriteLine(studentList.Max(s => s.Age));		//用Lambda表达式指定需要的数据

//例子2
//list中性别为男的,集合的年龄平均值
//studentList.Where(s => s.Gender == "男")表达式筛选集合
//Average(s => s.Age)表达式指定字段
Console.WriteLine($"男生平均年龄{studentList.Where(s => s.Gender == "男").Average(s => s.Age):0.00}");

//例子3
//年纪最大的同学的classId
var result = studentList.Where(s => s.Age == studentList.Max(s => s.Age));
foreach(var item in result)
{
    //输出选出的集合中学员的信息
    Console.WriteLine($"年纪最大的同学的名字:{item.Id}");
}

数据基础:整形,浮点型

整形数据
使用sizeof获取字节占用的大小:int占四个字节 大小为±21亿+
类型转化:隐式转换:小类型转大类型,显示类型:大类型转小类型(强制类型转换,可能会报错,产生溢出,可以加checked报错)

浮点数:
float:是system.single的别名,占用4byte字节(32bit位)【一个字节=8位】
double:8个字节(64bit)
decimal:16字节(128bit),但是数据范围比较小【字节主要为了保证精度】

编码的奥秘

将信息使用特定的符号组合表示出来的过程。
数据编码,算法编码
字符串编码:UTF-32,UTF-16,UTF-8【前三个是unicode】,ASCII,GB2312
UTF-32:使用四个字节存储编码,UTF-16:2-4字节【常用的用2个字节存储,小于10000则为2字节】,UTF-8:1-4字节

C#的角度讲解编码:
编码:可以直接从Encoding类中获取编码
Encoding类:属性:UTF-32,UTF-16(Unicode),UTF-8,BigEndianUnicode(每个字符:高位在前,低位在后面 – 【j举例两个字节:小端:7D 59 ⇒ 大端:59 7D】);方法:GetBytes。
解码:Encoding类:方法:GetString
加密

字符串类型

stringBuilder类拼接字符串性能提升(相较于string)
string类是不可修改的,每次的修改事实上是创建了一个新的字符串,每次都需要申请内存空间,都需要复制字符串到新的空间,最后销毁原有空间。
stringBuilder:优点1:默认初始值为16字符,32byte,超过空间大小 乘2 即在申请一个相同大小的空间。每次乘以2,也很可能浪费空间,所以实际是每次最大8000字符。优点2:每次扩容,不需要复制字符串,而是将原字符串与新空间连接起来、<使用链表>
string:常用属性:length,【index】,string.Empty字段,常用方法:判断空:IsNullOrEmpty,IsNullOrWhiteSpace,substring,copyto【复制到数组里面,且可以指定就】,ToCharArray
比较:Equals和compare。拆合:split,join,concat。增删改:insert,remove,replace,trim()【删除空白字符】,ToLower,ToUpper。查:contains,startwith,Endwith,indexof【返回索引】,格式化Format:string.Format,$“{}”,Tostring(“”)【包含一些数值格式,或者时间格式】

stringInfo类型:

StringInfo stringInfo = new StringInfo("XXXX");
MessageBox.Show($"字符个数为:{stringInfo.LengthInTextElements}","友情提示");

array属性和方法

数组基类:属性:rank,length;数组结构:resize,getLength,GetValue,setValue;整体操作:AsReadOnly【只读】,clear,Fill,clone,copy;遍历处理:foreach【对指定数组的每个元素执行指定操作】,TrueForAll【确定数组中的每个元素是否都与指定的相匹配】,converAll【将一种类型的数组转化为另一种类型的数组】,查找匹配:exists【是否包含】,Find【查找元素】,indexOf【返回满足条件的索引】,排序:sort【排序】,reverse【反转顺序】
复制:

string[] names = {"1","2","3","4"};

//错误的复制方式:只是指向了同一块地址空间,并没有复制
string[]  arr0 = names;
string[]  arry1 = new string[name.Length];
arr0 = names;

//正确的复制:
//1.使用for循环一个一个元素去复制
//2.使用CopyTo
string[]  arry2 = new string[name.Length];
names.CopyTo(arry2,0);
//3.
string[]  arry3 = new string[name.Length];
Array.Copy(names,arry3 ,name.Length);

类型转换:

//1.循环处理,转化为字符串数组【单个转换】	ToString
//2.ConvertAll转换数组
int[] arri = {18,22,15,8,75,27,32};
string[] arrs = Array.ConvertAll(arri ,i => i.ToString());
double[] arrd = Array.ConvertAll(arri ,i => Convert.ToDouble(i));

//数组转化成byte[] 1对多
//字符串转化为字节数组

穷举法

百钱百鸡问题:公鸡一只5钱,母鸡一只3钱,小鸡三只一钱,现有百钱,刚好购买百只鸡,如何购买。
嵌套for循环

进制转换问题

1.直接使用二进制与十六进制
int i = 0b1010;	//0b是二进制
i = 0x1a;	//0x是六进制

2.二进制转2816进制
int x = 16;
Console.WriteLine(Convert.ToString(x,2));	//输出2进制
Console.WriteLine(Convert.ToString(x,8));	//输出8进制
Console.WriteLine(Convert.ToString(x,16));	//输出16进制

3.二进制,八进制,十六进制字符串转十进制
Console.WriteLine(Convert.ToInt32("1010",2));
Console.WriteLine(Convert.ToInt32("10",8));
Console.WriteLine(Convert.ToInt32("1a",16));

使用Lambda表达式实现闭包
闭包是一种语言特性,它允许在函数内部定义的函数访问外部函数的局部变量。即使外层函数执行已终止。

委托

利用委托实现封装与隔离
委托类型规定方法的签名(方法类型):返回值类型,参数类型,个数,顺序
1.delegate委托关键字:【访问修辞】delegate 返回类型 委托名(参数列表)
2.实例化委托:委托类型 委托变量 = new 委托名(方法名)
3.使用委托:委托引用名(实参列表);委托引用?.Invoke(参数);委托间接调用方法,同一段代码,委托方法不一样,执行效果就不一样。

主要形式:事件处理,多线程委托,将方法(委托)作为另一个方法的参数,将不变的代码与变化的代码隔离,变化的代码使用委托调用

//1.定义委托的类型
public delegate double Cal(double x,double y);
//2.实例化委托
static double Add(double x,double y)
{
	return x+y;
}
Cal cal = new Cal(Add);		//主要是这句
//3.调用委托
double result = cal(6,8);
Console.WriteLine("委托Add计算结果为:{result}");

使用委托的好处:将不变和变化的部分分隔开来
委托作为参数:

static void Test(Cal f)
{
	double result = f(1.0,2.0);
}

//使用:
//方法1
Cal cal = Add;
Test(cal);
//方法2
Test(new Cal(Add));
//方法3
Test(Dec);

泛型委托

好处:简化委托的使用
泛型委托不用定义委托,直接实例化委托和执行委托,系统预定了两种泛型委托:Action【针对没有返回值的委托】,Func【针对有返回值的委托】。

//Action的调用【无返回值】
static void sayHi(string msg)
{
	Console.WriteLine(msg);
}
static void Main(string[] args)
{
	Action<string> action = sayHi;
	action("你好");
}
//Func【有返回值】
static double Add(double x,double y)
{
	return x+y;
}
Func<double,double,double> func =  ;		//最后一个参数是返回值
double result = func(1.2,1.6);

decimal类型:是指常量,需要加上m=>例如1.2m

进程操作入门:

进程在宏观上是并发的,在微观上是交替进行的,一个进程可以有多个线程,多个线程可共享进程资源。操作系统才是进程与线程的操作与管理者,C# 只是封装调用相应功能
使用process类可以在程序中进行启用进程
关闭程序时有安全关闭【提示是否进行修改的保存】:closeMainWindow()
也有不提示直接关闭:kill()
在关闭之前可查看是否关联过进程,查看是否有ID,如果有则表示进程是打开状态

异形动画窗体:制作桌面宠物精灵【未看】

GDI+绘图编程入门【未看】

Attribute特性与反射案例,自动化识别与使用类型

特性:为程序元素额外添加声明信息的一种方式
反射:运行时获取程序集中的元信息的一种能力
【实现:能自动识别并添加新的英雄,自动识别英雄支持的技能,并能够直接调用英雄的技能】

[Hero]		//添加特性,特性使用中括号【特姓名】
class{
	[Skill]		//添加特性,特性使用中括号【特姓名】
	//新的英雄类,给新英雄两个技能(方法)
	public void  神剑()
	{
		MessageBox.Show("段-神剑","提示");
	}
	[Skill]		//添加特性,特性使用中括号【特姓名】
	public void  微步()
	{
		MessageBox.Show("段-微步","提示");
	}
}

制作过程1:两个特性标签(Hero,Skill)的定义:就是继承了Attribute类的一个新的类型

public class HeroAttribute : Attribute
{
}
public class SkillAttribute : Attribute
{
}

制作过程2:如何自动识别和对应
在winform中:


namespace WindowsFormsApp1
{
    //特性的定义
    public class HeroAttribute : Attribute
    {
    }
    public class SkillAttribute : Attribute
    {
    }

    public partial class Form1 : Form
    {
        private List<Type> heroTypes;       //保存所有英雄类的类型
        private object selectedHero;    //当前选择的英雄对象

        public Form1()
        {
            InitializeComponent();

            //加载所有英雄类的类型
            heroTypes = Assembly.GetExecutingAssembly().GetTypes()          //获得现在执行代码的程序集里面所有的类型
                .Where(t => t.GetCustomAttributes(typeof(HeroAttribute), false).Any())      //进行筛选【通过HeroAttribute标签】
                .ToList();      //付给heroTypes列表

            //初始化英雄列表
            heroListBox.Items.AddRange(heroTypes.Select(t => t.Name).ToArray());    //select投影:只显示名字

        }

        private void heroListBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            //选择英雄的时候如何获得英雄的技能
            if (heroListBox.SelectedIndex == -1) return;    //如果未选定任何项退出
            //创建当前选择的英雄对象
            var selectedHeroType = heroTypes[heroListBox.SelectedIndex];        //表示点击的是第几个列表
            selectedHero = Activator.CreateInstance(selectedHeroType);  //通过Activator反射创建了实例

            //获取该英雄类型的所有技能方法
            var skillMethods = selectedHeroType.GetMethods()       //获得这个对象的所有的类型方法
                .Where(m => m.GetCustomAttributes(typeof(SkillAttribute), false).Any()) //通过SkillAttribute特性进行筛选
                .ToList();  //把筛选出来的添加到列表

            //初始化技能列表
            skillListBox.Items.Clear();
            skillListBox.Items.AddRange(skillMethods.Select(m => m.Name).ToArray());
        }


        private void skillListBox_DoubleClick(object sender, EventArgs e)
        {
            //双击技能时释放技能
            if (skillListBox.SelectedIndex == -1) return;    //如果未选定任何项退出
            //获取当前选择的技能方法
            var selectedSkillMethod = selectedHero.GetType()    //获得所有类型
                .GetMethod(skillListBox.SelectedItem.ToString());   //获得方法:选择的字符串的名称

            //调用该技能方法
            selectedSkillMethod?.Invoke(selectedHero, null);    //使用invoke执行【调用英雄的技能】

        }
    }
}

Reflection应用,简单使用反射,打破常规

类的私有成员,外部不能访问? ==》否:使用反射,可以访问私有成员。

class MyClass
{
    private string myField = "私有字段";
    private static string sField = "静态私有字段";
    private string myProperty { get; set; } = "私有属性";

    private void FunA() {
        Console.WriteLine("私有方法执行。");
    }

    private void FunB()
    {
        Console.WriteLine("静态私有方法执行。");
    }
}

通过反射获取私有成员,常规类使用是不能访问私有成员的。唯一的区别:使用了flags参数。

//实例化一个对象
MyClass myClass = new MyClass();
//无法点出任何一个私有成员
//使用反射
Type myType = myClass.GetType();    //获取这个类型的type

BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;    //关键参数flags:实例+私有成员

//获取私有成员1:获取私有字段
FieldInfo fieldInfo = myType.GetField("myField",flags);
fieldInfo.SetValue(myClass, "AI先锋");
Console.WriteLine(fieldInfo.GetValue(myClass));

//获取私有成员2:获取私有属性
PropertyInfo propertyInfo = myType.GetProperty("myProperty", flags);
Console.WriteLine(propertyInfo.GetValue(myClass));
propertyInfo.SetValue(myClass, "新的私有属性");
Console.WriteLine(propertyInfo.GetValue(myClass));

//获取私有成员3:获取私有方法
MethodInfo method = myType.GetMethod("FunA", flags);
method.Invoke(myClass,null);

如何使用静态的私有成员:

 ///
 ///使用静态的私有成员
 ///使用静态的私有成员
///
BindingFlags flags2 = BindingFlags.Static | BindingFlags.NonPublic; //依然是使用flags参数,但是设置了Static

FieldInfo fieldInfo2 = myType.GetField("sField", flags2);
Console.WriteLine(fieldInfo2.GetValue(null));

Type myType2 = myClass.GetType();
MethodInfo method2 = myType2.GetMethod("FunB", flags2);
method.Invoke(myClass,null);
method.Invoke(null,null); //调用静态的时,Invoke第一个参数可以设置为null,【因为静态方法属于类,不属于对象】

类外部使用私有成员,打破了类的封装性,可能导致代码的不稳定,一般不推荐使用反射使用私有成员。使用场景:1.调试代码 2.测试代码 3.

热拔插DLL动态加载类库 使用接口与反射制作插件程序【动态加载插件】,在程序允许过程中加载

a:实现效果:将插件的DLL拖入到之前设定的插件目录中,会识别到拖入的DLL并可进行运行,如果将拖入的DLL拖走,则界面上的加载会消失。
b:原理解析:
1.文件夹监视
文件夹中文件额变化会通过事件的方式进行通知
2.反射创建插件
程序获取到事件之后通过反射来创建DLL中的类型,创建接口
3.接口调用插件
主程序通过接口调用功能
c:代码讲解
代码模块:
插件接口:定义插件必须实现的元素
插件管理:创建文件夹监视,创建插件list列表:加载与卸载插件,与执行插件功能。
插件实现:编写业务功能代码,通过实现插件接口来调用
插件使用:通过插件管理类事件,感知插件,通过事件方式调用插件接口功能

代码实现:
插件接口项目:PluginBase
PluginBase:IPlugin.cs

namespace PluginBase
{
    public interface IPlugin:IDisposable
    {
        //接口
        Guid Guid { get; }      //用来标识插件
        string Menu { get; }        //分类,插件的分类
        string Name { get; }        //功能名称
        void Execute();         //功能代码
        void Load();        //加载:构造方法的简洁性,可以将一部分初始化逻辑放到load中
        void Dispose();     //1.实现此接口,一些非托管资源(如文件句柄,数据库连接,网络连接等)需要手动释放;
                            //2.关闭文件,取消订阅事件,释放定时器,dispose提供了统一的位置
    }
}

PluginManager.cs

在这里插入代码片

使用定义接口PluginBase的项目:ConsoleApp_TestDLL:Test.cs

namespace ConsoleApp_TestDLL
{
    public class Test : IPlugin
    {
        public Guid Guid =>  new Guid("2C44977A-76C1-431B-AF42-55BB400FC68F");  //如果有多个的话,可以使用静态的类来存储这个GUID

        public string Menu => "测试";

        public string Name => "Test";

        public void Dispose()
        {
            //为空,没有需要释放的东西
        }

        public void Execute()
        {
            Console.WriteLine("插件方法已经执行");
        }

        public void Load()
        {
            //为每个插件提供自定义的初始化方法
            //有些插件需要创建实例后初始化方法才能正常工作
        }
    }
}

转回去看插件项目中的管理类:PluginBase:PluginManager.cs【Manager整体框架】

namespace PluginBase
{
    public class PluginManager
    {
        private readonly string _pluginPath;    //插件管理类中的目录【监控的文件夹】
        private FileSystemWatcher? _watcher;       //监控器
        private readonly List<IPlugin> _plugins= new();
        public List<IPlugin> Plugins { get => _plugins; }   //监控到的有的插件的列表
        public event Action? PluginUpdated;     //事件:当监控到插件有增加或者减少的话通知主程序更新
        public PluginManager(string pluginPath) {
            //设置插件文件夹
            _pluginPath = pluginPath;
            //开启文件夹监控
            StartWatching();
        }
        ~PluginManager(){ 
            //停止监控文件夹
            Stopwatching();
        }

        private void Stopwatching()
        {
            throw new NotImplementedException();
        }

        private void StartWatching()
        {
            throw new NotImplementedException();
        }

    }
}

【外部使用的接口】:加载插件


        //向外部提供接口:
        //加载文件夹所有插件
        public void LoadPlugins()
        {
            if(!Directory.Exists(_pluginPath))
                return;
            _plugins.Clear();

            //检索所有的dll文件并且单个打开
            Array.ForEach(Directory.GetFiles(_pluginPath, "*.dll", SearchOption.AllDirectories), file => LoadPlugin(file));
            
        }

        //加载单个文件
        private void LoadPlugin(string pluginPath)
        {
            Assembly pluginAssembly;
            try
            {
                pluginAssembly = Assembly.LoadFrom(pluginPath); //程序集Assembly
            }
            catch(Exception ex) 
            {
                Console.WriteLine(ex.Message+ "Error pluginPath:"+pluginPath);
                return ;
            }

            //获取所有实现了IPlugin接口的类【筛选】
            var pluginTypes = pluginAssembly.GetTypes()
                .Where(type => typeof(IPlugin).IsAssignableFrom(type));

            if(pluginTypes is null ) return ;

            foreach (var pluginType in pluginTypes)
            {
                try
                {
                    //创建插件实例,使用反射创建对象,并转化为IPlugin接口
                    var plugin = Activator.CreateInstance(pluginType) as IPlugin;
                    //执行插件特定初始化操作【再使用接口调用方法操作】
                    plugin?.Load();

                    //添加到插件列表
                    if (plugin != null)
                        _plugins.Add(plugin);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }

卸载插件

//卸载插件接口
public void UnLoadPlugins() {
    StopWatching();     //停止监控
    _plugins.ForEach(plugin => plugin.Dispose());   //调用每个插件中的Dispose方法,将非托管的资源释放

    _plugins.Clear(); //维护的插件清单清空

    PluginUpdated?.Invoke();    //再通过一个事件通知主程序
}

//卸载单个DLL文件的方法
public void UnloadPlugin(string pluginPath)
{
    Assembly? pluginAssembly;
    try
    {
        //使用Location获取dll地址,与参数比较得到内存中对应的程序集
        pluginAssembly = AppDomain.CurrentDomain.GetAssemblies()
            .FirstOrDefault(asm => asm.Location == pluginPath);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
        return ;
    }

    //获取所有实现了IPlugin接口的类
    var pluginTypes = pluginAssembly?.GetTypes()
        .Where(type => typeof(IPlugin).IsAssignableFrom(type));

    if (pluginTypes is null ) return ;

    foreach (var pluginType in pluginTypes)
    {
        foreach(var plugin in _plugins)
        {
            if(plugin.GetType() == pluginType)
                plugin.Dispose();       //掉用筛选出来的类型的Dispose方法
        }
        _plugins.RemoveAll(r => r.GetType() == pluginType); //在插件列表中移除接口
    }
}

开启监控:

private void StartWatching()
{
    //开启文件夹监控
    if (!Directory.Exists(_pluginPath))
    {
        return;
    }
    _watcher = new FileSystemWatcher(_pluginPath, "*.dll");
    _watcher.EnableRaisingEvents = true;
    _watcher.IncludeSubdirectories = true;
    _watcher.Created += Watcher_Created;    //绑定事件
    _watcher.Deleted += Watcher_Deleted;    //绑定事件
}

private void Watcher_Deleted(object sender, FileSystemEventArgs e)
{
    UnloadPlugin(e.FullPath);
    PluginUpdated?.Invoke();
}

private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    LoadPlugin(e.FullPath);
    PluginUpdated?.Invoke();
}

执行插件的方法:

public void ExecuteFunc((Guid,string,string) cmd)
{
    //实现接口的方法
    var Item = Plugins.Where(r =>
    r.Guid == cmd.Item1 &&
    r.Menu == cmd.Item2 &&
    r.Name == cmd.Item3
    ).FirstOrDefault();
}

主程序如何使用插件:主程序为

public partial class Form1 : Form
{
    //插件管理器
    private readonly PluginManager _pluginManager;
    public Form1()
    {
        InitializeComponent();

        //实例化插件管理器,设置插件文件夹
        _pluginManager = new PluginManager(
            Path.Combine(Application.StartupPath, "plugins"));
        //实时更新插件
        ......
    }
}

此一部分源码在:
源码1

源码2

源码3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值