跳到內容

.NET 開發基金會/使用系統型別

來自華夏公益教科書


系統型別和集合:使用系統型別


使用系統型別

[編輯 | 編輯原始碼]

考試目標:透過使用 .NET Framework 2.0 系統型別來管理 .NET Framework 應用程式中的資料。

(參考 System 名稱空間)

值型別用法

[編輯 | 編輯原始碼]

以下是一些關於值型別的“用法”方面的說明。

值型別包含它們被分配的值

int a = 1;  // the variable "a" contains "1" of value type int

值型別也可以使用 new 關鍵字建立。使用 new 關鍵字會使用從型別預設建構函式獲得的預設值初始化變數

int a = new int(); // using the default constructor via the new keyword
return a;          // returns "0" in the case of type Int.

值型別可以在沒有初始化的情況下宣告,但必須在使用之前初始化為某個值

int a;     // This is perfectly acceptable
return a;  // NOT acceptable!  You can't use "a" because "a" doesn't have a value!

值型別不能等於 null。.NET 2.0 提供了 可空型別 來解決此限制,這將在下一節中討論,但 null 不是值型別的有效值

int a = null;  // Won't compile - throws an error.

如果將值型別複製到另一個值型別,則該值將被複製。更改副本的值不會影響原始值的值。第二個值僅僅是第一個值的副本 - 賦值後它們沒有任何關聯。這是相當直觀的

int var1 = 1;
int var2 = var1;  //the value of var1 (a "1" of type int) is copied to var2
var2 = 25;        // The "1" value in var2 is overwritten with "25"
Console.WriteLine("The value of var1 is {0}, the value of var2 is {1}", var1, var2);

這將導致輸出

The value of var1 is 1, the value of var2 is 25

更改副本的值(在本例中為 var2)對原始值(var1)的值沒有任何影響。這與引用型別不同,引用型別複製對值的引用,而不是值本身。

值型別不能從其他型別派生。

值型別作為方法引數預設情況下按值傳遞。將值型別的副本建立並作為引數傳遞給方法。如果在方法內部更改引數,則不會影響原始值型別的值。


Clipboard

要做的
最終,我們可以新增一個示例來展示一些內建值型別的用法:整數、浮點數、邏輯、字元和十進位制,以及值型別的按值引數傳遞


可空型別

[編輯 | 編輯原始碼]

請參閱MSDN

可空型別...

  • 是一個泛型型別
  • 是 System.Nullable 結構的例項。
  • 只能在值型別上宣告。
  • 用 System.Nullable<type> 或簡寫 type? 宣告 - 這兩個是可互換的。
System.Nullable<int> MyNullableInt;  // the long version 
int? MyNullableInt;                  // the short version
  • 接受底層型別的正常值範圍,以及 null
bool? MyBoolNullable;  // valid values: true || false || null

小心使用 可空 布林值!在 if, for, while 或邏輯評估語句中,可空布林值會將 null 值等同於 false - 它不會丟擲錯誤。

方法:T GetValueOrDefault() & T GetValueOrDefault(T defaultValue)
返回儲存的值,如果儲存的值設定為 null,則返回預設值。

屬性:HasValue & Value
可空型別有兩個只讀屬性:HasValueValue

HasValue 是一個布林屬性,如果 Value != null,則返回 true。它提供了一種方法,在使用可能會丟擲錯誤的型別之前,檢查你的型別是否為非 null

 int? MyInt = null;
 int MyOtherInt;
 MyOtherInt = MyInt.Value + 1;    // Error! You can't add null + 1!!
 if (MyInt.HasValue) MyOtherInt = MyInt.Value + 1; // This is a better way.

Value 返回你的型別的 value,null 或其他。

int? MyInt = 27;
if (MyInt.HasValue) return MyInt.Value;  // returns 27.
MyInt = null;
return MyInt; // returns null.

包裝/解包

包裝 是將來自非可空型別 N 的值 m 打包到可空型別 N? 的過程,透過表示式 new N?(m) 執行

解包 是將可空型別 N? 的例項 m 評估為型別 N 或 NULL 的過程,並透過 'Value' 屬性執行(例如 m.Value)。

注意:解包空例項會生成異常 System.InvalidOperationException

?? 運算子(又稱 空合併 運算子)

雖然不能單獨用於可空型別,但 ?? 運算子在你想使用預設值而不是 null 值時非常有用。?? 運算子返回語句的左運算元(如果非空),否則返回右運算元。

int? MyInt = null;
return MyInt ?? 27;  // returns 27, since MyInt is null

有關更多資訊,請參閱R. Aaron Zupancic 關於 ?? 運算子的部落格文章

構建值型別

[編輯 | 編輯原始碼]

構建值型別必須非常簡單。以下示例定義了一個自定義“點”結構,它只有兩個雙精度成員。請參閱裝箱和拆箱,以瞭解有關值型別到引用型別的隱式轉換的討論。

C# 程式碼示例

構建和使用自定義值型別(結構)

   using System;
   using System.Collections.Generic;
   using System.Text;
   //
   namespace ValueTypeLab01
   {
       class Program
       {
           static void Main(string[] args)
           {
               MyPoint p;
               p.x = 3.2;
               p.y = 14.1;
               Console.WriteLine("Distance from origin: " + Program.Distance(p));
               // Wait for finish
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
           // method where MyPoint is passed by value
           public static double Distance(MyPoint p)
           {
               return Math.Sqrt(p.x * p.x + p.y * p.y);
           }
       }
       // MyPoint is a struct (custom value type) representing a point
       public struct MyPoint
       {
           public double x;
           public double y;
       }
   }

使用使用者定義的值型別

[編輯 | 編輯原始碼]

上面的示例可以在這裡使用。請注意,p 變數不需要使用 new 運算子初始化。

使用列舉

[編輯 | 編輯原始碼]

以下示例展示了 System 列舉 DayOfWeek 的簡單用法。程式碼比測試表示一天的整數值要簡單得多。請注意,對列舉變數使用 ToString() 將給出值的字串表示形式(例如 “Monday” 而不是 “1”)。

可以使用反射列出可能的值。請參閱該部分了解詳細資訊。

有關 Enum 類的討論,請參閱MSDN

有一種特殊的列舉型別稱為標誌列舉。考試目標沒有特別提到它。如果您有興趣,請參閱MSDN

C# 示例

列舉的簡單用法

   using System;
   using System.Collections.Generic;
   using System.Text;
   //
   namespace EnumLab01
   {
       class Program
       {
           static void Main(string[] args)
           {
               DayOfWeek day = DayOfWeek.Friday;
               if (day == DayOfWeek.Friday)
               {
                   Console.WriteLine("Day: {0}", day);
               }
               DayOfWeek day2 = DayOfWeek.Monday;
               if (day2 < day)
               {
                   Console.WriteLine("Smaller than Friday");
               }
               switch (day)
               {
                   case DayOfWeek.Monday:
                       Console.WriteLine("Monday processing");
                       break;
                   default:
                       Console.WriteLine("Default processing");
                       break;
               }
               int i = (int)DayOfWeek.Sunday;
               Console.WriteLine("Int value of day: {0}", i);
               // Finishing
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
       }
   }

構建列舉

[編輯 | 編輯原始碼]

構建自定義列舉非常簡單,如以下示例所示。

C# 示例

宣告簡單列舉

   using System;
   using System.Collections.Generic;
   using System.Text;
   //
   namespace EnumLab02
   {
       class Program
       {
           public enum MyColor
           {
               None = 0,
               Red,
               Green,
               Blue
           }
           static void Main(string[] args)
           {
               MyColor col = MyColor.Green;
               Console.WriteLine("Color: {0}", col);
               // Finishing
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
       }
   }

使用引用型別

[編輯 | 編輯原始碼]

引用型別更常被稱為物件介面委託 都是引用型別,內建的引用型別System.ObjectSystem.String 也是如此。引用型別儲存在託管堆記憶體中。

與值型別不同,引用型別可以被賦值為null

複製引用型別會複製一個指向物件的引用,而不是物件的副本本身。這有時會顯得違反直覺,因為更改引用副本也會更改原始物件。

值型別儲存其被賦予的值,簡單明瞭 - 但引用型別儲存一個指向記憶體中位置的指標(在堆上)。將堆想象成一堆儲物櫃,引用型別持有儲物櫃號碼(在這個比喻中沒有鎖)。複製引用型別就像給別人一份你的儲物櫃號碼的副本,而不是一份其內容的副本。兩個指向相同記憶體的引用型別就像兩個人共享同一個儲物櫃 - 兩個人都可以修改其內容。

C# 程式碼示例

使用引用型別的示例

public class Dog
{
  private string breed;
  public string Breed { get {return breed;} set {breed = value;} }
  
  private int age;
  public int Age { get {return age;} set {age = value;} }
  
  public override string ToString()
  {
    return String.Format("is a {0} that is {1} years old.", Breed, Age);
  }
  
  public Dog(string dogBreed, int dogAge)
  {
    this.breed = dogBreed;
    this.age = dogAge;
  }
}

public class Example()
{
   public static void Main()
   {
     Dog myDog = new Dog("Labrador", 1);    // myDog points to a position in memory.
     Dog yourDog = new Dog("Doberman", 3);  // yourDog points to a different position in memory.

     yourDog = myDog; // both now point to the same position in memory, 
                    // where a Dog type has values of "Labrador" and 1
   
     yourDog.Breed = "Mutt";
     myDog.Age = 13; 

     Console.WriteLine("Your dog {0}\nMy dog {1}", yourDog.ToString(), myDog.ToString());
   }
}

由於你的狗變數和我的狗變數都指向相同的記憶體儲存,因此輸出將是

Your dog is a Mutt that is 13 years old.
My dog is a Mutt that is 13 years old.

作為操作引用型別的練習,你可能希望使用 String 和 StringBuilder 類。我們將它們與文字操作部分放在一起,但操作字串幾乎是所有程式的基本操作。

使用和構建陣列

[edit | edit source]

有關參考資訊,請參閱 MSDN

使用類

[edit | edit source]

構建自定義類

[edit | edit source]

使用介面

[edit | edit source]

構建自定義介面

[edit | edit source]

使用特性

[edit | edit source]

使用泛型型別

[edit | edit source]

本書的其他地方將主要演示使用 System 泛型型別的四大類。

  • 前面已經討論過可空型別。
  • 後面有一整節內容是關於泛型集合的。
  • 將在事件/委託部分討論泛型事件處理程式。
  • 泛型委託也將事件/委託部分以及泛型集合部分(比較器類)中討論。

如果在 Visual Studio 中複製下一個非常簡單的示例並嘗試向列表中新增除 int 之外的任何內容,程式將無法編譯。這演示了泛型的強型別功能。

泛型的簡單使用(C#)

非常簡單的泛型使用

   using System;
   using System.Collections.Generic;
   namespace GenericsLab01
   {
       class Program
       {
           static void Main(string[] args)
           {
               List<int> myIntList = new List<int>();
               myIntList.Add(32);
               myIntList.Add(10); // Try to add something other than an int 
                                  // ex. myIntList.Add(12.5);
               foreach (int i in myIntList)
               {
                   Console.WriteLine("Item: " + i.ToString());
               }
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
       }
   }

你可以使用 List<string> 代替 List<int>,你將獲得一個字串列表,價格相同(你使用的是同一個 List(T) 類)。

構建泛型

[edit | edit source]

主題討論中提到的 文章 中展示了自定義泛型集合的程式設計。

這裡有一個泛型函式的示例。我們使用交換兩個引用的微不足道的問題。雖然非常簡單,但我們仍然看到了泛型的基本好處。

  • 我們不必為每種型別重新編寫交換函式。
  • 泛化不會讓我們失去強型別(嘗試交換一個 int 和一個字串,它將無法編譯)。
簡單的自定義泛型函式(C#)

簡單的自定義泛型函式

   using System;
   using System.Collections.Generic;
   using System.Text;
   namespace GenericsLab03
   {
       class Program
       {
           static void Main(string[] args)
           {
               Program pgm = new Program();
               // Swap strings
               string str1 = "First string";
               string str2 = "Second string";
               pgm.swap<string>(ref str1, ref str2);
               Console.WriteLine(str1);
               Console.WriteLine(str2);
               // Swap integers
               int int1 = 1;
               int int2 = 2;
               pgm.swap<int>(ref int1, ref int2);
               Console.WriteLine(int1);
               Console.WriteLine(int2);
               // Finish with wait
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
           // Swapping references
           void swap<T>(ref T r1,ref T r2)
           {
               T r3 = r1;
               r1 = r2;
               r2 = r3;
           }
       }
   }


下一步是提供一個示例,其中包含一個泛型介面、一個實現該泛型介面的泛型類以及一個從該泛型類派生的類。該示例還使用介面和派生約束。

這是一個涉及員工和供應商的另一個簡單問題,它們除了都可以向“付款處理程式”請求付款外別無共同之處(參見 訪問者模式)。

問題是,如果你需要對特定型別的付款(僅針對員工)進行特定處理,則應該將邏輯放在哪裡。有無數種解決這個問題的方法,但使用泛型使以下示例變得清晰、明確且強型別。

另一個好處是,它與容器或集合無關,在這些容器或集合中你會發現幾乎所有泛型示例。

請注意,EmployeeCheckPayment<T> 類派生自 CheckPayment<T>,對型別引數 T 施加了更強的約束(必須是員工,而不僅僅是實現 IPaymentInfo)。這使我們有機會在 RequestPayment 方法中同時訪問所有付款邏輯(來自基類)以及所有員工公共介面(透過 sender 方法引數),而無需進行任何強制轉換。

自定義泛型介面和類(C#)

自定義泛型介面和類

   using System;
   using System.Collections.Generic;
   using System.Text;
   namespace GennericLab04
   {
       class Program
       {
           static void Main(string[] args)
           {
               // Pay supplier invoice
               CheckPayment<Supplier> checkS = new CheckPayment<Supplier>();
               Supplier sup = new Supplier("Micro", "Paris", checkS);
               sup.InvoicePayment();
               // Produce employee paycheck
               CheckPayment<Employee> checkE = new EmployeeCheckPayment<Employee>();
               Employee emp = new Employee("Jacques", "Montreal", "bigboss", checkE);
               emp.PayTime();
               // Wait to finish
               Console.WriteLine("Press ENTER to finish");
               Console.ReadLine();
           }
       }
       // Anything that can receive a payment must implement IPaymentInfo
       public interface IPaymentInfo
       {
           string Name { get;}
           string Address { get;}
       }
       // All payment handlers must implement IPaymentHandler
       public interface IPaymentHandler<T> where T:IPaymentInfo 
       {
           void RequestPayment(T sender, double amount);
       }
       // Suppliers can receive payments thru their payment handler (which is given by an  object factory)
       public class Supplier : IPaymentInfo
       {
           string _name;
           string _address;
           IPaymentHandler<Supplier> _handler;
           public Supplier(string name, string address, IPaymentHandler<Supplier> handler)
           {
               _name = name;
               _address = address;
               _handler = handler;
           }
           public string Name { get { return _name; } }
           public string Address { get { return _address; } }
           public void InvoicePayment()
           {
               _handler.RequestPayment(this, 4321.45);
           }
       }
       // Employees can also receive payments thru their payment handler (which is given by an  object factory)
       // even if they are totally distinct from Suppliers
       public class Employee : IPaymentInfo
       {
           string _name;
           string _address;
           string _boss;
           IPaymentHandler<Employee> _handler;
           public Employee(string name, string address, string boss, IPaymentHandler<Employee> handler)
           {
               _name = name;
               _address = address;
               _boss = boss;
               _handler = handler;
           }
           public string Name { get { return _name; } }
           public string Address { get { return _address; } }
           public string Boss { get { return _boss; } }
           public void PayTime()
           {
               _handler.RequestPayment(this, 1234.50);
           }
       }
       // Basic payment handler
       public class CheckPayment<T>  : IPaymentHandler<T> where T:IPaymentInfo
       {
           public virtual void RequestPayment (T sender, double amount) 
           {
               Console.WriteLine(sender.Name);
           }
       }
       // Payment Handler for employees with supplementary logic
       public class EmployeeCheckPayment<T> : CheckPayment<T> where T:Employee
       {
           public override void RequestPayment(T sender, double amount)
           {
               Console.WriteLine("Get authorization from boss before paying, boss is: " + sender.Boss);
	            base.RequestPayment(sender, amount);
           }
       }
   }

異常類

[edit | edit source]

一些指向 MSDN 的連結

  • 異常和異常處理 - MSDN
  • 處理和丟擲異常 - MSDN
  • 異常層次結構 - MSDN
  • 異常類和屬性 - MSDN

裝箱和拆箱

[edit | edit source]

參閱 MSDN

所有型別直接或間接地派生自 System.Object(順便說一下,包括值型別,透過 System.ValueType 派生)。這允許對“任何”物件的非常方便的引用,但會帶來一些技術上的問題,因為值型別沒有被“引用”。隨之而來的是裝箱和拆箱。

裝箱和拆箱使值型別能夠被視為物件。裝箱將值型別打包到 Object 引用型別的例項中。這允許值型別儲存在垃圾回收堆上。拆箱從物件中提取值型別。在此示例中,整數變數 i 被裝箱並賦值給物件 o。

int i = 123;
object o = (object) i;  // boxing

請注意,不必顯式將整數強制轉換為物件(如上面的示例所示)來導致整數被裝箱。呼叫其任何方法也會導致它被裝箱到堆上(因為只有裝箱形式的物件具有指向虛擬方法表的指標)。

int i=123;
String s=i.toString(); //This call will cause boxing

值型別還可以透過第三種方式裝箱。當您將值型別作為引數傳遞給期望物件的函式時,就會發生這種情況。假設有一個函式原型如下

void aFunction(object value)

現在假設從程式的其他部分,您像這樣呼叫此函式

int i=123;
aFunction(i); //i is automatically boxed

此呼叫會自動將整數轉換為物件,從而導致裝箱。

然後可以將物件 o 拆箱並分配給整數變數 i

o = 123;
i = (int) o;  // unboxing

裝箱和拆箱的效能

相對於簡單的賦值,裝箱和拆箱是計算量大的過程。當對值型別進行裝箱時,必須分配和構造一個全新的物件。在較小程度上,拆箱所需的轉換在計算上也是昂貴的。

TypeForwardedToAttribute 類

[編輯 | 編輯原始碼]

參見 MSDN

有關 CLR 中 TypeForwardToAttribute 的討論,請參見 MSDN
其他可能的連結:Marcus 的部落格NotGartner
華夏公益教科書