撰写了文章 更新于 2020-04-12 12:20:46
从C#开始的编程入门——扩展
我们使用一些基本类型、基础类库、第三方库时,可能会遇到一些觉得比较麻烦的情况,库作者提供的功能可能不能满足我们的需求。但是一般类库是以动态连接库的形式提供的,我们也无法直接修改源码。
这个时候我们可以在不修改源码的情况下为类扩展新的功能,并且不影响原本的功能。这就是C#中的扩展(extension)
我们首先考虑一个判断一个int是否为偶数的静态方法。
public static bool IsEven(int n) { return n % 2 == 0; }
很简单,拿到一个int参数,取余数然后判断一下。要调用这个方法也很简单,但是显得有点别扭,更进一步讲,就是感觉不怎么面向对象。
IsEven(2);
定义扩展方法
我们的想法是,要是可以直接在int类型的变量上“点”出这个方法就好了。实际上扩展就可以达到这样的效果。
定义扩展方法首先需要定义一个静态类,然后在其中定义我们的静态扩展方法(由于要求在静态类中定义扩展方法,所以也就只可能定义静态方法)。
public static class IntExtension { public static bool IsEven(this int n) { return n % 2 == 0; } }
主要需要注意的是方法签名中的参数。扩展方法必须要声明至少一个参数,这个参数在类型前面必须加上this代表这是要被扩展的类型。其他的参数可以根据需要定义。
使用
在使用扩展方法时,首先要保证定义扩展的类以及被引入当前的作用域。要调用扩展方法时,直接在被扩展的对象上直接调用,而方法定义中的第一个参数实际上就绑定到了对象上。
int n = 1; System.Console.WriteLine(n.IsEven());
组织你的代码
现在我们有了各种类、方法、扩展。我们给他们取了各种不同的名字,随着程序逐渐增长,我们会定义越来越多的类型和方法,很有可能会和来自其他地方的类型名称发生冲突,另一方面如果把所有东西都放在一个地方也不方便我们阅读。
一个文件一个类型
定义一个类或者结构时,我们应该考虑将其单独放在一个文件中。因为这类定义往往伴随着大量字段、方法的定义,会变得很长。结构也建议单独一个文件,方便修改,当然如果你的接口很简单并且能够确保变换不大,那么写在某个类的文件中也行。文件名通常以类型名称为名。
internal
访问权限修饰符除了用于类型的成员之外,还可以用于类型本身。
访问权限修饰符除了public、protected、private之外还有一个internal我们没讲到。internal的意思是“内部的”。被冠以internal的类型将只能在本程序集中使用,而不能被其他程序集使用。程序集可以理解为就是我们开发的整个程序最后生成的一个文件(具体来说可能是dll也可能是exe或者其它,取决于项目类型),如果我们开发的是一个库(library),最后很可能我们会分发给其他人,我们的程序集会被其他程序加载。这个时候我们的代码细节中某些部分可能不应当或者不需要暴露给用户,但是对于我们自己的程序集本身是安全的、必要的。这个时候就可以声明为internal。而如果声明为public,自然,其他程序集也可以自由使用这个类型。
一个类或结构直接定义在一个命名空间中时,其默认访问权限(也就是我们不显式指定访问权限修饰时)其访问权限为internal。一个结构或者类的访问权限可以由public、internal、private修饰。需要注意的是,派生类型的权限不能比基类更开放,道理很简单,如果某个地方不能访问到基类,那么有什么道理可以访问派生类?
命名空间
命名空间是又一种组织代码的方式。除了.NET提供的访问基础类库的命名空间(如我们已经熟悉的System)之外,我们也可以定义自己的命名空间。
定义命名空间时,以namespace关键字(即命名空间)开始,后接命名空间名称,花括号中可以进行类型的定义。
namespace Zoo { class Dog : Animal, IPayable { //…… } }
这个时候,Dog就只能被同在Zoo命名空间的类型或者引入了这个命名空间的代码访问到。
要注意的是,命名空间的定义并没有那么严格的限制。命名空间不和文件、类型(甚至是程序集)对应。来自不同文件的各个类型都可以被放进同一个命名空间,已经存在的命名空间也可以被“打开”放进更多的类型(比如你可以尝试把你的类型放进看起来很高贵的System命名空间)。
此外,命名空间也可以嵌套,就像目录一样。无论你是使用命令行工具还是IDE创建项目,通常都会为你创建一个以项目为名的命名空间。如果在项目目录下你创建了其他子文件夹,IDE会为你创建以文件夹为名的子命名空间,你也可以仿照这种做法来组织代码。要访问命名空间时,采用的是一样的成员访问运算符。
带点的命名空间
命名空间的名称中可以包含成员访问运算符(.),它可以使得命名空间看起来相互嵌套。不过实际上这样的命名空间没有任何特殊语义,只是看起来更像是一个“子命名空间”,如果位于A.B命名空间的代码没有using A的话,其实依然是不可访问A中的成员的。不过很多IDE和编辑器确实会按照.来提示相关的命名空间和成员。
namespace TheZoo.Animals{}
using指令
using指令大家已经见识到了,可以用它来偷懒。using某个命名空间之后,使用其中的类型我们就可以省去命名空间而使用较短的名称了。
using System; //…… Console.WriteLine(); // 全名为System.Console.WriteLine
除此之外,using还有其他功能(虽然主要都是为了偷懒)。
using static
using static指令可以使用类型中的静态成员而不需要再指定类型名称。比如基础类库中提供了一个静态类Math,其中包含了大量和数学有关的方法。我们讲过,对于静态类或一般类的静态成员,我们需要通过类型名称点出来。如果我们的代码中需要大量使用这些静态成员的话,类型名称会显得很冗余。using static指令会解决这个问题:
using static System.Math;
别名
using还可以给类型取别名以简化过长的名称。另一方面还可以防止某个命名空间下你不需要的类型来干扰你。
比如System里面除了Console类其实还包括了很多其他类,至少就现在来说我们主要还是用的Console,所以其实有潜在的命名冲突问题。这个时候我们可以不要using System(前提是你确实没用System里面提供的其他类型),而是给我们常用的Console取个别名,只引入它:
using Console = System.Console;
这样即使不using System,编译器也知道我们用的是System.Console了。
更多有关using指令,参见这里。