跳到内容

.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
华夏公益教科书