勇哥的C#知识经验圈点:委托

勇哥注:

这个系列的贴子主要是为了培训用(专职自动化设备的C#软件工程师),因此例子的知识范围被限制在行业范围内。

C#基础知识在网上是最多的存在,不足主要下面几点:

1. 内容零碎,没有大纲来组织
2. 什么功能都有讲,就是没有按实际项目的常用程度来组织
3. PLC转上位机的人员,很难把PLC的编程思想转到C#编程上来,需要在知识点讲解上对此问题有点拔

勇哥的这套贴子会有大纲,主要特点是补足以上几点问题,另外本套贴子属于经验性质的圈点知识,不属于完全小白式的教学。

如果读者有好的意见,请留言反馈。



委托


 C#的委托是一种特殊的类,它可以存储一个方法的引用,
 这样在程序运行时,它可以调用这个方法。
 它的作用就像是一个桥梁,可以将不同的类或函数连接起来。


(1)委托是保存方法的引用

(2)委托是个类,它可以做为函数参数

(3)泛型委托

(4)Func<T>和Action<T>泛型委托

(5)搞懂List<T>的Find方法并模拟写一个自己的Find函数

(6)匿名函数,这里用于方便说明委托

(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被三次赋值,最后一次和前两次对象是不同的。

这说明了委托的一个特点:

委托只验证签名与调用方法签名是否一致,并不关心是什么对象上调用该方法,

也不管是静态方法,还是实例方法.

image.png


(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函数的参数就是一个委托,凡是跟这个委托签名一样的函数都可以传到这个参数。

这就实现了一个函数的动作由它的委托参数来决定。这就大大增加了函数的调用灵活性。


输出结果:

image.png


带有多个参数的委托,怎么传参呢?


如下例子中,(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函数的签名是什么意思了,如下:

image.png

跳到方法签名处,可以看到参数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,看到的函数签名如下:

image.png


(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种应用:


C# 的多播委托的8种应用


另一个问题:

委托多播的方法回调,是按顺序进行的,还是并发的?

委托多播的方法回调是按顺序进行的。每个委托都包含对下一个委托的引用,形成一个链式结构。
当调用委托时,它会按顺序调用链中的每个方法。因此,方法回调是按照添加委托的顺序依次执行的。



(8)非UI线程中进行控件操作

注意:此话题重要程度A+++,必修项!

在C#中,在UI线程中,可以直接操作控件。但是在非UI线程中,直接操作控件会报下图所示的错误:

即经典的“线程间操作无效:从不是创建控件richTextBox的线程访问它

image.png

使用委托可以解决这个问题。


下面例子说的是:

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线程操作控件的问题。它的签名如下图所示:

image.png

而委托的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


演示代码下载:

支付5元或购买VIP会员后,才能查看本内容!立即支付升级会员查询订单


(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 勇哥注  我收回上面的话,正如下图作者谈到的,委托确实在高层上是解耦、满足开闭原则、反转思想等面向对象原则的工具,先前对委托的理解还是过于肤浅,惭愧!)

image.png


--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:
本帖最后由 勇哥,很想停止 于 2023-02-25 15:24:48 编辑

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年5月    »
12345
6789101112
13141516171819
20212223242526
2728293031
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864