文章出處

快樂的Lambda表達式(二)

  自從Lambda隨.NET Framework3.5出現在.NET開發者眼前以來,它已經給我們帶來了太多的欣喜。它優雅,對開發者更友好,能提高開發效率,天啊!它還有可能降低發生一些潛在錯誤的可能。LINQ包括ASP.NET MVC中的很多功能都是用Lambda實現的。我只能說自從用了Lambda,我腰也不酸了,腿也不疼了,手指也不抽筋了,就連寫代碼bug都少了。小伙伴們,你們今天用Lambda了么?但是你真的了解它么?今天我們就來好好的認識一下吧。

  本文會介紹到一些Lambda的基礎知識,然后會有一個小小的性能測試對比Lambda表達式和普通方法的性能,接著我們會通過IL來深入了解Lambda到底是什么,最后我們將用Lambda表達式來實現一些JavaScript里面比較常見的模式。

了解Lambda     

  在.NET 1.0的時候,大家都知道我們經常用到的是委托。有了委托呢,我們就可以像傳遞變量一樣的傳遞方法。在一定程序上來講,委托是一種強類型的托管的方法指針,曾經也一時被我們用的那叫一個廣泛呀,但是總的來說委托使用起來還是有一些繁瑣。來看看使用一個委托一共要以下幾個步驟:

  1. 用delegate關鍵字創建一個委托,包括聲明返回值和參數類型
  2. 使用的地方接收這個委托
  3. 創建這個委托的實例并指定一個返回值和參數類型匹配的方法傳遞過去

  復雜嗎?好吧,也許06年你說不復雜,但是現在,真的挺復雜的。

  后來,幸運的是.NET 2.0為了們帶來了泛型。于是我們有了泛型類,泛型方法,更重要的是泛型委托。最終 在.NET3.5的時候,我們Microsoft的兄弟們終于意識到其實我們只需要2個泛型委托(使用了重載)就可以覆蓋99%的使用場景了。

  • Action 沒有輸入參數和返回值的泛型委托
  • Action<T1, …, T16> 可以接收1個到16個參數的無返回值泛型委托
  • Func<T1, …, T16, Tout> 可以接收0到16個參數并且有返回值的泛型委托

  這樣我們就可以跳過上面的第一步了,不過第2步還是必須的,只是用Action或者Func替換了。別忘了在.NET2.0的時候我們還有匿名方法,雖然它沒怎么流行起來,但是我們也給它 一個露臉的機會。

Func<double, double> square = delegate (double x) {
	return x * x;
}

  最后,終于輪到我們的Lambda優雅的登場了。

// 編譯器不知道后面到底是什么玩意,所以我們這里不能用var關鍵字
Action dummyLambda = () => { Console.WriteLine("Hello World from a Lambda expression!"); };

// double y = square(25);
Func<double, double> square = x => x * x;

// double z = product(9, 5);
Func<double, double, double> product = (x, y) => x * y;

// printProduct(9, 5);
Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); };

// var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 });
Func<double[], double[], double> dotProduct = (x, y) =>
{
    var dim = Math.Min(x.Length, y.Length);
    var sum = 0.0;
    for (var i = 0; i != dim; i++)
        sum += x[i] + y[i];
    return sum;
};

// var result = matrixVectorProductAsync(...);
Func<double, double, Task<double>> matrixVectorProductAsync = async (x, y) =>
{
    var sum = 0.0;
    /* do some stuff using await ... */
    return sum;
};

 

  從上面的代碼中我們可以看出:

  • 如果只有一個參數,不需要寫()
  • 如果只有一條執行語句,并且我們要返回它,就不需要{},并且不用寫return
  • Lambda可以異步執行,只要在前面加上async關鍵字即可
  • Var關鍵字在大多數情況下都不能使用

  當然,關于最后一條,以下這些情況下我們還是可以用var關鍵字的。原因很簡單,我們告訴編譯器,后面是個什么類型就可以了。

Func<double,double> square = (double x) => x * x;

Func<string,int> stringLengthSquare = (string s) => s.Length * s.Length;

Action<decimal,string> squareAndOutput = (decimal x, string s) =>
{
    var sqz = x * x;
    Console.WriteLine("Information by {0}: the square of {1} is {2}.", s, x, sqz);
};

  現在,我們已經知道Lambda的一些基本用法了,如果僅僅就這些東西,那就不叫快樂的Lambda表達式了,讓我們看看下面的代碼。

var a = 5;
Func<int,int> multiplyWith = x => x * a;
var result1 = multiplyWith(10); //50
a = 10;
var result2 = multiplyWith(10); //100

  是不是有一點感覺了?我們可以在Lambda表達式中用到外面的變量,沒錯,也就是傳說中的閉包啦。

void DoSomeStuff()
{
    var coeff = 10;
    Func<int,int> compute = x => coeff * x;
    Action modifier = () =>
    {
        coeff = 5;
    };

    var result1 = DoMoreStuff(compute);

    ModifyStuff(modifier);

    var result2 = DoMoreStuff(compute);
}

int DoMoreStuff(Func<int,int> computer)
{
    return computer(5);
}

void ModifyStuff(Action modifier)
{
    modifier();
}

  在上面的代碼中,DoSomeStuff方法里面的變量coeff實際是由外部方法ModifyStuff修改的,也就是說ModifyStuff這個方法擁有了訪問DoSomeStuff里面一個局部變量的能力。它是如何做到的?我們馬上會說的J。當然,這個變量作用域的問題也是在使用閉包時應該注意的地方,稍有不慎就有可能會引發你想不到的后果。看看下面這個你就知道了。

var buttons = new Button[10];

for (var i = 0; i < buttons.Length; i++)
{
    var button = new Button();
    button.Text = (i + 1) + ". Button - Click for Index!";
    button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); };
    buttons[i] = button;
}

  猜猜你點擊這些按鈕的結果是什么?是”1, 2, 3…”。但是,其實真正的結果是全部都顯示10。為什么?不明覺歷了吧?那么如果避免這種情況呢?

var button = new Button();
var index = i;
button.Text = (i + 1) + ". Button - Click for Index!";
button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); };
buttons[i] = button;

  其實做法很簡單,就是在for的循環里面把當前的i保存下來,那么每一個表達式里面存儲的值就不一樣了。

  接下來,我們整點高級的貨,和Lambda息息相關的表達式(Expression)。為什么說什么息息相關,因為我們可以用一個Expression將一個Lambda保存起來。并且允許我們在運行時去解釋這個Lambda表達式。來看一下下面簡單的代碼:

Expression<Func<MyModel, int>> expr = model => model.MyProperty;
var member = expr.Body as MemberExpression;
var propertyName = member.Expression.Member.Name; 

  這個的確是Expression最簡單的用法之一,我們用expr存儲了后面的表達式。編譯器會為我們生成表達式樹,在表達式樹中包括了一個元數據像參數的類型,名稱還有方法體等等。在LINQ TO SQL中就是通過這種方法將我們設置的條件通過where擴展方法傳遞給后面的LINQ Provider進行解釋的,而LINQ Provider解釋的過程實際上就是將表達式樹轉換成SQL語句的過程。

Lambda表達式的性能

  關于Lambda性能的問題,我們首先可能會問它是比普通的方法快呢?還是慢呢?接下來我們就來一探究竟。首先我們通過一段代碼來測試一下普通方法和Lambda表達 式之間的性能差異。

class StandardBenchmark : Benchmark
{
    const int LENGTH = 100000;
    static double[] A;
    static double[] B;

    static void Init()
    {
        var r = new Random();
        A = new double[LENGTH];
        B = new double[LENGTH];

        for (var i = 0; i < LENGTH; i++)
        {
            A[i] = r.NextDouble();
            B[i] = r.NextDouble();
        }
    }

    static long LambdaBenchmark()
    {
        Func<double> Perform = () =>
        {
            var sum = 0.0;

            for (var i = 0; i < LENGTH; i++)
                sum += A[i] * B[i];

            return sum;
        };
        var iterations = new double[100];
        var timing = new Stopwatch();
        timing.Start();

        for (var j = 0; j < iterations.Length; j++)
            iterations[j] = Perform();

        timing.Stop();
        Console.WriteLine("Time for Lambda-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
        return timing.ElapsedMilliseconds;
    }

    static long NormalBenchmark()
    {
        var iterations = new double[100];
        var timing = new Stopwatch();
        timing.Start();

        for (var j = 0; j < iterations.Length; j++)
            iterations[j] = NormalPerform();

        timing.Stop();
        Console.WriteLine("Time for Normal-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
        return timing.ElapsedMilliseconds;
    }

    static double NormalPerform()
    {
        var sum = 0.0;

        for (var i = 0; i < LENGTH; i++)
            sum += A[i] * B[i];

        return sum;
    }
}
}

  代碼很簡單,我們通過執行同樣的代碼來比較,一個放在Lambda表達式里,一個放在普通的方法里面。通過4次測試得到如下結果:

  Lambda  Normal-Method

  70ms  84ms
  73ms  69ms
  92ms  71ms
  87ms  74ms

  按理來說,Lambda應該是要比普通方法慢很小一點點的,但是不明白第一次的時候為什么Lambda會比普通方法還快一點。- -!不過通過這樣的對比我想至少可以說明Lambda和普通方法之間的性能其實幾乎是沒有區別的。  

  那么Lambda在經過編譯之后會變成什么樣子呢?讓LINQPad告訴你。

  上圖中的Lambda表達式是這樣的:

Action<string> DoSomethingLambda = (s) =>
{
	Console.WriteLine(s);// + local
};

  對應的普通方法的寫法是這樣的:

void DoSomethingNormal(string s)
{
	Console.WriteLine(s);
}

  上面兩段代碼生成的IL代碼呢?是這樣地:

DoSomethingNormal:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  call        System.Console.WriteLine
IL_0007:  nop         
IL_0008:  ret         
<Main>b__0:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        System.Console.WriteLine
IL_0007:  nop         
IL_0008:  ret       

  最大的不同就是方法的名稱以及方法的使用而不是聲明,聲明實際上是一樣的。通過上面的IL代碼我們可以看出,這個表達式實際被編譯器取了一個名稱,同樣被放在了當前的類里面。所以實際上,和我們調類里面的方法沒有什么兩樣。下面這張圖說明了這個編譯的過程:

  上面的代碼中沒有用到外部變量,接下來我們來看另外一個例子。

void Main()
{
	int local = 5;

	Action<string> DoSomethingLambda = (s) => {
		Console.WriteLine(s + local);
	};
	
	global = local;
	
	DoSomethingLambda("Test 1");
	DoSomethingNormal("Test 2");
}

int global;

void DoSomethingNormal(string s)
{
	Console.WriteLine(s + global);
}

  這次的IL代碼會有什么不同么?

IL_0000:  newobj      UserQuery+<>c__DisplayClass1..ctor
IL_0005:  stloc.1     
IL_0006:  nop         
IL_0007:  ldloc.1     
IL_0008:  ldc.i4.5    
IL_0009:  stfld       UserQuery+<>c__DisplayClass1.local
IL_000E:  ldloc.1     
IL_000F:  ldftn       UserQuery+<>c__DisplayClass1.<Main>b__0
IL_0015:  newobj      System.Action<System.String>..ctor
IL_001A:  stloc.0     
IL_001B:  ldarg.0     
IL_001C:  ldloc.1     
IL_001D:  ldfld       UserQuery+<>c__DisplayClass1.local
IL_0022:  stfld       UserQuery.global
IL_0027:  ldloc.0     
IL_0028:  ldstr       "Test 1"
IL_002D:  callvirt    System.Action<System.String>.Invoke
IL_0032:  nop         
IL_0033:  ldarg.0     
IL_0034:  ldstr       "Test 2"
IL_0039:  call        UserQuery.DoSomethingNormal
IL_003E:  nop         

DoSomethingNormal:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery.global
IL_0008:  box         System.Int32
IL_000D:  call        System.String.Concat
IL_0012:  call        System.Console.WriteLine
IL_0017:  nop         
IL_0018:  ret         

<>c__DisplayClass1.<Main>b__0:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery+<>c__DisplayClass1.local
IL_0008:  box         System.Int32
IL_000D:  call        System.String.Concat
IL_0012:  call        System.Console.WriteLine
IL_0017:  nop         
IL_0018:  ret         

<>c__DisplayClass1..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret      

  你發現了嗎?兩個方法所編譯出來的內容是一樣的, DoSomtingNormal和<>c__DisplayClass1.<Main>b__0,它們里面的內容是一樣的。但是最大的不一樣,請注意了。當我們的Lambda表達式里面用到了外部變量的時候,編譯器會為這個Lambda生成一個類,在這個類中包含了我們表達式方法。在使用這個Lambda表達式的地方呢,實際上是new了這個類的一個實例進行調用。這樣的話,我們表達式里面的外部變量,也就是上面代碼中用到的local實際上是以一個全局變量的身份存在于這個實例中的。

用Lambda表達式實現一些在JavaScript中流行的模式

  說到JavaScript,最近幾年真是風聲水起。不光可以應用所有我們軟件工程現存的一些設計模式,并且由于它的靈活性,還有一些由于JavaScript特性而產生的模式。比如說模塊化,立即執行方法體等。.NET由于是強類型編譯型的語言,靈活性自然不如JavaScript,但是這并不意味著JavaScript能做的事情.NET就不能做,下面我們就來實現一些JavaScript中好玩的寫法。

回調模式

  回調模式也并非JavaScript特有,其實在.NET1.0的時候,我們就可以用委托來實現回調了。但是今天我們要實現的回調可就不一樣了。

void CreateTextBox()
{
	var tb = new TextBox();
	tb.IsReadOnly = true;
	tb.Text = "Please wait ...";
	DoSomeStuff(() => {
		tb.Text = string.Empty;
		tb.IsReadOnly = false;
	});
}

void DoSomeStuff(Action callback)
{
	// Do some stuff - asynchronous would be helpful ...
	callback();
}

  上面的代碼中,我們在DoSomeStuff完成之后,再做一些事情。這種寫法在JavaScript中是很常見的,jQuery中的Ajax的oncompleted, onsuccess不就是這樣實現的么?又或者LINQ擴展方法中的foreach不也是這樣的么?

返回方法

  我們在JavaScript中可以直接return一個方法,在.net中雖然不能直接返回方法,但是我們可以返回一個表達式。

Func<string, string> SayMyName(string language)
{
	switch(language.ToLower())
	{
		case "fr":
			return name => {
				return "Je m'appelle " + name + ".";
			};
		case "de":
			return name => {
				return "Mein Name ist " + name + ".";
			};
		default:
			return name => {
				return "My name is " + name + ".";
			};
	}
}

void Main()
{
	var lang = "de";
	//Get language - e.g. by current OS settings
	var smn = SayMyName(lang);
	var name = Console.ReadLine();
	var sentence = smn(name);
	Console.WriteLine(sentence);
}

  是不是有一種策略模式的感覺?這還不夠完美,這一堆的switch case看著就心煩,讓我們用Dictionary<TKey,TValue>來簡化它。來看看來面這貨:

static class Translations
{
	static readonly Dictionary<string, Func<string, string>> smnFunctions = new Dictionary<string, Func<string, string>>();

	static Translations()
	{
		smnFunctions.Add("fr", name => "Je m'appelle " + name + ".");
		smnFunctions.Add("de", name => "Mein Name ist " + name + ".");
		smnFunctions.Add("en", name => "My name is " + name + ".");
	}

	public static Func<string, string> GetSayMyName(string language)
	{
		//Check if the language is available has been omitted on purpose
		return smnFunctions[language];
	}
}

自定義型方法

  自定義型方法在JavaScript中比較常見,主要實現思路是這個方法被設置成一個屬性。在給這個屬性附值,甚至執行過程中我們可以隨時更改這個屬性的指向,從而達到改變這個方法的目地。

class SomeClass
{
	public Func<int> NextPrime
	{
		get;
		private set;
	}

	int prime;

	public SomeClass
	{
		NextPrime = () => {
			prime = 2;

			NextPrime = () => {
                   // 這里可以加上 第二次和第二次以后執行NextPrive()的邏輯代碼 return prime; }; return prime; } } }

  上面的代碼中當NextPrime第一次被調用的時候是2,與此同時,我們更改了NextPrime,我們可以把它指向另外的方法,和JavaScrtip的靈活性比起來也不差吧?如果你還不滿意 ,那下面的代碼應該能滿足你。

Action<int> loopBody = i => {
	if(i == 1000)
		loopBody = //把loopBody指向別的方法

	/* 前10000次執行下面的代碼 */
};

for(int j = 0; j < 10000000; j++)
	loopBody(j);

  在調用的地方我們不用考慮太多,然后這個方法本身就具有調優性了。我們原來的做法可能是在判斷i==1000之后直接寫上相應的代碼,那么和現在的把該方法指向另外一個方法有什么區別呢?

自執行方法

  JavaScript 中的自執行方法有以下幾個優勢:

  1. 不會污染全局環境
  2. 保證自執行里面的方法只會被執行一次
  3. 解釋完立即執行

  在C#中我們也可以有自執行的方法:

(() => {
	// Do Something here!
})();

  上面的是沒有參數的,如果你想要加入參數,也非常的簡單:

((string s, int no) => {
	// Do Something here!
})("Example", 8);

  .NET4.5最閃的新功能是什么?async?這里也可以

await (async (string s, int no) => {
	// 用Task異步執行這里的代碼
})("Example", 8);

// 異步Task執行完之后的代碼  

對象即時初始化

  大家知道.NET為我們提供了匿名對象,這使用我們可以像在JavaScript里面一樣隨意的創建我們想要對象。但是別忘了,JavaScript里面可以不僅可以放入數據,還可以放入方法,.NET可以么?要相信,Microsoft不會讓我們失望的。

//Create anonymous object
var person = new {
	Name = "Jesse",
	Age = 28,
	Ask = (string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42!");
	}
};

//Execute function
person.Ask("Why are you doing this?");

  但是如果你真的是運行這段代碼,是會拋出異常的。問題就在這里,Lambda表達式是不允許賦值給匿名對象的。但是委托可以,所以在這里我們只需要告訴編譯器,我是一個什么類型的委托即可。

var person = new {
	Name = "Florian",
	Age = 28,
	Ask = (Action<string>)((string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42!");
	})
};

  但是這里還有一個問題,如果我想在Ask方法里面去訪問person的某一個屬性,可以么?

var person = new
{
                Name = "Jesse",
                Age = 18,
                Ask = ((Action<string>)((string question) => {
                    Console.WriteLine("The answer to '" + question + "' is certainly 20. My age is " + person.Age );
                }))
};

  結果是連編譯都通不過,因為person在我們的Lambda表達式這里還是沒有定義的,當然不允許使用了,但是在JavaScript里面是沒有問題的,怎么辦呢?.NET能行么?當然行,既然它要提前定義,我們就提前定義好了。

dynamic person = null;
person = new {
	Name = "Jesse",
	Age = 28,
	Ask = (Action<string>)((string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42! My age is " + person.Age + ".");
	})
};

//Execute function
person.Ask("Why are you doing this?");  

運行時分支

  這個模式和自定義型方法有點類似,唯一的不同是它不是在定義自己,而是在定義別的方法。當然,只有當這個方法基于屬性定義的時候才有這種實現的可能。

public Action AutoSave { get; private set; }

public void ReadSettings(Settings settings)
{
	/* Read some settings of the user */

	if(settings.EnableAutoSave)
		AutoSave = () => { /* Perform Auto Save */ };
	else
		AutoSave = () => { }; //Just do nothing!
}

  可能有人會覺得這個沒什么,但是仔細想想,你在外面只需要調用AutoSave就可以了,其它的都不用管。而這個AutoSave,也不用每次執行的時候都需要去檢查配置文件了。

總結

  Lambda表達式在最后編譯之后實質是一個方法,而我們聲明Lambda表達式呢實質上是以委托的形式傳遞的。當然我們還可以通過泛型表達式Expression來傳遞。通過Lambda表達式形成閉包,可以做很多事情,但是有一些用法現在還存在爭議,本文只是做一個概述 :),如果有不妥,還請拍磚。謝謝支持 :)

還有更多Lambda表達式的新鮮玩法,請移步: 背后的故事之 - 快樂的Lambda表達式(二)

 原文鏈接: http://www.codeproject.com/Articles/507985/Way-to-Lambda


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()