勇哥注:
这个系列的贴子主要是为了培训用(专职自动化设备的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线程可靠吗?
http://www.skcircle.com/?id=1978
C# 勇哥关于winform.Show() ,winform.ShowDialog() 窗体卡死、显示阻塞、无法置顶问题的研究
http://www.skcircle.com/?id=1977
C# 勇哥关于多线程读写plc内存的研究续,解决UI控件读写的效率问题
http://www.skcircle.com/?id=1985
C# 界面定时器控件Timer的回调函数中有问题代码影响ui刷新效率的问题
http://www.skcircle.com/?id=1993
创建窗口句柄之前,不能在控件上调用Invoke或BeginInvoke
http://www.skcircle.com/?id=635
C# 委托的实验代码
http://www.skcircle.com/?id=461
C#中Invoke的用法
http://www.skcircle.com/?id=131
演示代码下载:
(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
版权声明:本文为博主原创文章,转载请附上博文链接!


少有人走的路



















