C# 7新功能概覽 - 3

by vivid 23. 八月 2017 16:28

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170818602
出刊日期: 2017/8/23

本文將延續本站《C#7新功能概覽 - 1》、《C#7新功能概覽 - 2》文章的說明,介紹C# 7 新增的新語法,並利用一些範例來了解這些語法。

區域函式(Local Function)

使用過JavaScript程式語言的設計師一定相當熟悉在函式之中宣告函式,現在C# 也擁有類似的功能了。以一個範例來說明,在C# 6 除了匿名函式這種特殊案例之外,標準的方法只能在類別之中宣告,例如以下的SayHi方法:

class Program
{
  
    static void Main(string[] args)
    {
        Console.WriteLine(SayHi("mary"));
    }

    static string SayHi(string s)
    {
        return $"Hi, {s}";
    }
}

 

而在C# 7中,可以這樣改寫,在Main方法中直接宣告SayHi方法,這就叫區域函式(Local Function),參考以下範例程式碼:

class Program
{
     static void Main(string[] args)
     {
         string SayHi(string s)
         {
             return $"Hi, {s} ";
         }
         Console.WriteLine(SayHi("Mary"));
     }
}

區域函式(Local Function)可以直接存取它的外部函式宣告的變數值,例如以下範例程式碼,SayHi方法可以使用到外部Main方法宣告的today變數:

class Program
{
    static void Main(string[] args)
    {
        string today = DateTime.Now.ToShortDateString();

        string SayHi(string s)
        {
            return $"Hi, {s} , today is {today}";
        }
        Console.WriteLine(SayHi("Mary"));
    }
}

 

這個範例的執行結果參考如下:

 

clip_image002

圖 1:使用區域函式(Local Function)。

理所當然,在區域函式(Local Function)中定義的變數,有效範例隸屬區塊或函式等級,因此以下範例程式碼便無法在區域函式SayHi所在的外部函式Main方法讀取到SayHi中定義的name變數,而發生編譯錯誤:

class Program
{
    static void Main(string[] args)
    {
        string SayHi(string s)
        {
            var name = s;
            return $"Hi, {s} ";
        }

        Console.WriteLine(name); //Error
        Console.WriteLine(SayHi("Mary"));
    }
}

 

Expression-bodied Member

在C# 6定義類別的方法(Method)與屬性(Auto Property)時,可以直接使用Lambda Expression,讓程式又變短了,此語法稱之為Expression-bodied member,可以應用在定義方法(Expression-bodied method)與屬性(Expression-bodied property)。撰寫Expression-bodied method時,語法結構如下列範例的GetName()方法,在「=>」符號右方直接撰寫方法程式碼,若方法有回傳值,連return關鍵字都可以省略。而Expression-bodied property語法類似Expression-bodied method,如下範例中的FullName屬性,因為FullName後方沒有小括號,可以區別出它是屬性,方法名稱的後方需要有小括號。

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee();
        emp.LastName = "Lee";
        emp.FirstName = "Mary";
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

Expression-bodied constructor

C# 則擴充了Expression-bodied member,可以應用在建構函式(Constructor)、解構函式(Finalizer),以及含getter與setter的屬性語法。參考以下定義Expression-bodied constructor範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary");
        emp.LastName = "Lee";
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName) => FirstName = fName;

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

 

因為Expression-bodied member是用在只有一行程式碼的情境,若Employee的建構函式需要傳入兩個以上的參數,然後在方法中寫兩行程式碼來做初始設定,則Visual Studio將無法編譯程式碼。目前取代的作法是利用Tuple物件Deconstrunting的功能來初始化屬性值,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

但據說目前的Roslyn編譯器針對這樣的寫法,程式的效能不是很好,可能會再Visual Studio 2017 Update 1時有所修訂,改善其執行效能,可參閱以下的討論串:https://github.com/dotnet/roslyn/issues/16869#issuecomment-280800193

Expression-bodied Finalizer

解構函式也可以改用Expression-bodied Finalizer的寫法,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

Expression-bodied get / set accessor

在C# 6定義屬性時,可以利用get存取子,撰寫讀取屬性的程式碼;set存取子則用來撰寫設定資料的程式碼,例如以下的FirstName屬性:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
        Console.WriteLine(emp.FirstName); // Mary
    }
}
class Employee
{
    public Employee(string fName, string lName)
    {
        FirstName = fName;
        LastName = lName;
    }
    private string defaultFName;
    public string FirstName
    {
        get { return defaultFName; }
        set { defaultFName = value; }
    }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

在C# 7則可以改用Expression-bodied get / set accessor語法,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
        Console.WriteLine(emp.FirstName); // Mary

    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    // Expression-bodied get / set accessors.
    private string defaultFName;
    public string FirstName {
        get => defaultFName;
        set => defaultFName = value;
    }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

丟出例外

在C# 6 的throw屬於陳述式(Statement),不是運算式(Expression),而C# 陳述式(Statement)都是以分號結尾,例如「int i = 10;」而運算式(Expression)通常會計算出一個值,例如「1+2」。參考以下範例程式碼,若使用LINQ Find()方法查詢集合資料,找不到滿足的資料時,將回傳null值,在C# 6,我們需要另外再利用if程式碼判斷是否要丟出例外錯誤:

class Program
{
    static void Main(string[] args)
    {
        List<Employee> list = new List<Employee>(){
            new Employee { Name = "Mary" },
            new Employee { Name = "Candy"},
            new Employee { Name = "Amy" }
        };

        var r = list.Find(s => s.Name.StartsWith("a"));
        if (r == null)
        {
            throw new InvalidOperationException("Could not find data");
        }
    }
}
class Employee
{
    public string Name { get; set; }
}

 

 

而在C# 7 則可以改寫如下得到一樣的效果:

class Program
{
    static void Main(string[] args)
    {

        List<Employee> list = new List<Employee>(){
            new Employee { Name = "Mary" },
            new Employee { Name = "Candy"},
            new Employee { Name = "Amy" }
        };
        var r = list.Find(s => s.Name.StartsWith("a")) ?? throw new InvalidOperationException("Could not find data");
    }
}
class Employee
{
    public string Name { get; set; }
}

 

非同步方法回傳型別

在C# 6 一個非同步方法(async method)的回傳值只能是void、Task或Task<T>型別,若回傳Task或Task<T>,這兩種都是參考型別物件,因此記憶體的配置會比單純的實值型別(Value Type)來得多,這可能會造成效能上的瓶頸。參考以下範例程式碼,AddAsync非同步方法利用Task物件進行非同步的兩數相加的運算(非同步運算通常是應用在I/O或CPU密集的工作上,比較簡單的運算邏輯不需要使用到非同步,本範例純粹為了說明):

class Program
{
     static void Main(string[] args)
     {
         Task<int> task = AddAsync(10, 20);
         Console.WriteLine($"Result : {task.Result}");
     }
     static async Task<int> AddAsync(int x, int y)
     {
         var r = await Task.Run(() => x + y);
         return r;
     }
   
}

 

而C# 7的非同步方法現在則是允許你回傳一個較為輕量的實值型別,取代回傳一個參考型別物件,這樣可以有效的改善記憶體的使用。只要型別提供一個可存取的GetAwaiter()方法,就可以當作非同步方法的回傳型別。

不過要使用這個功能,需要先在專案中安裝「System.Threading.Tasks.Extensions 」套件,使用Nuget套件管理員下載與安裝「System.ValueTuple」套件的步驟如下,從Visual Studio 2017開發工具「Solution Explorer」視窗選取專案名稱。再從「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入install-package指令:

Install-Package System.Threading.Tasks.Extensions

然後讓你的非同步方法,回傳ValueTask<TResult>型別即可,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        var value = AddAsync(10, 20);
        Console.WriteLine($"Result : {value.Result}");
    }
    static async ValueTask<int> AddAsync(int x, int y)
    {
        var r = await Task.Run(() => x + y);
        return r;
    }
}

 

數值表達語法

有時我們會需要進行一些位元的運算,像是搭配 [Flags] Attribute來設計多選效果。可參閱本站此篇文章《http://blogs.uuu.com.tw/Articles/post/2017/06/14/使用列舉與旗標設計多選.aspx》來了解更多設計的細節。以此篇文章的範例舉例,以下定義一個列舉型別,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),例如以下C# 6 語法程式碼,列舉上方套用[Flags] Attribute,而Main方法中宣告一個myOptions變數,使用「|」(OR)運算子設定兩個值Day與Night:

class Program
{
    [Flags]
    public enum ShowOptions
    {
        Day = 1,
        Night = 2,
        WeekDay = 4,
        Holiday = 8
    }
    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

 

若是C# 7可以使用「0b」開始來表達位元資料,它代表二進位(Binary)數字:

class Program
{
    [Flags]
    public enum ShowOptions
    {
        Day = 0b00000001,
        Night = 0b00000010,
        WeekDay = 0b00000100,
        Holiday = 0b00001000
    }

    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

 

程式中太多「0」了,看了不免眼花瞭亂我們可以加上數字分隔符號(_)區隔之,參考以下範例程式碼:

[Flags]
public enum ShowOptions
{
    Day = 0b0000_0001,
    Night = 0b0000_0010,
    WeekDay = 0b000_00100,
    Holiday = 0b0000_1000
}

數字分隔符號(_)可以隨意出現多次,參考以下範例程式碼:

[Flags]
public enum ShowOptions
{
     Day = 0b00_000_001,
     Night = 0b00_000_010,
     WeekDay = 0b0_000_100,
     Holiday = 0b00_001_000
}

 

除了int、long型別之外,數字分隔符號(_)還可以使用在decimal、float、double等等型別,參考以下範例程式碼:

double salary1 = 22_000.00;
Console.WriteLine(salary1);
float salary2 = 22_000.00F;
Console.WriteLine(salary2);
decimal salary3 = 22_000.00M;
Console.WriteLine(salary3);

Ref locals and returns(傳參考區域變數與傳參考回傳值)

在C# 中方法提供ref;out類型的參數,允許傳遞參考(Pass by Reference),而新的C# 7「Ref locals and return(傳參考區域變數與傳參考回傳值)」功能現在讓方法可以回傳變數的參考(Return by reference),這個功能可以讓程式儘量避免複製值的動作,取而代之的是,可以透過參考(Reference)直接存取到特定記憶體內容,這樣可以讓程式更有效率,特別適用在數學計算上,例如回傳矩陣(Matrix)某個項目的參考到呼叫端。

參考以下範例程式碼,建立一個Employee物件,設定Age屬性為「50」,接著將Employee物件傳到GetAge方法,這個方法會回傳Age屬性值:

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        int GetAge(Employee emp)
        {
            return emp.Age;
        }
        int age = GetAge(employee);
        Console.WriteLine(age);
        age = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

這個範例一執行,Main方法中兩個Console.WriteLine印出的答案分別是「50」、「50」。在C# 7我們可以撰寫程式碼,回傳欄位(Field)的參考(Reference),參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        ref int GetAge(Employee emp)
        {
            return ref emp.Age;
        }
        int x = GetAge(employee);
        Console.WriteLine(x);
        x = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

在GetAge方法使用ref 關鍵字,然後在方法中return 這行也必需要加上ref關鍵字,表示要回傳參考(return by reference),否則編譯將會失敗。

這次執行程式碼,兩個Console.WriteLine印出的答案分別是「50」、「50」。因為「int x」這行變數宣告,會將方法中「return ref」這行回傳的emp.Age內的值,複製到x變數的記憶體(by value),所以當你修改x為「999」時,是修改到x記憶體的內容,不是emp.Age的內容。x此時不算是傳參考區域變數(ref locals)。

參考以下範例程式碼,現在在x區域變數明確地加上「ref」修飾詞;這種區域變數稱做「參考區域變數(ref locals)」,讓方法回傳參考時,x會記住參考:

 

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        ref int GetAge(Employee emp)
        {
            return ref emp.Age;
        }
        ref int x = ref GetAge(employee);
        Console.WriteLine(x);
        x = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

這次執行程式,兩個Console.WriteLine印出的答案分別是「50」、「999」。當你修改x的值為「999」時,這次是修改到emp.Age記憶體的內容。

特別要注意,宣告ref 變數時,需要順帶初始化,例如這行程式碼:

ref int x = ref GetAge(employee);

不可以將宣告和初始化分開寫兩行,以下程式碼將不能夠編譯:

ref int x = 0;
x = ref GetAge(employee);

另外宣告x區域變數這行程式,也可以改用var語法宣告,讓編譯器自動判斷型別:

ref var x = ref GetAge(employee);

目前「傳參考區域變數與傳參考回傳值(Ref locals and returns)」功能只可用在變數、物件欄位(Field),陣列(Array),不可以用在物件屬性(Property)、事件(Event)、List集合…等等。

Tags:

.NET Magazine國際中文電子雜誌 | C# | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List