程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C# 3.0語言新特性(語言規范):3 拉姆達表達式

C# 3.0語言新特性(語言規范):3 拉姆達表達式

編輯:關於C語言

原文:《C# Version 3.0 Specification》,Microsoft
翻譯:lover_P
C# 2.0中引入了匿名方法,允許在期望出現委托的時候以“內聯(in-line)”的代碼替代之。盡管匿名方法提供了函數式編程語言中的很多表達能力,但匿名方法的語法實在是太羅嗦了,並且很不自然。拉姆達表達式(Lambda expressions)為書寫匿名方法提供了一種更加簡單、更加函數化的語法。

拉姆達表達式的書寫方式是一個參數列表後跟=>記號,然後跟一個表達式或一個語句塊。

expression:
assignment
non-assignment-expression

non-assignment-expression:
conditional-expression
lambda-expression
query-expression

lambda-expression:
(  lambda-parameter-listopt  )  =>  lambda-expression-body
implicitly-typed-lambda-parameter  =>  lambda-expression-body

lambda-parameter-list:
explicitly-typed-lambda-parameter-list
implicitly-typed-lambda-parameter-list

explicitly-typed-lambda-parameter-list:
explicitly-typed-lambda-parameter
explicitly-typed-lambda-parameter-list  ,  explicitly-typed-lambda-parameter

explicitly-typed-lambda-parameter:
parameter-modifieropt  type  identifIEr

implicitly-typed-lambda-parameter-list:
implicitly-typed-lambda-parameter
implicitly-typed-lambda-parameter-list  ,  implicitly-typed-lambda-parameter

implicitly-typed-lambda-parameter:
identifIEr

lambda-expression-body:
expression
block

拉姆達表達式的參數可以具有顯式的或隱式的類型。在一個具有顯式類型的參數列表中,每個參數的類型都是顯式聲明的。在一個具有隱式類型的參數列表中,參數的類型是從拉姆達表達式出現的上下文中推斷出來的——具體來說,是當拉姆達表達式被轉換為一個兼容的委托類型時,該委托類型提供了參數的類型。

當拉姆達表達式只有一個具有隱式類型的參數時,參數列表中的括號可以省略。換句話說,下面這種形式的拉姆達表達式:

(  param  )  =>  expr

可以簡寫為:

param  =>  expr

下面給出的是拉姆達表達式的一些例子:

x => x + 1                         // 隱式類型,以表達式作為拉姆達表達式體

x => { return x + 1; }          // 顯式類型,以語句塊作為拉姆達表達式體

(int x) => x + 1                 // 顯式類型,以表達式作為拉姆達表達式體

(int x) => { return x + 1; }  // 顯式類型,以語句塊作為拉姆達表達式體

(x, y) => x * y                  // 多個參數

() => Console.WriteLine()      // 沒有參數

通常,C# 2.0規范中提到的匿名方法規范同樣適用於拉姆達表達式。拉姆達表達式是匿名方法在功能行上的超集,提供了下列附加的功能:

l          拉姆達表達式允許省略參數類型並對其進行推斷,而匿名方法要求參數類型必須顯式地聲明。

l          拉姆達表達式體可以是表達式或語句塊,而匿名方法體只能是語句塊。

l          在類型參數推導和方法重載抉擇時,拉姆達表達式可以被作為參數傳遞。

l          以一個表達式作為表達式體的拉姆達表達式可以被轉換為表達式樹。

注意

PDC 2005技術預覽版編譯器並不支持以一個語句塊作為表達式體的拉姆達表達式。當必需一個語句塊時,請使用C# 2.0中的匿名方法語法。

3.1 拉姆達表達式轉換

和匿名方法表達式類似,拉姆達表達式可以歸類為一種擁有特定轉換規則的值。這種值沒有類型,但可以被隱式地轉換為一個兼容的委托類型。特別地,當滿足下列條件時,委托類型D兼容於拉姆達表達式L:

l          D和L具有相同數量的參數。

l          如果L具有顯式類型的參數列表,D中每個參數的類型和修飾符必須和L中相應的參數完全一致。

l          如果L具有隱式類型的參數列表,則D中不能有ref或out參數。

l          如果D具有void返回值類型,並且L的表達式體是一個表達式,若L的每個參數的類型與D的參數一致,則L的表達式體必須是一個可接受為statement-expression的有效表達式。

l          如果D具有void返回值類型,並且L的表達式體是一個語句塊,若L的每個參數的類型與D的參數一致,則L的表達式體必須是一個有效語句塊,並且該語句塊中不能有帶有表達式的return語句。

l          如果D的返回值類型不是void,並且L的表達式體是一個表達式,若L的每個參數的類型與D的參數一致,則L的表達式體必須是一個可以隱式轉換為D的返回值類型的有效表達式。

l          如果D的返回值類型不是void,並且L的表達式體是一個語句塊,若L的每個參數的類型與D的參數一致,則L的表達式體必須是一個有效的語句塊,該語句塊不能有可達的終點(即必須有return語句,譯者注),並且每個return語句中的表達式都必須能夠隱式轉換為D的返回值類型。

後面的例子將使用一個范型委托Func<A, R>,表示一個函數,它具有一個類型為A的參數,返回值類型為R:

delegate R Func<A, R>(A arg);

在下面的賦值中:

Func<int, int> f1 = x => x + 1;           // Ok

Func<int, double> f2 = x => x + 1;        // Ok

Func<double, int> f3 = x => x + 1;        // Error

每個拉姆達表達式的參數和返回值類型通過將拉姆達達表達式賦給的變量的類型來檢測。第一個賦值將拉姆達表達式成功地轉換為了委托類型Func<int, int>,因為x的類型是int,x + 1是一個有效的表達式,並且可以被隱式地轉換為int。同樣,第二個賦值成功地將拉姆達表達式轉換為了委托類型Func<int, double>,因為x + 1的結果(類型為int)可以被隱式地轉換為double類型。然而,第三個賦值將會產生一個編譯期錯誤,因為x給定的類型是double,x + 1的結果(類型為double)不能被隱式地轉換為int。

3.2 類型推斷

當在沒有指定類型參數的情況下調用一個范型方法時,一個類型推斷過程回去嘗試為該調用推斷類型參數。被作為參數傳遞給范型方法的拉姆達表達式也會參與這個類型推斷過程。

最先發生的類型推斷獨立於所有參數。在這個初始階段,不會從作為參數的拉姆達表達式推斷出任何東西。然而,在初始階段之後,將通過一個迭代過程從拉姆達表達式進行推斷。特別地,當下列條件之一為真時將會完成推斷:

l          參數是一個拉姆達表達式,以後簡稱為L,從其中未得到任何推斷。

l          相應參數的類型,以後簡稱為P,是一個委托類型,其返回值類型包括了一個或多個方法類型參數。

l          P和L具有相同數量的參數,P中每個參數的修飾符與L中相應的參數一致,或者如果L具有隱式類型的參數列表時,沒有參數修飾符。

l          P的參數類型不包含方法類型參數,或僅包含於已經推斷出來的類型參數相兼容的一組類型參數。

l          如果L具有顯式類型的參數列表,當推斷出來的類型被P中的方法類型參數取代了時,P中的每個參數應該具有和L中相應參數一致的類型。

l          如果L具有隱式類型的參數列表,當推斷出來的類型被P中的方法類型參數取代了並且作為結果的參數類型賦給了L時,L的表達式體必須是一個有效的表達式或語句塊。

l          可以為L推斷一個返回值類型。這將在後面描述。

對於每一個這樣的參數,都是通過關聯P的返回值類型和從L推斷出的返回值類型來從其上進行推斷的,並且新的推斷將被添加到累積的推斷集合中。這個過程一直重復,直到無法進行更多的推斷為止。

在類型推斷和重載抉擇中,拉姆達表達式L的“推斷出來的返回值類型”通過以下步驟進行檢測:

l          如果L的表達式體是一個表達式,則該表達式的類型就是L的推斷出來的返回值類型。

l          如果L的表達式體是一個語句塊,若由該塊中的return語句中的表達式的類型形成的集合中恰好包含一個類型,使得該集合中的每個類型都能隱式地轉換為該類型,並且該類型不是一個空類型,則該類型即是L的推斷出來的返回值類型。

l          除此之外,無法從L推斷出一個返回值類型。

作為包含了拉姆達表達式的類型推斷的例子,請考慮System.Query.Sequence類中聲明的Select擴展方法:

namespace System.Query

{

    public static class Sequence

    {

        public static IEnumerable<S> Select<T, S>(

            this IEnumerable<T> source,

            Func<T, S> selector)

        {

            foreach(T element in source) yIEld return selector(element);

        }

    }

}

假設使用using語句導入了System.Query命名空間,並且定義了一個Customer類,具有一個類型為string的屬性Name,Select方法可以用於從一個Customer列表中選擇名字:

List<Customer> customers = GetCustomerList();

IEnumerable<string> names = customers.Select(c => c.Name);

對擴展方法Select的調用將被處理為一個靜態方法調用:

IEnumerable<string> names = Sequence.Select(customers, c => c.Name);

由於沒有顯式地指定類型參數,將通過類型推斷來推導類型參數。首先,customers參數被關聯到source參數,T被推斷為Customer。然後運用上面提到的拉姆達表達式類型推斷過程,C的類型是Customer,表達式c.Name將被關聯到selector參數的返回值類型,因此推斷S是string。因此,這個調用等價於:

Sequence.Select<Customer, string>(customers, (Customer c) => c.Name)

並且其返回值類型為IEnumerable<string>。

下面的例子演示了拉姆達表達式的類型推斷是如何允許類型信息在一個范型方法調用的參數之間“流動”的。對於給定的方法:

static Z F<X, Y, Z>(X value, Func<X, Y> f1, Func<Y, Z> f2) {

    return f2(f1(value));

}

下面這個調用:

double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => TotalSeconds);

的類型推斷過程是這樣的:首先,參數"1:15:30"被關聯到value參數,推斷X為string。然後,第一個拉姆達表達式的參數s具有推斷出來的類型string,表達式TimeSpan.Parse(s)被關聯到f1的返回值類型,推斷Y是System.TimeSpan。最後,第二個拉姆達表達式的參數t具有推斷出來的類型System.TimeSpan,並且表達式t.TotalSeconds被關聯到f2的返回值類型,推斷Z為double。因此這個調用的結果類型是double。

3.3 重載抉擇

參數列表中的拉姆達表達式將影響到特定情形下的重載抉擇(也稱重載分析,重載解析等,即從幾個重載方法中選擇最合適的方法進行調用的過程,譯者注)。

下面是新添加的規則:對於拉姆達表達式L,且其具有推斷出來的返回值類型,當委托類型D1和委托類型D2具有完全相同的參數列表,並且將L的推斷出來的返回值類型隱式轉換為D1的返回值類型要優於將L的推斷出來的返回值類型隱式轉換為D2的返回值類型時,稱L到D1的隱式轉換優於L到D2的隱式轉換。如果這些條件都不為真,則兩個轉換都不是最優的。

下面的例子講解了這一規則。

class ItemList<T> : List<T>

{

    public int Sum<T>(Func<T, int> selector) {

        int sum = 0;

        foreach(T item in this) sum += selector(item);

        return sum;

    }

 

    public double Sum<T>(Func<T, double> selector) {

        double sum = 0;

        foreach(T item in this) sum += selector(item);

        return sum;

    }

}

ItemList<T>有兩個Sum方法。每個都帶有一個selector參數,用於從列表項目中依次選取值進行求和。選擇的值或者是int或者是double,結果也相應的是int或double。

可以使用Sum方法來根據一份產品明細表對一個訂單進行求和:

class Detail

{

    public int UnitCount;

    public double UnitPrice;

    ...

}

 

void ComputeSums() {

    ItemList<Detail> orderDetails = GetOrderDetails(...);

    int totalUnits = orderDetails.Sum(d => d.UnitCount);

    double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);

    ...

}

在對orderDetails.Sum的第一個調用中,兩個Sum方法都是可以的,因為拉姆達表達式d => d.UnitCount與Func<Detail, int>和Func<Detail, double>都兼容。然而,重載抉擇選用了第一個Sum方法,因為轉換到Func<Detail, int>要優於轉換到Func<Detail, double>。

在對orderDetails.Sum的第二個調用中,只有第二個Sum方法是可用的,因為拉姆達表達式d => d.UnitPrice * d.UnitCount產生的值的類型是double。因此重載抉擇選用第二個Sum方法進行調用。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved