代码改变世界

我眼中委托的真正面貌(二)

2009-05-07 15:34  独孤残云  阅读(1953)  评论(10编辑  收藏  举报

在首页发表第一篇随笔之后,得到了很多朋友的支持,也增强了我的自信心,再次对大家表示感谢。呵呵……

 

对于上一篇随笔,不少朋友留下了很不错的见解,也有不少朋友提出了很有代表性的问题。所以,在正文开始之前,我想先就这些问题阐述一下自己的观点,也请朋友们给予批评指正:

首先来说,feiyang朋友提出了有关委托与函数指针间的连带关系。我很赞同这样的说法。

其实,对C++有所了解的朋友都会知道:一个类内部所包含的方法,其实在类内部仅仅表现为一个函数指针,其实现部分并未占用类的内存空间。假如我们在一个类内部声明了一个委托实例对象,同时为它挂载了其他类内部的一个方法(当然,这个方法必须是共有型的),这样,在本类内部就同样拥有了这个方法的函数指针(可以理解为它被存储在委托对象的内部)——这就相当于在本类内部声明了一个一模一样的方法。

而事实上,假如我们通过已声明的委托对象来调用相应的方法,C#的编译器将自动将其作为本类的声明方法来对待(从C#编译器的安全角度来论证,这个说法是行得通的)。其实目标委托对象并非真正意义上的类方法,而仅仅只是作为一个目标方法的代理方法而存在,这便是委托(代理)一词的由来。

另外feiyang兄也提到了有关委托与接口回调机制的区别与联系问题,我已经在上一篇随笔中给予了回复,其中谈到了有关接口回调方法所存在的一些限制。在这里,我们不妨尝试一下,使用接口回调是否可以实现C#的事件机制:

52)使用接口回调仿真控件的事件机制(尝试)

首先,我们建立一个控件项目,代码如下:

namespace InfceCalbckEvCtrl

{

    public partial class UserControl1 : UserControl

    {

        //声明接口类型

        public interface ICallbackEvn

        {

            void ShowObjTxt(string Txt);

        }

 

        //定义(但不实例化)接口对象

        public ICallbackEvn ObjCallEvn;

 

        public UserControl1()

        {

            InitializeComponent();

        }

 

        private void button1_Click(object sender, EventArgs e)

        {

            ObjCallEvn.ShowObjTxt("接口回调仿真事件调用成功!");

        }

    }

}

 

而后,我们同样构建一个上层的Demo程序来调用它:

 

namespace InCallDemo

{

    public partial class Form1 : Form, InfceCalbckEvCtrl.UserControl1.ICallbackEvn

    {

        public Form1()

        {

            InitializeComponent();

        }

 

        private void Form1_Load(object sender, EventArgs e)

        {

            //接口对象实例化  

            userControl11.ObjCallEvn = new Form1();

        }

 

        public void ShowObjTxt(string Txt)

        {

            MessageBox.Show(Txt);

        }

    }

}

 

需要大家留心的地方我已经用红笔标明了:

1> 首先Form1类不但要继承自Form类,同时还必须继承InfceCalbckEvCtrl.UserControl1控件类中的ICallbackEvn接口——这就是所谓的继承关系的限制。

2> 同时也只能是利用接口来默认挂载本类中被命名为void ShowObjTxt(string Txt)的方法,如果大家仍然想为目标接口挂载另外一个方法(当然这个方法不能再以void ShowObjTxt(string Txt)来命名了)来实现其他的功能,那么对不起,请您重新定义原有的接口好了——这就是所谓方法同名的限制(其实这里的限制不单单只有同名,还包括了同方法数量的限制)。

同时,我稍稍查阅其它网友有关回调所贴的一些帖子,个人感觉使用委托的人数比使用接口来完成回调的人数要多。

以上是我的观点,欢迎大家给予批评指针,也请大家尽量发表自己的看法,谢谢。呵呵……

 

以下是有关委托的一些稍稍复杂的用法举例:

6.使用委托数组

namespace ObjFunDelegate
{
    
delegate double CompuFun(double parama,double paramb);

    
class Program
    {
        
static void Main(string[] args)
        {
                     
//声明委托数组
            CompuFun[] computefun = new CompuFun[2];

                     
//为数组中的多个对象分别挂载不同的方法
            computefun[0= new CompuFun(DeleMathFuns.AddFun);

            computefun[
1= new CompuFun(DeleMathFuns.MultiplyFun);

                     
//通过下标分别调用不同委托对象中的不同方法
            for (int i = 0; i < 2; i++)

            {
                Console.WriteLine(
"调用方法[{0}]:", i);
                Console.WriteLine(
"所得结果:{0}", computefun[i](55));
            }
            Console.ReadLine();
        }
    }

    
class DeleMathFuns
    {
        
public static double AddFun(double a, double b)
        {
            
return a + b;
        }

        
public static double MultiplyFun(double a, double b)
        {
            
return a * b;
        }
    }
}

 

 

这段代码可以将委托的对象性质以及方法性质兼备(实际上我们不妨先将委托对象理解为独立的函数指针(集合)对象)的特性淋漓尽致的表现出来:委托对象中挂载的方法我们可以随时随地方便的调用,同时我们又可以按照普通数组的管理方法来管理这多个委托对象。使我们的程序有条不紊的运行。

不过在C#中,多个方法可不一定非得要对应多个委托对象呢。呵呵……

7.多路广播委托

namespace ObjFunDelegate
{
    
delegate void CompuFun(double parama,double paramb);

    
class Program
    {
        
static void Main(string[] args)
        {
            CompuFun computefun;

            computefun 
= new CompuFun(DeleMathFuns.AddFun);

            computefun 
+= new CompuFun(DeleMathFuns.MultiplyFun);

            computefun(
55);

            Console.ReadLine();
        }
    }

    
class DeleMathFuns
    {
        
public static void AddFun(double a, double b)
        {
            Console.WriteLine(
"AddFun方法调用结果为{0}", a + b);
        }

        
public static void MultiplyFun(double a, double b)
        {
            Console.WriteLine(
"MultiplyFun方法调用结果为{0}", a * b);
        }
    }
}

 

 

这段代码在第6段代码的基础上发生了小小的改动:为了说明问题方便我将委托的返回临行改为void,同时在DeleMathFuns类相应的方法中直接输出结果。比之第6段代码你能发现什么问题吗?没错,现在是多个方法挂接到同一个委托对象上了。在委托对象被调用时,挂接于委托对象之上的方法会按照多个方法挂接的先后顺序依次对其执行。这便是所谓的多路广播委托。

在这里我要强调一个问题,可用于多路广播委托的方法返回值必须是void型,这也是为什么我要在6的基础上对7的委托返回类型加以改动的原因。道理很简单,如果各个方法含有返回值,现在将其捆在一个委托对象之上,那么在调用时返回值要送给谁呢。呵呵……

我们从C#中为什么会引入委托开始探讨,到这里C#中委托的基本用法已经阐述完毕,下面,我们可以结合C#的一些高级论题对委托的功能进行一下更深层次的挖掘。

8.委托的跨线程操控

在此,我以人们常说的跨线程操控控件为例。首先,如果你已经掌握了C#多线程的基本用法,不妨自己试着实现一下这段程序。在未掌握委托之前,我曾经这样写过:

using System.Threading;

namespace MulTrdDelegate
{
    
public partial class Form1 : Form
    {
        
public Form1()
        {
            InitializeComponent();
        }

        
//初始化子线程对象
        private Thread demoThread = null;

        
private void button1_Click(object sender, EventArgs e)
        {
            demoThread 
= new Thread(new ThreadStart(ThreadProcUnsafe));

            demoThread.Start();
        }

        
public void ThreadProcUnsafe()
        {
            textBox1.Text 
= "这个控件的内容是由子线程实现的";
        }
    }
}

 

 

确定子线程创建无误,编译也通过了。但是,运行时却出现了这样的错误:

 多线程错误

原来,C#中是不支持跨线程操控控件的。不过,出于安全考虑,这样做也是合情合理,这样可以避免多个线程同时操控一个控件所带来的程序运行错误。那么,如何来实现这样的效果呢?

方法一:将 Control.CheckForIllegalCrossThreadCalls 属性设置为 false。你可以尝试给窗体添加Form_Load()事件,然后在事件函数体中对这个属性进行设置。

编译就会发现,程序已经可以顺利执行了。这个方法比较简单,但始终不太安全。所以我们不推荐这样的用法。

方法二:用委托来实现

using System.Threading;

namespace MulTrdDelegate
{
    
public partial class Form1 : Form
    {
        
public Form1()
        {
            InitializeComponent();
        }

        
//定义委托类型
        public delegate void TreadDelgate();

        
//初始化子线程对象
        private Thread demoThread = null;

        
private void button1_Click(object sender, EventArgs e)
        {
            demoThread 
= new Thread(new ThreadStart(ThreadProcUnsafe));

            demoThread.Start();
        }

        
public void ThreadProcUnsafe()
        {
            SetText();
        }
 
        
private void SetText()
        {
            
string text = "这个控件的内容是由子线程实现的";

            
if (this.textBox1.InvokeRequired)
            {
                TreadDelgate Objdelegate 
= new TreadDelgate(SetText);

                
this.Invoke(Objdelegate, new object[] {});
            }
            
else
            {
                
this.textBox1.Text = text;
            }
        }
    }
}

 

编译运行,你会发现程序顺利的通过了。对比上一段代码,应该很容易发现,这一段代码在上一段代码的基础上新增了委托的定义,再就是关键的SetText()方法。

那么,大家不妨来分析一下这个方法的代码内容:

TreadDelgate Objdelegate = new TreadDelgate(SetText);  这一句当然是声明委托对象实例。

比较难理解的可能是 this.textBox1.InvokeRequired ,当目标控件textBox1将要被其他的子线程操控时,其相应的InvokeRequired 属性值即为真。

另外一句this.Invoke(Objdelegate, new object[] {}); 查一下MSDN文档,它的意思是:在拥有控件的基础窗口句柄线程上,用指定的参数列表执行指定的委托。

看明白意思了吧,呵呵…… 这几句代码所实现得功效贯穿下来就可以描述为——如果目标控件textBox1 将要被其他线程操控,就自动创建主线程的委托,并用它挂载SetText()方法,然后以子线程控制主线程端以委托执行相应的方法。

(未完待续)