217

C++ 朋友关键字允许将 a指定class A为其class B朋友。这允许Class B访问private/的protected成员class A

我从来没有读过任何关于为什么 C#(和 VB.NET)忽略它的内容。对这个较早的 StackOverflow 问题的大多数答案似乎都在说它是 C++ 的一个有用部分,并且有充分的理由使用它。根据我的经验,我不得不同意。

在我看来,另一个问题实际上是在问如何friend在 C# 应用程序中做类似的事情。虽然答案通常围绕嵌套类,但它似乎不如使用friend关键字那么优雅。

最初的《设计模式》一书在其示例中经常使用它。

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

(顺便说一句,internal关键字不是一回事,它允许整个程序集中的所有类访问internal成员,同时friend允许您让某个类完全访问另一个

4

22 回答 22

109

在旁注中。使用朋友不是违反封装,相反,它是关于执行它。就像访问器+修改器、运算符重载、公共继承、向下转换一样,它经常被误用,但这并不意味着关键字没有,或者更糟的是,没有一个不好的用途。

请参阅另一个线程中的Konrad Rudolph消息,或者如果您愿意,请参阅C++ FAQ 中的相关条目

于 2008-10-15T10:01:45.600 回答
70

在编程中有朋友或多或少被认为是“肮脏的”并且容易被滥用。它破坏了类之间的关系并破坏了 OO 语言的一些基本属性。

话虽如此,这是一个不错的功能,我自己在 C++ 中使用过很多次;并且也想在 C# 中使用它。但我敢打赌,因为 C# 的“纯”OOness(与 C++ 的伪 OOness 相比)MS 决定因为 Java 没有朋友关键字 C# 也不应该(开个玩笑;))

严肃地说:内部不如朋友,但它确实完成了工作。请记住,您很少会不通过 DLL 将代码分发给 3rd 方开发人员;因此,只要您和您的团队了解内部类及其用途,就可以了。

编辑让我澄清一下friend关键字如何破坏OOP。

私有和受保护的变量和方法可能是 OOP 中最重要的部分之一。对象可以保存只有它们可以使用的数据或逻辑的想法允许您编写独立于您的环境的功能实现 - 并且您的环境不能更改它不适合处理的状态信息。通过使用friend,您将两个类的实现耦合在一起——这比只耦合它们的接口要糟糕得多。

于 2008-10-15T03:40:47.333 回答
51

对于信息,.NET 中另一个相关但不完全相同的东西是[InternalsVisibleTo],它允许程序集指定另一个程序集(例如单元测试程序集),该程序集(有效地)具有对类型/成员的“内部”访问权限原始组件。

于 2008-10-15T05:41:25.453 回答
14

事实上,C# 提供了以纯 OOP 方式获得相同行为而无需特殊词的可能性 - 它是私有接口。

至于问题什么是朋友的C#等价物?被标记为与本文重复,没有人提出真正好的实现 - 我将在这里给出这两个问题的答案。

主要思想来自这里:什么是私有接口?

比方说,我们需要一些类来管理另一个类的实例并在它们上调用一些特殊方法。我们不想给任何其他类调用这个方法的可能性。这与朋友 c++ 关键字在 c++ 世界中所做的完全相同。

我认为实际实践中的一个很好的例子可能是全状态机模式,其中一些控制器更新当前状态对象并在必要时切换到另一个状态对象。

你可以:

  • 公开 Update() 方法的最简单和最糟糕的方法 - 希望每个人都能理解它为什么不好。
  • 下一种方法是将其标记为内部。如果你把你的类放到另一个程序集中就足够了,但即便如此,该程序集中的每个类都可以调用每个内部方法。
  • 使用私有/受保护的接口 - 我遵循这种方式。

控制器.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();
    }
}

程序.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 派生的可能性中描述的技术。

控制器.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();
    }
}

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

于 2016-05-15T13:06:17.513 回答
13

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

如果有人有一个不能用接口模拟的合理使用“朋友”的例子,请分享!我想更好地理解 C++ 和 C# 之间的区别。

于 2008-10-15T03:40:04.013 回答
13

使用friendC++ 设计人员可以精确控制 private* 成员的暴露对象。但是,他不得不揭露每一位私人成员。

internalC# 设计人员可以精确控制他公开的私有成员集。显然,他只能暴露一个私人成员。但是,它将暴露给程序集中的所有类。

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

正如你所看到的,我们有两个维度可以破坏封装。 friend沿着一个维度破坏它,沿着另一个维度internal去做。哪一个是封装概念中更严重的突破?很难说。friend但如果两者兼而有之,那就太好了internal。此外,这两者的一个很好的补充是第 3 类关键字,它将逐个成员使用(like internal)并指定目标类(like friend)。

* 为简洁起见,我将使用“私人”而不是“私人和/或受保护”。

- 尼克

于 2011-01-02T05:36:27.003 回答
8

您可以使用 C# 关键字“internal”接近 C++“朋友” 。

于 2008-10-15T03:28:55.690 回答
8

Friend 在编写单元测试时非常有用。

虽然这会以轻微污染类声明为代价,但它也是编译器强制提醒,哪些测试实际上可能关心类的内部状态。

我发现一个非常有用且干净的习惯用法是当我有工厂类时,使它们成为他们创建的具有受保护构造函数的项目的朋友。更具体地说,当时我有一个工厂负责为报表编写器对象创建匹配的渲染对象,渲染到给定的环境。在这种情况下,您对报表编写器类(如图片块、布局带、页眉等)与其匹配的呈现对象之间的关系有一个单一的了解。

于 2009-01-19T16:27:31.887 回答
8

C# 缺少“friend”关键字,原因与缺少确定性破坏的原因相同。改变惯例让人感觉很聪明,好像他们的新方法优于别人的旧方法。这都是关于骄傲。

说“朋友班不好”与“不要使用 gotos”或“Linux 比 Windows 更好”等其他不合格的说法一样短视。

“friend”关键字与代理类相结合是一种仅将类的某些部分暴露给特定其他类的好方法。代理类可以充当所有其他类的可信屏障。“public”不允许任何这样的定位,如果真的没有概念上的“is a”关系,那么使用“protected”来获得继承的效果是很尴尬的。

于 2010-04-09T05:52:54.590 回答
7

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

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

于 2008-10-15T08:07:42.460 回答
4

不要再为这种限制找借口了。朋友不好,内心好?它们是一样的,只是那个朋友可以让你更精确地控制谁可以访问,谁不可以。

这是为了强制执行封装范式?所以你必须编写访问器方法,现在怎么办?你应该如何阻止每个人(除了 B 类的方法)调用这些方法?你不能,因为你也无法控制它,因为缺少“朋友”。

没有一种编程语言是完美的。C# 是我见过的最好的语言之一,但是为缺少功能找个愚蠢的借口对任何人都没有帮助。在 C++ 中,我想念简单的事件/委托系统、反射(+自动反序列化)和 foreach,但在 C# 中,我想念运算符重载(是的,一直告诉我你不需要它)、默认参数、一个 const这是无法避免的,多重继承(是的,一直告诉我你不需要它,接口是一个足够的替代品)以及决定从内存中删除实例的能力(不,这不是非常糟糕,除非你是修补匠)

于 2010-11-15T07:41:53.543 回答
3

我只会回答“如何”的问题。

这里有很多答案,但是我想提出一种“设计模式”来实现该功能。我将使用简单的语言机制,其中包括:

  • 接口
  • 嵌套类

例如,我们有 2 个主要课程:学生和大学。学生有只有大学允许访问的 GPA。这是代码:

public interface IStudentFriend
{
    Student Stu { get; set; }
    double GetGPS();
}

public class Student
{
    // this is private member that I expose to friend only
    double GPS { get; set; }
    public string Name { get; set; }

    PrivateData privateData;

    public Student(string name, double gps) => (GPS, Name, privateData) = (gps, name, new PrivateData(this);

    // No one can instantiate this class, but Student
    // Calling it is possible via the IStudentFriend interface
    class PrivateData : IStudentFriend
    {
        public Student Stu { get; set; }

        public PrivateData(Student stu) => Stu = stu;
        public double GetGPS() => Stu.GPS;
    }

    // This is how I "mark" who is Students "friend"
    public void RegisterFriend(University friend) => friend.Register(privateData);
}

public class University
{
    var studentsFriends = new List<IStudentFriend>();

    public void Register(IStudentFriend friendMethod) => studentsFriends.Add(friendMethod);

    public void PrintAllStudentsGPS()
    {
        foreach (var stu in studentsFriends)
            Console.WriteLine($"{stu.Stu.Name}: stu.GetGPS()");
    }
}

public static void Main(string[] args)
{
    var Technion = new University();
    var Alex     = new Student("Alex", 98);
    var Jo       = new Student("Jo", 91);

    Alex.RegisterFriend(Technion);
    Jo.RegisterFriend(Technion);
    Technion.PrintAllStudentsGPS();

    Console.ReadLine();
}
于 2015-01-19T22:47:03.597 回答
2

自 .Net 3 以来就有 InternalsVisibleToAttribute,但我怀疑他们只是在单元测试兴起后才添加它来满足测试程序集的需求。我看不出有很多其他使用它的理由。

它在装配级别工作,但它在内部不工作的地方工作;也就是说,您想要分发一个程序集,但希望另一个非分布式程序集具有访问它的特权。

非常正确地,他们要求朋友程序集是强键控,以避免有人在受保护的程序集旁边创建假朋友。

于 2011-07-11T21:25:53.277 回答
2

我已经阅读了许多关于“朋友”关键字的聪明评论,我同意它是有用的东西,但我认为什么“内部”关键字不太有用,而且它们对于纯 OO 编程仍然不好。

我们有什么?(说“朋友”我也说“内部”)

  • 使用“朋友”是否会使代码对 oo 不那么纯粹?
  • 是的;

  • 不使用“朋友”会使代码更好吗?

  • 不,我们仍然需要在类之间建立一些私有关系,并且只有打破我们美丽的封装才能做到,所以它也不好,我可以说它比使用“朋友”更邪恶。

使用朋友会产生一些本地问题,不使用它会给代码库用户带来问题。

我看到的编程语言的常见良好解决方案是这样的:

// c++ style
class Foo {
  public_for Bar:
    void addBar(Bar *bar) { }
  public:
  private:
  protected:
};

// c#
class Foo {
    public_for Bar void addBar(Bar bar) { }
}

你怎么看待这件事?我认为它是最常见和纯面向对象的解决方案。您可以对任何您想要的课程开放访问您选择的任何方法。

于 2012-06-05T07:29:57.387 回答
1

我怀疑它与 C# 编译模型有关——在运行时构建 IL JIT 编译它。即:C# 泛型与 C++ 泛型根本不同的原因相同。

于 2008-10-15T03:29:59.087 回答
1

您可以将其保密并使用反射来调用函数。如果您要求测试框架测试私有函数,则测试框架可以执行此操作

于 2010-06-04T13:24:44.413 回答
1

我以前经常使用朋友,我不认为这有任何违反 OOP 或任何设计缺陷的迹象。有几个地方它是用最少的代码达到正确目的的最有效方法。

一个具体的例子是创建接口组件,为其他软件提供通信接口。通常有一些重量级的类可以处理协议的复杂性和对等点的特性,并提供一个相对简单的连接/读/写/转发/断开模型,涉及在客户端应用程序和程序集之间传递消息和通知。这些消息/通知需要包装在类中。属性通常需要由协议软件操作,因为它是它们的创建者,但是很多东西必须对外界保持只读。

声明协议/“创建者”类对所有创建的类具有亲密访问权是违反 OOP 的,这简直是愚蠢的——创建者类不得不在上升过程中对每一位数据进行比特处理。我发现最重要的是最小化“OOP for OOP's Sake”模型通常导致的所有 BS 额外代码行。额外的意大利面只会制造更多的错误。

人们知道您可以在属性、属性和方法级别应用 internal 关键字吗?它不仅适用于顶级类声明(尽管大多数示例似乎都表明了这一点。)

如果您有一个使用friend关键字的C++类,并且想在C#类中模拟它: 1. 将C#类声明为public 2. 声明在C++中受保护的所有属性/属性/方法,因此朋友可以访问为internal in C# 3. 为所有内部属性和属性的公共访问创建只读属性

我同意它与朋友不是 100% 相同,并且单元测试是需要类似朋友的东西的一个非常有价值的例子(协议分析器日志记录代码也是如此)。然而 internal 提供了对你想要暴露的类的暴露,而 [InternalVisibleTo()] 处理其余的——似乎它是专门为单元测试而生的。

至于朋友“变得更好,因为你可以明确地控制哪些类可以访问”——一群可疑的邪恶类首先在同一个程序集中做什么?对您的程序集进行分区!

于 2011-01-30T09:41:40.240 回答
1

可以通过分离接口和实现来模拟友谊。这个想法是:“需要一个具体的实例,但限制该实例的构造访问”。

例如

interface IFriend { }

class Friend : IFriend
{
    public static IFriend New() { return new Friend(); }
    private Friend() { }

    private void CallTheBody() 
    {  
        var body = new Body();
        body.ItsMeYourFriend(this);
    }
}

class Body
{ 
    public void ItsMeYourFriend(Friend onlyAccess) { }
}

尽管ItsMeYourFriend()只有公共Friend类可以访问它,因为没有其他人可以获取Friend该类的具体实例。它有一个私有构造函数,而工厂New()方法返回一个接口。

有关详细信息,请参阅我的文章Friends and internal interface members at no cost with coding to interfaces

于 2012-06-21T19:47:40.580 回答
1

有人建议使用朋友可以使事情失控。我同意,但这并不会降低它的用处。我不确定这位朋友是否一定会伤害 OO 范式,而不是让所有班级成员公开。当然,该语言将允许您将所有成员公开,但它是一个有纪律的程序员,会避免这种类型的设计模式。同样,一个有纪律的程序员会在有意义的特定情况下保留使用friend。在某些情况下,我觉得内部暴露太多了。为什么要向程序集中的所有内容公开一个类或方法?

我有一个 ASP.NET 页面,它继承了我自己的基页,而基页又继承了 System.Web.UI.Page。在此页面中,我有一些代码以受保护的方法处理应用程序的最终用户错误报告

ReportError("Uh Oh!");

现在,我有一个包含在页面中的用户控件。我希望用户控件能够调用页面中的错误报告方法。

MyBasePage bp = Page as MyBasePage;
bp.ReportError("Uh Oh");

如果 ReportError 方法受到保护,它就不能这样做。我可以将其设为内部,但它会暴露给程序集中的任何代码。我只是希望它暴露给当前页面(包括子控件)的一部分的 UI 元素。更具体地说,我希望我的基本控件类定义完全相同的错误报告方法,并简单地调用基本页面中的方法。

protected void ReportError(string str) {
    MyBasePage bp = Page as MyBasePage;
    bp.ReportError(str);
}

我相信朋友之类的东西可以在语言中有用和实现,而不会使语言不像“OO”那样,也许作为属性,这样你就可以让类或方法成为特定类或方法的朋友,允许开发人员提供非常具体访问。也许像......(伪代码)

[Friend(B)]
class A {

    AMethod() { }

    [Friend(C)]
    ACMethod() { }
}

class B {
    BMethod() { A.AMethod() }
}

class C {
    CMethod() { A.ACMethod() }
}

在我之前的示例中,可能有以下内容(可以争论语义,但我只是想表达这个想法):

class BasePage {

    [Friend(BaseControl.ReportError(string)]
    protected void ReportError(string str) { }
}

class BaseControl {
    protected void ReportError(string str) {
        MyBasePage bp = Page as MyBasePage;
        bp.ReportError(str);
    }
}

在我看来,朋友概念对它的风险并不比将事情公开,或创建公共方法或属性来访问成员。如果有什么朋友允许数据可访问性的另一个级别的粒度,并允许您缩小该可访问性,而不是通过内部或公共来扩大它。

于 2012-07-18T20:09:38.157 回答
0

如果你正在使用 C++ 并且你发现你自己使用了friend关键字,这是一个非常强烈的迹象,你有一个设计问题,因为为什么一个类需要访问其他类的私有成员?

于 2008-10-15T05:28:24.933 回答
0

BSD

有人说,朋友伤害了纯粹的OOness。我同意。

也有人说朋友帮忙封装,我也同意。

我认为友谊应该添加到 OO 方法中,但不像 C++ 中那样。我想要一些我的朋友类可以访问的字段/方法,但我不希望他们访问我所有的字段/方法。就像在现实生活中一样,我会让我的朋友访问我的个人冰箱,但我不会让他们访问我的银行账户。

可以按如下方式实现

    class C1
    {
        private void MyMethod(double x, int i)
        {
            // some code
        }
        // the friend class would be able to call myMethod
        public void MyMethod(FriendClass F, double x, int i)
        {
            this.MyMethod(x, i);
        }
        //my friend class wouldn't have access to this method 
        private void MyVeryPrivateMethod(string s)
        {
            // some code
        }
    }
    class FriendClass
    {
        public void SomeMethod()
        {
            C1 c = new C1();
            c.MyMethod(this, 5.5, 3);
        }
    }

这当然会产生编译器警告,并会损害智能感知。但它会做的工作。

另一方面,我认为一个自信的程序员应该在不访问私有成员的情况下进行测试单元。这完全超出了范围,但请尝试阅读有关 TDD 的信息。但是,如果您仍然想这样做(有 c++ 之类的朋友),请尝试类似

#if UNIT_TESTING
        public
#else
        private
#endif
            double x;

所以你在没有定义 UNIT_TESTING 的情况下编写所有代码,当你想要进行单元测试时,你将#define UNIT_TESTING 添加到文件的第一行(并在#if UNIT_TESTING 下编写进行单元测试的所有代码)。这应该小心处理。

既然我认为单元测试是使用朋友的一个坏例子,我会举一个例子,为什么我认为朋友可以是好的。假设您有一个破坏系统(类)。随着使用,破碎系统会磨损,需要翻新。现在,您希望只有获得许可的技工才能修复它。为了使示例不那么琐碎,我会说机械师将使用他的个人(私人)螺丝刀来修复它。这就是为什么 mechanic 类应该是 BreakSystem 类的朋友。

于 2012-07-05T08:06:55.663 回答
0

友谊也可以通过使用“代理”——一些内部类来模拟。考虑以下示例:

public class A // Class that contains private members
{
  private class Accessor : B.BAgent // Implement accessor part of agent.
  {
    private A instance; // A instance for access to non-static members.
    static Accessor() 
    { // Init static accessors.
      B.BAgent.ABuilder = Builder;
      B.BAgent.PrivateStaticAccessor = StaticAccessor;
    }
    // Init non-static accessors.
    internal override void PrivateMethodAccessor() { instance.SomePrivateMethod(); }
    // Agent constructor for non-static members.
    internal Accessor(A instance) { this.instance = instance; }
    private static A Builder() { return new A(); }
    private static void StaticAccessor() { A.PrivateStatic(); }
  }
  public A(B friend) { B.Friendship(new A.Accessor(this)); }
  private A() { } // Private constructor that should be accessed only from B.
  private void SomePrivateMethod() { } // Private method that should be accessible from B.
  private static void PrivateStatic() { } // ... and static private method.
}
public class B
{
  // Agent for accessing A.
  internal abstract class BAgent
  {
    internal static Func<A> ABuilder; // Static members should be accessed only by delegates.
    internal static Action PrivateStaticAccessor;
    internal abstract void PrivateMethodAccessor(); // Non-static members may be accessed by delegates or by overrideable members.
  }
  internal static void Friendship(BAgent agent)
  {
    var a = BAgent.ABuilder(); // Access private constructor.
    BAgent.PrivateStaticAccessor(); // Access private static method.
    agent.PrivateMethodAccessor(); // Access private non-static member.
  }
}

当仅用于访问静态成员时,它可能会简单得多。这种实现的好处是所有类型都在友谊类的内部范围内声明,并且与接口不同,它允许访问静态成员。

于 2014-01-02T15:59:17.230 回答