.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N191221401
出刊日期: 2019/12/11
隨著.NET Core 3與Visual Studio 2019開發工具的問市,C# 程式語言也演進到8.0版啦,C# 8 新增了許多有趣的功能,每次改版都希望讓程式碼能夠變短、再變短。在這篇文章中,讓我們來看看一些新增的語法。
唯讀結構成員(Readonly Struct Member)
結構(Struct)的成員可以宣告為唯讀(readonly),也就是說在定義時可以套用「readonly」關鍵字,表示它的成員是不可以被修改的。
參考以下範例程式碼,「Retangle」結構改寫「ToString」方法,顯示出「Retangle」的「Width」、「Height」屬性,以及「Area」屬性的值,「ToString」方法套用「readonly」關鍵字代表它是一個唯讀方法,不會變更其它屬性值:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
Retangle r = new Retangle( 10 , 20 );
Console.WriteLine( r ); // Retangle Width : 10 Height : 20 Area : 200
}
}
struct Retangle {
public Retangle( int w , int h ) {
Width = w;
Height = h;
}
public int Width { get; set; }
public int Height { get; set; }
public readonly int Area {
get {
return Width * Height;
}
}
public readonly override string ToString() {
return $" Retangle Width : {Width} Height : {Height} Area : { Area } ";
}
}
}
特別注意,「Area」屬性也要套用「readonly」關鍵字,否則編譯時會出現以下的警告訊息:
Call to non-readonly member 'Retangle.Area.get' from a 'readonly' member results in an implicit copy of 'this'.
這是因為「ToString」方法使用到了「Area」屬性的關係,由於「Area」屬性並不會修改「Area」屬性值,你可以為其套用「readonly」關鍵字。至於「Width」與「Height」兩個自動實作屬性(Auto-implemented properties)則不必特別加上「readonly」關鍵字,編譯器會自動將自動實作屬性的「getter」視為唯讀屬性。
預設介面實作(default interface implementation)
以往設計介面(Interface)時,我們總是遵循一套鐵律,介面一旦定義完成,就不要修改,以免破壞既有實作者的程式碼。介面只能包含定義的部分,不能實作。但現在隨著C# 8.0版問市之後,這些規則都將改變了。
在C# 8.0版後,宣告介面成員時,可以加上預設的實作。如此可讓介面因為新功能而改版時更為簡單,並在介面預設實作不符所需時,允許進行改寫。參考以下範例程式碼,展示一個簡單的介面範例,「ISpeak」介面包含一個「Speak」方法;「Person」類別則實作了這個方法:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
Person p = new Person();
p.Speak();
}
}
interface ISpeak {
void Speak();
}
class Person : ISpeak {
public void Speak() {
Console.WriteLine( "English" );
}
}
}
爾後若程式改版,想要在「ISpeak」介面新增一個方法,例如修改程式碼如下,「Speak」方法有一個多載的版本,可以傳入一個參數,參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
Person p = new Person();
p.Speak();
}
}
interface ISpeak {
void Speak();
void Speak( string lang );
}
class Person : ISpeak {
public void Speak() {
Console.WriteLine( "English" );
}
}
}
這個程式碼編譯時將會失敗,這是因為現有的「Person」類別未實作介面中所有的成員,如此介面加入新功能時,將會破壞既有實作者「Person」類別的程式碼,請參考下圖所示:

圖 1:介面新增將影響既有實作者。
現在我們可以在新增介面成員時,加上預設的實作,這樣就不會影響到既有實作者的程式碼,而既有的「Person」物件,若要叫用介面預設方法,需要轉型成介面,例如修改程式碼如下,這樣程式碼就可以順理的編譯、執行了,參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
Person p = new Person();
p.Speak(); // English
( (ISpeak) p ).Speak(); // English
( (ISpeak) p ).Speak( "Some Language" ); // Some Language
ISpeak p2 = new Person();
p2.Speak(); // English
p2.Speak( "Some Language" ); // Some Language
}
}
interface ISpeak {
void Speak();
void Speak( string lang ) => Console.WriteLine( lang );
}
class Person : ISpeak {
public void Speak() {
Console.WriteLine( "English" );
}
}
}
現在C# 8介面中可以定義靜態成員,包含:靜態欄位(static field)、靜態方法(static method)。同時介面成員可以設定任意成員存取修飾詞(Access Modifier),例如:「public」、「private」、「internal」、「protected」等等。
因為預設介面實作提供的功能較為跼限,你可以透過參數(Parameter)來增加設計上的彈性,例如我們的「ISpeak」介面「Speak」方法將文字列印到主控台時,想要前置一個「*」字元,我們可以改寫程式如下,利用靜態欄位設定預設的前置字元為「*」號,你也可以叫用「SetPrefix」靜態方法來設定前置字元為「@」號,參考以下範例程式碼:
using System;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
Person p = new Person();
( (ISpeak) p ).Speak( "Some Language" ); // *Some Language
ISpeak.SetPrefix( "@" );
ISpeak p2 = new Person();
p2.Speak( "Some Language" ); // @Some Language
}
}
interface ISpeak {
private static string prefix = "*";
public static void SetPrefix( string p ) {
prefix = p;
}
void Speak();
void Speak( string lang ) => Console.WriteLine( prefix + lang );
}
class Person : ISpeak {
public void Speak() {
Console.WriteLine( "English" );
}
}
}
using 宣告(Using declaration)
using 宣告(using declaration)是一個變數宣告的語法,前置「using」關鍵字,用來告知編譯器,在執行超出變數有效範圍時,自動釋放相關的資源。舉例來說,若有一個存取檔案的程式如下,在檔案不存在時,利用「File」類別的「Create」方法建立此檔案,並接著馬上將檔案刪除,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
if ( !File.Exists( "test.txt" ) ) {
FileStream f = File.Create( "test.txt" );
}
File.Delete( "test.txt" );
}
}
}
這個範例執行時將會發生例外錯誤,因為建立檔案後,未釋放檔案相關資源,所以想要刪除檔案時,就會得到例外錯誤,請參考下圖所示:

圖 2:未釋放檔案相關資源例外錯誤。
在C#之前的版本,我們可以將檔案建立語法包在「using」區塊之中,來解決這個問題,請參考以下範例程式碼,語法稍為囉嗦一些,但可以解決IOExeption問題,當程式執行超出「using」區塊,會自動叫用「IDisposable」介面的「Dispose」方法來釋放相關的檔案資源:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
if ( !File.Exists( "test.txt" ) ) {
using ( FileStream f = File.Create( "test.txt" ) ) {
};
}
File.Delete( "test.txt" );
}
}
}
而在C# 8版,我們可以使用using 宣告(using declaration)讓語法更為簡潔一些,以達到相同的效果,請參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
if ( !File.Exists( "test.txt" ) ) {
using FileStream f = File.Create( "test.txt" );
}
File.Delete( "test.txt" );
}
}
}
靜態區域函式(Static Local Function)
參考以下範例程式碼,在C# 7版中,可以在方法(Main)中直接宣告「SayHi」方法,這就叫區域函式(Local Function),但區域函式只可以是實體方法,不可以是靜態方法:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
string SayHi( string s ) {
return $"Hi, {s} ";
}
Console.WriteLine( SayHi( "Mary" ) );
}
}
}
在C# 8版中新增了靜態區域函式(Static Local Function),區域函式前方可以套用「static」關鍵字,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
static string SayHi( string s ) {
return $"Hi, {s} ";
}
Console.WriteLine( SayHi( "Mary" ) );
}
}
}
在C# 7中,區域函式(Local Function)可以直接存取它的外部函式宣告的變數值,例如以下範例程式碼,「SayHi」方法可以使用到外部「Main」方法宣告的「today」變數:
using System;
using System.IO;
namespace ConsoleApp1 {
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" ) ); // Hi, Mary , today is 11/5/2019
}
}
}
若將「SayHi」改寫成C# 8 的區域函式(Local Function),參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
string today = DateTime.Now.ToShortDateString();
static string SayHi( string s ) {
return $"Hi, {s} , today is {today}";
}
Console.WriteLine( SayHi( "Mary" ) );
}
}
}
則在編譯過程中,會產生語法錯誤,顯示靜態區域函式(Static Local Function)無法參考到「today」變數:

圖 3:靜態區域函式(Static Local Function)無法參考到外部函式變數。
要解決這個問題,只要使用參數傳遞的方式,將外部函式宣告的變數值傳入靜態區域函式(Static Local Function)即可,改寫程式碼如下:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
string today = DateTime.Now.ToShortDateString();
static string SayHi( string s , string today ) {
return $"Hi, {s} , today is {today}";
}
Console.WriteLine( SayHi( "Mary" , today ) ); // Hi, Mary , today is 11/5/2019
}
}
}
Disposable ref struct
結構(struct)和類別(class)很像,都可在其中宣告欄位、方法、屬性等成員,結構適用在表示少量資料的情況。預設結構會配置在堆疊(Stack)這塊記憶體中,在某些情況下可能會配置在受管理的堆積(Managed Heap),例如進行裝箱(Boxing、或轉型成介面時。而C# 7.2新增「Ref struct」功能,在宣告結構時,可以套用「ref」關鍵字。
我們先來看一個標準結構範例,參考以下範例程式碼,「ContactInfo」結構包含「Name」、「Title」、「Phone」三個欄位以及一個「Display」方法,我們叫用了「ToString」方法來顯示這些欄位的資訊:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string[] args ) {
ContactInfo customer;
customer.Name = "王小明";
customer.Title = "先生";
customer.Phone = "(02)1234-5678";
customer.Display( );
Console.WriteLine( customer.ToString( ) ); // Name: 王小明 Title: 先生 Phone: (02)1234-5678
}
}
struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display( ) {
Console.WriteLine( " Name: " + Name +
" Title: " + Title +
" Phone: " + Phone );
}
}
}
若修改程式將「ContactInfo」指派給「object」型別的變數時,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string[] args ) {
ContactInfo customer;
customer.Name = "王小明";
customer.Title = "先生";
customer.Phone = "(02)1234-5678";
customer.Display( );
Console.WriteLine( customer.ToString( ) ); // Name: 王小明 Title: 先生 Phone: (02)1234-5678
object boxcustomer = customer;
}
}
struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display( ) {
Console.WriteLine( " Name: " + Name +
" Title: " + Title +
" Phone: " + Phone );
}
}
}
請參考下圖所示,從IL來看,當你將實值型別變數(struct)指派給「object」型別變數,便自動進行「Boxing」動作,將「struct」放到「Managed Heap」之中。

圖 4:自動進行「Boxing」動作。
此外參考以下程式碼,若將結構當做「MyClass」類別的成員,當你建立類別實體時,因為「ContactInfo」結構是類别的成員,同樣會將結構配置於「Managed Heap」之中。
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
};
MyClass c = new MyClass();
c.ContactInfo = customer;
}
}
struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( " Name: " + Name +
" Title: " + Title +
" Phone: " + Phone );
}
}
class MyClass {
public ContactInfo ContactInfo;
}
}
ref 結構(Ref struct)
C# 8 新增「Ref struct」,它的特性包含如下:
- · 方法參數、區域變數等等的值,只能配置在堆疊(Stack)當中,如此可以減少GC的負擔,不用負責回收記憶體。
- · 不能宣告成類別或標準結構的靜態(static)或實體(Instance)成員,無法進行裝箱(Boxing)。
- · 不能夠當做「async」方法或「lambda」運算式的參數
- · 不能夠動態繫結(Binding)、Boxing、Unboxing或轉換。
- · 不能實作介面。
若將前文範例中的「ContactInfo」結構定義成「ref 結構」,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
ContactInfo customer;
customer.Name = "王小明";
customer.Title = "先生";
customer.Phone = "(02)1234-5678";
customer.Display();
Console.WriteLine( customer.ToString() ); // Error
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
}
}
則叫用「ToString」方法則會失敗,這個方法是繼承「System.Object」的「ToString」方法而來,因為「ref struct」不允許Boxing,程式將無法通過編譯,請參考下圖所示:

圖 5:「ref struct」不允許Boxing。
因為不允許Boxing,所以無法將結構指派給「object」型別的變數,以下程式碼一樣無法通過編譯:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
};
object boxcustomer = customer; // error
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
}
}
錯誤訊息請參考下圖所示:

圖 6:不允許Boxing。
同樣的,將「ref struct」宣告為「MyClass」類別成員也會發生語法錯誤,無法編譯,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
};
MyClass c = new MyClass();
c.ContactInfo = customer;
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
}
class MyClass {
public ContactInfo ContactInfo; // error
}
}
錯誤訊息請參考下圖所示:

圖 7:「ref struct」不可宣告為「MyClass」類別成員。
「ref struct」也無法實作介面,例如以下範例程式碼,試圖實作「IDisposable」介面:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string[] args ) {
ContactInfo customer;
customer.Name = "王小明";
customer.Title = "先生";
customer.Phone = "(02)1234-5678";
customer.Display( );
}
}
ref struct ContactInfo : IDisposable {
public string Name;
public string Title;
public string Phone;
public void Display( ) {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
}
}
程式將無法進行編譯,請參考下圖所示:

圖 8:「ref struct」無法實作介面。
Disposable ref struct
那們我們現在回到主題,C# 8新增的「Disposable ref structs」功能,就是因為原來的「ref struct」無法新增介面,所以便無法實作「IDisposable」介面來釋放相關資源,當然也不能夠將結構宣告在using的區塊之中,例如以下程式碼將會有編譯錯誤:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
using ( ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
} ){ } ; // error
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
}
}
錯誤訊息請參考下圖所示:

圖 9:「ref struct」不能夠宣告在using的區塊之中。
在C# 8 只要在「ref struct」之中,加上一個「public」的「Dispose」方法,上述的問題就迎刃而解,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
using ( ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
} ) { } ;
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
public void Dispose() {
}
}
}
或搭配C# 8 「using宣告」新語法來服用,參考以下範例程式碼:
using System;
using System.IO;
namespace ConsoleApp1 {
class Program {
static void Main( string [] args ) {
using ContactInfo customer = new ContactInfo() {
Name = "王小明" ,
Title = "先生" ,
Phone = "(02)1234-5678"
};
}
}
ref struct ContactInfo {
public string Name;
public string Title;
public string Phone;
public void Display() {
Console.WriteLine( "Name:" + Name +
"Title: " + Title +
"Phone: " + Phone );
}
public void Dispose() {
}
}
}