C程序经典实例-类和泛型1.1

第 1 章 类和泛型

1.0 简介

        本章的范例涵盖了 C# 语⾔的基础,主题包括类和结构,如何使⽤它们,它们有哪些不同,何时使⽤类以及何时使⽤结构。在此基础上,我们将构建具有各种固有功能(如可排序、可搜索、可处理和可克隆)的类。此外,我们将深⼊讨论联合类型、字段初始化、lambda、局部⽅法、单路和多路⼴播委托、闭包、函数对象等主题。本章也包含了解析命令⾏参数的范例,这是开发⼈
员⼀直喜爱的主题。
        在开始展⽰这些范例之前,让我们回顾⼀下关于类、结构、泛型的⾯向对象能⼒的关键信息。类⽐结构灵活得多。结构可以跟类⼀样实现接⼝,但与类不同的是,它们不能继承⾃类或结构。这种限制使得你⽆法创建结构层次关系,⽽这⽤类可以做到。通过抽象基类实现的多态性也是在结构中⽆法使⽤的,因为除了装箱成Object 、ValueType 和 Enum ,结构⽆法从另⼀个类派⽣。
        结构与其他的值类型⼀样,都是从System.ValueType 隐式派⽣的。乍看之下,结构类似于类,但它们实际上有很⼤的差别。在设计应⽤程序时,知道何时使⽤结构优于使⽤类将对你有很⼤的帮助。不正确地使⽤结构可能会使代码性能低下、难以修改。
        结构相对于引⽤类型有两个性能优势。⾸先,如果⼀个结构是在栈上分配的(即不包含在引⽤类型内),访问结构及其数据的速度要快于访问堆中引⽤类型的速度。引⽤类型的对象必须要跟随它在堆上的引⽤以获取它们的数据。不过,这种性能优势相对于结构的第⼆个性能优势就相形⻅绌了:要清理在栈上为结构分配的内存,只需要在⽅法调⽤返回时修改栈指针所指向的地址即可。这个调⽤要远远快于垃圾回收器⾃动清理托管堆上分配的引⽤类型。然⽽垃圾回收器的成本是延后的,所以不会⽴刻被⼈注意到。
        当以传值⽅式传⼊其他⽅法时,结构的性能就⽐不上类了。因为结构存在于栈上,当以传值⽅式传⼊⼀个⽅法时,结构及其数据必须复制到⼀个新的局部变量(⽅法⽤于接收结构的参数)中。这⼀复制过程相⽐将⼀个引⽤传⼊⽅法要花费更多的时间,除⾮结构的⼤⼩与机器的指针⼤⼩相同或更⼩⼀些;因此,在 32 位的机器上,传⼊⼀个 32 位⼤⼩的结构与传⼊⼀个引⽤(与指
针⼤⼩相同)的成本是相同的。在类和结构之间选择时,要记得这⼀点。尽管创建、访问和销毁类对象可能需要更⻓时间,但并不能抵消将结构多次按值传⼊⼀个或多个⽅法产⽣的性能下降。保持较⼩的结构体可以减⼩按值传递时所产⽣的性能下降。

以下情况应使⽤类。
其同⼀性很重要。结构在按值传⼊⽅法时会被隐式复制。
有较⼤的内存占⽤。
其字段需要初始化。
需要从⼀个基类继承。
需要多态⾏为;也就是说,你需要实现⼀个抽象基类,并从此基类派⽣出多个相似的类。(注意,多态性也可以通过接⼝实现,但通常并不适合在⼀个值类型中实现接⼝。这是因为当结构转换为接⼝时,会因装箱操作⽽导致性能损失。)
以下情况应使⽤结构。
其⾏为⽅式类似于原语类型(int 、long 、byte等)。仅占⽤较⼩的内存。
        调⽤⼀个需要将结构体以传值⽅式传⼊的P/Invoke ⽅法。平台调⽤ (Platform Invoke,
P/Invoke)允许托管代码调⽤ DLL 内公开的⾮托管⽅法。许多时候,⾮托管 DLL 内的⽅法都需要传⼊⼀个结构参数。使⽤结构是执⾏此操作的⼀种⾼效⽅法,并且在需要按值传⼊时是唯⼀的途径。需要降低垃圾回收对应⽤程序性能的影响。其字段只需要被初始化为默认值。对于数值类型,
这个值为 0 ;对于布尔类型,则为 false ;对于引⽤类型,则为 null 。注意在 C# 6.0 中,结构可以拥有默认构造函数并将字段初始化为⾮默认值。不需要继承⼀个基类(除了 ValueType 之外,所有结构都继承它)。不需要多态⾏为。
        当把结构传递给需要⼀个对象参数的⽅法时,例如Framework 类库(FCL)中的任何⾮泛型集合类型,它们也可能会引起性能降低。把⼀个结构(对此问题⽽⾔其实是任何简单类型)传⼊⼀个需要对象参数的⽅法中将会导致结构被装箱。装箱 (boxing)是指将⼀个值类型包装在⼀个对象中。这种操作⽐较耗时,并且可能导致性能降低。
        最后,将泛型功能加⼊进来就能够编写类型安全且⾼效的基于集合和模式的代码了。泛型提供相当强⼤的编程能⼒,但是要求你正确使⽤它。如果你考虑把ArrayList 、Queue 、Stack 和 Hashtable 对象转换成其对应的泛型对象,可以阅读⼀下 1.9 节和 1.10 节中的范例。你将看到这种转换并⾮总是很简单,有⼀些原因可能导致你根本不想执⾏这种转换。

1.1 创建联合类型的结构

1.1.1 问题

        你需要创建⼀种数据类型,其⾏为⽅式类似于 C++ 中的联合类型。联合类型主要⽤于互操作场景,其中⾮托管代码接受和 / 或返回⼀个联合类型;我们建议你不要在其他情况下使⽤它。

1.1.2 解决⽅案

        使⽤⼀个结构,并⽤ StructLayout 特性标记它(在构造函数中指定 LayoutKind.Explicit 布局类型)。此外,利⽤ FieldOffset 特性标记结构中的每个字段。下⾯的结构定义了⼀个联合类型,其中可以存储⼀个带符号数值。

using System.Runtime.IteropServices;
[StructLayoutAttribute(LayoutKind.Explicit)]
struct SignedNumber
{
[FieldOffsetAttribute(0)]
public sbyte Num1;
[FieldOffsetAttribute(0)]
public short Num2;
[FieldOffsetAttribute(0)]
public int Num3;
[FieldOffsetAttribute(0)]
public long Num4;
[FieldOffsetAttribute(0)]
public float Num5;
[FieldOffsetAttribute(0)]
public double Num6;
}

下⼀个结构类似于 SignedNumber 结构,不同之处是除了带符号的数值之外,它还可以包含 String 类型。
 

[StructLayoutAttribute(LayoutKind.Explicit)]
struct SignedNumberWithText
{
[FieldOffsetAttribute(0)]
public sbyte Num1;
[FieldOffsetAttribute(0)]
public short Num2;
[FieldOffsetAttribute(0)]
public int Num3;
[FieldOffsetAttribute(0)]
public long Num4;
[FieldOffsetAttribute(0)]
public float Num5;
[FieldOffsetAttribute(0)]
public double Num6;
[FieldOffsetAttribute(16)]
public string Text1;
}
1.1.3 讨论

        联合类型是⼀种在 C++ 代码中较为常⻅的结构类型;不过,有⼀种⽅式可以使⽤ C# 中的结构数据类型来复制其结构。联合 (union)是⼀种结构,在内存中的特定位置为该结构接受多种类型。例如,SignedNumber结构是使⽤ C# 结构创建的⼀个联合类型的结构。这种结构可以接受任何类型的带符号的数值类型(sbyte 、int 和 long 等),但它只在结构中的同⼀个位置(同⼀偏移量)接受这种数字类型。由于 StructLayoutAttribute 可以同时应⽤于结构和类,在创建联合数据类型时也可以使⽤类。
        注意 FieldOffsetAttribute 将值 0 传递给它的构造函数。这表明这个字段距离结构开始处的偏移量为 0字节。可以将这个特性与 StructLayoutAttribute结合使⽤,⼿动强制指定结构中的字段开始于什么位置(即每个字段在内存中相对于这个结构开始处的偏移量)。FieldOffsetAttribute 只能与设置为LayoutKind.Explicit 的StructLayoutAttribute ⼀起使⽤。此外,它不能⽤于结构内的静态成员。
        联合类型可能会带来⼀些问题,因为⼏种类型实质上是相互叠加在⼀起的。最⼤的问题是如何从联合类型结构中提取正确的数据类型。思考⼀下,如果你选择在SignedNumber 结构中存储 long 数值类型的值long.MaxValue ,会发⽣什么情况。随后,你可能会偶然尝试从这个结构中提取⼀个 byte 数据类型值。这样操作,你将会只取回这个 long 值中的第⼀字节。另⼀个问题是在正确的偏移位置开始字段。SignedNumberWithText 联合类型在偏移量为 0 的位置叠加了⼤量带符号的数值数据类型。这个结构中的最后⼀个字段位于内存中距离这个结构开始处偏移量为 16字节的位置。如果你意外地把字符串字段 Text1 覆盖在任何其他带符号的数值数据类型之上,在运⾏时将得
到⼀个异常。基本规则是:允许你把⼀种值类型叠加在另⼀种值类型之上,但是不能把⼀种引⽤类型叠加于⼀种值类型之上。如果⽤以下特性标记 Text1 字段:

[FieldOffsetAttribute(14)]
就会在运⾏时引发下⾯这个异常(注意,编译器不会捕
获这个问题)。
System.TypeLoadException: Could not load type
'SignedNumberWithText' from
assembly 'CSharpRecipes, Version=1.0.0.0,
Culture=neutral,
PublicKeyToken=fe85c3941fbcc4c5' because it contains an
object field at
offset 14 that is incorrectly aligned or overlapped by a
non-object field.

在 C# 中使⽤复杂的联合类型时,必须保证正确的偏移量。

1.1.4 参考

MSDN ⽂档中的“StructLayoutAttribute 类”主题。

1.2 使类型可排序

1.2.1 问题

        你有⼀种数据类型,它将存储为 List<T> 或SortedList<K,V> 的元素。你想使⽤List<T>.Sort ⽅法或者 SortedList<K,V> 的内部排序机制来⾃定义此数据类型在数组中的排序⽅式。此外,你可能需要在 SortedList 集合中使⽤这种类型。

1.2.2 解决⽅案

        例 1-1 演⽰了如何实现 IComparable<T> 接⼝。例 1-1中展⽰的 Square 类实现了这个接⼝,使得 List<T>和 SortedList<K,V> 集合能够排序和查找这些Square 对象。
例 1-1 :通过实现 IComparable<T> 使类型可排序

public class Square : IComparable<Square>
{
public Square(){}
public Square(int height, int width)
{
this.Height = height;
this.Width = width;} p
ublic int Height { get; set; }
public int Width { get; set; }
public int CompareTo(object obj)
{
Square square = obj as Square;
if (square != null)
return CompareTo(square);
throw
new ArgumentException(
"Both objects being compared must be of
type Square.");
} p
ublic override string ToString()=>
($"Height: {this.Height}
Width: {this.Width}");
public override bool Equals(object obj)
{
if (obj == null)
return false;
Square square = obj as Square;
if(square != null)
return this.Height == square.Height;
return false;
} p
ublic override int GetHashCode()
{
return this.Height.GetHashCode() |
this.Width.GetHashCode();
} p
ublic static bool operator ==(Square x, Square y)
=> x.Equals(y);
public static bool operator !=(Square x, Square y)
=> !(x == y);
public static bool operator <(Square x, Square y) =>(x.CompareTo(y) < 0);
public static bool operator >(Square x, Square y) =>
(x.CompareTo(y) > 0);
public int CompareTo(Square other)
{
long area1 = this.Height * this.Width;
long area2 = other.Height * other.Width;
if (area1 == area2)
return 0;
else if (area1 > area2)
return 1;
else if (area1 < area2)
return -1;
else
return -1;
}
}

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BinaryStarXin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值