学习CLR via C#(第4版)(四) - 类型和成员变量

类型和成员变量

6.1类型的各种成员

在一个类型中,可以定义0个或多个以下种类的成员:

1)常量 常量就是指出数据值恒定不变的符号。这些符号通常用于使代码更容易阅读和维护。常量通常与类型关联,而不与类型的实例关联。从逻辑上讲,常量始终是静态成员

2)字段 字段表示一个只读或可读/写的数据值。字段可以是静态的,这时是类型状态的一部分字段也可以是实例(非静态)的,这时字段是对象状态的一部分。强烈建议将字段声明成为私有字段,防止类型或对象状态被外部代码破坏。

3)实例构造器 实例构造器是将新对象的实例字段初始化为良好初始化状态(即完成预期初始化)的一种特殊方法。

4)类型构造器 类型构造器是将类型的静态字段初始化为良好初始化状态(即完成预期初始化)的一种特殊方法。

5)方法 方法是更改或查询类型或对象状态的函数。作用于类型时,称为静态方法;作用于对象时,称为实例方法。方法一般会对类型或对象的字段执行读写操作。

6)操作符重载 操作符重载实际是一个方法,它定义了将一个特定的操作符作用于对象时,应该如何操作这个对象。由于不是所有编程语言都支持操作符重载,所以操作符重载方法不是"公共语言规范"(CLS)的一部分。

7)转换操作符 转换操作符是定义如何显式或隐式地将对象从一种类型转型为另一种类型的方法。不是所有编程语言都支持转换操作符,所以转换操作符方法不是"公共语言规范"(CLS)的一部分。

8)属性 利用属性(property),可以使用一种简单的、字段风格的语法来设置或查询类型或对象的部分逻辑状态,同时保证状态不被破坏。作用于类型的称为静态属性,作用于对象的称为实例属性。属性可以是没有参数的(比较普遍),但也可以有参数的(相当少见,但对集合类来说很常见)。

9)事件 利用静态事件,一个类型可以向一个或多个静态或实例方法发出通知。而利用实例事件(非静态),一个对象可以向一个或多个静态或实例方法发出通知。提供事件的类型或对象的状态发生改变,通常就会引发事件。事件包含两个方法,允许静态或实例方法登记或注销对该事件的关注。除了这两个方法,事件通常还使用一个委托字段来维护已登记的方法集。

10)类型 类型可定义嵌套于其中的其他类型。通常用这个方法将大的、复杂的类型分解成更小的构建单元,以简化实现。

无论使用什么编程语言,它的编译器都必须能处理你的源代码,为上述的每一种成员生成元数据和IL代码。无论使用哪种编程语言,元数据的格式都是完全一致的。所以才使得CLR能成为"公共语言运行时"。元数据是所有语言都生成和使用的公共信息。

CLR使用公共元数据格式决定常量、字段、构造器等成员在运行时的行为。简单地说,元数据是整个.NET Framework开发平台的关系,它实现了编程语言、类型和对象的无缝集成。

6.2类型的可见性

在文件范围中定义类型时,可以将类型的可见性指定为public或internal。

public类型不仅对它定义的程序集中的所有代码可见,还对其它程序集中的代码可见。

internal类型仅对定义程序集中的所有代码可见,对其它程序集中的代码不可见。定义类型时,如果不显式定义类型的可见性,C#编译器默认将类型的可见性设为internal。

CLR和C#可以通过友元程序集(friend assembly)来使得一个程序集对另一个可见性为internal的程序集进行public操作。

构建程序集时,可以使用System.Runtime.CompilerServices命名空间中定义一个名为InternalsVisibleTo的attribute来标明它认为的"友元"的其它程序集。这个attribute要获取一个字符串参数,这个字符串参数要标识友元程序集的名称和公钥。

using Systeml
using System.Runtime.CompilerServices 
 
[assembly:InternalsVisibleTo("Winterllect,publickKey=12345678....asdf")]
.....

"友元程序集"功能只适合发布时间相同的程序集,最好是打包一起发布。因为友元程序集相互依赖程度很好,错开太久可能发生版本不兼容的问题。

6.3成员的访问性

定义类型的成员(包括嵌套类型)时,可指定成员的可访问性(accessibility)。

CLR和C#中的成员可访问性对比:

CLR术语C#术语描述
Privateprivate成员只能由定义类型或任何嵌套类型中的方法访问
Familyprotected访问仅限于包含类或从包含类派生的类型
Family and Assembly不支持成员只能由定义类型、任何嵌套类型或者同一程序集中定义的任何派生类型中的方法访问
Assemblyinternal访问仅限于当前程序集
Family or Assemblyprotected internal访问仅限于从包含类派生的当前程序集或类型
Publicpublic访问不受限制

在C#中如果没有显示声明成员的可访问性,编译器通常默认选private。

CLR要求接口类型的所有成员都具有public访问性。所以C#就禁止开发人员显示指定接口的可访问性。

一个派生类重写它的基类中定义的一个成员时,C#编译器要求原始成员和重写成员具有相同的访问性。但CLR不然,从一个基类派生时,CLR允许放宽成员的可访问性的限制,但不可收紧。因为CLR承诺派生类总是可以转换成为基类,并获取对基类方法的访问权控制。

类的默认修饰符是什么,类成员的默认类型是什么,接口成员的默认修饰符是什么?

类的默认修饰符是internal;类成员的默认类型是private;接口成员的默认修饰符是public,但禁止开发人员显式指定。

以下代码可否通过编译,为什么

class MyClass
{
    public virtual void TestOne()
    {
    }
 
    protected virtual void TestTwo()
    {
    }
 
    internal virtual void TestThree()
    {
    }
}
 
class MyClassTwo : MyClass
{
    private override void TestOne()
    {
        base.TestOne();
    }
 
    public override void TestTwo()
    {
        base.TestTwo();
    }
 
    public override void TestThree()
    {
        base.TestThree();
    }
}

不可以。一个派生类重写他的基类中定义的成员时,C#编译器要求原始成员可重写成员拥有相同的可访问性。

6.4静态类

在C#中,要用static关键字定义不可实例化的类。这个关键字只能应用于类,不能引用于结构 (值类型)。这是因为CLR总是允许值类型实例化。

C#编译器对静态类做了如下限制:

1)静态类必须直接从基类System.Object派生,从其他任何基类派生都是无意义的。继承只适用于对象,而你不能创建静态类的实例。

2)静态类不能实现任何接口,因为只有使用类的实例,才能调用接口的方法。

3)静态类只能定义静态成员(字段、方法、属性和事件),任何实例成员都将导致编译器报错。

4)静态类不能作为字段、方法参数或局部变量使用,因为它们都代表引用了一个实例的变量。

使用关键字static定义一个类,将导致C#编译器将该类同时标记为abstract(不可实例化)和sealed(不可派生)。编译器不会再类型中生成一个实例构造方法(.ctor)。

静态类被编译后,IL中的修饰符是什么

abstract sealed. 详见如下:

namespace StaticClass
{
    static class Test
    {
    }
}

在这里插入图片描述

6.5分布类、结构和接口

partial这个关键字告诉C#编译器,一个类、结构或者接口的定义源代码可能分散到一个或者多个源代码文件中。

主要有三方面原因促使我们将某个类型的源代码分散到多个文件中:

1)源代码控制 使用partinal关键字,可以将类型的代码分散到多个源代码文件中,每个文件都可以单独签出使多个开发人员同时编辑该类型。

2)在同一个文件中,将一个类或结构分解成不同的逻辑单元

3)代码拆分

小结:

partial关键字可将一个类、结构或者接口的定义源代码分散到多个文件中。(如果这个类过大的话)

(其实在《重构》这本书中讲过,一个过大的类往往是需要重构的。可能你用partial这种方法解决反而掩盖了本身需要重构的问题)

partial是C#编译器阶段处理的时候就用到的,此时已然把几个partial类合在了一起。CLR对此功能一无所知。

何时用分部类、结构和接口?

**1)**用于源代码控制。如多个开发人员共同写一个类,使用partial关键字标记;

**2)**在同一个文件中,讲一个类或结构分解成不同的逻辑单元。

**3)**代码拆分。如新建一个winform程序,可以发现Form1.cs下,有一个Form1.Designer.cs文件,其中声明了一个partial class Form1,这个类中的代码是自动生成的设计器相关代码,还有一个Form1文件,其中声明了一个继承了Form类的partial class Form1,可以在这个类中编辑自己的代码,这样将代码拆分开来,无论自己的代码如何修改都不会影响设计器的代码。

分部类型完全由C#编译器实现,CLR对分部类型一无所知

6.6组件、多态和版本控制

组件软件编程(Component Software programming,CSP)正是OOP发展到极致的一个成功。下面列举了组件的一些特点。

1)组件(.NET中成为程序集)有"已经发布"的意思

2)组件有自己的标识(名称、版本、语言文化和公钥)

3)组件永远维持自己的标识(程序集中的代码永远不会静态链接到另一个程序集中:.NET总是使用动态链接)

4)组件清楚指明它所依赖的组件(引用元数据表)。

5)组件要文档化它的类和成员。

6)组件必须指明它要求的安全权限。

7)组件要发布一个在任何"维护版本"中都不会改变的接口(对象)。“维护版本”(servicing version)代表组件的新版本,它向后兼容于组件的原始版本。通常,"维护版本"包含bug修复、安全补丁或者一些小的功能增强。但在"维护版本"中,不能要求任何新的依赖关系,也不能要求任何附加的安全权限。

CSP的很大一部分都涉及到版本控制。组件随时间不断变化,并根据不同的时间表来发布。

.NET中,版本号包括4个部分:主版本号(mahor version)、次版本号(minor version)、内部版本(build number)和修订号(recision)。

major/minor部分通常代表程序集的一个连续的、稳定的功能集,而build/revision部分通常代表对这个功能的一次维护。

将某个组件(程序集)中定义的一个类型作为另一个组件(程序集)中的一个类型的基类使用时,便会发生版本控制问题。

C#提供了5个能影响组件版本控制的关键字,可将它们应用类型以及/或者类型成员。

C#关键字类型方法/属性/事件常量/字段
abstract表示不能构造该类型的实例表示为了构造派生类型的实例,派生类型必须重写并实现这个成员不允许
virtual不允许表示这个成员可由派生类重写不允许
override不允许表示派生类型重写了基类型的成员不允许
sealed表示该类型不能用作基类表示这个成员不能被派生类型重写,只能将该关键字应用于准备重写一个虚方法的方法不允许
new引用于嵌套类型、方法、属性、时间、常量或字段时,表示该成员与基类型中相似的成员无任何关系

6.6.1CLR如何调用虚方法、属性和事件

以下Employee类定义了三种不同的方法:

internal class Employee {
    //非虚实例方法
    public         int32     GetYearsEmployed() { ... }
    //虚方法
    public virtual String    GenProgressReport() { ... }
    //静态方法
    public static  Employee  Lookup(String name) { ... }     
}

编译器编译上述代码,会生成的程序集的方法定义表中写入三个记录项,每个记录项都用一组**标识(flag)**来指明该方法是实例方法、虚方法还是静态方法。

CLR提供了两个方法调用指令:

1)call 这个调用指令可调用静态方法、实例方法和虚方法。用call指令调用静态方法时,必须指定是哪个类型定义了要由CLR调用的方法。用call指令调用实例方法或虚方法时,必须指定引用了一个对象的变量。call指令假定该变量不为null。换言之,该变量本身的类型指明了要由CLR调用的方法是在哪个类型中定义的。如果变量的类型没有定义该方法,就检查基类型来查找匹配的方法。call指令经常用于以非虚的方式调用一个虚方法

2)callvirt 这个调用指令可调用实例方法和虚方法,但不能调用静态方法。用callvirt指令调用实例方法或虚方法时,必须指定引用了一个对象的变量。用callvirt指定调用非虚实例方法时,变量的类型指明了最终由CLR调用的方法是在哪个类型中定义的。用callvirt指令调用虚实例方法时,CLR会调查发出调用的哪个对象的实际类型,然后以多态方式调用。为了确定类型,用来发出调用的变量决不能为null。换言之,编译这个调用时,JIT编译器会生成代码来验证变量的值是不是为null。正是由于要进行这种额外的检查,所有callvirt指令执行速度比call稍慢。注意,即使callvirt指令调用的是一个非虚的实例方法,也会执行这种null检查。

​ C#使用callvirt指令调用所有的实例方法。

​ 编译器调用由值类型定义的方法时,会倾向使用call指令,因为值类型是密封的。这就意味着即使你非要在值类型中写一些虚方法也不用考虑多态的问题。

​ 无论是call还是callvirt来调用实例方法或虚方法,这些方法通常接受一个隐藏的this实参作为方法的第一个参数。this实参引用的是要操作的对象。

设计一个类型时,应尽量减少所定义的虚方法的数量。首先,调用虚方法的速度比调用非虚方法慢。其次,JIT编译器不能内嵌虚方法,这进一步影响性能。第三,虚方法使组件的版本控制变得更脆弱。第四,定义一个基类型时,经常需要提供一组重载的简便方法。如果希望这些方法是多态的,最好就是是最复杂的方法称为虚方法,使所有重载的简便方法成为非虚方法。

public class Set {
    private Int32 m_length = 0;
    
    //这个重载的简便方法是非虚的
    public Int32 Find(Object value) {
        return Find(value,0,m_length);
    }
 
    //这个重载的简便方法是非虚的
    public Int32 Find(Object value, Int32 startIndex) {
        return Find(value, startIndexx, m_length - startIndex);    
    }
 
    //功能最丰富的方法是虚方法,可以被重写
    public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex){
        //可被重写的实现放在这里....
    }
    
    //其他方法
}

虚方法没有非虚方法调用快,过多的应用虚方法会降低性能。

6.6.2合理使用类型的可见性和成员的可访问性

首先,在定义一个新的类型时,编译器应默认是密封类,使它不能作为基类使用。C#编译器默认是非密封类,但允许开发人员使用关键字sealed显示地将类标记为密封。

使用密封类比非密封类更好,理由有三:

1)版本控制 可以容易的将密封类改成非密封类,但非密封类改成密封类就麻烦许多。

2)性能 如上所说,调用虚方法的性能比不上调用非虚方法,因为CLR为了判断是哪个类型定义了要调用的方法,必须在运行时查找对象的类型。但是,如果JIT编译器看到的是使用密封类型对虚方法调用,就可以采用非虚的方式调用虚方法。

3)安全性和可预测性 类必须保护它自己的状态,不允许自己被破坏。当类处于非密封非私有的,派生类就能访问和更改基类的状态。

定义类是遵循的一些原则:

1)定义类时,除非确定要将一个类作为基类使用,并允许派生类对它进行特化处理,否则总是显式地把它指定为sealed类。另外,默认将类指定为internal类,除非希望在程序集外部公开这个类。

2)在类的内部,将数据字段定义为private。

3)在类的内部,将自己的方法、属性和事件定义为private和非虚。当然,会将某个方法、属性和事件定义为public,以便公开类的某些功能。virtual是最后会考虑的,因为虚成员会放弃许多控制,丧失独立性,变得彻底依赖于派生类的正确行为。

定义类时除非确定要将其作为基类,并允许派生类进行特化,否则应该显示指定sealed。

6.6.3 对类型进行版本控制时的虚方法的处理

使用override重写基类虚方法。

略(virtual,override,new关键字)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值