为什么C#不提供类似于C++中的“友元”关键字?

224

C++友元关键字允许类A指定类B为其友元。这允许类B访问类Aprivate/protected成员。

我从未读过任何关于为什么C#(和VB.NET)没有使用它的内容。早期StackOverflow问题的大多数答案似乎都在说这是C++的一个有用部分,有很好的理由使用它。根据我的经验,我必须同意。

另一个问题对我来说似乎真正想知道如何在C#应用程序中做类似于friend的事情。虽然答案通常围绕嵌套类展开,但似乎不太像使用friend关键字那样优雅。

原始设计模式书籍在其示例中经常使用它。

因此,总之,为什么C#缺少friend,以及在C#中模拟它的“最佳实践”方法(或方法)是什么?

顺便提一下,internal 关键字并不是同一件事,它允许整个程序集中的所有类都可以访问 internal 成员,而 friend 则允许你给一个特定的类完全访问另一个类的权限。


5
我认为“内部受保护”是一个不错的妥协方案。 - Gulzar Nazim
3
没问题。如我上面所说,GOF书籍经常在示例中使用这个词。对我来说,它并不比"internal"更令人困惑。 - Ash
7
我可以看到某些情况下它们非常有用,比如已经提到的单元测试。但是你真的想让你的朋友访问你的私人信息吗? - danswain
2
很容易回答:C#提供了“internal”作为访问修饰符,它允许访问同一模块/程序集中的所有代码。这消除了类似于friend的需求。在Java中,关键字“protected”在访问同一包中的内容方面表现类似。 - sellibitze
3
@sellibitze,我在问题中提到了与internal的区别。Internal的问题在于程序集中的所有类都可以访问internal成员。这会破坏封装性,因为其中许多类可能不需要访问。 - Ash
显示剩余3条评论
22个回答

114

顺便说一下,使用友元函数并不是为了违反封装原则,相反它强化了封装。就像访问器+修改器、运算符重载、公共继承、向下转型等等, 友元函数经常被误用,但这并不意味着关键字没有作用,甚至更糟。

请参考Konrad Rudolph在其他主题中的回答,或者如果您愿意,请查看C++ FAQ中相关条目


...以及在FQA中对应的条目:http://yosefk.com/c++fqa/friend.html#fqa-14.2 - Josh Lee
31
+1表示“赞同”,"使用友元并不是为了违反封装,相反它是为了强制执行封装"的意思是,使用友元可以让某些类或函数访问其他类的私有成员,但这并不会破坏封装性,反而可以保证更好的封装性。 - Nawaz
3
我认为这是一个语义观点问题,也许与具体情况有关。将一个类设置为“友元”可以让它访问到你所有的私有成员,即使你只需要让它访问其中一个。有一个很有力的论点认为,让另一个不需要这些成员的类访问字段会违反封装原则。在C++中,有些地方是必要的(如重载运算符),但需要仔细考虑封装的权衡。看起来C#的设计者认为这种权衡不值得。 - GrandOpener
3
封装是关于强制执行不变量的概念。当我们将一个类定义为另一个类的友元时,我们明确表明这两个类在第一个类的不变量管理方面紧密相连。使类成为友元仍然比直接公开所有属性(作为公共字段)或通过设置器暴露所有属性要好。 - Luc Hermitte
3
没错。当你想使用“朋友”这个词,但又不能用的时候,你只能使用“内部的”或“公开的”,这些词更容易被那些不该干涉的事物所干扰,尤其是在团队环境中。 - Hatchling

70
在编程中有朋友被认为是“肮脏”的,容易被滥用。它会破坏类之间的关系,并削弱面向对象语言的一些基本属性。
话虽如此,这是一个不错的功能,我在C++中已经使用了很多次,也想在C#中使用它。但我敢打赌,因为C#比C++更“纯粹”的面向对象(相对于C++的伪面向对象),微软决定因为Java没有friend关键字,C#也不应该有(开个玩笑;))
严肃地说:internal并不像friend那样好,但它确实能完成工作。请记住,你很少会将代码通过DLL分发给第三方开发人员;只要你和你的团队知道内部类及其用途,你就应该没问题。
编辑:让我澄清一下friend关键字如何削弱面向对象编程。
私有和受保护的变量和方法可能是面向对象编程中最重要的部分之一。对象可以保存只有它们自己可以使用的数据或逻辑,这个想法允许你独立于环境编写功能实现,而且你的环境不能改变它不适合处理的状态信息。使用friend会将两个类的实现耦合在一起,这比仅耦合它们的接口要糟糕得多。

177
“internal”这个C#关键字实际上比C++中的“friend”更容易破坏封装性。使用“friend”,所有者可以明确地授予任何想要的人权限。而“internal”则是一个开放性的概念,更为模糊。 - Nemanja Trifunovic
21
Anders应该因他省略的特性而受到赞扬,而不是因为他包含的特性。 - Mehrdad Afshari
21
我不同意C#的internal让封装性受损,相反我认为它可以用来在程序集内部创建一个封装的生态系统。在这个生态系统中,一些对象可以共享不暴露给应用程序其余部分的internal类型。我使用"friend"程序集技巧来创建这些对象的单元测试程序集。 - Quibblesome
27
这段话的意思是:“这不合理。friend 不会让任意类或函数都可以访问私有成员,而是只有被标记为 friend 的特定函数或类可以这样做。拥有私有成员的类仍然完全控制对它的访问。你可以把公共成员方法也放在同一个范畴里。两者都确保只有在类中明确列出的方法才能访问类的私有成员。” - jalf
8
我认为恰恰相反。如果一个程序集有50个类,其中3个类使用某个实用类,那么今天你唯一的选择是将该实用类标记为“internal”。这样做不仅使得这个实用类可以供这3个类使用,而且还可以供程序集中其余的类使用。如果你只想提供更细粒度的访问权限,使得只有这三个类可以访问该实用类,那怎么办呢?这实际上使它更符合面向对象的思想,因为它改进了封装性。 - zumalifeguard
显示剩余13条评论

51

提供一些信息,.NET 中另一个相关但并非完全相同的功能是 [InternalsVisibleTo],它允许一个程序集指定另一个程序集(例如单元测试程序集),该程序集(实际上)可以访问原始程序集中的类型/成员。


4
好的提示,因为很可能经常有人寻找朋友,希望找到一种能够进行单元测试并具有更多内部“知识”的方法。 - SwissCoder

16
事实上,C# 提供了一种纯 OOP 的方式来实现相同的行为,而不需要特殊的关键词——私有接口。

由于问题 “C# 中 friend 的等价物是什么?” 被标记为重复,并且那里没有人提出真正好的实现方法,所以我将在这里回答这两个问题。

主要思想取自于这里:“什么是私有接口?”

假设我们需要一个类来管理另一个类的实例并调用它们的某些特殊方法。我们不想让任何其他类调用这些方法。这正是在 C++ 世界中 friend 关键字所起的作用。

我认为在实际应用中,一个很好的例子就是完整状态机模式,其中某个控制器更新当前状态对象,并在必要时切换到另一个状态对象。

您可以:

  • 最简单也是最糟糕的让Update()方法公开的方式-希望每个人都明白为什么这样做很糟糕。
  • 下一种方式是将其标记为内部的。如果您将类放到另一个程序集中,那么这已经足够好了,但即使在该程序集中的每个类都可以调用每个内部方法。
  • 使用私有/受保护接口-我采用了这种方式。

Controller.cs

public class Controller
{
    private interface IState
    {
        void Update();
    }

    public class StateBase : IState
    {
        void IState.Update() {  }
    }

    public Controller()
    {
        //it's only way call Update is to cast obj to IState
        IState obj = new StateBase();
        obj.Update();
    }
}

Program.cs

class Program
{
    static void Main(string[] args)
    {
        //it's impossible to write Controller.IState p = new Controller.StateBase();
        //Controller.IState is hidden
        var p = new Controller.StateBase();
        //p.Update(); //is not accessible
    }
}

好的,那继承呢?

我们需要使用由于显式接口成员实现不能声明为虚拟的技术描述中所述的技术,并将IState标记为受保护的,以便从Controller进行派生。

Controller.cs

public class Controller
{
    protected interface IState
    {
        void Update();
    }

    public class StateBase : IState
    {
        void IState.Update() { OnUpdate(); }
        protected virtual void OnUpdate()
        {
            Console.WriteLine("StateBase.OnUpdate()");
        }
    }

    public Controller()
    {
        IState obj = new PlayerIdleState();
        obj.Update();
    }
}

PlayerIdleState.cs

public class PlayerIdleState: Controller.StateBase
{
    protected override void OnUpdate()
    {
        base.OnUpdate();
        Console.WriteLine("PlayerIdleState.OnUpdate()");
    }
}

最后,举个例子来测试通过继承的方式控制器类: ControllerTest.cs

class ControllerTest: Controller
{
    public ControllerTest()
    {
        IState testObj = new PlayerIdleState();
        testObj.Update();
    }
}

希望我涵盖了所有情况,我的回答有用。

这是一个非常棒的答案,需要更多的赞。 - KthProg
@Steve Land,你可以像ControllerTest.cs中所示一样使用继承。在派生测试类中添加任何必要的公共方法,这样你就可以访问基类中所有受保护的成员了。 - max_cn
@AnthonyMonterrosa,我们正在隐藏更新来自所有外部资源的方式,为什么你想要与别人分享它呢?当然,你可以添加一些公共方法来访问私有成员,但这就像是为其他类打开了一个后门。无论如何,如果你需要它进行测试,可以使用继承来创建类似于ControllerTest类的东西,在那里进行任何测试案例。 - max_cn
这是一种非常冗长和复杂的方式,以实现 "友元" 所提供的较劣效果(人们仍然可以将其转换为接口并调用任何他们想要的函数)。在我的日常工作中,C# 中缺乏友元几乎每天都在困扰着我。 "纯面向对象编程" 的论点是完全错误的:有时两个类需要拥有一个私有的共享状态。友元为此提供了支持。而 "纯面向对象编程" 却没有。 - kaalus
@kaalus 在这种情况下,除了控制器类之外的所有其他类都不知道这个私有/受保护的接口,因此它们无法将其转换为该接口。此解决方案还具有优于友元声明的优点:您可以打开类上的特定部分以访问私有接口,因此您的“友元”类只能使用其中的特定部分。这也将在接口定义中记录。 - Daniel Bauer
显示剩余2条评论

14

在C++中,使用friend可以精确控制哪些私有成员被暴露给外部,但是每个私有成员都必须被暴露。

在C#中,使用internal可以精确控制要暴露的私有成员集合。显然,可以只暴露一个私有成员,但它将会被程序集中的所有类所暴露。

通常,设计者希望仅向少数其他类公开一些私有方法。例如,在类工厂模式中,可能希望仅通过类工厂CF1实例化类C1。因此,类C1可以具有受保护的构造函数和友元类工厂CF1。

正如您所看到的,我们有两个维度可以破坏封装。 friend沿着一个维度破坏它,internal则沿着另一个维度破坏它。哪一个更糟糕呢?很难说。但同时拥有friendinternal是很好的。此外,对这两个关键字的良好补充是第三种类型的关键字,它将基于成员逐个使用(类似于internal),并指定目标类(类似于friend)。

*为简洁起见,我将使用“私有”代替“私有和/或受保护”。

- 尼克


13

通过在C#中使用接口,您应该能够完成与C++中“friend”的用途相同的事情。这需要您明确定义在两个类之间传递哪些成员,这是额外的工作,但也可能使代码更易于理解。

如果有人有一个合理使用“friend”的例子,无法使用接口进行模拟,请分享!我想更好地了解C++和C#之间的区别。


好的观点。你有机会看一下之前由monoxide提出并在上面链接的SO问题吗?我很想知道如何在C#中解决这个问题最好的方法是什么? - Ash
我不确定如何在C#中实现,但我认为Jon Skeet对monoxide的问题的回答指向了正确的方向。C#允许嵌套类。虽然我还没有尝试编写和编译解决方案 :) - Parappa
我认为中介者模式是“友元”会很好的一个例子。我重构了我的表单,加入了中介器。我希望我的表单能够调用中介器中类似于“事件”的方法,但实际上我并不希望其他类也可以调用这些“事件”方法。使用“友元”功能可以让表单访问这些方法,而无需将其暴露给程序集中的所有其他类。 - Vaccano
3
用接口方式替换 'Friend' 的问题在于,为了使对象暴露一个接口,任何人都可以将该对象转换为该接口并访问方法!将接口限定为内部、保护或私有也不起作用,因为如果这样做,你可以只将所需的方法限定在某个范围内!想象一下在购物中心里的母亲和孩子。公共就是世界上所有人。内部只是购物中心里的人。私有只属于母亲或孩子。'Friend' 是母亲和女儿之间的关系。 - Mark A. Donohoe
1
这是一个例子:https://dev59.com/GG025IYBdhLWcg3wkXAF我来自C++背景,每天都会因为缺少友元而发疯。在我的几年C#经验中,由于缺少友元,我已经将数百个本应该是私有且更安全的方法变成了公共方法。个人认为友元是应该添加到.NET中最重要的东西。 - kaalus
接口很好地完成了这个任务。良好结构化的代码应该防止意外误用,而不是故意恶意使用。如果你严格地向外部分发一个接口,用户读取你的代码并窃取一个被记录为内部使用的接口,那不是你代码的缺陷,那是有人故意捣乱。无论你做什么,某些人总是可以使用不安全的代码来玩弄位。你编写代码的工作就是制造出一些难以被错误使用的东西。但要做到完全不可能被错误使用是不可能的。 - GrandOpener

8
C#缺少“friend”关键字,原因与它缺少确定性销毁一样。改变惯例让人感觉聪明,好像他们的新方法比别人的旧方法更优越。这都是关于骄傲的问题。
说“友元类是不好的”就像其他没有资格的声明一样短视,比如“不要使用goto”或“Linux比Windows更好”。
“friend”关键字与代理类相结合是一种只将类的特定部分暴露给特定的其他类的好方法。代理类可以作为可信障碍阻止所有其他类。 “public”不允许任何此类针对性,而使用“protected”通过继承获得效果如果真的没有概念上的“是一个”关系,则很笨拙。

C#缺少确定性销毁,因为它比垃圾回收慢。确定性销毁需要为每个创建的对象花费时间,而垃圾回收仅需要为当前活动对象花费时间。在大多数情况下,这更快,系统中存在的短暂对象越多,垃圾回收相对就会更快。 - Edward Robertson
1
@Edward Robertson:确定性析构在没有定义析构函数的情况下不需要任何时间。但是在这种情况下,垃圾回收就更糟糕了,因为你需要使用一个较慢和不确定的 finalizer。 - kaalus
1
...而内存并不是唯一的资源。人们经常表现得好像这是真的。 - cubuspl42

8

在编写单元测试时,友元函数非常有用。

虽然这会导致类声明略微污染,但它也是编译器强制提醒测试实际上可能关心类的内部状态的好方法。

我发现一个非常有用且简洁的习惯是,当我有工厂类时,将它们作为创建受保护构造函数项的朋友。更具体地说,当我有一个单一的工厂负责为报表编写器对象创建匹配的呈现对象并呈现给给定环境时。在这种情况下,您可以了解报表编写器类(例如图片块、布局带、页面标题等)与它们的匹配呈现对象之间的关系的唯一知识点。


我本打算对“friend”在工厂类中的有用性发表评论,但很高兴看到已经有人提到了它。 - Edward Robertson

8
您可以使用 C# 关键字 "internal" 接近 C++ 中的“友元”功能。

3
接近了,但还差一点。我想这取决于你如何构建你的程序集/类,但是使用 friend 来最小化允许访问的类数量会更加优雅。比如在一个实用/辅助程序集中,不是所有的类都应该可以访问内部成员。 - Ash
3
@Ash:你需要逐渐习惯将程序集视为应用程序的基本构建块,这样 internal 关键字就会更加合理,并在封装性和实用性之间提供出色的平衡。虽然 Java 的默认访问权限设置甚至更好。 - Konrad Rudolph

7

实际上,这不是C#的问题,而是IL的基本限制。C#受此限制,任何其他寻求可验证性的.Net语言也受到此限制。这个限制还包括在C++/CLI中定义的托管类(规范第20.5节)。

话虽如此,我认为Nelson对为什么这是一个坏事有很好的解释。


7
“朋友”不是贬义词,相反它比“内部的”要好得多。而且纳尔逊的解释是错误的。 - Nawaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接