.NET 開發基金會/使用系統型別
| .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)的值沒有任何影響。這與引用型別不同,引用型別複製對值的引用,而不是值本身。
值型別不能從其他型別派生。
值型別作為方法引數預設情況下按值傳遞。將值型別的副本建立並作為引數傳遞給方法。如果在方法內部更改引數,則不會影響原始值型別的值。
請參閱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
可空型別有兩個只讀屬性:HasValue 和 Value。
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 關於 ?? 運算子的部落格文章
構建值型別必須非常簡單。以下示例定義了一個自定義“點”結構,它只有兩個雙精度成員。請參閱裝箱和拆箱,以瞭解有關值型別到引用型別的隱式轉換的討論。
構建和使用自定義值型別(結構)
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
列舉的簡單用法
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();
}
}
}
構建自定義列舉非常簡單,如以下示例所示。
宣告簡單列舉
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.Object 和 System.String 也是如此。引用型別儲存在託管堆記憶體中。
與值型別不同,引用型別可以被賦值為null。
複製引用型別會複製一個指向物件的引用,而不是物件的副本本身。這有時會顯得違反直覺,因為更改引用副本也會更改原始物件。
值型別儲存其被賦予的值,簡單明瞭 - 但引用型別儲存一個指向記憶體中位置的指標(在堆上)。將堆想象成一堆儲物櫃,引用型別持有儲物櫃號碼(在這個比喻中沒有鎖)。複製引用型別就像給別人一份你的儲物櫃號碼的副本,而不是一份其內容的副本。兩個指向相同記憶體的引用型別就像兩個人共享同一個儲物櫃 - 兩個人都可以修改其內容。
使用引用型別的示例
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 之外的任何內容,程式將無法編譯。這演示了泛型的強型別功能。
非常簡單的泛型使用
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 和一個字串,它將無法編譯)。
簡單的自定義泛型函式
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 方法引數),而無需進行任何強制轉換。
自定義泛型介面和類
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 的連結
裝箱和拆箱
[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
裝箱和拆箱的效能
相對於簡單的賦值,裝箱和拆箱是計算量大的過程。當對值型別進行裝箱時,必須分配和構造一個全新的物件。在較小程度上,拆箱所需的轉換在計算上也是昂貴的。
參見 MSDN
- 有關 CLR 中 TypeForwardToAttribute 的討論,請參見 MSDN
- 其他可能的連結:Marcus 的部落格,NotGartner
