装箱和拆箱的实质是复制
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显示出,重新实现屏蔽仅当通过接口调用成员时有效,从基类调用时无效。这个特性通常不尽人 意,因为它有二义性。重新实现主要是为了重载显式实现的接口成员。