访问修饰符决定了哪些代码可以使用某个类型或成员。类正是用它们把其他代码所依赖的部分(公开接口)和可以自由修改的部分(内部实现)分开的。C# 有六种访问修饰符,另外还有你什么都不写时的默认值。
六种修饰符
| 修饰符 | 所在类型 | 派生类型,同一程序集 | 其他代码,同一程序集 | 派生类型,其他程序集 | 其他代码,其他程序集 |
|---|---|---|---|---|---|
public | 是 | 是 | 是 | 是 | 是 |
protected internal | 是 | 是 | 是 | 是 | 否 |
protected | 是 | 是 | 否 | 是 | 否 |
internal | 是 | 是 | 是 | 否 | 否 |
private protected | 是 | 是 | 否 | 否 | 否 |
private | 是 | 否 | 否 | 否 | 否 |
程序集就是一个编译好的项目:它生成的 .dll 或 .exe。“同一程序集”的意思是“同一个项目”,这就是为什么 internal 的效果是“对我的代码可见,对我的库的使用者隐藏”。
private 与 public:封装
private 是成员的默认修饰符,也是大多数字段的正确选择。类通过 public 方法和属性公开调用者需要的东西,而这些方法负责保持对象数据的一致性。
输出:
False
True
Priya: 180
因为 balance 是 private 的,修改它的唯一途径就是 Deposit 和 TryWithdraw,而这两个方法都会执行规则检查。程序中的其他代码无论怎么写,都不可能产生负数余额。这就是封装带来的好处:规则只存在于一个地方。
private 是按类型划分的,而不是按对象。Account 的方法可以读取另一个 Account 实例上的 other.balance,Equals 和比较方法通常就是这样写的。
protected:对派生类可见
protected 成员对外部隐藏,但对继承自声明类的那些类可用。
输出:
Sam: 4000.00
Rita: 6400.00
有一条规则常常让人意外。在 Manager 内部,你可以在 this 或另一个 Manager 上使用 baseSalary,但不能在任意一个 Employee 上使用:
class Manager : Employee
{
public decimal Compare(Employee other)
{
return baseSalary - other.baseSalary;
// error CS1540: Cannot access protected member 'Employee.baseSalary' via a qualifier
// of type 'Employee'; the qualifier must be of type 'Manager' (or derived from it)
}
}
原因是:other 可能是一个同样派生自 Employee 的 Contractor,而 protected 授予的访问权限只针对你自己这一支继承分支,而不是你的兄弟分支。
protected 字段会让每个派生类永远和这个字段绑定在一起。很多代码库更倾向于使用 protected 属性或方法,或者让字段保持 private,只给派生类它们真正需要的东西。
internal:在程序集内部可见
internal 成员和类型可以被同一项目中的任何代码使用,而项目之外的任何代码都不能使用。库会把它用在公开类所依赖的辅助代码上:
// In the MyShop.Pricing library project
public class PriceCalculator
{
public decimal Total(Cart cart) => TaxRules.Apply(cart.Subtotal);
}
internal static class TaxRules // callers of the library cannot see this class
{
internal static decimal Apply(decimal amount) => amount * 1.2m;
}
不写修饰符时,顶层类型就是 internal。这就是为什么在类库中忘记标记为 public 的类,在引用它的项目里会“找不到”。
单元测试项目是独立的程序集,所以它们也看不到 internal 代码。标准的解决办法是在库中加一个特性:
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyShop.Pricing.Tests")]
protected internal 与 private protected
这两个修饰符以相反的方式组合了前面的修饰符,名字也很容易混淆:
protected internal是 protected 或 internal:同一程序集中的任何代码,加上其他程序集中的派生类。它是范围更宽的那个。private protected(C# 7.2)是 protected 且 internal:只有同时位于同一程序集中的派生类。它是范围更窄的那个,适合那些扩展点应该只留在你的库内部的基类。
public class ReportBase
{
protected internal string Title; // same assembly, or subclasses anywhere
private protected int RowLimit = 500; // subclasses in this assembly only (C# 7.2)
}
什么都不写时的默认值
| 位置 | 默认值 | 允许的修饰符 |
|---|---|---|
| 在命名空间中声明的类型 | internal | public、internal、file(C# 11) |
| 类的成员 | private | 全部六种 |
| 结构体的成员 | private | public、internal、private(结构体不能被继承) |
| 嵌套类型 | private | 全部六种 |
| 接口成员 | public | 从 C# 8 起允许显式修饰符 |
| 枚举成员 | public | 无 |
写明修饰符只需要多写一个词,却能给下一个读代码的人省去一个疑问,所以大多数代码风格指南都要求写上修饰符,即使它和默认值相同。
访问器级别的访问控制
属性或索引器可以给其中一个访问器设置比属性本身更严格的修饰符:
输出:
report.pdf: 100%
访问器的修饰符必须比属性的更严格,而且两个访问器中只能有一个设置修饰符。
可访问性不一致
public 成员不能公开一个可见性比它自己更低的类型,否则调用者会拿到一个他们无权指名的东西:
internal class Discount { }
public class Checkout
{
public Discount Current() => null;
// error CS0050: Inconsistent accessibility: return type 'Discount' is less accessible than method 'Checkout.Current()'
}
同样的规则也适用于参数类型(CS0051)和属性类型(CS0053)。修复方法是让类型和成员一样可见,或者让成员和类型一样隐藏。
如何选择修饰符
从能满足需要的最严格的修饰符开始,只有在调用者确实需要时才放宽。以后把某样东西变得更可见很容易,而且永远不会影响任何人;把它变得更不可见,则会破坏所有用过它的调用者。实践中:字段用 private,构成类主要用途的成员用 public,辅助方法用 private,只在库内部使用的类型用 internal。
常见问题
C# 有哪些访问修饰符?
共有六种:public(任何代码)、private(仅限所在类型)、protected(所在类型及其派生类型)、internal(同一程序集中的任何代码)、protected internal(同一程序集,或任何地方的派生类型)以及 private protected(同一程序集中的派生类型,C# 7.2)。C# 11 还新增了 file,用于只在一个源文件中可见的类型。
C# 的默认访问修饰符是什么?
类或结构体的成员(字段、方法、属性、嵌套类型)默认是 private。顶层类型(直接在命名空间中声明的类)默认是 internal。接口成员和枚举成员是 public。
C# 中的 protected 是什么意思?
protected 成员在它自己的类内部以及任何从它派生的类内部可见,但对其他代码不可见。派生类只能通过自己的类型来使用它:在 Manager 内部,this.baseSalary 可以使用,但访问另一个 Employee 对象上的 baseSalary 会报错 CS1540。
C# 中的 internal 是什么?
internal 让类型或成员对同一程序集(同一个项目编译出的 .dll 或 .exe)中的所有代码可见,而在程序集外部不可见。库就是用它把辅助类排除在公开 API 之外的。[assembly: InternalsVisibleTo("MyLib.Tests")] 可以让测试项目也看到它们。
如何修复“不可访问,因为它具有一定的保护级别”?
这是错误 CS0122:代码在使用一个它无权访问的成员,最常见的是一个保持默认 private 的字段。如果外部代码需要读取它,请公开一个 public 属性或方法,而不要直接把字段改成 public。如果它是来自另一个项目的类型,那它很可能是 internal。