`
RednaxelaFX
  • 浏览: 3049234 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

委托与方法和隐藏参数

    博客分类:
  • C#
阅读更多
之前正好发了些帖子是关于CLR里的委托的,然后看到老赵说事件也应该是对象才好,我想提一下委托与方法和隐藏参数的关系,以此来类比到“事件对象”。

在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:
@jeffz_cn 写道
C#中的事件无法作为一个对象传递,我认为是一个语言设计上的失误

@rednaxelafx 写道
RT @jeffz_cn: C#中的事件无法作为一个对象传递,我认为是一个语言设计上的失误。//如果可以传递会是怎样的场景?

@jeffz_cn 写道
@rednaxelafx 最简单的,我可以把事件传入一个方法,然后再方法里添加handler咯。

@rednaxelafx 写道
@jeffz_cn 就是说如果你把指向实例方法的委托看作把实例curry了的方法话,“事件对象”就应该是把实例curry了的事件?外加可以用别名来引用事件的能力

看看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#没有专门语法来支持它的话,用起来不会太好看,或许也就失去了创建这种对象的意义。

更新:老赵说就是这样的,他发了一帖在这里:把事件当作对象进行传递
分享到:
评论

相关推荐

    地址栏参数隐藏

    8. **URL编码**:即使无法完全隐藏参数,也可以对URL参数进行编码,防止因为特殊字符导致的安全问题,但这并不意味着数据被加密。 在开发过程中,合理运用上述技术能够有效地提升Web应用的安全性。同时,理解并实施...

    C# 委托 和 事件在 .net Framework中的应用

    这种设计虽然直观,但不利于扩展,因为每添加一个新语言就需要修改枚举和方法。 如果我们使用委托,可以创建一个新的委托类型,例如`GreetingDelegate`,它定义了一个接受`string`参数并返回`void`的方法。然后,`...

    对C#中委托和事件很生动的描述

    封装性保证了对象的内部状态对外部是隐藏的,外部代码不能直接访问对象内部的成员,而是通过对象暴露的接口和方法来间接访问。这种设计可以减少系统各部分之间的耦合,增加模块的独立性和可复用性。当事件作为封装的...

    C#_重载重写_隐藏_数组_集合_委托

    - 通过接口,可以减少类型暴露的属性和方法,从而便于保护类型对象。 综上所述,C#中的重载、重写、隐藏、数组、集合和委托都是编程中非常重要的概念。掌握这些概念可以帮助开发者更加高效地进行软件开发工作。

    C#事件与委托详细解说

    1. 声明一个委托类型,定义其参数列表和返回类型,使其与目标方法匹配。 2. 创建委托实例,传入要调用的方法名。 3. 使用委托实例来调用相应的方法。 例如: ```csharp public delegate void MyDelegate(string ...

    委托和事件 C# 课程设计 课件

    总结一下,委托是C#中的一种强大的工具,允许你传递方法作为参数,实现回调机制和事件处理。事件则进一步封装了委托,提供了安全的发布/订阅模型,使得类可以通知其他对象其状态的变化。在实际开发中,理解并熟练...

    委托与事件ppt,讲义,代码

    这里的`EventHandler`是一个内置的委托类型,它接受`object`类型的`sender`参数和`EventArgs`类型的`e`参数。`OnMyEvent`方法是事件的触发器,`?.`操作符确保在没有订阅者时不会抛出异常。 讲义可能涵盖以下内容: ...

    工厂模式范例 详解枚举事件索引器委托有参委托属性方法等

    事件通常与委托(Delegate)一起使用,委托是引用方法的类型,相当于函数指针。有参委托意味着它可以传递参数,使得接收事件的处理函数能够根据传入的参数执行相应的操作。 索引器(Indexer)在C#中提供了类似数组...

    委托和事件代码示例完整版

    3. **匿名方法与Lambda表达式**:在创建委托实例时,我们可以使用匿名方法或Lambda表达式,这使得代码更加简洁易读。 4. **事件处理**:委托在事件处理中起到桥梁作用,连接事件发布者和事件订阅者。 **事件...

    C# 中的委托和事件究竟有何不同

    在C#编程语言中,委托和事件是两个关键的概念,它们在处理方法调用和对象间的通信方面扮演着重要角色。对于任何C#开发者来说,理解这两者的差异至关重要,尤其是在面试过程中,这个问题经常被用来评估候选人的专业...

    jquery 隐藏显示行

    除了 `hide()` 和 `show()`,jQuery 还提供了一个更灵活的方法 `toggle()`,它可以交替执行隐藏和显示操作: ```javascript $("tr").click(function() { $(this).toggle(); }); ``` 上面的代码将为每个表格行添加...

    C# 中的委托和事件.doc

    ### C# 中的委托与事件详解 #### 一、引言 在.NET Framework中,委托和事件是非常重要的概念。它们不仅频繁地出现在各种应用程序中,也是实现特定设计模式(如观察者模式)的关键组成部分。本文将通过两个示例来...

    c#委托和事件的讲解

    #### 2.7 委托和方法的异步调用 .NET Framework提供了多种方式进行异步调用,如`BeginInvoke`/`EndInvoke`和`async/await`等。通过异步调用,可以在不阻塞主线程的情况下执行耗时操作,从而提高应用程序的性能和...

    C#委托和事件的学习资料

    它定义了一种方法签名,允许我们存储和传递方法作为参数,或者将方法作为其他方法的返回值。这样,我们就可以在运行时动态地组合和调用方法,实现回调机制或者策略模式等设计模式。 委托的关键特性有: 1. **类型...

    c#中的委托和事件-讲的很好

    委托可以作为其他方法的参数,这使得我们可以创建更通用的函数,这些函数能够在执行时调用不同的实现。例如,排序算法(如`Array.Sort()`)可以接受一个比较方法作为参数,这样就可以根据不同的比较逻辑对数组进行...

    C#用Lambda和委托实现模板方法

    接下来,我们创建一个名为`map`的模板方法,它接受一个`mapfun`类型的委托和一个整数数组作为参数。模板方法负责遍历数组并将每个元素传递给委托处理,这样就可以将重复的逻辑封装起来。 ```csharp static void map...

    proj.zip_委托

    4. **类的继承**:这是面向对象编程的核心概念之一,一个类可以继承另一个类的属性和方法。项目中可能包含了不同类之间的层次结构,以及如何通过继承来实现代码复用和扩展性。 5. **new 虚函数继承**:在C#中,如果...

    事件与委托一个很好的例子

    委托允许我们将方法作为参数传递给其他方法,或者存储在变量中,这样就可以在之后的某个时间调用这些方法。C#提供了两种类型的委托:`Action`和`Func`。`Action`用于无返回值的方法,而`Func`则对应有返回值的方法。...

    委托工厂与用户可编程接口

    标题中的“委托工厂与用户可编程接口”是一个关于软件设计模式和技术的话题,它涉及到如何在应用程序中创建灵活、可扩展的用户界面,特别是在处理多时区时钟显示的情况下。在这个场景中,用户能够自定义他们想要查看...

Global site tag (gtag.js) - Google Analytics