.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),參考以下範例程式碼:
{
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"));
}
}
這個範例的執行結果參考如下:
圖 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後方沒有小括號,可以區別出它是屬性,方法名稱的後方需要有小括號。
{
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的寫法,參考以下範例程式碼:
{
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密集的工作上,比較簡單的運算邏輯不需要使用到非同步,本範例純粹為了說明):
{
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>型別即可,參考以下範例程式碼:
{
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:
{
[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」了,看了不免眼花瞭亂我們可以加上數字分隔符號(_)區隔之,參考以下範例程式碼:
public enum ShowOptions
{
Day = 0b0000_0001,
Night = 0b0000_0010,
WeekDay = 0b000_00100,
Holiday = 0b0000_1000
}
數字分隔符號(_)可以隨意出現多次,參考以下範例程式碼:
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屬性值:
{
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),參考以下範例程式碼:
{
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會記住參考:
{
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集合…等等。