四、Extension Method的本质

通过上面一节的介绍,我们知道了在C#中如何去定义一个Extension Method:它是定义在一个Static class中的、第一个Parameter标记为this关键字的Static Method。在这一节中,我们来进一步认识Extension Method。

和C# 3.0的其他新特性相似,Extension Method仅仅是C#这种.NET Programming Language的新特性而已。我们知道,C#是一种典型的编译型的语言,我们编写的Source Code必须先经过和C# Compiler编译成Assembly,才能被CLR加载,被JIT 编译成Machine Instruction并最终被执行。C# 3.0的这些新的特性大都影响Source被C# Compiler编译成Assembly这个阶段,换句话说,这些新特仅仅是Compiler的新特性而已。通过对Compiler进行修正,促使他将C# 3.0引入的新的语法编译成相对应的IL Code,从本质上看,这些IL Code 和原来的IL并没有本质的区别。所有当被编译生成成Assembly被CLR加载、执行的时候,CLR是意识不到这些新的特性的。

从Extension Method的定义我们可看出,Extension Method本质上是一个Static Method。但是我们往往以Instance Method的方式进行调用。C# Compiler的作用很明显:把一个以Instance Method方式调用的Source Code编译成的于对应于传统的Static Method调用的IL Code。

虽然Extension Method本质上仅仅是一个Static Class的Static Method成员,但是毕竟和传统的Static Method有所不同:在第一个Parameter前加了一个this关键字。我们现在来看看他们之间的细微的差异。我们先定义一个一般的Static Method:

public static Vector Adds(Vector v, Vector v1)
{
return new Vector { X = v.X + v1.X, Y = v.Y + v1.Y };
}

注:Vector的定义参见《深入理解C# 3.0的新特性(2):Extension Method - Part I》。

我们来看看通过Compiler进行编译生成的IL:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 50 (0x32)
.maxstack 2
.locals init ([0] class Artech.ExtensionMethod.Vector v,
[1] class Artech.ExtensionMethod.Vector '<>g__initLocal0')
IL_0000: nop
IL_0001: newobj instance void Artech.ExtensionMethod.Vector::.ctor()
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: ldc.r8 1.
IL_0011: callvirt instance void Artech.ExtensionMethod.Vector::set_X(float64)
IL_0016: nop
IL_0017: ldloc.1
IL_0018: ldc.r8 2.
IL_0021: callvirt instance void Artech.ExtensionMethod.Vector::set_Y(float64)
IL_0026: nop
IL_0027: ldloc.1
IL_0028: stloc.0
IL_0029: ldloc.0
IL_002a: ldloc.0
IL_002b: call class Artech.ExtensionMethod.Vector Artech.ExtensionMethod.Extension::Adds(class Artech.ExtensionMethod.Vector,
class Artech.ExtensionMethod.Vector)
IL_0030: stloc.0
IL_0031: ret
} // end of method Program::Main

对了解IL的人来说,对上面的IL code应该很容易理解。

我们再来看看对于通过下面的方式定义的Extension Method:

public static class Extension
{
public static Vector Adds(this Vector v, Vector v1)
{
return new Vector { X = v.X + v1.X, Y = v.Y + v1.Y };
}
}

对于得IL如下:

.method public hidebysig static class Artech.ExtensionMethod.Vector
Adds(class Artech.ExtensionMethod.Vector v,
class Artech.ExtensionMethod.Vector v1) cil managed
{
.custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 )
// Code size 53 (0x35)
.maxstack 3
.locals init ([0] class Artech.ExtensionMethod.Vector '<>g__initLocal0',
[1] class Artech.ExtensionMethod.Vector CS$1$0000)
IL_0000: nop
IL_0001: newobj instance void Artech.ExtensionMethod.Vector::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldarg.0
IL_0009: callvirt instance float64 Artech.ExtensionMethod.Vector::get_X()
IL_000e: ldarg.1
IL_000f: callvirt instance float64 Artech.ExtensionMethod.Vector::get_X()
IL_0014: add
IL_0015: callvirt instance void Artech.ExtensionMethod.Vector::set_X(float64)
IL_001a: nop
IL_001b: ldloc.0
IL_001c: ldarg.0
IL_001d: callvirt instance float64 Artech.ExtensionMethod.Vector::get_Y()
IL_0022: ldarg.1
IL_0023: callvirt instance float64 Artech.ExtensionMethod.Vector::get_Y()
IL_0028: add
IL_0029: callvirt instance void Artech.ExtensionMethod.Vector::set_Y(float64)
IL_002e: nop
IL_002f: ldloc.0
IL_0030: stloc.1
IL_0031: br.s IL_0033
IL_0033: ldloc.1
IL_0034: ret
} // end of method Extension::Adds

通过比较,我们发现和上面定义的一般的Static Method生成的IL唯一的区别就是:在Adds方法定义最开始添加了下面一段代码:

.custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 )

这段添加的IL代码很明显,就是在Adds方法上添加一个Customer Attribute:System.Runtime.CompilerServices.ExtensionAttribute。ExtensionAttribute具有如下的定义:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class ExtensionAttribute : Attribute
{
}

所以下面Extension Method的定义

public static Vector Adds(this Vector v, Vector v1)
{
return new Vector { X = v.X + v1.X, Y = v.Y + v1.Y };
}

和下面的定义是等效的

[ExtensionAttribute]
public static Vector Adds(Vector v, Vector v1)
{
return new Vector { X = v.X + v1.X, Y = v.Y + v1.Y };
}

但是,System.Runtime.CompilerServices.ExtensionAttribute和其他Custom Attribute不一样,因为它是为了Extension Method的而定义的,我们只能通过添加this Key word的语法来定义Extension Method。所以当我们将System.Runtime.CompilerServices.ExtensionAttribute直接运用到Adds方法会出现下面的Compile Error:

Do not use 'System.Runtime.CompilerServices.ExtensionAttribute'. Use the 'this' keyword instead.

上面我们比较了Extension Method本身IL和一般Static Method IL,现在我们看看当我们以Instance Method方式调用Extension Method的IL。假设我们通过下面的方式调用Adds。 

class Program
{
static void Main(string[] args)
{
var v = new Vector { X = 1, Y = 2 };
v = v.Adds(v);
}
}

下面是Main Method的IL:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 50 (0x32)
.maxstack 2
.locals init ([0] class Artech.ExtensionMethod.Vector v,
[1] class Artech.ExtensionMethod.Vector '<>g__initLocal0')
IL_0000: nop
IL_0001: newobj instance void Artech.ExtensionMethod.Vector::.ctor()
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: ldc.r8 1.
IL_0011: callvirt instance void Artech.ExtensionMethod.Vector::set_X(float64)
IL_0016: nop
IL_0017: ldloc.1
IL_0018: ldc.r8 2.
IL_0021: callvirt instance void Artech.ExtensionMethod.Vector::set_Y(float64)
IL_0026: nop
IL_0027: ldloc.1
IL_0028: stloc.0
IL_0029: ldloc.0
IL_002a: ldloc.0
IL_002b: call class Artech.ExtensionMethod.Vector Artech.ExtensionMethod.Extension::Adds(class Artech.ExtensionMethod.Vector,
class Artech.ExtensionMethod.Vector)
IL_0030: stloc.0
IL_0031: ret
} // end of method Program::Main

通过上面的IL,我们看到调用的是Artech.ExtensionMethod.Extension的Adds方法。

IL_002b: call class Artech.ExtensionMethod.Vector Artech.ExtensionMethod.Extension::Adds(class Artech.ExtensionMethod.Vector,
class Artech.ExtensionMethod.Vector)

通过对IL的分析,我们基本上看出了Extension Method的本质。我们再来简单描述一下对Compiler的编译过程:当Compiler对Adds方法的调用进行编译的过程的时候,它必须判断这个Adds方式是Vector Type的成员还是以Extension Method的方式定义。Extension Method的优先级是最低的,只有确定Vector中没有定义相应的Adds方法的时候,Compiler才会在引用的Namespace中查看这些Namespace中是否定义有对应的Adds Extension Method的Static Class。找到后作进行相应的编译,否则出现编译错误。

五、一个完整的Extension Method的Sample

在介绍了Extension Method的本质之后,我们通过一个相对完整的Sample进一步了解Extension Method的运用,通过这个Sample,我们还可以粗略了解LINQ的原理。

C# 3.0为LINQ定义了一系列的Operator:select, from,where,orderby..., 促使我们按照OO的方式来处理各种各样的数据,比如XML,Relational DB Data,C#中IEnumeratable<T> Object。比如:

var names = new List<string> { "Tom Cruise", "Tom Hanks", "Al Pacino", "Harrison Ford" };
var result = names.Where(name => name.StartsWith("Tom"));
foreach(var name in result)
{
Console.WriteLine(name);
}

我们通过上面的Code,从一系列的姓名列表中("Tom Cruise", "Tom Hanks", "Al Pacino", "Harrison Ford")筛选名字(First Name)为Tom的姓名。通过Where Operator,传入一个以Lambda Expression表示的筛选条件(name => name.StartsWith("Tom"))。Where Operator就是通过Extension Method的方式定义的。

在这里提供的Sample就是定义一个完成Where Operator相同功能的Operator,我们把这个Operator起名为When。

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

namespace Artech.ExtensionMethod
{
public delegate TResult Function<Tparam, TResult>(Tparam param);

public static class Extension
{
public static IEnumerable<TSource> When<TSource>(this IEnumerable<TSource> source, Function<TSource, bool> predicate)
{
return new WhenEnumerator<TSource>(source, predicate);
}

}

public class WhenEnumerator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>
{
private IEnumerable<TSource> _source;
private Function<TSource, bool> _predicate;
private IEnumerator<TSource> _sourceEnumerator;

public WhenEnumerator(IEnumerable<TSource> source, Function<TSource, bool> predicate)
{
this._source = source;
this._predicate = predicate;
this._sourceEnumerator = this._source.GetEnumerator();
}

IEnumerable Members#region IEnumerable<TSource> Members

public IEnumerator<TSource> GetEnumerator()
{
return new WhenEnumerator<TSource>(this._source, this._predicate);
}

#endregion

IEnumerable Members#region IEnumerable Members

IEnumerator IEnumerable.GetEnumerator()
{
throw new Exception("The method or operation is not implemented.");
}

#endregion

IEnumerator Members#region IEnumerator<TSource> Members

public TSource Current
{
get { return this._sourceEnumerator.Current; }
}

#endregion

IDisposable Members#region IDisposable Members

public void Dispose()
{
//throw new Exception("The method or operation is not implemented.");
}

#endregion

IEnumerator Members#region IEnumerator Members

object IEnumerator.Current
{
get
{
return this._sourceEnumerator.Current;
}
}

public bool MoveNext()
{
if (!this._sourceEnumerator.MoveNext())
{
return false;
}

while (!this._predicate(this._sourceEnumerator.Current))
{
if (!this._sourceEnumerator.MoveNext())
{
return false;
}
}

return true;
}

public void Reset()
{
this._sourceEnumerator.Reset();
}

#endregion
}
}

我们来看看我们新的LINQ Operator:When的定义。我首先定义了一个Generic Delegate:Function。实际上他定义了一个一元函数y = f(x),TParam和TResult为参数和返回值得类型。

public delegate TResult Function<Tparam, TResult>(Tparam param);

接着在Static Class Extesnion中定义了Extension Method:When。该方法包含两个参数,其中一个是执行筛选的数据源,另一个是用于判断数据源每个对象是否满足你所定义的筛选条件的断言。返回一个我们自定义的、实现了IEnumerable的WhenEnumerator对象。

public static class Extension
{
public static IEnumerable<TSource> When<TSource>(this IEnumerable<TSource> source, Function<TSource, bool> predicate)
{
return new WhenEnumerator<TSource>(source, predicate);
}
}

WhenEnumerator的定义是实现When Extension Method的关键,我们现在着重来介绍它的具体实现。WhenEnumerator实现了Interface Enumerable<T>,为了简单,我们也它对应的Enumerator的实现也定义在同一个Class中,所以WhenEnumerator实现了两个Interface:IEnumerable<TSource>, IEnumerator<TSource>。

以下3个成员分别代表:用于执行筛选的数据源、用于判断是否满足筛选条件的断言以及数据源的Enumerator对象。

private IEnumerable<TSource> _source;
private Function<TSource, bool> _predicate;
private IEnumerator<TSource> _sourceEnumerator;

通过返回一个WhenEnumerator对象,实现了IEnumerable<TSource>的GetEnumerator()方法。

public IEnumerator<TSource> GetEnumerator()
{
return new WhenEnumerator<TSource>(this._source, this._predicate);
}

对于另一个Interface IEnumerator<TSource>,直接调用数据源的Enumerator的同名方法实现了Current,和Reset()。对于MoveNext()则通过如下的方式实现:把当前的位置设置在下一个满足筛选条件的Element上。

public bool MoveNext()
{
if (!this._sourceEnumerator.MoveNext())
{
return false;
}

while (!this._predicate(this._sourceEnumerator.Current))
{
if (!this._sourceEnumerator.MoveNext())
{
return false;
}
}

return true;
}

到现在为止,这个新的LINQ Operator被创建,现在我们可以按照使用Where operator的方式来调用When。

我们可以通过Delegate的方式来使用When Operator:

class Program
{
static void Main()
{
var names = new List<string> { "Tom Cruise", "Tom Hanks", "Al Pacino", "Harrison Ford" };
var result = names.When(delegate(string name) { return name.StartsWith("Tom"); });
foreach (var name in result)
{
Console.WriteLine(name);
}
}
}

输出结果:

Tom Cruise
Tom Hanks

我们也可以通过Lambda Expression的方式来使用When Operator:

static void Main()
{
var names = new List<string> { "Tom Cruise", "Tom Hanks", "Al Pacino", "Harrison Ford" };
var result = names.When(name=>name.StartsWith("Tom"));
foreach (var name in result)
{
Console.WriteLine(name);
}
}

显然这种方式更简洁。

Deferred Evaluation

对于LINQ,有一个非常重要的特征:Deferred Evaluation。在了解这个特征之前,我们来看一个例子:

static void Main()
{
var names = new List<string> { "Tom Cruise", "Tom Hanks", "Al Pacino", "Harrison Ford" };
var result1 = names.When(name=>name.StartsWith("Tom"));
names[0] = "Stephen Chou";
var result2 = names.When(name => name.StartsWith("Tom"));

foreach (var name in result1)
{
Console.WriteLine(name);
}

foreach (var name in result2)
{
Console.WriteLine(name);
}
}

运行程序,你会发现两个foreach loop显示的结果都是一样的:Tom Hanks。为什么result1实在第一个Element被改动之前返回的,但我们最终输出的结果却反映的是改动之后的数据源。通过我们上面的定义,你很容易得到答案。在这里我要说的是LINQ的一个重要的特性Deferred Evaluation:在调用Operator的时候并不会有任何的任何数据获取的过程,这个阶段的任务是创建一个同于获取数据的表达式。只要你真正所用到这个数据的时候,采用重数据源中通过你构建的表达式通过查询获取数据。

Extension Method[下篇]的更多相关文章

  1. [C#] Extension Method 扩展方法

    当我们引用第三方的DLL.或者Visual Studio自己的库的时候,或许会发现这样的一个情况,如果这个类型有一个XX的方法就好了.这时候我们可以用到扩展方法,是我们的代码更加灵活和高效. 这里我举 ...

  2. 动态linq表达式新方法,Dynamic LINQ Extension Method

    Remember those old posts on Dynamic LINQ? You are probably aware that Microsoft has made its impleme ...

  3. Extension Method[上篇]

    在C#3.0中,引入了一些列新的特性,比如: Implicitly typed local variable, Extension method,Lambda expression, Object i ...

  4. C# Note21: 扩展方法(Extension Method)及其应用

    前言 今天在开会时提到的一个概念,入职3个多月多注重在项目中使用C#的编程知识,一直没有很认真地过一遍C#的全部语法,当我们新人被问及是否了解Extension Method时,一时之间竟不能很通俗准 ...

  5. [译文]c#扩展方法(Extension Method In C#)

    原文链接: https://www.codeproject.com/Tips/709310/Extension-Method-In-Csharp 介绍 扩展方法是C# 3.0引入的新特性.扩展方法使你 ...

  6. C#编译问题'System.Collections.Generic.IEnumerable' does not contain a definition for 'Where' and no extension method 'Where' accepting a first argument

    &apos;System.Collections.Generic.IEnumerable<string>&apos; does not contain a definiti ...

  7. Swift protocol extension method is called instead of method implemented in subclass

    Swift protocol extension method is called instead of method implemented in subclass protocol MyProto ...

  8. Extension method for type

    扩展其实真的很简单 msdn是这样规定扩展方法的:"扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的. 它们的第一个参数指定该方法作用于哪个类型,并且该参数以 this 修饰符为 ...

  9. [C#]Stream.Write Extension Method

    在处理Stream型态时常会使用到Stream.Write这个方法,每次都会有种疑问就是,大多数的处理都是要将Buffer整个写入,為何偏偏每次都要将索引带0,长度带為Buffer的大小呢?另外在处理 ...

随机推荐

  1. BootStrap中Affix控件的使用方法及如何保持布局的美观

    Affix是BootStrap中的一个很有用的控件,他能够监视浏览器的滚动条的位置并让你的导航始终都在页面的可视区域.一开始的时候,导航在页面中是普通的流式布局,占有文档中固定的位置,当页面滚动的时候 ...

  2. 开源 侧滑 和 Tab滑动翻页 控件

    侧滑 https://github.com/jfeinstein10/SlidingMenu Tab滑动翻页 https://github.com/astuetz/PagerSlidingTabStr ...

  3. php 操作mongodb

    在这里首先说一下mongo 客户端安装完成有时会启动失败     这里解决办法就是 删除 D:\mongodb\db 下的 mongod.lock文件即可 再重新启动 首先下载mongodb php扩 ...

  4. JS中如何判断null

    var exp = null; if (exp == null) { alert("is null"); } exp 为 undefined 时,也会得到与 null 相同的结果, ...

  5. 使用jQuery的9个误区

    千万别忘记了使用最新的版本哦,毕竟每个版本更新肯定会在功能或性能上有所提升,或者修复了几个Bug,但有时惰性让人不想再去研究新版本的变化,因此,提醒你别忘记了在新项目用新的一定比旧版本要好. AD: ...

  6. jdbc学习(一)——SqlServer、Oracle和MySQL

    一.jdbc介绍 jdbc全称:java数据库连接(Java Database Connectivity),是sun公司定义的一套访问数据库的规范(接口和类,由各种数据库公司进行实现),主要放在jav ...

  7. pyunit实现数据测试框架

    PyUnit提供的动态方法,只编写一个测试类来完成对整个软件模块的测试,这样对象的初始化工作可以在setUp()方法中完成,而资源的释放则可以在tearDown()方法中完成. 使用PyUnit可以像 ...

  8. [译] ASP.NET 生命周期 – ASP.NET 请求生命周期(三)

    使用特殊方法处理请求生命周期事件 为了在全局应用类中处理这些事件,我们会创建一个名称以 Application_ 开头,以事件名称结尾的方法,比如 Application_BeginRequest.举 ...

  9. EXTJS 4.2 资料 控件之tabpanel 静态生成tabpanel

    //**************页面主体开始***************** var tabpanel = Ext.createWidget('tabpanel', { activeTab: 0, ...

  10. 如何在Linux中关闭apache服务(转)

    ??? 最近在写一个简单的http服务器,调试的时候发现apache服务器也在机器上跑着,所以得先把apache关掉.当时装apache的时候就是用了普通的sudo get,也不知道装到哪儿了.到网上 ...