委托与方法和隐藏参数

本文探讨了C#中委托与事件的概念,特别是闭合委托如何为实例方法创建别名及其实现机制。同时,文章还讨论了如何将事件视为对象进行传递的可能性。
之前正好发了些帖子是[url=http://rednaxelafx.iteye.com/blog/460893]关于CLR里的委托[/url]的,然后看到老赵说事件也应该是对象才好,我想提一下委托与方法和隐藏参数的关系,以此来类比到“事件对象”。

在C++、Java、C#等支持面向对象编程的语言里,“静态方法”与“实例方法”有显著的不同。一方面,静态方法不是虚方法,不参与继承相关的多态(但是可以参与参数化多态——泛型),而实例方法可以是虚方法,可以通过继承和覆盖达到多态的目的;另一方面,静态方法不需要实例的信息,而实例方法需要跟实例来提供执行环境,因而需要一个隐藏参数,“this”。下文的讨论主要针对后一方面。

考虑这样一段Java代码:
public class Accumulator {
private int value;

public Accumulator(int initValue) {
this.value = initValue;
}

public void add(int increment) {
this.value += increment;
}

public int value() {
return this.value;
}
}

//...

public class Program {
public static void main(String[] args) {
Accumulator acc = new Accumulator(0);
acc.add(2);
acc.add(3);
System.out.println(acc.value()); // 5
}
}

Accumulator中的add()与currentValue()都是实例方法,并且可以很明显看出它们对实例所提供的环境的依赖——它们使用了“this”,通过this才可以访问到实例变量value。上例中的acc.add(2),虽然参数列表里只有一个2,但方法的“接收者”(receiver)也很明确的出现在代码中,就是“点”之前的acc,它作为一个隐藏参数被传到了方法中;如果要把所有实际上用到的参数都显式写明,用伪代码可以写成:Accumulator.add(acc, 2)。

如果换用Python来写就会更明显:
class Accumulator:
def __init__(self, init_value = 0):
self.__value = init_value

def add(self, increment):
self.__value += increment

def value(self):
return self.__value

acc = Accumulator()
acc.add(2)
acc.add(3)
print(acc.value())

Python要求声明实例方法时将提供环境的对象作为显式的第一个参数,习惯上命名为self。使用实例方法时则与Java等语言一样,把“self”作为隐藏参数传递。

如果不考虑虚方法的多态语义,把上面的Java代码换成C来写,会是类似:
typedef struct tagAccumulator {
int value;
} Accumulator;

Accumulator* Accumulator_new() {
return (Accumulator*)malloc(sizeof(Accumulator));
}

void Accumulator_init(Accumulator* this, int init_value) {
this->value = init_value;
}

void Accumulator_add(Accumulator* this, int increment) {
this->value += increment;
}

int Accumulator_value(Accumulator* this) {
return this->value;
}

void Accumulator_free(Accumulator* this) {
free(this);
}

(这里模仿Java等语言的语义,把分配空间与初始化的步骤分离了。实际要在C里模拟面向对象编程倒不会这么写……我只是为了突出“this”而已)

C里可以用裸的函数指针,它只是指向函数的代码,而不关心函数可能需要的执行环境。对上述Accumulator的例子中的add(),如果创建函数指针并使用,可以是这样:
#include <stdio.h>

typedef void (*add_ptr_t)(Accumulator*, int);

int main() {
add_ptr_t pAdd = &Accumulator_add; // create pointer to "member function"
Accumulator* acc = Accumulator_new(); // object allocation
Accumulator_init(acc, 0); // object initialization

// call "member function" through pointer
pAdd(acc, 2);
pAdd(acc, 3);

printf("%d\n", Accumulator_value(acc));

Accumulator_free(acc);
return 0;
}

(注:虽然我在注释里写了“member function”,注意我并不是指C++里的member function/pointer to member function。只是笼统的模拟一个概念而已)

说了半天,C#都还没登场呢,于是扯回正题,来看看C#的委托:
using System;

public class Accumulator {
public int Value { get; private set; }

public Accumulator(int initValue) {
Value = initValue;
}

public void Add(int increment) {
Value += increment;
}
}

static class Program {
static void Main(string[] args) {
var acc = new Accumulator(0);

// create a closed delegate
Action<int> add = acc.Add;

// invoke instance method
acc.Add(2);
// invoke instance method through closed delegate
add(3);

Console.WriteLine(acc.Value);
}
}

上例中,第20行的acc.Add是通过一个对象实例去引用一个MethodGroup,C#的自动转换规则为Accumulator.Add()方法针对acc实例创建了一个“闭合委托”(closed delegate),然后将该委托赋值给add变量。要怎么知道一个委托是“闭合”的还是“开放”(open)的呢?只要看看委托的Target属性是否为null就行。上例的add变量所引用的委托就是闭合的,add.Target == acc。也就是说,通过闭合委托调用一个方法,就跟直接通过实例调用实例方法一样,带有一个隐藏参数,“this”;开放委托则不包含“this”隐藏参数,看上去就跟C里的函数指针很类似。
虽然C#默认是为静态方法创建开放委托,为实例方法创建闭合委托,但可以通过反射为实例方法创建出开放委托来:
using System;

public class Accumulator {
public int Value { get; private set; }

public Accumulator(int initValue) {
Value = initValue;
}

public void Add(int increment) {
Value += increment;
}
}

static class Program {
static void Main(string[] args) {
var acc = new Accumulator(0);
var addInfo = typeof(Accumulator).GetMethod("Add");
// create an open delegate
var openAdd = (Action<Accumulator, int>)Delegate.CreateDelegate(
typeof(Action<Accumulator, int>), addInfo);

// invoke instance method
acc.Add(2);
// invoke instance method through open delegate
openAdd(acc, 3);

Console.WriteLine(acc.Value);
}
}

通过开放委托来调用实例方法,就很明显的展现出“this”隐藏参数的存在。

废那么话扯到C#的开放与闭合委托,我是想说明什么呢?先看看先前的几条tweet:
[quote="@jeffz_cn"]C#中的事件无法作为一个对象传递,我认为是一个语言设计上的失误[/quote]
[quote="@rednaxelafx"]RT @jeffz_cn: C#中的事件无法作为一个对象传递,我认为是一个语言设计上的失误。//如果可以传递会是怎样的场景?[/quote]
[quote="@jeffz_cn"]@rednaxelafx 最简单的,我可以把事件传入一个方法,然后再方法里添加handler咯。[/quote]
[quote="@rednaxelafx"]@jeffz_cn 就是说如果你把指向实例方法的委托看作把实例curry了的方法话,“事件对象”就应该是把实例curry了的事件?外加可以用别名来引用事件的能力[/quote]
看看C#的闭合委托为我们提供了怎样的能力:
1、为方法创建别名。看这个例子:
using System;

public class Foo {
public void Bar() {
Console.WriteLine("Foo.Bar()");
}
}

public class Baz {
public void Quux() {
Console.WriteLine("Baz.Quux()");
}
}

static class Program {
static void Invoke(Action action) {
action();
}

static void Main(string[] args) {
Foo foo = new Foo();
Baz baz = new Baz();
Invoke(foo.Bar);
Invoke(baz.Quux);
}
}

本来Foo和Baz两个类没有任何继承关系,Bar()与Quux()两个方法的名字也不同,所以不可能通过继承多态的方式来统一的调用它们。但是由于C#的闭合委托给予了我们对实例方法创建“别名”的能力(例中action与foo.Bar之间、action与baz.Quux之间建立了别名关系),我们就可以在Invoke()方法里以统一的方式去调用这两个实例方法。
可能有人会想到跟duck-typing类比,不过这个跟duck-typing比较不同——duck-typing是要求方法名和参数列表都匹配,不关心接收者类型;闭合委托是方法名和接收者类型都不关心,只要参数列表的类型和个数都匹配就行。

2、可以将它看作对应的开放委托将“this”给curry化之后的形态,也就是:
// create a closed delegate from an open delegate by currying
Action<int> closedAdd = (int increment) => openAdd(acc, increment);
// invoke the closed delegate as usual
closedAdd(3);

因为闭合委托包含了对实例的引用,所以有足够信息对实例的状态做查询和操作。像是前面例子里的add、closedAdd,调用它们就达到了改变acc.Value的值的效果。

如果说方法的核心操作就是“调用”,那么事件呢?应该有两点:
1、添加/去除事件的监听器
2、发送一个事件
C#只允许在声明事件的类里发送事件,从外部来看实际上就只有第一点功能。

跟方法与实例的关系一样,C#里事件也有静态事件或者实例事件;后者必须依赖于实例才可以正常运行。C#里,在声明事件的类中通过事件的名字引用到的是它背后的委托,而不是事件本身;在声明事件的类外部则变成对add_XXX或remove_XXX方法语法糖,这两个方法返回void。如果要改变事件的“状态”,也就是为事件添加或去除监听器的话,必须要有类或实例的引用才行。老赵想说的,说不定是这样:(伪代码)
using System;

// declare an event type, just like declaring a delegate type
public event Action MyEventType;

public class Foo {
// declare an event
public event Action Bar;

public void TriggerBar() {
var bar = Bar;
if (null != bar) bar();
}
}

static class Program {
static void PopulateEventHandlers(MyEventType ev) {
ev += () => Console.WriteLine("Howdy");
ev += () => Console.WriteLine("Doody");
}

static void Main(string[] args) {
var foo = new Foo();
PopulateEventHandlers(foo.Bar);
foo.TriggerBar();
// should print:
// Howdy
// Doody
}
}

如果C#支持这种语言结构,那么代码中
1、ev就与foo.Bar建立了别名关系
2、ev隐藏着“this”参数,指向foo。

不知道老赵说的“事件对象”是不是像这样的呢?如果是的话,要自己实现出包装事件的对象看来并不困难,不过我一时能想到的办法都要用反射,至少要在开始用一次反射。真正的问题是C#没有专门语法来支持它的话,用起来不会太好看,或许也就失去了创建这种对象的意义。

更新:老赵说就是这样的,他发了一帖在这里:[url=http://www.cnblogs.com/JeffreyZhao/archive/2009/09/07/treat-event-as-an-object.html]把事件当作对象进行传递[/url]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值