CLR via CSharp 08 接口

接口

CLR并不支持多继承,所以所有的托管编程语言也支持不能。

类和接口继承

CLR只支持类的单继承,而且最终肯定是从Object派生。

所谓接口,是对一组方法签名进行的同一命名(这就是实质了)。

定义接口

CLR中的接口相对于Java中的接口而言,还能够定义:

  • 事件
  • 无参属性
  • 有参数性(C#的索引器)

当然以上说的三种,本质上也是方法。

在CLR看来,接口定义就是类定义。CLR会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。接口本身可以是public, protected, internal的,但是其所定义的成员必须是public的。

实现接口的方法必须是public的,编译器会自动把它设置成public virtual sealed,即不可继承的。此时,虽然派生类不能再用override关键字重写该方法,但是可以通过再次实现该接口,来实现这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Animal
{
void Eat();
}

public class Human : Animal
{
public void Eat() { } // 默认为 public virtual sealed
}

/*
public class BabyHuman : Human
{
public override void Eat() { } // 不存在的,回编译失败
}
*/

public class BabyHuman : Human, Animal
{
public void Eat() { } // 属于自己的Eat实现
}

如果程序员手动设置该方法为virtual的,编译器就不会鸡贼地给你加上sealed,会保持方法的不封闭性。

1
2
3
4
5
6
7
8
9
public class Human : Animal
{
public virtual void Eat() { }
}

public class BabyHuman : Human
{
pulbic override void Eat() { }
}

关于接口调用方法

CLR允许定义接口类型的字段、参数或局部变量。这意味着面向接口编程会变得很方便。

1
2
3
4
5
6
Animal a = new Human();

public void AnimalRun(Animal animal)
{
animal.Run();
}

值类型与接口

值类型亦可以实现任意多个接口。但是,值类型的实例在转换为接口类型时必须装箱,这是因为接口变量是引用,必须指向堆上的对象,使CLR能:

  • 检查对象的类型指针对象
  • 判断对象的确切类型(因故才能知道都有些什么方法可以调用)

显式和隐式调用接口方法的区别

接口可以被隐式实现。只是要要求实现的方法必须是public的,且方法签名必须完全相同(加了virtual也可以)。如此,编译器会自动将接口的方法和隐式实现的方法相匹配。匹配之后,生成的元数据中,该类的方法表中的两个记录项(注意,其实还是两个不同的方法,只是链接在了一起)的引用指向同一个实现。此时,通过接口或者直接通过类来访问方法,都会最后调用同一个实现。

1
2
3
4
5
6
7
8
9
10
11
12
class SimpleType : IDisposable {
public void Dispose() { Console.WriteLine("public Dispose"); }
}

public static void main(string[] args)
{
SimpleType s = new SimpleType();
s.Dispose();

IDisaposable d = s;
d.Dispose();
}

我们也可以显式实现接口方法,注意不能带public或者其他任意权限修饰符,也不能加virtual。

1
2
3
4
sealed class SimpleType : IDisposable {
public void Dispose() { Console.WriteLine("public Dispose"); }
void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }
}

如果显式实现了方法,就会分别把两个记录项引用到对应的方法上。

当我们要同时实现两个具有相同签名的接口的时候,也是需要显示接口方法实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IWindow
{
Object GetMenu();
}

public interface IResturant()
{
Object GetMenu();
}

public class KFC : IWindow, IResturant
{
public Object GetMenu {}
public Object IWindow.GetMenu {}
public Object IResturant.GetMenu {}
}

当然,使用的时候也得用对应的接口去引用这个类的对象,才能调用对应的方法。

谨慎使用显示接口方法实现。因为:

  • 没有IDE的智能提示
  • 增加装箱的可能性 值类型的实例在转换成接口的时候(由于是显式实现所以必须用接口引用才能调用方法),必须进行装箱。
  • 显式实现的方法不能由派生类调用

泛型接口

多使用泛型接口,某种程度上来说可能会降低装箱的机会。比如,实现了IComprable<Int32>之后,再传入一个Int32类型的对象,就不会被强转成Object。

注意,一个类可以同时实现多次同一个泛型接口,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public sealed class Number : IComparable<Int32>, IComparable<String> 
{
private Int32 m_val = 5;

public Int32 CompareTo(Int32 n)
{
return m_val.CompareTo(n);
}

public Int32 CompareTo(String s)
{
return m_val.Compare(Int32.Parse(s));
}
}

接口泛型约束

就是普通的约束,真的没必要多弄一小节出来呢。

1
2
3
4
5
6
7
8
class Foo
{
/// <summary>只要是实现了这两个接口的类型都可以作为参数</summary>
public void Bar<T>(T param) where T: IComparable, IConvertable
{
// ...
}
}

字符、字符串和文本处理

字符

在.Net中,char类型总是表示成16位Unicode代码值。System.Char是一个结构,当然也就是一个值类型。

这个类型提供了两个只读常量字段:

  • MinValue '\0'
  • MaxValue '\uffff'

静态方法GetUJnicodeCategory返回一个枚举类型System.Globalization.UnicodeCategory的一个值,代表这个字符是控制字符、货币符号、小写字母、大写字母、标点符号、数学符号还是其他字符。

1
2
3
4
5
6
7
8
static void Main(string[] args)
{
Char c = '你'; // OtherLetter
Char c1 = 'H'; // UppercaseLetter
Console.WriteLine(Char.GetUnicodeCategory(c));
Console.WriteLine(Char.GetUnicodeCategory(c1));
Console.ReadLine();
}

Char还提供了各种方法,包括IsDigit, IsLetter等巨多方法,可以帮助你判断char的类型。

Equal方法,当两个char代表的unicode码相同时返回true。

最后还有一个神奇的方法,可以返回字符的数值形式(注意,不是返回unicode码,而是字符字面意义上对应的数字)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Main(string[] args)
{
Double d;
d = Char.GetNumericValue('3');
Console.WriteLine(d); // 3

d = Char.GetNumericValue('¼');
Console.WriteLine(d); // 0.25

d = Char.GetNumericValue('A');
Console.WriteLine(d); // -1

Console.ReadLine();
}

Char实例转换成各种数值类型

  • 强转成Int32 效率最高,因为编译器会生成IL指令来转换,不必调用任何方法
  • System.Convert提供的静态方法,全都是checked的,会检查是否溢出
  • IConvertible接口 效率最差。Char类型和FCL中所有的值类型都实现了该接口,使得可以使用ToUInt16, ToChar类似的方法。效率最差是因为每次值类型要执行这个接口的方法的时候,都需要装箱
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
static void Main(string[] args)
{
Char c;
Int32 n;

c = (Char) 65;
Console.WriteLine(c); // A

n = (Int32)c;
Console.WriteLine(n); // 65

c = unchecked((Char) (65536 + 65));
Console.WriteLine(n); // A

try
{
c = Convert.ToChar(70000);
}
catch(OverflowException)
{
Console.WriteLine("overflow");
}

// 发生了装箱操作
c = ((IConvertible)65).ToChar(null);
Console.WriteLine(n); // A

Console.ReadLine();
}

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
2
3
4
5
6
7
8
9
10
11
.method private hidebysig static void  Main(string[] args) cil managed
{
.entrypoint
// Code size 8 (0x8)
.maxstack 1
.locals init ([0] string s)
IL_0000: nop
IL_0001: ldstr "halo"
IL_0006: stloc.0
IL_0007: ret
} // end of method Program::Main

如果字符串是在代码里用常量拼起来的,那么在编译期间就会把它们串起来:

1
String s = "are " + "u " + "ok";

IL代码

1
IL_0001:  ldstr      "are u ok"

但是如果想在程序运行期间拼字符串,最好还是用StringBuilder好。

使用逐字字符串构造,在前面加一个@符号即可,字符串的内容不会被转义。

字符串是不可变的

任何字符串一经创建后,就不可更改。为了避免爆内存,CLR可以共享一个String对象给各个引用,这个技术叫 字符串留用

String是一个封闭类,不能作为基类。

字符串留用

Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//