.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
