为什么C++不提供C++风格的”frind”关键字?

Why does C# not provide the C++ style 'friend' keyword?

C++好友关键字允许EDOCX1 0指定EDCOX1×1作为它的朋友。这使得class B可以访问class Aprivate/protected成员。

我从来没有读过任何关于为什么这个被排除在C(和vb.net)之外的东西。对这个早期堆栈溢出问题的大多数答案似乎是说它是C++的有用部分,并且有充分的理由使用它。以我的经验,我不得不同意。

在我看来,另一个问题实际上是问如何在C应用程序中执行类似于friend的操作。虽然答案通常围绕着嵌套类,但它不像使用friend关键字那样优雅。

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

所以,总的来说,为什么c缺少friend,用c模拟它的"最佳实践"方法(或方法)是什么?

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


侧记使用friend并不是违反封装,而是强制执行封装。像访问器+变异器、运算符重载、公共继承、向下转换等,它经常被误用,但并不意味着关键字没有或更糟的坏用途。

在另一个线程中查看Konrad Rudolph的消息,或者如果您更喜欢在C++ FAQ中查看相关条目。


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

也就是说,这是一个很好的特性,我自己在C++中已经用了很多次了,并且也想在C语言中使用它。但我敢打赌,因为C的"纯"ONONess(与C++的伪ONESS相比)MS决定,因为Java没有朋友关键词C不应该(只是开玩笑);

严肃地说:内部不如朋友好,但它确实能完成工作。请记住,很少有人会将代码分发给第三方开发人员,而不是通过DLL;只要您和您的团队知道内部类及其使用,您就可以了。

编辑让我澄清朋友关键字是如何破坏OOP的。

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


对于信息,在.NET中另一个相关但不完全相同的东西是[InternalsVisibleTo],它允许一个程序集指定另一个程序集(例如单元测试程序集),该程序集(实际上)可以"内部"访问原始程序集中的类型/成员。


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

如果有人有一个合理使用"friend"的例子,不能用界面来模拟,请分享它!我想更好地理解C++和C语言之间的区别。


使用EDCOX1×0,一个C++设计器对私有成员的暴露有精确的控制。但是,他不得不揭露每一个私人成员。

有了internal,一个C设计师可以精确控制他要公开的一组私人成员。很明显,他只能暴露一个私人成员。但是,它将暴露在程序集中的所有类中。

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

如您所见,我们有两个维度可以突破封装。friend沿一个维度破坏,internal沿另一个维度破坏。哪一个是封装概念中更糟的突破?很难说。但最好同时提供friendinternal。此外,这两个关键字的一个很好的补充是第三种类型的关键字,它将以成员为基础(如internal)并指定目标类(如friend)。*为了简洁起见,我将使用"private"而不是"private和/或protected"。- Nick


你可以用C++的"内部"来接近C++的"朋友"。


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

就问题而言,C等同于朋友是什么?被标记为本文的副本,没有人提出真正良好的实现-我将在这里显示两个问题的答案。

主要想法是:什么是私有接口?

比如说,我们需要一些类来管理另一个类的实例,并对它们调用一些特殊的方法。我们不想给任何其他类调用这个方法的可能性。这也是C++ C++中朋友C++关键词所做的事情。

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

你可以:

  • 使update()方法成为公共方法的最简单和最糟糕的方法-hope每个人都明白为什么这是坏的。
  • 下一种方法是将其标记为内部。如果你把你的类到另一个程序集,但即使是该程序集中的每个类无法调用每个内部方法。
  • 使用私有/受保护的接口-我是这样做的。

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
class Program
{
    static void Main(string[] args)
    {
        //it's impossible to write Controller.IState p = new StateBase();
        //Controller.IState is hidden
        StateBase p = new StateBase();
        //p.Update(); //is not accessible
    }
}

那么,遗产呢?

我们需要使用中描述的技术,因为显式接口成员实现不能声明为虚拟的,并且将IState标记为受保护的,以便也可以从控制器派生。

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(播放列表状态).cs

1
2
3
4
5
6
7
8
public class PlayerIdleState: Controller.StateBase
{
    protected override void OnUpdate()
    {
        base.OnUpdate();
        Console.WriteLine("PlayerIdleState.OnUpdate()");
    }
}

最后举例说明如何测试类控制器抛出继承:控制器测试.cs

1
2
3
4
5
6
7
8
class ControllerTest: Controller
{
    public ControllerTest()
    {
        IState testObj = new PlayerIdleState();
        testObj.Update();
    }
}

希望我能涵盖所有的案例,我的答案是有用的。


C缺少"friend"关键字的原因与它缺少确定性破坏的原因相同。改变传统会让人感觉聪明,好像他们的新方法比别人的旧方法优越。一切都是为了骄傲。

说"friend classes is bad"和其他不合格的语句(如"don't use gotos"或"linux is better than windows")一样短视。

"friend"关键字与代理类相结合是一种只将类的某些部分公开给特定的其他类的好方法。代理类可以作为对所有其他类的可信屏障。"public"不允许任何这样的目标,如果没有概念上的"is a"关系,那么使用"protected"来获得继承的效果是很尴尬的。


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

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

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


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

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


不要为这个限制找借口。朋友是坏的,但内心是好的?他们是一样的,只有那个朋友能让你更精确地控制谁被允许进入,谁不被允许。

这是为了加强封装范式吗?所以你必须写访问方法,现在怎么办?如何阻止所有人(类B的方法除外)调用这些方法?你不能,因为你也不能控制这一切,因为你失去了"朋友"。

没有一种编程语言是完美的。C是我见过的最好的语言之一,但是为缺少的特性找一些愚蠢的借口对任何人都没有帮助。在C++中,我错过了简单事件/委派系统、反射(+自动去序列化)和前缀,但是在C i中,我错过了运算符重载(是的,不断告诉我你不需要它),默认参数,一个不可回避的const,多重继承(是的,不断告诉我你不需要它,接口是足够的代表。以及决定从内存中删除一个实例的能力(不,这并不糟糕,除非你是一个修补者)


从.NET 3开始就有了InternalsVisibleToAttribute,但我怀疑他们添加它只是为了在单元测试增加后满足测试程序集的需要。我看不出使用它的其他原因。

它在程序集级别工作,但它在内部不工作的情况下工作;也就是说,您希望分发一个程序集,但希望另一个非分布式程序集具有对它的特权访问。

很正确,它们要求朋友程序集具有强键控,以避免有人在受保护的程序集旁边创建一个假装的朋友。


我读过很多关于"friend"关键字的聪明评论,我同意它是什么有用的东西,但我认为"internal"关键字不太有用,它们对于纯OO编程仍然很糟糕。

我们有什么?(说到"朋友",我也说到"内在的")。

  • 使用"friend"会使代码不那么纯粹吗?
  • 对;

  • 使用"friend"会使代码更好吗?

  • 不,我们仍然需要在类之间建立一些私人关系,只有打破漂亮的封装,我们才能做到这一点,所以它也不好,我可以说它比使用"朋友"更邪恶。

使用friend会产生一些局部问题,而不是使用friend会给代码库用户带来问题。

编程语言的常见好解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// c++ style
class Foo {
  public_for Bar:
    void addBar(Bar *bar) { }
  public:
  private:
  protected:
};

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

你觉得怎么样?我认为这是最常见的纯面向对象解决方案。您可以打开任何方法来访问您选择的任何类。


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

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

声明协议/"创建者"类对所有创建的类都有亲密的访问权是违反OOP的,这是很愚蠢的——创建者类在向上的过程中不得不对每一个数据点都进行咀嚼。我发现最重要的是最小化"面向OOP的OOP"模型通常导致的所有额外的BS代码行。额外的意大利面只会制造更多的虫子。

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

如果你有一个使用朋友关键字的C++类,并且希望在C类中仿真它:1。宣布c类为公共类2。声明所有在C++中被保护的属性/属性/方法,从而可以在C语言中访问朋友。三。为对所有内部属性和属性的公共访问创建只读属性

我同意这与friend不同,单元测试是friend(协议分析器日志代码)等需求的一个非常有价值的例子。然而,internal提供了对您想要暴露的类的暴露,而[internalvisibleto()]处理其余的类——似乎它是为单元测试而生的。

至于朋友"因为你可以明确地控制哪些类可以访问而变得更好"——一开始一群可疑的邪恶类在同一个程序集中做什么?分割你的程序集!


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

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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和Internal Interface Members With Coding to Interfaces。


有些人认为,利用朋友可以让事情失去控制。我同意,但这并不能削弱它的效用。我不确定,朋友一定会伤害OO范式,而不仅仅是让所有的班级成员公开。当然,该语言允许您公开所有成员,但它是一个训练有素的程序员,可以避免这种类型的设计模式。同样地,一个训练有素的程序员也会保留在特定情况下使用friend的权利。在某些情况下,我觉得内部暴露太多。为什么要向程序集中的所有内容公开类或方法?

我有一个ASP.NET页继承了我自己的基本页,反过来又继承了system.web.ui.page。在这个页面中,我有一些代码可以处理受保护方法中应用程序的最终用户错误报告。

1
ReportError("Uh Oh!");

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

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

如果reportError方法受保护,则无法执行此操作。我可以使它成为内部的,但是它暴露在程序集中的任何代码中。我只希望它暴露在当前页面(包括子控件)的UI元素中。更具体地说,我希望我的基控件类定义完全相同的错误报告方法,并且只调用基页中的方法。

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

我相信类似于friend的东西可以在语言中有用并实现,而不会使语言变得更像"oo",也许是作为属性,这样您就可以让类或方法成为特定类或方法的朋友,从而允许开发人员提供非常具体的访问。也许是……(伪代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Friend(B)]
class A {

    AMethod() { }

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

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

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

在我前面的例子中,可能有如下的内容(人们可以争论语义,但我只是想把这个想法表达清楚):

1
2
3
4
5
6
7
8
9
10
11
12
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);
    }
}

如我所见,朋友概念对它没有比公开事物,或者创建公共方法或属性来访问成员更大的风险。如果朋友允许数据可访问性的另一个粒度级别,并且允许您缩小可访问性,而不是使用内部或公共扩展它。


如果您正在使用C++,并且发现自己使用"朋友"关键字,这是一个非常有力的指示,您有一个设计问题,因为为什么Heck一个类需要访问其他类的私有成员??


我怀疑这与C编译模型有关——在运行时构建IL-the-JIT编译模型。也就是说,C类泛型与C++泛型根本不同的原因相同。


您可以将其保持为私有,并使用反射来调用函数。如果您要求测试框架测试一个私有函数,测试框架就可以做到这一点。


B.S.D.

有人说,朋友伤纯真。我同意。

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

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

我们可以按照以下步骤来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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);
        }
    }

这当然会生成一个编译器警告,并且会损害IntelliSense。但它能完成这项工作。

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

1
2
3
4
5
6
#if UNIT_TESTING
        public
#else
        private
#endif
            double x;

因此,在不定义单元测试的情况下编写所有代码,并且在希望进行单元测试时,将"定义单元测试"添加到文件的第一行(并在"如果单元测试"下编写执行单元测试的所有代码)。那应该小心处理。

因为我认为单元测试对于朋友的使用是一个坏的例子,所以我会举一个例子来说明为什么我认为朋友可以是好的。假设你有一个破坏系统(类)。使用后,制动系统会磨损,需要进行翻修。现在,你只想让一个有执照的技工来修理它。为了使这个例子不那么琐碎,我想说修理工会用他的私人螺丝刀来修理它。这就是为什么机械课应该是BreakingSystem课的朋友。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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.
  }
}

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


我只回答"如何"的问题。

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

  • 界面
  • 嵌套类

例如,我们有两个主要班级:学生和大学。学生的GPA只有大学才允许进入。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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();
}