談C# 編譯器編譯前的程式碼擴展行為

從2000年C#誕生以來,至今已經過了11年多了,C#的版本號也來到了4.0,在筆者所接觸的語言中,C#算是一個相當具有活力的程式語言,設計者Anders Hejlsberg的大膽且創新的特質充分的反映在這個語言上,

其每次的改版都會出現許多大膽的嘗試,目的是為了讓程式設計師能更快速、簡潔的運用C#來完成一些過去看來很複雜的動作,當然!所謂的大膽及創新的嘗試,通常也會引起正反兩面的意見。

本文列出一些C#中可以減少程式設計師撰寫程式碼數量的特色(語法、指令),其中有些是大家已經耳熟能詳的

 

C# 編譯器

 

  從2000年C#誕生以來,至今已經過了11年多了,C#的版本號也來到了4.0,在筆者所接觸的語言中,C#算是一個相當具有活力的程式語言,設計者Anders Hejlsberg的大膽且創新的特質充分的反映在這個語言上,

其每次的改版都會出現許多大膽的嘗試,目的是為了讓程式設計師能更快速、簡潔的運用C#來完成一些過去看來很複雜的動作,當然!所謂的大膽及創新的嘗試,通常也會引起正反兩面的意見。

  本文列出一些C#中可以減少程式設計師撰寫程式碼數量的特色(語法、指令),其中有些是大家已經耳熟能詳的,只是為了文章的完整性,就請容我多廢話一會。

 

 

Property

 

  C#誕生的年代,正是物件導向程式語言蓬勃發展的年代,因此C#大膽地採用與Delphi相同的概念,提供了定義屬性及事件的專屬語法,每個屬性在通過編譯器編譯後,其實都隱含著兩個函式,set與get,在IL層級中,

設定屬性其實是通過呼叫set這個函式來完成的,而取值則是通過get,下圖是C#寫法,及其被編譯為IL的樣子。

圖1

由於編譯器會為屬性產生set_屬性名或是get_屬性名的函式,因此如果設計師想命名同樣的函式,C#編譯器就會發出抱怨。

圖2

不管屬性是否提供set,都不會影響這個結果。

圖3

 

 

Auto-Implemented Properties

 

  C# 3.0添加了Auto-Implemented Properties語法,讓屬性的定義方式更加簡潔。

圖4

由圖4可以看到,C#編譯器自動產生了兩個private變數,你應該也注意到了其變數名是以<開頭的,這在C#語法中是不被允許的(IL則放寬許多),所以不會發生使用者宣告

的變數與編譯器所產生的變數衝突的情況。

而Auto-Implemented Properties有個限制,那就是屬性必須要同時擁有get跟set才能使用這個語法。

 

Object Initializer

 

Object Initializer語法出現在C# 3.0,與其一併出現的還有Collection Initializer,此處只列出Object Initializer。

圖5

其實就是展開,沒啥特別的。

 

Event

 

  事件的專屬語法在C# 1.0 就存在了,與屬性相同,其也是展開為兩個函式,add與remove。

圖6

保留函式名部分與Property類似。

圖7

而Auto-Implemented Event特色其實在C#初版就已經存在了,如圖8。

圖8

 

 

var

 

  var宣告式在C# 3.0出現,用法如圖9。

圖9

我給了這種宣告一個名詞,叫右決議式,有興趣深入探討這個語法的讀者可參考我的另一篇文章。

http://www.dotblogs.com.tw/code6421/archive/2010/01/23/13225.aspx

 

 

dynamic

 

   dynamic宣告式出現在C# 4.0,主要是提供Late-Binding功能,用法如圖10。

圖10

在筆者的另一篇文章中也探討了此宣告式。

http://www.dotblogs.com.tw/code6421/archive/2010/01/23/13225.aspx

其實不用想太多,就是Reflection。

 

using

 

  using區段在C# 1.2 (Visual Studio 2003)時出現,主要是簡化try..finally寫法,如圖11。

圖11

 

 

yield

 

  yield指令出現在C# 2.0,主要是簡化IEnumerator/IEnumerable所帶來的繁瑣建置動作,這需要較多的說明,假設我們有下面這個資料類別。


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

namespace TraditionEnumerable
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

需求是這個類別產生的物件會被放到一個Collection中,而我們必須在這個Collection中找到與條件相符的物件,例如Name屬性中包含特定字元的物件,此時

可能會寫下類似下列的程式碼。


static List<Person> GenerateData()
{
            List<Person> result = new List<Person>();
            result.Add(new Person() { Name = "code6421", Age = 15 });
            result.Add(new Person() { Name = "tom", Age = 16 });
            result.Add(new Person() { Name = "mary", Age = 17 });
            result.Add(new Person() { Name = "david", Age = 18 });
            result.Add(new Person() { Name = "baden", Age = 15 });
            result.Add(new Person() { Name = "rich", Age = 19 });
            result.Add(new Person() { Name = "jad", Age = 25 });
            result.Add(new Person() { Name = "peter", Age = 35 });
            result.Add(new Person() { Name = "mark", Age = 29 });
            result.Add(new Person() { Name = "moce", Age = 28 });
            result.Add(new Person() { Name = "jerry", Age = 21 });
            return result;
}

static Person[] TraditionFilter(List<Person> source, string partName)
{
            List<Person> result = new List<Person>();
            foreach (var item in source)
            {
                if (item.Name.Contains(partName))
                    result.Add(item);
            }
            return result.ToArray();
}

static void TestTrnditionFilter()
{
            List<Person> source = GenerateData();
            foreach (var item in TraditionFilter(source, "e"))
                Console.WriteLine(item.Name);
            Console.ReadLine();
}

請注意,這是設想在C#尚未支援LINQ時的解法。

程式碼本身並沒有問題,結果是完全正確的,但這種寫法缺乏擴充性,因為假設呼叫者只需要符合條件的前兩筆,這種寫法每次都會從頭找到尾,只是浪費時間而已。

所以,可以改成以下的寫法。


static Person[] TraditionFilter2(List<Person> source, string partName,int max)
{
            List<Person> result = new List<Person>();
            foreach (var item in source)
            {
                if (item.Name.Contains(partName))
                    result.Add(item);
                if (result.Count == max)
                    break;
            }
            return result.ToArray();
}

static void TestTrnditionFilter2()
{
            List<Person> source = GenerateData();
            foreach (var item in TraditionFilter2(source, "e", 2))
                Console.WriteLine(item.Name);
            Console.ReadLine();
}

多了max,可以解決呼叫者僅需要前幾筆資料的需求,但這種寫法仍然存在擴充性不足的問題,如果呼叫者想在找到特定資料後就停止往下找,那麼這種寫法就不適用了。

也就是說,原本是找A開頭的,但在巡覽(foreach)中看到了A1234,就想停止往下找時,這種寫法就完全不符合需求了。

  總歸一句,這兩種寫法之所以不符合需求的主因是,我們未把停止尋找的控制權交給使用者所致,最後可以改為以下版本。


static Person[] TraditionFilter3(List<Person> source, string partName, 
ref int startIndex, int max)
{
            List<Person> result = new List<Person>();
            int start = startIndex;
            for (int i = start; i < source.Count; i++)
            {
                startIndex = i;
                Person item = source[i];
                if (item.Name.Contains(partName))
                    result.Add(item);
                if (result.Count == max)
                    break;
            }
            startIndex++;
            if (startIndex >= source.Count)
                startIndex = -1;
            return result.ToArray();
}

static void TestTrnditionFilter3()
{
            List<Person> source = GenerateData();
            int startIndex = 0;
            do
            {
                Person[] result = TraditionFilter3(source, "e", ref startIndex, 2);
                foreach (var item in result)
                    Console.WriteLine(item.Name);
                ConsoleKeyInfo keyInfo = Console.ReadKey();
                if (keyInfo.Key == ConsoleKey.Enter)
                    continue;
                else
                    break;
            }
            while (startIndex != -1);
}

第三個版本雖然滿足了需求,但使用者需要自行管控startIndex這個變數,就便利性而言,這個版本並不完美。

要完美的版本,得利用IEnumerable/IEnumerator。

 


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

namespace TraditionEnumerable
{
    public class FilterIterator : IEnumerable<Person>
    {
        private IEnumerable<<Person>_innerList = null;
        private string _partName;

        public IEnumerator<Person> GetEnumerator()
        {
            return new FilterEnumerator(_innerList, _partName);
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return new FilterEnumerator(_innerList, _partName);
        }

        public FilterIterator(IEnumerable<Person> source, string partName)
        {
            _innerList = source;
            _partName = partName;
        }

        public class FilterEnumerator : IEnumerator<Person>        
{
            private IEnumerable<Person> _innerList = null;
            private IEnumerator<Person> _innerEnumerator = null;
            private Person _current = null;
            private string _partName;

            object System.Collections.IEnumerator.Current
            {
                get
                {
                    return _current;
                }
            }

            public Person Current
            {
                get
                {
                    return _current;
                }
            }

            public void Dispose()
            {
                if (_innerEnumerator != null)
                    _innerEnumerator.Dispose();
            }

            public bool MoveNext()
            {
                if (_innerEnumerator == null)
                    _innerEnumerator = _innerList.GetEnumerator();
                while (_innerEnumerator.MoveNext())
                {
                    if (_innerEnumerator.Current.Name.Contains(_partName))
                    {
                        _current = _innerEnumerator.Current;
                        return true;
                    }
                }
                return false;
            }

            public void Reset()
            {
                _current = null;
                if (_innerEnumerator != null)
                    _innerEnumerator.Dispose();
            }

            public FilterEnumerator(IEnumerable<Person> source, string partName)
            {
                _innerList = source;
                _partName = partName;
            }
        }
       
    }
}

static void TestTrnditionFilter4()
{
            List<Person> source = GenerateData();
            FilterIterator result = new FilterIterator(source, "e");
            int index = 0;
            foreach (var item in result)
            {
                Console.WriteLine(item.Name);
                index++;
                if (index % 2 == 0)
                {
                    Console.WriteLine("more?");
                    if (Console.ReadKey().Key == ConsoleKey.Enter)
                        continue;
                    else
                        break;
                }
            }
}

第四個版本解決了兩個問題,第一個是這個版本不再將資料由原Collection搬出至另一個Collection,所以減少了記憶體的耗用量,第二個是其實作了IEnumerable及IEnumerator,

可被直接使用於foreach語句中,說穿了,就是Iterator的Iterator(Enumerator的Enumerator)。

  但,自己實作這個Enumerable/Enumerator實在太費事了,所以C#提供了yield,幫設計師產生。

 


static IEnumerable<Person> FilterPerson(IEnumerable<Person> source, string partName)
{
            foreach (var item in source)
            {
                if (item.Name.Contains(partName))
                    yield return item;
            }
}

static void TestTrnditionFilter5()
{
            List<Person> source = GenerateData();
            int index = 0;
            foreach (var item in FilterPerson(source, "e"))
            {
                Console.WriteLine(item.Name);
                index++;
                if (index % 2 == 0)
                {
                    Console.WriteLine("more?");
                    if (Console.ReadKey().Key == ConsoleKey.Enter)
                        continue;
                    else
                        break;
                }
            }
}

圖12是yield所帶來的展開效果。

圖12

<FilterPerson>_d_b是C# 編譯器產生的類別,其實就是一個Enumerable/Enumerator。

圖13

C# 編譯器會把設計師寫在yield函式中的程式碼搬到這個新類別來,搬移方法是,將函式的參數產生為此類別的私有變數,產生一個this<>…變數來存放yield函式所在的物件。

  對這種大幅度的擴展,我有些疑慮,所以寫下了圖14的碼。

圖14

static應該很好處理,那member呢?

圖15

看來,得來點猛的。

圖16

呵呵,好可愛的編譯器不是?

 

 

Extension Method

 

  Extension Method出現在C# 3.0,主要功能是將一個函式於程式撰寫時期附加到另一個物件上去,請注意,是程式撰寫時期,所以它並沒有破壞物件導向中的封裝。

 


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

namespace ExtensionMethod.Ext
{
    public static class Ext
    {
        public static void ShowUpper(this string source)
        {
            Console.WriteLine(source.ToUpper());
        }
    }
}

使用端只要using這個ExtensionMethod.Ext命名空間,即可在string物件上使用這個ShowUpper函式。

圖17

由圖17可以看到,C# 編譯器會擴展Extension Method函式呼叫的部分。

由於Extension Method 的可見度是由Namespace控制,因此可延伸成另一種應用,設計師可建構兩個同名同參數且不同實作的Extension Method 於兩個命名空間中,

使用者於使用時可在僅更改命名空間引用部分,就可在彼此間切換,如下所示。

LogInt.cs


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

namespace ExtensionMethod
{
    public class LogInstance
    {
    }
}

LogFileExt.cs


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

namespace ExtensionMethod.Log.File
{
    public static class LogFileExt
    {
        public static void Log(this LogInstance c, string message)
        {
            Console.WriteLine("log to file");
        }
    }
}

LogMailExt.cs


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

namespace ExtensionMethod.Log.Mail
{
    public static class LogFileMail
    {
        public static void Log(this LogInstance c, string message)
        {
            Console.WriteLine("log to mail");
        }
    }
}

使用端可以是下面這樣。

 

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ExtensionMethod.Log.Mail;

namespace ExtensionMethod
{
    class Program
    {
        static void Main(string[] args)
        {
            LogInstance o = new LogInstance();
            o.Log("test");
            Console.Read();
        }
    }
}

只要將using ExtensionMethod.Log.Mail換成using ExtensionMethod.Log.File,便可引用到LogFileExt.cs中的Extension Method,而不需修改到內層的程式碼。

 


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ExtensionMethod.Log.File;

namespace ExtensionMethod
{
    class Program
    {
        static void Main(string[] args)
        {
            LogInstance o = new LogInstance();
            o.Log("test");
            Console.Read();
        }
    }
}

類似的應用出現在ASP.NET MVC/ASP.NET 4.0的Routing技巧。

 

 

LINQ Expression

 

  LINQ出現在C# 3.0中,當時這種將查詢語法整合至程式語言內的舉動是一個相當大膽的嘗試,在現在看來,這個做法確實減少了程式設計師重複撰寫查詢程式碼的時間。 

當查對LINQ語句與C#間的轉譯動作時,不難發現,LINQ Framework其實就是一群Extension Method的結合,對編譯器而言,所有的LINQ語句都會被轉譯為對Extension Method的呼叫。

圖18

所以圖18中的程式,也可以寫成圖19這樣。

圖19

 

LINQ To Objects Executing Model

 

  LINQ To Objects內層的做法與先前我們所寫的應用yield來進行查詢的手法類似,一樣都是以Iterator的Iterator來進行比對動作,以Where來說,它會建立一個對應提供資料來源的Iterator,

在其MoveNext函式中呼叫使用者所傳入的條件來進行比對動作,類似這種Iterator的Iterator也出現在Join、Group等函式呼叫上。

  也因為LINQ Framework是以Extension Method來建置的,所以設計師可以寫下以下的程式碼,替換掉LINQ預設的Where動作。

 

MyLinq.cs


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

namespace ProgressiveQuery.MyLinq
{
    public static class MyLinq
    {
        public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T,bool> predicate)
        {
            Console.WriteLine("query start");
            foreach (var item in source)
            {
                if(predicate(item))
                    yield return item;
            }
        }
    }
}

Program.cs


using System;
using System.Collections.Generic;
using System.Text;
using ProgressiveQuery.MyLinq;

namespace ProgressiveQuery
{
    class Program
    {

        static void Main(string[] args)
        {
            int[] list = new int[] { 1, 2, 3, 4, 5 };

            var result = from s1 in list where s1 > 3 select s1;
            foreach (var item in result)
                Console.WriteLine(item);
            Console.ReadLine();
        }
    }
}

執行結果如圖20。

圖20

與yield相同,在觸發Enumerator的MoveNext函式之前,比對動作都不會被執行。

 


using System;
using System.Collections.Generic;
using System.Text;
using ProgressiveQuery.MyLinq;

namespace ProgressiveQuery
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] list = new int[] { 1, 2, 3, 4, 5 };

            var result = from s1 in list where s1 > 3 select s1;
            Console.WriteLine("start foreach");
            foreach (var item in result)
                Console.WriteLine(item); 
       Console.ReadLine();
        }
    }
}

圖21

更精確地說,任何透過LINQ查詢式所傳回結果集來巡覽並取用元素的動作,都會觸發MoveNext,也就是會觸發比對動作。

我們也可以對LINQ 查詢式所傳回的結果集進行多次的查詢,也就是疊加式的查詢法。

 

MyLinq.cs


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

namespace ProgressiveQuery.MyLinq
{
    public static class MyLinq
    {
        public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T,bool> predicate)
        {
            Console.WriteLine("query start:" + source.GetType().ToString());
            foreach (var item in source)
            {
                Console.WriteLine("compare");
                if(predicate(item))
                    yield return item;
            }
        }
    }
}

 

Program.cs


using System;
using System.Collections.Generic;
using System.Text;
using ProgressiveQuery.MyLinq;

namespace ProgressiveQuery
{
    class Program
    {
        static void Second(IEnumerable<int> source)
        {
            var result = from s1 in source where s1 < 5 select s1;
            foreach (var item in result)
                Console.WriteLine(item);
        }

        static void Main(string[] args)
        {
            int[] list = new int[] { 1, 2, 3, 4, 5 };

            var result = from s1 in list where s1 > 3 select s1;
            Second(result);
            Console.ReadLine();
        }
    }
}

執行結果如圖22。

圖22

由圖22可以發現,在疊加式查詢下,比對動作是由最後一個查詢式開始,且進行7次的比對動作(為何是7次 ?因為雖然比對動作是由最後一個查詢式開始,但

最後一個查詢式所巡覽的是第一個查詢式的結果,而第一個查詢式會發生5次的比對動作,拋出兩個符合條件(4,5)的元素給最後一個查詢式比對,所以就是7次,

Iterator的Iterator是這個邏輯的關鍵)。

   另外有一點要特別注意,每次對擁有比對式的查詢結果集進行巡覽並取用元素的動作都會觸發比對動作(例如foreach),所以如果想對結果即進行重複的巡覽動作,

記得先呼叫ToList或是ToArray,這兩個函式會直接對該結果即進行巡覽及比對,進而產生出一個無比對式的查詢結果集,巡覽無比對式的結果集比巡覽有比對式的結果集快多了。

 

Lambda Expression

 

   Lambda Expression出現在C# 3.0,與LINQ同一時間誕生,其說穿了就是C# 2.0中匿名Delegate的加強版。

圖23

Lambda Expression強大的地方在於,它允許設計師在匿名delegate中直接取用外部的變數,此時編譯器會將該外部變數提出宣告為類別私有變數,這樣才能在

編譯器產生出的delegate中存取該變數。

但這種作法也帶來了後遺症,如下圖。

圖24

由於存取的是一個隨時會改變的foreach暫時參數,且Thread執行順序又不確定,所以會產生不正確的結果。

圖25

針對這種情況,應該使用傳遞state物件的方式。

 


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

namespace LambdaExpr
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] list = new int[] { 1, 2, 3, 4, 5 };
            foreach (var item in list)
            {
                Thread th = new Thread((state) =>
                    {
                        Console.WriteLine((int)state);
                    });
                th.Start(item);
            }
            Console.ReadLine();
        }
    }
}

 

LINQ To SQL/LINQ To Entity  Executing Model

 

  LINQ To SQL/LINQ To Entity與LINQ To Objects的執行模式略有不同,在LINQ To SQL/LINQ To Entity中,所呼叫的Extension Method是定義於Queryable中的,如下圖。

圖26

當C#編譯器看到LINQ查詢式的資料來源是實作IQueryable介面之物件時(LINQ To SQL與LINQ To Entity的Table/ObjectSet類別都實做了這個介面),會將查詢式轉譯為Expression Tree,

然後傳入接收Expression Tree的Extension Method中,此例就是Queryable.Where函式,其會回傳一個實作IEnumerable/IEnumerator介面的物件,在列舉動作發生時,也就是

IEnumerable的GetEnumerator函式被呼叫時,這些Expression Tree會被轉譯為SQL指令(在LINQ To SQL/LINQ To Entity情況下,LINQ To Objects會編譯該Expression Tree成Delegate)。

圖27

 

Expression Trees

 

  Expression Tree除了用在LINQ To SQL/LINQ To Entity之外,也可以用在動態產生delegate的情況下,如下所示。

 


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

namespace UsingExpr
{
    class Program
    {
        static void Main(string[] args)
        {
            ParameterExpression paramx = Expression.Parameter(typeof(int), "x");
            ParameterExpression paramy = Expression.Parameter(typeof(int), "y");
            BlockExpression body = Expression.Block(paramx, paramy, 
                      Expression.Add(paramx, paramy));
            Expression<Func<int, int, int>> result =
                      Expression.Lambda<Func<int, int, int>>(body, paramx, paramy);
            Func<int, int, int> func = result.Compile();
            Console.WriteLine(func(5, 5));
            Console.ReadLine();
        }
    }
}

執行結果如圖28。

圖28

 

Direct call with Expression Trees

 

  如果必要,設計師也可以跳過LINQ運算式,自行產生Expression Tree拋給LINQ TO SQL/LINQ To Entity來執行。

 


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

namespace DirectCallLQS
{
    class Program
    {
        static void Main(string[] args)
        {
            ParameterExpression param = Expression.Parameter(typeof(Customer), "s1");
            Expression<Func<Customer, bool>> expr =  
                       Expression.Lambda<Func<Customer, bool>>(Expression.Call(Expression.Property(
                param, typeof(Customer).GetProperty("CustomerID").GetGetMethod()), typeof(string).GetMethod("Contains"),
           new Expression[] { Expression.Constant("V", typeof(string)) }), new ParameterExpression[] { param });

            DataClasses1DataContext context = new DataClasses1DataContext();
            var result = context.Customers.Where(expr);
            foreach (var item in result)
                Console.WriteLine(item.CustomerID);
            Console.ReadLine();
        }
    }

執行結果如圖29。

圖29

 

LINQ To SQL/LINQ To Entity對於疊加式查詢的處理方法與LINQ To Objects不同,在呼叫GetEnumerator之前所疊加上去的查詢句,

最終都會被整合剖析為單一SQL 語句。

圖30