接口
CLR并不支持多继承,所以所有的托管编程语言也支持不能。
类和接口继承
CLR只支持类的单继承,而且最终肯定是从Object派生。
所谓接口,是对一组方法签名进行的同一命名(这就是实质了)。
定义接口
CLR中的接口相对于Java中的接口而言,还能够定义:
- 事件
- 无参属性
- 有参数性(C#的索引器)
当然以上说的三种,本质上也是方法。
在CLR看来,接口定义就是类定义。CLR会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。接口本身可以是public, protected, internal的,但是其所定义的成员必须是public的。
实现接口的方法必须是public的,编译器会自动把它设置成public virtual sealed
,即不可继承的。此时,虽然派生类不能再用override
关键字重写该方法,但是可以通过再次实现该接口,来实现这个方法。
1 | public interface Animal |
如果程序员手动设置该方法为virtual
的,编译器就不会鸡贼地给你加上sealed
,会保持方法的不封闭性。
1 | public class Human : Animal |
关于接口调用方法
CLR允许定义接口类型的字段、参数或局部变量。这意味着面向接口编程会变得很方便。
1 | Animal a = new Human(); |
值类型与接口
值类型亦可以实现任意多个接口。但是,值类型的实例在转换为接口类型时必须装箱,这是因为接口变量是引用,必须指向堆上的对象,使CLR能:
- 检查对象的类型指针对象
- 判断对象的确切类型(因故才能知道都有些什么方法可以调用)
显式和隐式调用接口方法的区别
接口可以被隐式实现。只是要要求实现的方法必须是public的,且方法签名必须完全相同(加了virtual也可以)。如此,编译器会自动将接口的方法和隐式实现的方法相匹配。匹配之后,生成的元数据中,该类的方法表中的两个记录项(注意,其实还是两个不同的方法,只是链接在了一起)的引用指向同一个实现。此时,通过接口或者直接通过类来访问方法,都会最后调用同一个实现。
1 | class SimpleType : IDisposable { |
我们也可以显式实现接口方法,注意不能带public或者其他任意权限修饰符,也不能加virtual。
1 | sealed class SimpleType : IDisposable { |
如果显式实现了方法,就会分别把两个记录项引用到对应的方法上。
当我们要同时实现两个具有相同签名的接口的时候,也是需要显示接口方法实现的。
1 | public interface IWindow |
当然,使用的时候也得用对应的接口去引用这个类的对象,才能调用对应的方法。
谨慎使用显示接口方法实现。因为:
- 没有IDE的智能提示
- 增加装箱的可能性 值类型的实例在转换成接口的时候(由于是显式实现所以必须用接口引用才能调用方法),必须进行装箱。
- 显式实现的方法不能由派生类调用
泛型接口
多使用泛型接口,某种程度上来说可能会降低装箱的机会。比如,实现了IComprable<Int32>
之后,再传入一个Int32类型的对象,就不会被强转成Object。
注意,一个类可以同时实现多次同一个泛型接口,就像这样:
1 | public sealed class Number : IComparable<Int32>, IComparable<String> |
接口泛型约束
就是普通的约束,真的没必要多弄一小节出来呢。
1 | class Foo |
字符、字符串和文本处理
字符
在.Net中,char类型总是表示成16位Unicode代码值。System.Char
是一个结构,当然也就是一个值类型。
这个类型提供了两个只读常量字段:
- MinValue
'\0'
- MaxValue
'\uffff'
静态方法GetUJnicodeCategory
返回一个枚举类型System.Globalization.UnicodeCategory
的一个值,代表这个字符是控制字符、货币符号、小写字母、大写字母、标点符号、数学符号还是其他字符。
1 | static void Main(string[] args) |
Char还提供了各种方法,包括IsDigit
, IsLetter
等巨多方法,可以帮助你判断char的类型。
Equal
方法,当两个char代表的unicode码相同时返回true。
最后还有一个神奇的方法,可以返回字符的数值形式(注意,不是返回unicode码,而是字符字面意义上对应的数字)。
1 | static void Main(string[] args) |
Char实例转换成各种数值类型
- 强转成Int32 效率最高,因为编译器会生成IL指令来转换,不必调用任何方法
- System.Convert提供的静态方法,全都是
checked
的,会检查是否溢出 - IConvertible接口 效率最差。Char类型和FCL中所有的值类型都实现了该接口,使得可以使用
ToUInt16
,ToChar
类似的方法。效率最差是因为每次值类型要执行这个接口的方法的时候,都需要装箱。
1 | static void Main(string[] args) |
System.String
System.String
代表一个不可变(immutable)的顺序字符集,直接派生于Object,是引用类型。String的对象永远存在于堆上,虽然看起来有String a = "HeWol"
这种形式的写法,但是注意,其数据永远不会存在于线程栈上。
String类型同时实现了几个接口:
- IComparable
可以和其他字符串进行大小比较 - ICloneable 可以克隆
- IConvertible 可以转换成其他类型
- IEnumerable/IEnumeratble
迭代器,可以用来遍历所有char字符 - IEquatable
???
构造字符串
C#不允许用new
字符串来构造string对象,必须使用简化语法
1 | String s = "Hello"; |
通过查看IL代码,可以看到新建字符串对象并不是用的newobj
指令,而是ldstr
(load string)指令。
1 | .method private hidebysig static void Main(string[] args) cil managed |
如果字符串是在代码里用常量拼起来的,那么在编译期间就会把它们串起来:
1 | String s = "are " + "u " + "ok"; |
IL代码
1 | IL_0001: ldstr "are u ok" |
但是如果想在程序运行期间拼字符串,最好还是用StringBuilder
好。
使用逐字字符串构造,在前面加一个@
符号即可,字符串的内容不会被转义。
字符串是不可变的
任何字符串一经创建后,就不可更改。为了避免爆内存,CLR可以共享一个String对象给各个引用,这个技术叫 字符串留用。
String是一个封闭类,不能作为基类。