糖衣下的秘密 – C# 語法糖

C# 並不是唯一一個提供語法糖的程式語言,但卻是少數敢持續在新版本中加進數量堪稱大量語法糖的語言,單以語法糖這點來說,C# 算是發揮得相當極致。

文/黃忠成

  所謂語法糖,指的是加進新的語法規則,把原本需要多行的程式碼濃縮進去,就語意上來說,應該與原本的程式碼無異,但事實是如此嗎? 那可不見得,由於編譯器的分叉處理,通常語法糖會走向新的叉路,所以期待它長出濃縮前完全一致的程式碼,是很不現實的天真想法,在某些情況下甚至會更糟,這都是預期內的結果,這也是編譯器需要不停地調整及更新的緣故,這點C# 倒是做的不錯。  

  我並不是很喜歡使用新事物,原因是不想承受那種不安的感覺,基於個人興趣的關係,對於底層的東西總會多花點時間去了解,當了解越多,越理解到把一件事簡單化必定帶來意料外產物這個道理。這次由於重灌電腦,電腦中不想 安裝那麼多個Visual Studio,所以只留了2017,既然用都用了,也順便把一些語法糖引入舊有程式中,本文以下面這個例子來作主軸。

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

namespace ConsoleApp3
{
    class Program
    {
        private double _value;

        public double Value
        {
            get
            {
                return _value;
            }
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            Random r = new Random();
            p._value = r.Next(1000);
        }
    }
}

這個程式中沒有新的語法糖,我的舊程式裡有很多類似的property get/set寫法,由於只用Visual Studio 2017,也就順手調整了其中幾個,變成下面的寫法。

class Program
{
        private double _value;

        public double Value => _value;

        static void Main(string[] args)
        {
            Program p = new Program();
            Random r = new Random();
            p._value = r.Next(1000);
        }
}

好,那問題來了,請問這兩種屬性存取方式產生的結果是一樣的嗎?這視乎你所謂的結果是什麼,如果是Value的值毫無疑問兩者皆是取用_value,但如果你的結果是過程的話,那可不一定。我會發現過程不同是因為舊程式裡有段作業需要效能,只要是在0.001 ms以上的差距都會影響其效能的表現,該程式在我引入新語法後有了劇烈的變化,致使我追尋其原因,看下面這個測速的例子。

class Program
{
        private double _value;

        public double Value => _value;

        public double NoSugar
        {
            get
            {
                return _value;
            }
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            Random r = new Random();
            p._value = r.Next(1000);

            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 20000000; i++)
            {
                double s = p.Value;
            }
            sw.Stop();

            Console.WriteLine($"Sugar:{sw.ElapsedTicks}");
            sw.Reset();

            sw.Start();
            for (int i = 0; i < 20000000; i++)
            {
                double s = p.NoSugar;
            }
            sw.Stop();

            Console.WriteLine($"No Sugar:{sw.ElapsedTicks}");
            Console.Read();

        }
}

你覺得結果是什麼?

訝異嗎?這代表編譯器產生了不同的程式碼,只是如果使用大多數的反編譯回C#的工具,你多數會得到同樣的程式碼,但只要由IL角度去看,一切就明瞭了。

會影響最後程式碼的除了語法糖外,還有Debug/Release的差異,上面是在Debug模式下的結果,下面是Release。

當然,我們大可以說只要切到Release模式就沒事了(當然,如果你認真的話,會發現就算執行同一種語法,時間也會有些許差距,這是正常的,因為電腦環境及Runtime執行期不同,有些微差距是可以理解的),只是有趣的是,為何新語法糖在Debug/Release會產生同樣的程式碼,而傳統寫法不會?這唯一能解釋的就只有兩種可能,一種是舊的Debug機制需要而產生多餘的程式碼,一種是新的語法糖認為那些多餘的程式碼對於近代的Debug是無用的。

不管如何,不要期待語法糖會產生出與濃縮前一樣的程式碼。

PS: 多數情況下,這種效能差異是可以被忽略的,除非你真的連ms以下都要計較。