勇哥注:
这个系列的贴子主要是为了培训用(专职自动化设备的C#软件工程师),因此例子的知识范围被限制在行业范围内。
C#基础知识在网上是最多的存在,不足主要下面几点:
1. 内容零碎,没有大纲来组织
2. 什么功能都有讲,就是没有按实际项目的常用程度来组织
3. PLC转上位机的人员,很难把PLC的编程思想转到C#编程上来,需要在知识点讲解上对此问题有点拔
勇哥的这套贴子会有大纲,主要特点是补足以上几点问题,另外本套贴子属于经验性质的圈点知识,不属于完全小白式的教学。
如果读者有好的意见,请留言反馈。
委托
C#的委托是一种特殊的类,它可以存储一个方法的引用, 这样在程序运行时,它可以调用这个方法。 它的作用就像是一个桥梁,可以将不同的类或函数连接起来。
(1)委托是保存方法的引用
(3)泛型委托
(5)搞懂List<T>的Find方法并模拟写一个自己的Find函数
(7)多播委托
(7.1)
(8)非UI线程中进行控件操作
(9)委托在多窗体间进行传值
(10)延伸阅读参考
(11)满足开闭原则,消除swith或者if语句
(1)委托是保存方法的引用
这一种用法,对于熟悉C语言的同学来说,委托就像是函数指针。
类:
namespace WindowsFormsApp2 { public delegate void DebugOutDelgate(string msg); public class Class委托1 { public static void OutMsg1(string msg) { Debug.WriteLine($"史强说:{msg}"); } public static void OutMsg2(string msg) { Debug.WriteLine($"罗辑说:{msg}"); } } public class Class委托1_1 { public static void OutMsg1(string msg) { Debug.WriteLine($"维德说:{msg}"); } } }
调用者:
DebugOutDelgate fun1; fun1 = Class委托1.OutMsg1; fun1("hello"); fun1= Class委托1.OutMsg2; fun1("hello"); fun1 = Class委托1_1.OutMsg1; fun1("hello");
同一个委托变量fun1被三次赋值,最后一次和前两次对象是不同的。
这说明了委托的一个特点:
委托只验证签名与调用方法签名是否一致,并不关心是什么对象上调用该方法,
也不管是静态方法,还是实例方法.
(2)委托是个类,它可以做为函数参数
上节说委托是保存方法的引用,这里其实就是回答了怎么使用这个方法的引用。
类:
public delegate string StrMethodDelegate(string msg); public class Class委托2 { public static string StringMethod(string msg,StrMethodDelegate method) { return method(msg); } public static string CovertUpper(string msg) { return msg.ToUpper(); } public static string CovertLow(string msg) { return msg.ToLower(); } public static string AddSymbol(string msg) { return $"\"{msg}\""; } }
调用者:
//三个需求 //1、将一个字符串数组中每个元素都转换成大写 //2、将一个字符串数组中每个元素都转换成小写 //3、将一个字符串数组中每个元素两边都加上 双引号 string[] names = { "abCDefG", "HIJKlmnOP", "QRsTuvW", "XyZ" }; foreach(var m in names) { fun1(Class委托2.StringMethod(m, Class委托2.CovertUpper)); fun1(Class委托2.StringMethod(m, Class委托2.CovertLow)); fun1(Class委托2.StringMethod(m, Class委托2.AddSymbol)); }
程序循环中,StringMethod函数的参数就是一个委托,凡是跟这个委托签名一样的函数都可以传到这个参数。
这就实现了一个函数的动作由它的委托参数来决定。这就大大增加了函数的调用灵活性。
输出结果:
带有多个参数的委托,怎么传参呢?
如下例子中,(1)这样的写法是错误的。
(2)(3)的写法是正确的。
即你需要在调用函数中把委托要用到的参数一并传过去就可以了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { delegate int CalDelgate(int a, int b); static void Main(string[] args) { //var res = calTwoData(11, 22, cal1(11, 22)); (1) var res1= calTwoData(11, 22, cal1); //(2) var res2 = calTwoData(33, 44, cal2); //(3) } static int calTwoData(int a,int b, CalDelgate method) { return method(a, b); } static int cal1(int a,int b) { return a + b; } static int cal2(int a, int b) { return a - b; } } }
(3)泛型委托
泛型委托的代码可以指定类型参数,就像实例化泛型类或调用泛型方法一样
public delegate void Del<T>(T item); public static void Notify(int i) { } Del<int> m1 = Notify;
在设计模式定义事件时,泛型委托特别有用,因为发件人参数可以为强类型,无需在它和 Object 之间强制转换。
因此,勇哥会在讲C# “事件”时再谈泛型委托。
下面给个实际例子
需求: 比较A、B两个值大小,大返回A,小返回B,相等返回缺省值。
注意A,B可以是任何数据类型。
类:
public delegate int CompDelegate<T>(T data1,T data2); public class Class委托4 { public static T IsMax<T>(T data1,T data2, CompDelegate<T> way) { var res = way(data1, data2); if (res > 0) return data1; else if (res < 0) return data2; else return default(T); } }
调用:
下面调用演示了IsMax()函数不变的情况下,泛型委托处理不同数据类型的情况。
var res2= Class委托4.IsMax<int>(50, 20, (int x1, int x2) => { if (x1 > x2) return 1; if (x1 < x2) return -1; return 0; }); var res3 = Class委托4.IsMax<string>("abc", "kkk", (string x1, string x2) => { /* 小于零 strA 在排序顺序中位于 strB 之前。 零 strA 与 strB 在排序顺序中出现的位置相同。 大于零 strA 在排序顺序中位于 strB 之后。 */ return String.Compare(x1, x2); });
(4) Func<T>和Action<T>泛型委托
Func<T>和Action<T>都是.NET Framework内置的泛型委托,也就是说你和它们的时候,少了用delegate关键字做函数签名的定义,所以使用上要方便一些。
1. Func<T>
简单的说,它就是有返回值的泛型委托。
Func<T1> (有返回值)------无参数类型,T1为返回值类型 Func<T1,T2>(有返回值)------T1为0-16个参数类型,T2为返回值类型 Func<T1,T2,T3>(有返回值)------T1和T2为0-16个参数类型,T3为返回值类型 也就是说 参数最后一位就是返回值类型(返回值的类型和Func输出参数类型相同) 例如下面的委托,有3个int参数,返回值为string类型 Func<int,int,int,string>
2. Action<T>
简单的说,它就是无返回值的泛型委托
例如下面的委托,有2个int参数,无返回值。 Action<int,int> Action<T>最多为16个参数,最少为0个参数,也就是说可以是无参数并且无返回值的委托(非泛型) 例如下面的委托:无参也无返回值。 Action m4 = () => { Debug.WriteLine("hello"); }; m4();
下面是一段演示代码:
Func<int> m1 = () => { Debug.WriteLine("Func output"); return 1; }; m1(); //无参无返回值委托 Action m4 = () => { Debug.WriteLine("hello"); }; m4(); Action<int,int> act1 = (int x,int y) => { Debug.WriteLine($"x={x},y={y}"); }; act1(11, 22);
(5) 搞懂List<T>的Find方法并模拟写一个自己的Find函数
委托可以做为函数的参数,知道了这个你就应该知道了List<T>.Find函数的签名是什么意思了,如下:
跳到方法签名处,可以看到参数match就是一个委托。
// // 摘要: // 搜索与指定谓词所定义的条件相匹配的元素,并返回整个 System.Collections.Generic.List`1 中的第一个匹配元素。 // // 参数: // match: // System.Predicate`1 委托,用于定义要搜索的元素的条件。 // // 返回结果: // 如果找到与指定谓词定义的条件匹配的第一个元素,则为该元素;否则为类型 T 的默认值。 // // 异常: // T:System.ArgumentNullException: // match 为 null。 public T Find(Predicate<T> match);
这个委托是:
public delegate bool Predicate<in T>(T obj);
此委托的签名是个包含一个T参数的函数,并且此函数返回值是bool。
由于它是系统定义的委托,所以你愿意的话也可以直接使用它。
调用Find方法,就是一个Lamda表达式就可以了,它创建了一个匿名方法。
var res = names.ToList().Find(s => s == "abCDefG");
我们试下自己弄一个方法,不用Lamda表达式的匿名方法。
public bool method1(string msg) { if (msg == "abCDefG") return true; return false; } var res = names.ToList().Find(method1);
效果是一样的。
现在勇哥写一个自己的Find方法。
下面的代码中有两个知识点:
1。 泛型委托
2。 扩展方法
(有了它,才可以List<t>.MyFind,否则MyFind就只能是独立的方法。
扩展方法看上去违反违反开放/封闭原则,但实际上在C#中扩展方法是Linq的基础,这是个有趣的话题)
public delegate bool PreFunDelegate<in T>(T obj); public static class Class委托3 { public static T MyFind<T>(this List<T> list, PreFunDelegate<T> way) { foreach(var m in list) { if (way(m)) return m; } throw new ArgumentException("match 为 null"); } }
调用者:
string[] names = { "abCDefG", "HIJKlmnOP", "QRsTuvW", "XyZ" }; //调用自己的find方法 var res= names.ToList().MyFind(s => s == "XyZ");
鼠标指向MyFind,看到的函数签名如下:
(6) 匿名函数,这里用于方便说明委托
匿名函数是“委托”之外的知识,因为在此处使用可以方便书写委托例子,因此这里顺便介绍一下。
匿名方法是没有名称只有主体的方法。
委托可以通过匿名方法调用,也可以通过命名方法调用,即,通过向委托对象传递方法参数。
private void button2_Click(object sender, EventArgs e) { //调用自己的find方法 res= names.ToList().MyFind(s => s == "XyZ"); //利用delegate关键字创建委托实例的 PreFunDelegate<string> method1 = delegate (string msg) { if (msg == "XyZ") return true; return false; }; //这也是委托实例的匿名方法,和上面区别是使用Lamda表达式 PreFunDelegate<string> method2 = (string msg) => { if (msg == "XyZ") return true; return false; }; //传入匿名方法,调用MyFind res = names.ToList().MyFind(method2); //传入使用命名方法实例化的委托 res = names.ToList().MyFind(new PreFunDelegate<string>(PreFun3)); } public bool PreFun3(string msg) { if (msg == "XyZ") return true; return false; }
(7)多播委托
委托对象的一个有用属性在于可通过使用 + 运算符将多个对象分配到一个委托实例。
多播委托包含已分配委托列表。 此多播委托被调用时会依次调用列表中的委托。
例如:
public delegate void DelTest(); DelTest del = T1; del += T2; del += T3; del+= T4; del -= T3;
看到此处,你是不是发现委托跟事件是不是很像?
没错,事件是基于委托的。
测试代码:
delegate void CustomDel(string s); class Class委托5 { static void Hello(string s) { Console.WriteLine($" Hello, {s}!"); } static void Goodbye(string s) { Console.WriteLine($" Goodbye, {s}!"); } public static void test() { CustomDel hiDel, byeDel, multiDel, multiMinusHiDel; hiDel = Hello; byeDel = Goodbye; multiDel = hiDel + byeDel; multiMinusHiDel = multiDel - hiDel; Console.WriteLine("hiDel:"); hiDel("A"); Console.WriteLine("byeDel:"); byeDel("B"); Console.WriteLine("multiDel:"); multiDel($"C,{multiDel.GetInvocationList().Count()}"); Console.WriteLine("multiMinusHiDel:"); multiMinusHiDel("D"); } }
输出结果:
hiDel: Hello, A! byeDel: Goodbye, B! multiDel: Hello, C,2! Goodbye, C,2! multiMinusHiDel: Goodbye, D!
注意看multiDel的结果,勇哥通过multiDel.GetInvocationList() 方法返回委托数组,并取得数量2。
当一个委托变量多播的时候,此方法可以获取到委托列表(也就是委托数组)。
其实多播的效果就是系统会自动遍历这个委托数组。
勇哥注:多播委托是非常灵活的一种能力,没有它就没有事件。
下面是勇哥总结的多播委托的8种应用:
另一个问题:
委托多播的方法回调,是按顺序进行的,还是并发的?
委托多播的方法回调是按顺序进行的。每个委托都包含对下一个委托的引用,形成一个链式结构。 当调用委托时,它会按顺序调用链中的每个方法。因此,方法回调是按照添加委托的顺序依次执行的。
(8)非UI线程中进行控件操作
注意:此话题重要程度A+++,必修项!
在C#中,在UI线程中,可以直接操作控件。但是在非UI线程中,直接操作控件会报下图所示的错误:
即经典的“线程间操作无效:从不是创建控件richTextBox的线程访问它”
使用委托可以解决这个问题。
下面例子说的是:
1。 文本框如无字符串1234,则richtext中输出不同的内容
2。 类“Class委托6”的MainLogic()方法被放在线程中调用,目的是让它工作在非UI线程中
3。 两个委托控件操作的方法,一个是读textbox,一个是写rictextbox,他们的函数的返回值与参数都一样,这样做是为了共有同一个委托。
类:
public delegate string RtbDispDelegate(string text); //③ public class Class委托6 { RtbDispDelegate[] _method; public Class委托6(RtbDispDelegate[] m) //④ { _method = m; } public void MainLogic() { //主逻辑处理代码 //显示输出 DisRtbMsg("msg1..."); if (GetTxt() == "1314") DisRtbMsg("1314..."); else DisRtbMsg("\nmsg2..."); } private void DisRtbMsg(string data) { _method[0](data); } private string GetTxt() { return _method[1](""); } }
调用者:
private void button2_Click(object sender, EventArgs e) { var uiTheadId = Thread.CurrentThread.ManagedThreadId; //① //这里演示解决跨线程调用UI的问题 Task.Factory.StartNew(() => { new Class委托6(new RtbDispDelegate[] { RtbDispMethod1, TxtGetStringMethod1 }).MainLogic(); }); } public string TxtGetStringMethod1(string para) { if(this.InvokeRequired) //② { Func<string,string> fun = TxtGetStringMethod1; return (string)this.textBox1.Invoke(fun,new object[] { "" }); } else { return this.textBox1.Text; } } public string RtbDispMethod1(string msg) { var CurrentTheadId = Thread.CurrentThread.ManagedThreadId; //① if (this.InvokeRequired) //② { Func<string,string> act = RtbDispMethod1; this.richTextBox1.Invoke(act,new object[] { msg }); return ""; } else { this.richTextBox1.AppendText(msg); return ""; } }
程序运行效果:
如果文本框内容是"1314",则左边的RichText显示的内容不同。
详细解析一下代码:
1、什么是UI线程和非UI线程?
在注释①中,uiTheadId 值是1,CurrentTheadId是3,证明这两个地方不是一个线程中。
代码是在线程ID为3的线程中,企图输出信息到RichtextBox,以及读取TextBox的值。
而创建这两个控件的线程是1号ID的线程,它被我们称为UI线程。
在C#中,规定了不可以在UI线程以外的线程访问控件。
2、InvokeRequired是什么鬼?
注释②的InvokeRequired的作用如下:
如果其值为true,那么就需要执行控件的Invoke方法,否则可以直接进行控件操作。
// // 摘要: // 获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法, 因为调用方位于创建控件所在的线程以外的线程中。 // // 返回结果: // 如果控件的 true 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用), 则为 System.Windows.Forms.Control.Handle;否则为 // false。
3、Invoke()方法?
C#中,控件和委托都有Invoke()方法。
控件的Invoke()方法的目的就是解决非UI线程操作控件的问题。它的签名如下图所示:
而委托的Invoke()方法没啥子意义,和直接调用委托对象是一样的,如下:
//所有的委托类型,编译器都会自动生成一个invoke 方法. Action<string> x=Console.WriteLine x("2"); x.Invoke("2");
但是事件的Invoke方法却是我们常用的,如:
PropertyChanged?.Invoke(this, new TextboxPropertyChangedEventArgs(value));
4、为啥定义注释③那样的委托?
委托RtbDispDelegate即有参数,也有返回值。
但是读控件读的函数应该是没有参数只有返回值 ,而控件写的函数应该没有返回值,只有写内容的参数。
勇哥是希望这个委托即可以写控件,也可以读控件,为了兼容两个动作,所以写了这个特殊的委托。
因此在注释④处,你可以看到传入的是委托数组,调用者在数组的0元素传入写控件的委托,1元素再传入读控件的委托。
(9)委托在多窗体间进行传值
这种需求指的是:窗体B上做个操作,另一个窗体A上会进行互动显示。
看到此处的读者应该很快能想到,只需要把A窗体的委托方法传到窗体B的属性或者构造函数中去就可以了。
演示代码略。。。
还是放上代码吧,方便大家验证下想法。
窗体2代码:
public partial class Form2 : Form { public Action<string> Act { get; set; } = null; public Form2() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { if(Act!=null && textBox1.Text.Length>0) { Act(textBox1.Text); } } }
调用者:
public void SetTxtString(string data) { if(this.InvokeRequired) { Action<string> act = SetTxtString; this.textBox1.Invoke(act, new object[] { data }); } else { this.textBox1.Text = data; } } //这里演示用委托进行多窗体传值 Form2 win2 = new Form2(); win2.Act = SetTxtString; win2.StartPosition = FormStartPosition.CenterParent; win2.ShowDialog();
效果如下:
fomr2弹出后,按确定,其文本框内容会回显到窗体1的文本框中。
(10)延伸阅读参考
C#中只使用Invokerequired来判断是不是UI线程可靠吗?
C# 勇哥关于winform.Show() ,winform.ShowDialog() 窗体卡死、显示阻塞、无法置顶问题的研究
C# 勇哥关于多线程读写plc内存的研究续,解决UI控件读写的效率问题
C# 界面定时器控件Timer的回调函数中有问题代码影响ui刷新效率的问题
创建窗口句柄之前,不能在控件上调用Invoke或BeginInvoke
C# 委托的实验代码
C#中Invoke的用法
演示代码下载:
(11)满足开闭原则,消除swith或者if语句
勇哥注:此部分为2023/12/13号新增
例子:
不同语种的问候
语种有:英语、汉语、俄语
问候用户: 小德
hello, 小德 //英语
你好,小德 //汉语
aaaa, 小德 //俄语
先用开闭原则来写:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication25 { public enum yzEnum { 英语,汉语,俄语 } class Program { static void Main(string[] args) { ISay say; say = new 说汉语(); Console.WriteLine(say.SayHello("小张")); say = new 说英语(); Console.WriteLine(say.SayHello("小张")); Console.ReadKey(); } } public interface ISay { string SayHello(yzEnum type,string userName); } public class 说汉语 : ISay { public string SayHello(yzEnum type, string userName) { if(type== yzEnum.汉语) return $"你好,{userName}"; return ""; } } public class 说英语 : ISay { public string SayHello(yzEnum type, string userName) { if (type == yzEnum.英语) return $"hello,{userName}"; return ""; } } public class 说俄语 : ISay { public string SayHello(yzEnum type, string userName) { if (type == yzEnum.俄语) return $"abdke,{userName}"; return ""; } } }
但是开闭原则写的话,会有类爆炸的可能,因为语言可能被要求要支持几百种,这样你的类就要相应的变成几百个。
下面使用委托来实现:
我利用委托的多播功能实现了支持几百种语言的要求。
你只需要把下面的例子中的类换成函数就可以了,这样就避免了类爆炸。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication25 { public enum yzEnum { 英语,汉语,俄语 } class Program { public delegate string SayHelloDelegate(yzEnum type,string userName); static void Main(string[] args) { SayHelloDelegate say; say =new 说汉语().SayHello; say += new 说英语().SayHello; say += new 说俄语().SayHello; var str=sel(say, yzEnum.英语, "小德"); Console.WriteLine(str); Console.ReadKey(); } private static string sel(SayHelloDelegate del, yzEnum type,string username) { foreach (var m in del.GetInvocationList()) { var res = (m as SayHelloDelegate).Invoke(type, username); if (res.Length > 0) return res; } return ""; } } public interface ISay { string SayHello(yzEnum type,string userName); } public class 说汉语 : ISay { public string SayHello(yzEnum type, string userName) { if(type== yzEnum.汉语) return $"你好,{userName}"; return ""; } } public class 说英语 : ISay { public string SayHello(yzEnum type, string userName) { if (type == yzEnum.英语) return $"hello,{userName}"; return ""; } } public class 说俄语 : ISay { public string SayHello(yzEnum type, string userName) { if (type == yzEnum.俄语) return $"abdke,{userName}"; return ""; } } }
勇哥这个例子的灵感来于知乎上的一张截图,它讨论委托的意义。
原文对于委托的总结有一部分超乎我的意料之外,例如作者认为委托主要解决程序的可扩展性和满足开闭原则。
这两方面在上面的例子里都有体现。
然而这方面勇哥认为只是多播委托的扩展应用,委托最重要最重要的应用,还是一座桥梁,把不同的类与函数连接起来。
(2023.12.16 勇哥注 我收回上面的话,正如下图作者谈到的,委托确实在高层上是解耦、满足开闭原则、反转思想等面向对象原则的工具,先前对委托的理解还是过于肤浅,惭愧!)
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

