• 沒有找到結果。

装箱和拆箱的实质是复制

3.5 访问权限修饰符

...

Point p1 = new Point(); // p1.x和p1.y都是0 Point p2 = new Point(1,1); // p2.x和p2.y都是1 以下的示例包含三个编译时错误:

public struct Point {

int x = 1; // 不合法:不能初始化字段

int y;

public Point() {} // 不合法:不能有无参数的构造方法

public Point(int x) { this.x = x; } // 不合法:必须指定y值 }

如果把struct替换成class,上面的写法都合法。

3.5 访问权限修饰符

为了提高封装性,类或类成员会在声明中添加五个访问权限修饰符之一,来限制其他类和其他程序集 对它的访问权限:

public

完全访问权限;枚举类型成员或接口隐含的访问权限。

internal

仅可访问程序集和友元程序集;非嵌套类型的默认访问权限。

private

仅在包含类型可见;类和结构体成员的默认访问权限。

protected

仅在包含类型和子类中可见。

protected internal

protected和internal的访问权限并集Eric Lippert是这样解释的:默认情况下尽可能将所 有成员定义为私有,然后每一个修饰符都会提高其访问级别。所以用protected internal修 饰的成员在两个方面的访问级别都提高了。

提示: CLR有对protected和internal访问权限交集的定义,但C#并不支持。

3.5.1 举例

Class2在本程序集外可访问,而Class1不可以:

class Class1 {} // Class1是internal访问权限的(默认)

public class Class2 {}

ClassB有对本程序集的其他类提供的字段x;ClassA没有:

class ClassA { int x; } // x是private访问权限的(默认)

class ClassB { internal int x; }

Subclass里的函数可以调用Bar但不能调用Foo:

class BaseClass {

void Foo(){} // Foo是private访问权限的(默认)

protected void Bar() {}

}

class Subclass: BaseClass {

void Test1() { Foo(); } // 出错:不能访问Foo void Test2() { Bar(); } // 正确

}

3.5.2 友元程序集

在高级语义应用中,加上System.Runtime.CompilerServices.InternalsVisibleTo属性,就可 以把internal成员提供给其他的友元程序集,用如下方法指定友元程序集:

[assembly: InernalsVisibleTo("Friend")]

如果友元程序集有强命名(见第18章),必须指定其完整的160字节公共键值:

[assembly: InternalsVisibleTo( "StrongFriend, PublicKey = 0024f000048c...")]

可以用LINQ查询从强命名的程序集中提取完整的公共键值(第8章详细介绍LINQ):

String key = string.Join("",

Assembly.GetExecutingAssembly().GetName().GetPublicKey() .Select(b=>b.ToString("x2"))

.ToArray());

提示: 在LINQPad中和本例相辅相成的一个例子,是浏览程序集后复制程序集的完整公共键值到剪贴板。

3.5.3 程序集的权限封顶

类权限是它内部声明的成员访问权限的封顶。关于权限封顶最常用的示例是internal类中的public 成员。例如:

class C { public void Foo() {}}

类C的(默认)访问权限是internal,它作为Foo的最高访问权限,把Foo的权限改为internal。

Foo指定为public的原因一般是,如果类C的访问权限要改成public的,重构会更容易。

3.5.4 访问权限修饰符的限制

当重载基类的函数时,重载函数的访问权限必须一致。例如:

class BaseClass { protected virtual void Foo() {} }

在C#中创建类

class Subclass1 : BaseClass { protected override void Foo() {} } // 正确 class Subclass2 : BaseClass { public override void Foo() {} } // 错误

(一个例外情况是,如果在另一个程序集中重写一个protected internal方法,那么重写方法必须 修饰为protected。)

编译器会阻止使用任何不一致的访问权限修饰符。例如,子类可以比基类访问权限低,但不能比基类 访问权限高:

internal class A {}

public class B: A {} //错误

3.6 接口

接口和类相似,但接口只为成员提供定义而不提供实现。接口和类的不同之处有:

接口的成员都是隐含抽象的。相反,类可以包含抽象成员和有具体实现的成员。

一个类(或结构体)可以实现多个接口。相反,类只能继承一个类,而结构体完全不支持继承

(只能从System.ValueType派生)。

接口声明和类声明很类似,但接口不提供其成员的实现,因为它的所有成员都是隐式定义为抽象的,

这些成员将由实现接口的类或结构体实现。接口只能包含方法、属性、事件、索引器,这些正是类中 可以定义为抽象的成员。

下面是System.Collections命名空间中IEnumerator接口的定义:

public interface IEnumerator {

pool MoveNext();

object Current { get; } void Reset();

}

接口成员总是隐式地定义成public的,并且不能用访问权限修饰符声明。实现接口意味着为其所有成 员提供public的实现:

internal class Countdown : IEnumerator {

int count = 11;

public bool MoveNext() { return count -->0; } public object Current { get { return count; } } public void Reset() { throw new NotSupportedException(); } }

可以把对象隐式转换为它实现的任意一个接口。例如:

IEnumerator e = new Countdown();

While(e.MoveNext())

Console.Write(e.Current); // 10987654210

提示:尽管Countdown是internal权限的类,通过把Countdown实例转换成IEnumerator,其内部实现 IEnumerator接口的成员可以作为public成员访问。例如,如果同程序集中的一个公有类定义了如下 方法:

Public static class Util {

Public static object GetCountDown() {

Return new Countdown();

} }

另一个程序集的调用者可以执行:

IEnumerator e = (IEnumerator) Util.GetCountDown();

e.MoveNext();

如果IEnumerator被定义成internal,上面的方法则不正确。

3.6.1 扩展接口

接口可以从其他接口派生。例如:

public interface IUndoable { void Undo(); } public interface IRedoable : IUndoable { void Redo(); }

IRedoable继承所有IUndoable的成员。换而言之,实现IRedoable的类型还必须实现IUndoable的成员。

3.6.2 显式接口实现

当实现多个接口时,有时成员标识符会有冲突。显式实现接口成员可以解决冲突。看下面的例子:

interface I1 { void Foo(); } interface I2 { int Foo(); } public class Widget: I1,I2 {

public void Foo() {

Console.WriteLine("Widget's implementation of I1.Foo");

}

int I2.Foo() {

Console.WriteLine("Widget's implementation of I2.Foo");

Return 42;

} }

因为I1和I2都有Foo标识符,Widget显式实现了I2的Foo方法。这是两个同名方法在同一个类中同时 存在。调用显式实现成员的唯一方法是先转换为相应的接口:

Widget w = new Widget();

w.Foo(); // Widget对I1.Foo的实现 ((I1)w).Foo(); // Widget对I1.Foo的实现 ((I2)w).Foo(); // Widget对I2.Foo的实现

在C#中创建类 另一个使用显式实现接口成员的原因是,隐藏那些和类的正常用法差异很大或有严重干扰性的成员。

例如:实现ISerializable接口的类型,通常需要避免强调它的ISerializable成员,除非显式转 换成这个接口。

3.6.3 虚方法实现接口成员

默认情况下,接口成员的实现是隐式定义为sealed。为了能重载,必须在基类中标识为virtual或者 abstract。例如:

public interface IUndoable { void Undo(); } public class TextBox: IUndoable

{

Public virtual void Undo() {

Console.WriteLine("TextBox.Undo");

} }

public class RichTextBox : TextBox {

public override void Undo() {

Console.WriteLine("RichTextBox.Undo");

} }

不管从基类还是接口中调用接口成员,调用的都是子类的实现:

RichTextBox r = new RichTextBox();

r.Undo(); //RichTextBox.Undo

((IUndoable)r).Undo(); // RichTextBox.Undo ((TextBox)r).Undo(); // RichTextBox.Undo

显式实现的接口成员不能标识为virtual的,也不能实现通常意义的重载。但是它可以被重新实现。

3.6.4 在子类中重新实现接口

子类可以重新实现基类中已经被实现的任意一个接口。不管基类中该成员是不是virtual的,当通过 接口调用时,重新实现都能够屏蔽成员的实现。它不管接口成员是隐式还是显式实现都有效,但后者 效果更好。

下面的例子中,TextBox显式实现IUndoable.Undo,所以不能标识为virtual。为了实现重载,

RichTextBox必须重新实现IUndoable的Undo方法:

public interface IUndoable { void Undo(); } public class TextBox : IUndoable

{

void IUndoable.Undo() { Console.WriteLine("TextBox.Undo"); } }

public class RichTextBox : TextBox, IUndoable {

public new void Undo() { Console.WriteLine ("RichTextBox.Undo"); } }

从接口调用成员的重新实现时,调用的是子类的实现:

RichTextBox r = new RichTextBox();

r.Undo(); // RichTextBox.Undo 例1 ((IUndoable)r).Undo(); // RichTextBox.Undo 例2 假定RichTextBox定义不变,如果TextBox隐式实现Undo:

public class TextBox : IUndoable {

public void Undo() { Console.WriteLine( TextBox.Undo ); } }

这样,为我们提供了另一种调用Undo的方法,如例3所示,它将“中断”系统。

RichTextBox r = new RichTextBox();

r.Undo(); // RichTextBox.Undo 例1 ((IUndoable)r).Undo(); // RichTextBox.Undo 例2 ((TextBox)r).Undo(); // TextBox.Undo 例3

例3显示出,重新实现屏蔽仅当通过接口调用成员时有效,从基类调用时无效。这个特性通常不尽人 意,因为它有二义性。重新实现主要是为了重载显式实现的接口成员。

相關文件