C++ 的 friend 关键字提供了一种打破封装边界的手段:它允许一个非成员函数或者另一个类访问当前类的私有成员与保护成员。C# 中并没有名为 friend 的关键字,但相同的问题域依然存在——当两个类需要紧密协作时,如何在不全面公开私有数据的前提下共享访问权?这篇文章会把 C# 中可用的等价方案逐一拆解。

C++ friend 关键字到底做了什么
在 C++ 中,friend 并不是访问修饰符,而是一条声明语句。它通常写在类定义的内部,用来告诉编译器:某个函数或类被授予了访问本类私有成员的权限。友元声明可以是普通全局函数、其他类的成员函数,也可以是整个类。声明之后,被授权的函数或类仍然不是当前类的成员,只是拥有了特殊访问权。
class BankAccount {
private:
double balance;
// 全局函数成为友元
friend void audit(const BankAccount& acc);
// 类成为友元
friend class Auditor;
};
void audit(const BankAccount& acc) {
// 合法:友元可以读取私有成员
if (acc.balance < 0) {
// 发送警告
}
}
从这个例子可以看到,friend 声明让 audit 函数和 Auditor 类可以直接操作 balance。友元关系是单向的:BankAccount 不能反过来访问 Auditor 的私有成员,除非 Auditor 也主动声明 BankAccount 为友元。另外,友元关系不能被继承,这保证了友元授权只作用于声明的那个类或函数。
这种机制在 C++ 中非常常用,例如运算符重载需要访问对象的私有表示,或者两个抽象高度耦合的类之间需要共享内部状态。C++ 的设计者希望通过 friend 在封装和灵活之间提供一道可控的闸门。
C# 的访问修饰符为什么没有 friend
C# 的访问控制体系与 C++ 有本质区别。C# 提供 public、private、protected、internal、protected internal 和 private protected 这几种访问修饰符,它们都作用于成员或者类型,但没有任何一个关键字表示“指定某个外部类型可以访问我的私有成员”。这是因为 C# 编译生成的元数据运行在 CLR 的验证机制之上,访问检查不是单纯的编译期行为,还涉及运行时的安全性。
在 C++ 中,friend 是编译期的概念,编译器看到 friend 声明后就允许对应的符号访问私有成员。编译完成后,生成的机器码里并不存在一份友元授权表。而 C# 的访问控制需要被 CLR 的访问检查规则识别,元数据中必须有明确的访问级别描述。如果引入类似 friend 的关键字,就必须在程序集元数据中保存一份外部类型清单,CLR 在加载类型时还需要校验这个关系,这会显著增加运行时复杂性和攻击面。
因此 C# 选择了另一条路:将友元的粒度放大到程序集级别。这就是 internal 修饰符的由来。internal 成员只能被同一程序集内的代码访问,相当于把整个程序集视为一个大的友元集合。如果想进一步把友元关系扩展到外部程序集,C# 提供 InternalsVisibleTo 特性来显式声明。
用 internal 和 InternalsVisibleTo 模拟友元
InternalsVisibleTo 是语言层面最接近 friend 的方案。用法很直观:把需要共享的成员声明为 internal,然后在当前程序集的 AssemblyInfo.cs 文件中添加一行特性,指定哪个外部程序集可以看见这些 internal 成员。这样指定程序集就像被加了 friend 标记一样,可以访问本程序集内部的 internals。
// 程序集:Bank.dll 中的 AssemblyInfo.cs
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("BankAudit")]
namespace Banking
{
public class BankAccount
{
internal double Balance { get; set; }
// 这个方法只允许友元程序集调用
internal void Freeze()
{
// 冻结账户
}
}
}
在同一个解决方案中,如果存在另一个名为 BankAudit 的程序集,那么它可以直接访问 BankAccount 的 Balance 属性和 Freeze 方法。此时 BankAudit 就相当于 C++ 中的友元类。与 friend 不同,InternalsVisibleTo 的粒度是程序集而不是单个类型。如果确有必要,可以把外部程序集作为一道屏障,或者使用条件编译把内部类再包装一层。
这个方案的优点是安全性和可维护性都不错。internal 限制在程序集内部,默认情况下外界完全看不到。通过 InternalsVisibleTo 向选定的强命名程序集开放,控制力度依然在开发者手里。缺点是友元关系是程序集级别的,如果外部程序集非常大,那么所有 internal 成员都会暴露给它,而不能像 C++ friend 那样精确到某个类。
需要特别注意的是,如果被开放的程序集没有强名称,那么 InternalsVisibleTo 特性的字符串中不能随便写程序集名,通常还要附带公钥。例如要开放给强命名的 BankAudit 程序集,写法会变成 InternalsVisibleTo("BankAudit, PublicKey=002400000480000...")。这个细节很容易被忽略,但一旦拼错,程序集就看不到 internal 成员。
嵌套类:C# 里的天然友元
C# 的嵌套类拥有一个非常特殊的权限:嵌套类可以直接访问外部类的私有成员,不需要任何额外声明。这本质上就是一种友元类关系。与 C++ friend 不同,这种访问权是自动获得的,而且嵌套类本身仍然可以拥有自己的访问级别。
public class BankAccount
{
private double balance;
// 私有嵌套类,外部无法直接创建
private class AccountAuditor
{
internal void ShowBalance(BankAccount account)
{
// 直接访问外部类的私有字段
Console.WriteLine(account.balance);
}
}
public void RunAudit()
{
var auditor = new AccountAuditor();
auditor.ShowBalance(this);
}
}
在这个例子中,AccountAuditor 是 BankAccount 的嵌套类,它可以直接读取 balance。外部代码无法访问 AccountAuditor,因为它是私有的。如果需要让外部类使用这个嵌套类,可以把嵌套类设为 internal 或者 public,但那样外部类也能通过嵌套类间接访问私有成员。
嵌套类作为友元的优势在于作用域非常清晰:它被声明在外部类的内部,天然表达了“这个类只服务于 BankAccount”的意图。缺点是嵌套类无法单独存在,并且它的实例通常会持有对外部类实例的引用。如果你需要一个与主类生命周期独立的友元类,嵌套类并不是最佳选择。
反射:最后的手段
除了编译期的机制之外,C# 还可以借助反射在运行时绕过访问控制。通过 Type.GetField 或 GetProperty 并传入 BindingFlags.NonPublic 和 BindingFlags.Instance,即使字段是 private 的,也能在运行时读取或修改它的值。这种方法相当于把 C++ friend 变成了万能钥匙,任何代码都能做到,而不需要声明权限。
using System.Reflection;
public class BankAccount
{
private double balance = 1000.0;
}
class Program
{
static void Main()
{
var account = new BankAccount();
var field = typeof(BankAccount)
.GetField("balance", BindingFlags.NonPublic | BindingFlags.Instance);
var value = (double)field.GetValue(account);
Console.WriteLine(value);
}
}
反射的优点是灵活,甚至在程序集之间也能访问 private 成员,而且不需要改目标类型。缺点也很明显:首先,反射调用比直接调用慢得多,在高频场景下会产生不容忽视的性能开销。其次,字符串形式的字段名在编译期无法检查,一旦类型内部重构,代码就会悄悄失效。更严重的是,这种访问绕过了编译器和 CLR 的访问保护,不利于安全边界。
因此在生产代码中,反射通常只用于框架场景,比如序列化库、对象关系映射工具或者单元测试。在这些场景里,目标类型不是自己控制的,或者访问私有成员是测试断言的一部分。如果你只是想在两个自研类之间共享数据,反射明显是杀鸡用牛刀。
如何选择适合自己的方案
综合来看,C# 没有与 C++ friend 关键字一一对应的语言结构,但有足够的替代手段。如果你的需求是让一个外部程序集访问本程序集的内部 API,InternalsVisibleTo 是最自然的选择,它比 C++ friend 的粒度大,但整体的架构表达更清晰。如果只是某个局部类需要访问另一类的私有成员,嵌套类会让代码结构更紧凑。
如果需求发生在同一个程序集内,最简单的方式其实是直接把成员设为 internal。因为 internal 本身就把访问范围限制在当前程序集,而程序集内部的所有类都可以访问它。这与 C++ 中整个程序集互称朋友的效果类似。只有当访问要跨越程序集边界时,才需要引入 InternalsVisibleTo。
反射虽然有最强的穿透力,但也是安全性和性能最差的一种方式。除非你正在编写框架或者测试工具,否则不建议把它作为日常的友元模拟手段。从架构角度讲,频繁打破封装通常意味着职责分配需要调整。读者可以重新审视类之间的边界,看看是否有更好的接口抽象。但在必须共享私有数据的场景下,上面这些方案能让你优雅地解决问题。
friend关键字C#访问控制InternalsVisibleTo修改时间:2026-08-28 05:30:26