.Net学习难点讨论系列4 – .Net委托类型

不像Windows API中使用C语言风格的函数指针这种不安全的方式进行回调。.Net中此功能使用使用更为安全和面向对象的委托(delegate)来完成。委托是一个类型安全的对象,它指向程序中另一个以后会被调用的方法(或多个方法)。

委托类型包含3个重要信息:

  • 它所调用的方法的名称
  • 该方法的参数(可选)
  • 该方法的返回值(可选)

当上述信息被提供后,委托可以在运行时动态调用其指向的方法。很重要的一点:.Net中每个委托都被自动赋予同步或异步访问方法的能力。

定义委托

在C#中使用delegate关键字创建一个委托。我们称这种类为委托类。委托类的实例成为委托对象。从概念上说,委托对象是一种指向一个或多个方法(静态或非静态)的引用。要求是此委托匹配它指向的方法的签名。

如下委托可以指向一任何传入两个整数返回一个整数的方法。

public   delegate   int  BinaryOp( int  x,  int  y); 

 

  • 定义委托后,系统生成一个派生自MulticastDelegate类的密封类。此类中有3个方法:
  • Invoke()方法,用来以同步方式调用委托维护的每个方法。(不能在C#中显示调用此方法,Invoke()在后台被调用)
  • BeginInvoke()与EndInvoke()方法在第二个线程上异步调用当前方法。

开发人员创建第二个执行线程的原因调用比较耗时的方法。(相当于委托顺带实现了一些System.Threading命名空间管理的线程问题)

这个委托的密封类大概如下:

sealed   class  BinaryOp : System.MulticastDelegate
{
      
public  BinaryOp( object  target,  uint  functionAddress);
      
public   int  Invoke( int  x,  int  y);
      
public  IAsyncResult BeginInvoke( int  x,  int  y, AsyncCallback cb,  object  state);
      
public   int  EndInvoke(IAsyncResult result);
}

 

下面给一个简单的委托示例:

      //  这个委托指向任何一个传入两个整数并返回一个整数的方法
     public   delegate   int  BinaryOp( int  x,  int  y);

    
#region  SimpleMath class
    
public   class  SimpleMath
    {
        
public   int  Add( int  x,  int  y)
        { 
return  x  +  y; }
        
public   int  Subtract( int  x,  int  y)
        { 
return  x  -  y; }
        
public   static   int  SquareNumber( int  a)
        { 
return  a  *  a; }
    }
    
    
#endregion
    
    
class  Program
    {
        
static   void  Main( string [] args)
        {
            Console.WriteLine(
" ***** Simple Delegate Example *****/n " );
            
//  创建一个指向SimpleMath.Add()方法的BinaryOp对象
            SimpleMath m  =   new  SimpleMath();
            BinaryOp b 
=   new  BinaryOp(m.Add);

            
//  使用委托调用调用Add()方法
            
//  此处也是Invoke()被调用的位置
            Console.WriteLine( " /n10 + 10 is {0} " , b( 10 10 ));
            Console.ReadLine();
        }
    }

 

委托类型安全的体现

如果传入一个与委托声明不匹配的方法,将在编译时报错。如上例中如果传入int SquareNumber(int),将会导致一个编译时错误。

获取委托中调用函数列表的方法,示例:

假设有名为delObj的委托对象,使用如下方式得到调用函数的信息

    Delegate d  in  delObj.GetInvocationList()
    Console.WriteLine(
" Method Name: {0} " , d.Method);
    Console.WriteLine(
" Target Name: {0} " , d.Target);

 

Method 属性表示调用的函数的签名, Target 表示调用的函数所在的对象的类型名,所以如果委托调用的是一个静态方法则 Target 不会有任何显示,只有当委托调用的是一个实例方法时, Target 属性才有值。

更完整的委托应用(示例来自 C# .Net3.0 高级程序设计),代码:

汽车类:

 

using  System;
using  System.Collections.Generic;
using  System.Text;

namespace  CarDelegate
{
    
public   class  Car
    {
        
//  定义委托类型
         public   delegate   void  AboutToBlow( string  msg);
        
public   delegate   void  Exploded ( string  msg);

        
//  定义各自委托类型的对象
         private  AboutToBlow almostDeadList;
        
private  Exploded explodedList;

        
//  将成员添加到调用列表
         public   void  OnAboutToBlow(AboutToBlow clientMethod)
        { almostDeadList 
+=  clientMethod; }

        
public   void  OnExploded(Exploded clientMethod)
        { explodedList 
+=  clientMethod; }

        
//  由调用列表移除方法
         public   void  RemoveAboutToBlow(AboutToBlow clientMethod)
        { almostDeadList 
-=  clientMethod; }

        
public   void  RemoveExploded(Exploded clientMethod)
        { explodedList 
-=  clientMethod; }

        
//  内部状态成员
         private   int  currSpeed;
        
private   int  maxSpeed;
        
private   string  petName;

        
//  汽车坏了吗?
         bool  carIsDead;

        
public  Car()
        {
            maxSpeed 
=   100 ;
        }

        
public  Car( string  name,  int  max,  int  curr)
        {
            currSpeed 
=  curr;
            maxSpeed 
=  max;
            petName 
=  name;
        }

        
public   void  SpeedUp( int  delta)
        {
            
//  如果汽车坏了,触发Exploded事件
             if  (carIsDead)
            {
                
if  (explodedList  !=   null )
                    explodedList(
" Sorry, this car is dead " );
            }
            
else
            {
                currSpeed 
+=  delta;

                
//  几乎要坏了?
                 if  ( 10   ==  maxSpeed  -  currSpeed
                    
&&  almostDeadList  !=   null )
                {
                    almostDeadList(
" Careful buddy!  Gonna blow! " );
                }

                
//  还好!
                 if  (currSpeed  >=  maxSpeed)
                    carIsDead 
=   true ;
                
else
                    Console.WriteLine(
" ->CurrSpeed = {0} " , currSpeed);
            }
        }
    }
}

 

主函数:

 

namespace  CarDelegate
{
    
class  Program
    {
        
static   void  Main( string [] args)
        {
            
//  制造一辆车
            Car c1  =   new  Car( " SlugBug " 100 10 );

            
//  注册事件处理函数          
            Car.Exploded d  =   new  Car.Exploded(CarExploded);
            c1.OnAboutToBlow(
new  Car.AboutToBlow(CarIsAlmostDoomed));
            c1.OnAboutToBlow(
new  Car.AboutToBlow(CarAboutToBlow));
            c1.OnExploded(d);

            
//  加速 (这将触发事件)
            Console.WriteLine( " /n***** 加速 ***** " );
            
for  ( int  i  =   0 ; i  <   6 ; i ++ )
                c1.SpeedUp(
20 );

            
//  由调用列表移除CarExploded方法
            c1.RemoveExploded(d);

            Console.WriteLine(
" /n***** 加速 ***** " );
            
for  ( int  i  =   0 ; i  <   6 ; i ++ )
                c1.SpeedUp(
20 );

            Console.ReadLine();
        }

        
public   static   void  CarAboutToBlow( string  msg)
        { Console.WriteLine(msg); }

        
public   static   void  CarIsAlmostDoomed( string  msg)
        { Console.WriteLine(
" Critical Message from Car: {0} " , msg); }

        
public   static   void  CarExploded( string  msg)
        { Console.WriteLine(msg); }
    }
}

 

对多路广播的支持

.Net委托内置多路,即一个委托可以维护一个可调用方法的列表而不只是单独一个方法,使用重载过的+=运算符可以向一个委托对象添加多个方法。关于对对路广播的支持可以参考上述示例。

在多路广播的支持中有一个需要注意的问题,一个委托调用的多个方法需要无参数且无返回值,因为在调用委托时,即使传入了参数也不知道具体应该传给哪 一个方法,即使这些方法有返回值也不知道该接受那个函数的返回值。所以说直接不要调用有参数及返回值的方法,这点与事件关联多个事件处理方法时对处理方法 签名的要求相同(可以参见本系列介绍事件的文章)。

注意:我们可以用调用方法的语法"调用"委托对象。这样会调用委托对象所引用的方法。 (事件的触发与委托的调用相同,本来事件就是一个委托类型的对象)。这些方法的调用是在调用委托的方法所在的线程中完成的。这种调用称同步调用。

C#2.0编译器的委托类推测功能

C#编译器引入了在创建委托变量时可以推测其类型的能力。这样就可以将一个方法赋给隐式创建的委托对象。

示例:

public   class  Program {
  
delegate   void  Deleg1();
  
delegate   string  Deleg2(  string  s );
  
static   void  f1() {
    System.Console.WriteLine(
" f1() called. " );
  }
  
static   string  f2( string  s) {
    
string  _s = string .Format(  " f2() called with the param / " { 0 }/ " . "  , s );
    System.Console.WriteLine( _s );
    
return  _s;
  }
  
public   static   void  Main() {
     Deleg1 d1 
=  f1;  //  代替 Deleg1 d1 = new Deleg1( f1 );
     d1();
     Deleg2 d2 
=  f2;  //  代替 Deleg2 d2 = new Deleg2( f2 );
      string  s  =  d2( " hello " );
  }
}

 

委托协变 (covariance)

允许创建一个委托,其返回的对象的类型是继承关系的,示例代码:

 

//  简单的集成关系的两个类
     class  Car
    {
        
public   override   string  ToString()
        {
            
return   " A stateless car " ;
        }
    }
    
class  SportsCar : Car
    {
        
public   override   string  ToString()
        {
            
return   " A stateless sports car " ;
        }
    } 
    
    
class  Program
    {
        
//  定义一个返回Car或SportsCar的委托
         public   delegate  Car ObtainVehicalDelegate();

        
//  委托指向目标
         public   static  Car GetBasicCar()
        { 
return   new  Car(); }

        
public   static  SportsCar GetSportsCar()
        { 
return   new  SportsCar(); }
        

        
static   void  Main( string [] args)
        {
            ObtainVehicalDelegate targetA 
=   new  ObtainVehicalDelegate(GetBasicCar);
            Car c 
=  targetA();
            Console.WriteLine(c);

            
//  协变允许指定这样的目标方法
            ObtainVehicalDelegate targetB  =   new  ObtainVehicalDelegate(GetSportsCar);
            SportsCar sc 
=  (SportsCar)targetB();
            Console.WriteLine(sc);
            Console.ReadLine();
        }
    }

 

委托逆变,其中参数具有集成关系,委 托签名的参数类型(派生类型)比方法具有的参数类型(基类型)更具体。定义一个参数类型是派生类型的委托,这个委托可以接收具有基类型参数的方法,因为派 生类型隐式转换成了基类型。注意此方法必须接收与委托签名相同的参数类型(派生类型),虽然方法的签名中参数是基类型。

接下来说一下委托在多线程程序中的应用,主角有两个:ThreadStart和ParameterizedThreadStart,它们都定义与System.Threading命名空间下。

使用这两个委托,你可以以编程方式创建此线程来分担一些任务,步骤如下:

  1. 创建一个方法作为新线程的入口点。
  2. 创建一个ParameterizedThreadStart(或ThreadStart)委托,并把之前所定义的方法传给委托的构造函数。
  3. 创建一个Thread对象,并把ParameterizedThreadStart或ThreadStart委托作为构造函数的参数。
  4. 建立任意初始化线程的特性(名称、优先级等)。
  5. 调用Thread.Start()方法。

完成上述步骤,在第2步建立的委托所指向的方法将在线程中尽快开始执行。

ThreadStart委托指向一个没有参数、无返回值的方法,它在调用一个被设计用来仅仅在后台运行、而没有更多的交互时非常有用。它的局限在于 无法给这个函数出入参数,所以在.Net2.0中出现了ParameterizedThreadStart了方法,它可以接受一个包含了任意个数的参数 (传给它要调用的方法的)的Object类型对象做参数(即允许用户为新线程要执行的方法传入一个对象作为参数)。但注意这两种委托指向的函数的返回值都 必须是void。

看看示例代码,首先是ThreadStart委托的:

public   class  Printer
    {
        
public   void  PrintNumbers()
        {
            
// 具体实现省略
        }
    }

    
class  Program
    {
        
static   void  Main( string [] args)
        {
            Printer p 
=   new  Printer();
            Thread bgroundThread 
=   new  Thread( new  ThreadStart(p.PrintNumbers));
           
            
//  控制此线程是否在后台运行?
            bgroundThread.IsBackground  =   true ;

            bgroundThread.Start();
        }
}

 

接下来的代码示例了ParameterizedThreadStart 委托的使用:

      // 包装参数的类
     class  AddParams
    {
        
public   int  a;
        
public   int  b;
        
public  AddParams( int  numb1,  int  numb2)
        {
            a 
=  numb1;
            b 
=  numb2;
        }
    }

    
class  Program
    {
        
static   void  Main( string [] args)
        {
            Console.WriteLine(
" 主线程ID:{0} " , Thread.CurrentThread.GetHashCode());

            
// 生成要传入的参数
            AddParams ap  =   new  AddParams( 10 10 );

            Thread t 
=   new  Thread( new  ParameterizedThreadStart(Add));
            t.Start(ap);
        }

        
// ParameterizedThreadStart委托调用的方法
        
// 使用AddParams类对象做参数
         public   static   void  Add( object  data)
        {
            
if  (data  is  AddParams)
            {
                Console.WriteLine(
" 后台线程ID: {0} " , Thread.CurrentThread.GetHashCode());

                AddParams ap 
=  (AddParams)data;
                Console.WriteLine(
" {0} + {1} is {2} " , ap.a, ap.b, ap.a  +  ap.b);
            }
        }
}

 

详细通过上面两段简单的代码示例,你已经对ThreadStart和ParameterizedThreadStart的使用有了全面的了解。

另外有一点需要说的,有些情况下可以省略这个委托对象的构造,即构造Thread对象时,直接向Thread的构造函数传入一个方法的名称,而不用先构造一个委托的对象。传入的方法既可以是静态方法也可以是实例方法。

    另外委托在异步编程中的作用见异步编程 的文章

参考资料:

C#与.Net3.0高级程序设计

C#与.Net2.0实战

CLR via C# 第二版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值