.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