扩展方法

思考并回答以下问题:

  • 为什么说密封类和抽象类相反?密封类的应用场景是什么?
  • 什么是静态类?什么时候选择静态类而不是实例类?
  • 静态类可以被继承吗?为什么?
  • 什么是扩展方法?怎么使用?

本章涵盖:

  • 密封类
  • 静态类
  • 扩展方法

密封类

抽象类必须用作基类,它不能像独立的类那样被实例化。密封类与它相反。

  • 密封类只能被用作独立的类,它不能被用作基类。
  • 密封类使用sealed修饰符标注。

例如,下面的类是一个密封类。将它用作其他类的基类会产生一个编译错误。

1
2
3
4
5
// ↓ 关键字
sealed class MyClass
{
...
}

静态类

静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。静态类的一个常见的用途可能就是创建一个包含一组数学方法和值的数学库。

关于静态类需要了解的重要事情如下。

  • 类本身必须标记为static.
  • 类的所有成员必须是静态的。
  • 类可以有一个静态构造函数,但不能有实例构造函数,不能创建该类的实例。
  • 静态类是隐式密封的,也就是说,不能继承静态类。

可以使用类名和成员名,像访问其他静态成员那样访问它的成员。

下面的代码展示了一个静态类的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ↓ 类必须标记为静态的
static public class MyMath
{
public static float PI = 3.14f;
// 成员必须是静态的
public static bool IsOdd(int x)
{
return x%2 == 1;
}
// 成员必须是静态的
public static int Times2(int x)
{
return 2 * x;
}
}

class Program
{
static void Main()
{
int val =3;
// 使用类名和成员名
Console.WriteLine("{0} is odd is {1}.", val, MyMath.IsOdd(val));
Console.WriteLine("{0} * 2 = {1}.", val, MyMath.Times2(val));
}
}

这段代码产生以下输出:

1
2
3 is odd is True.
3 * 2 = 6.

扩展方法

在迄今为止的内容中,你看到的每个方法都和声明它的类关联。扩展方法特性扩展了这个边界,允许编写的方法和声明它的类之外的类关联。

想要知道可以如何使用这个特性,请看下面的代码。它包含类MyData,该类存储3个double类型的值,并含有一个构造函数和一个名称为Sum的方法,该方法返回3个存储值的和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyData
{
// 字段
private double D1;
private double D2;
private double D3;

// 构造函数
public MyData(double d1, double d2, double d3)
{
D1 = d1;
D2 = d2;
D3 = d3;
}

// 方法Sum
public double Sum()
{
return D1 + D2 + D3;
}
}

这是一个非常有限的类,但假设它还含有另外一个方法会更有用,该方法返回3个数据的平均值。使用已经了解的关于类的内容,有几种方法可以实现这个额外的功能。

  • 如果你有源代码并可以修改这个类,当然,你只需要为这个类增加一个新方法。
  • 然而,如果不能修改这个类(如这个类在一个第三方类库中),那么只要它不是密封的,你就能把它用作一个基类并在派生自它的类中实现这个额外的方法。

然而,如果不能访问代码,或该类是密封的,或有其他的设计原因使这些方法不能工作,就不得不在另一个类中使用该类的公有可用成员编写一个方法。

例如,可以编写一个下面这样的类。下面的代码包含一个名称为ExtendMyData的静态类,它含有一个名称为Average的静态方法,该方法实现了额外的功能。注意该方法接受MyData的实例作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class ExtendMyData
{
// ↓ MyData类的实例
public static double Average(MyData md)
{
return md.Sum() / 3; // 使用MyData的实例
}
}

class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
// ExtendMyData.Average调用静态方法 ↓ MyData的实例
Console.WriteLine("Average: {0}", ExtendMyData.Average(md));
}
}

这段代码产生以下输出:

1
Average: 4

尽管这是非常好的解决方案,但如果能在类的实例自身上调用该方法,而不是创建另一个作用于它的类的实例,将会更优雅。下面两行代码阐明了它们的区别。第一行使用刚展示的方法:在另一个类的实例上调用静态方法。第二行展示了我们愿意使用的形式:在对象自身上调用实例方法。

扩展方法允许你使用第二种形式,即使第一种形式可能是编写这种调用的正常方法。

1
2
ExtendMyData.Average(md); // 静态调用形式
md.Average(); // 实例调用形式

通过对方法Average的声明做一个小小的改动,就可以使用实例调用形式。需要做的修改是在参数声明中的类型名前增加关键字this,如下面所示。把this关键字加到静态类的静态方法的第一个参数上,把该方法从类ExtendMyData的常规方法改变为类MyData的扩展方法。现在两种调用形式都可以使用。

1
2
3
4
5
6
7
8
9
// 必须是一个静态类
static class ExtendMyData
{
// 必须是公有的和静态的 ↓ 关键字 ↓ 类型
public static double Average(this MyData md)
{
...
}
}

扩展方法的重要要求如下。

  • 声明扩展方法的类必须声明为static.
  • 扩展方法本身必须声明为static.
  • 扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它所扩展的类的名称。

图1阐明了扩展方法的结构。

图1 扩展方法的结构

下面的代码展示了一个完整的程序,包括类MyData和声明在类ExtendMyData中的扩展方法Average。注意方法Average完全如同它是MyData的实例成员那样调用!图2阐明了这段代码。类MyData和ExtendMyData共同起到期望类的作用,带有3个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace ExtensionMethods
{
sealed class MyData
{
private double D1, D2, D3;
public MyData(double d1, double d2, double d3)
{
D1 = d1; D2 = d2; D3 = d3;
}
public double Sum()
{
return D1 + D2 + D3;
}
}

static class ExtendMyData
{ // ↓ 关键字和类型
public static double Average(this MyData md)
{
return md.Sum() / 3;
}
}

class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine("Sum: {0}", md.Sum());
// ↓ 当做类的实例成员来调用
Console.WriteLine("Average: {0}", md.Average());
}
}
}

这段代码产生以下输出:

1
2
Sum: 12
Average: 4

0%