C# 8新功能概覽 - 1

by vivid 11. 十二月 2019 06:02

.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」類別的程式碼,請參考下圖所示:

clip_image002

圖 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" );
    }
  }
}

 

 

這個範例執行時將會發生例外錯誤,因為建立檔案後,未釋放檔案相關資源,所以想要刪除檔案時,就會得到例外錯誤,請參考下圖所示:

clip_image004

圖 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」變數:

clip_image006

圖 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」之中。

clip_image008

圖 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,程式將無法通過編譯,請參考下圖所示:

clip_image010

圖 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 );
    }
  }
}

錯誤訊息請參考下圖所示:

clip_image012

圖 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
  }
}

錯誤訊息請參考下圖所示:

clip_image014

圖 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 );
    }
  }
}

 

程式將無法進行編譯,請參考下圖所示:

clip_image016

圖 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 );
    }
  }
}

錯誤訊息請參考下圖所示:

 

clip_image018

圖 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() {
    }
  }
}

Tags:

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

新增評論




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






NET Magazine國際中文電子雜誌

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

月分類Month List