欢迎光临散文网 会员登陆 & 注册

第 32 讲:面向对象编程(四):索引器

2021-05-04 10:50 作者:SunnieShine  | 我要投稿

上回我们说完了属性的基本用法,它把字段包装起来,避免外部调用的时候出现赋值的错误。今天我们来讲一下第二类属性:索引器(Indexer)。

索引器也称为有参属性(Parameterful Property),因为它除了和属性的基本用法差不多以外,还可以带有一些自定义的额外参数信息。索引这个词语在 C 语言里就已经存在了,不过它不能自定义。C 语言里,我们取一个数组或者指针对应位置的数据的时候,会使用索引运算符,就是那个中括号语法。

在 C# 里,为了灵活使用语法,C# 贴心地为我们提供了自定义索引器的机制,并把索引器当成了面向对象的一种成员,可见地位还是很高的。

Part 1 引例

假设我们设计了两个数据结构,一个是链表节点(Node),一个则是叫链表(List)。假设我们暂时不考虑那些增删改查的处理代码,而只关心基本的过程的话:

这里啰嗦一下前面好像没有说过的语法点。因为它们经常用到,但因为这些东西比起别的知识点来说不是那么重要,所以我们不必单独拿一节内容给大家讲,干脆就在这里说了。

首先是这里的 List 类下的 Start 属性和 Node 类下的 Next 属性。这两个属性是没有 set 方法的。在 C# 里,这个语法是允许的,这种属性被称为只读属性(Read-only Property)。这里的“只读”和字段的“只读”略有不同。字段里,“只读”指的是字段的数据无法修改而标记的 readonly 修饰符;而属性里,因为属性是两个方法(getset 方法)的整合形式的成员,因此我们是无法对方法标记 readonly 修饰符的;而另一方面,因为属性只有 get 方法,因此它仅用来取值,所以用户就无法使用 属性 = 数值 的赋值语法对后台的这个字段作赋值了。因此从这个角度来说,它是“只读”的:只用来读取数据的。从另外一个角度来说,我们无法使用 属性 = 数值 的赋值语法,也就无从通过别的方式对这个后台字段赋值了。后台字段此时就是只读的了,因此这个字段可以标记 readonly 修饰符。

第二。我们可以看到 Node 类型里,有一个 Node 自己这个数据类型的字段。这个语法称为递归类型成员(Recursed Member)。按道理讲,如果这个 A 类型下有 A 类型的字段的话,那么数据存储就可能无休止地使得存储空间膨胀起来直到撑爆内存。但实际上是这样的吗?C# 其实并不会导致这种情况的发生。它采用了指针的概念。Node 类型是用类写成的,每一个 C# 的类里,所有用到的类类型的成员,都会被认为一个特殊的指针。换句话说,这个成员实际上是一个指针变量,它指向的就是另外一块内存区域,存储的就是这个成员的数据内容了。而就原本的这个对象来说,假设它只有一个 int 类型的数据,和一个 A 这个类类型的数据的话,那么整体这个对象的存储空间只占据 int 的大小,和一个指针类型的存储大小的总和。因此,数据本身并不会像我们想的那样:放在一起导致内存被撑爆。

最后,因为类类型的东西在存储到别的对象里作为一个成员而存在的时候,它是一个指针。因此,类类型的所有数据,它们的默认数值是 null。这个 null 在字符串里讲过,它表示“没有内存分配”。是的,引用类型一般都较大,而且大小不固定,所以用 null 专门表达这些类型的对象“还没有内存分配”。那么,如果我们没有针对字段赋初始数值的话,所有字段会被赋值为“它这个类型的默认数值”。比如说 int 类型的默认数值是 0,而 Node 类型的默认数值就是 null 了。如果不写,就等价于在字段最后追加 = null;

大概就这个样子。下面我们针对于这里的 List 类型来思考取元素的问题。

我们认为 List 是一个链表,因为它带有一个起头节点(Start 属性)。那么,我们通过移动“指针”,来达到遍历链表的过程。

比如这段代码。我们假设初始化的时候,给 temp 这个临时变量赋值 Start。然后,让 temp 不断执行 temp = temp.Next 这样的过程。tempNode 类型的,而 Node 类型里包含 Next 属性,这个 Next 属性的意思是“当前链表节点的下一个节点;如果链表没有下一个节点的话,这个属性的数值就是 null”。那么不断 temp = temp.Next 就是在不断让 temp 的“指针”移动到下一个节点上去的过程。直到中间的条件 temp != null 不成立的时候,循环退出。

在循环体里,我们不断执行 count++ 操作,这表示每移动一次 temp 的“指针”,count 就会自动增加一个单位。对,没有错,这个 count 就是在记录整根链表的总节点个数。

C# 里,所有类类型的东西都是可以直接使用 == null!= null 的语法判别对象是不是 null 这个数值的。但是,这个等号和不等号后面是不能写别的东西的,比如 == 3。因为 Node 类型怎么可能可以和一个 int 作比较呢?要比较也只能比较两个 Node 类型才对啊。

C# 的 ==!= 在值类型(就是前面说的那些个系统自带类型,string 除外)里,是判别数值是不是相同;但在类里,==!= 默认是判断指针是否一致,即两个对象是否指向的是同一块内存空间。显然,我们可以构造出两个数值上完全一样的 Node 类型的变量,但我们使用 ==!= 是无法判别里面存储的数值是否一致的,因此一定要注意,==!= 在类里的比较行为。

所以,我们可以使用属性 Length,并把这段代码抄进去,来计算链表的总长度。

是的,代码就这么写就完事了。也不是很难,对吧。

Part 2 索引器的语法

索引器之所以还有个名字叫做有参属性,是因为它的格式和属性的语法基本一样,也是使用 getset 来表达传入的信息。不过因为它还会带有额外参数,因此还是有不一样的语法。

假设,我们使用索引器来表达“这个链表的第几个节点”。这个“几”就是索引器参数。那么它的语法是这样的:

可以从代码里看到,get 是我们要实现的,set 方法实现起来太复杂了(牵扯到链表里上下两个节点的指针的变动),因此我们暂时不考虑,也没有必要这里讲这么难的东西,毕竟这不是数据结构的课程。

我们只是改良了一下 Length 属性的  get 方法的代码。我们使用 p 表示和记录当前走到第几个节点了。如果 p 一直往下移动,走到和 index 的数值一样的时候,我们就认为,我们走到了这个节点上,于是就把 temp 的数据(之前说过 temp 是类类型的变量,因此它表达的是一个指针,相当于把指针作为数值)返回出去。另外一方面,如果我们传入的 index 不正常的话(比如链表还没有那么长,但你传入的 index 超过了这个总长度,显然就没有意义),于是我们返回 null 默认表达一个隐式信息:index 不正常。当然,这里你抛异常也行。

这里我们要说的是两个点。一个是语法上 public Node this[in index] 里的 Node,另一个是 this[int index]

第一,Node 表达的意思和属性里表达的意思是一样的。写在索引器声明之前,如果是 get 方法,就表示这个属性的返回值类型;如果是 set 方法,就表示赋值的这个隐式变量 value 是什么类型的。索引器也是一样,get 方法里,表示这个返回值的类型是 Node;在 set 方法里则表示 value 这个隐式变量是 Node 类型的(从外部传进来的数值)。虽然我们这里 set 方法没有实现,但你肯定知道它的用法,对吧。

第二,this[int index]。这个语法看起来有点怪,我们说一下意思。this 是一个 C# 的关键字,它大概有四个用法:

  • 索引器声明里(this[int index] 里这个 this);

  • 构造器串联调用(: this(参数) 里这个 this,这个还没讲);

  • this 引用(this.成员 里这个 this,这个也没讲);

  • 扩展方法的参数(this 类型 参数名,这个是 C# 3 才有的特性,所以还没有讲)。

当然了,这些语法我们之后慢慢都会接触到,不过我们这里需要了解的是其中的第一个用法。这里的 this 你可以理解成“一个万能替换变量”。当 Node 类型的变量出来之后,这个变量名就可以直接配合索引器运算符 [] 来表示取值。数组里,我们使用 arr[3] 来取 arr 这个数组的第 4 个元素的数值,而这里的 arr 就是一个数值类型的变量,它的写法格式就和这里的 this[参数] 是一样的,而具体碰到什么变量了,就把这个变量替换掉这里的 this 来表达索引器,比如说 list[3] 来取链表的第 4 个节点。这就是为什么索引器的声明语法这么奇怪的原因。

另外,在索引器参数里,我们是写成类似方法的参数一样的形式,先写类型,然后再写参数名。这个参数名是可以在 getset 方法里用的。另外因为方法的特殊性,set 方法里你还可以用 value 这个隐式变量。

那么,语法我们就说到这里。下面我们来说一下索引器的使用。

Part 3 索引器的用法

下面我们说一下索引器的用法。其实也没啥好说的,和属性还有字段的方式完全一样,放在等号左侧就表示赋值(自动调用 set 方法);放在别的地方作为表达式计算的时候,就用的是 get 方法)。比如说:

这表示什么呢?这显然就表示我们要取出 list[2] 这个节点,然后取 Value 属性作为结果输出。显然 list[2] 就是 a 了,因此我们可以看到输出的结果是 1。

Part 4 其它的问题

最后说两个无关紧要的问题,可能你会有这样的疑惑。

4-1 参数个数是否无限制

索引器的参数(比如前面这个 int index)可以无限制往后追加吗?

实际上,是可以的。索引器参数并没有规定非得只能有一个。你看,C# 里,矩形数组不就是可以后面用逗号分隔每一个数值吗?索引器在声明出来的时候,你完全可以写很多参数进去,比如这样:

这样是可以的。只是你在调用索引器的时候,你也得写这么多参数进去:variable[0, 1, 2, 3.0, 4F, "hello"]

4-2 索引器是否可以重载

既然构造器、方法都能重载,那么索引器呢?

是的,索引器也能重载,毕竟它也是有参的存在。方法、构造器之所以可以重载,是因为它们都是有参数的成员。索引器是可以重载的,因为它也可以自定义参数和参数类型。只是我们从 C 语言里学习过来,可能有一种经验主义思维,以为 C# 索引器好像只能用 int 一样。实际上,并不是。因此,只要满足前面那些个重载规则,索引器也是可以重载的。


第 32 讲:面向对象编程(四):索引器的评论 (共 条)

分享到微博请遵守国家法律