哔哩哔哩视频:【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.二进制转2,8,16进制
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