【问题标题】:C# equivalent to C++ friend keyword?C# 相当于 C++ 的朋友关键字?
【发布时间】:2013-10-08 18:07:11
【问题描述】:

我是 C# 新手,我遇到了一个问题,在 C++ 中我通常会使用 friend 标识符。现在我知道 C# 中不存在 friend 关键字,但我没有任何解决此问题的经验(除了将所有类变量设为公共属性,如果可以的话,我想避免)。

我有以下场景:

public class A 
{
    public string Info { get; set; }
    /* much more data */
}

public class B
{
    private A m_instanceOfA;

    public B(A a) { m_instanceOfA = a; }

    public Info { get return A.info; set A.Info  = value; }

    /* And some more data of its own*/
}

public class C
{
    private A m_instanceOfA;

    // I need a constructor of C, which needs to set C.m_instanceOfA
    // to the same value as b.m_instanceOfA.
    public C(B b) { m_instanceOfA = b.m_instanceOfA ; } // <--- Not allowed!

    /* And some more data of its own*/
}

有没有其他聪明的方法,在不公开B.m_instanceOfA 的情况下,让C 访问这个变量(仅在构造函数中)?

【问题讨论】:

标签: c# oop friend


【解决方案1】:

您可以使用下面显示的技巧在 Bob 上创建一个方法 FriendRecieveMessageFromAlice,该方法只能由 Alice 调用。一个邪恶的类 Eve 将无法在不使用私有成员反射的情况下调用该方法。

我很想知道其他人之前是否建议过这个或其他解决方案。几个月来我一直在寻找解决这个问题的方法,但我从来没有见过一个能确保真正的 friend 语义的解决方案,前提是不使用反射(你几乎可以用它规避任何事情)。

爱丽丝和鲍勃

public interface IKey { }

public class Alice
{
    // Alice, Bob and Carol must only have private constructors, so only nested classes can subclass them.
    private Alice() { }
    public static Alice Create() { return new Alice(); }

    private class AlicePrivateKey : Alice, IKey { }

    public void PublicSendMessageToBob() {
        Bob.Create().FriendRecieveMessageFromAlice<AlicePrivateKey>(42);
    }

    public void FriendRecieveMessageFromBob<TKey>(int message) where TKey : Bob, IKey {
        System.Console.WriteLine("Alice: I recieved message {0} from my friend Bob.", message);
    }
}

public class Bob
{
    private Bob() { }
    public static Bob Create() { return new Bob(); }

    private class BobPrivateKey : Bob, IKey { }

    public void PublicSendMessageToAlice() {
        Alice.Create().FriendRecieveMessageFromBob<BobPrivateKey>(1337);
    }

    public void FriendRecieveMessageFromAlice<TKey>(int message) where TKey : Alice, IKey {
        System.Console.WriteLine("Bob: I recieved message {0} from my friend Alice.", message);
    }
}

class Program
{
    static void Main(string[] args) {
        Alice.Create().PublicSendMessageToBob();
        Bob.Create().PublicSendMessageToAlice();
    }
}

夏娃

public class Eve
{
    // Eve can't write that, it won't compile:
    // 'Alice.Alice()' is inaccessible due to its protection level
    private class EvePrivateKey : Alice, IKey { }

    public void PublicSendMesssageToBob() {
        // Eve can't write that either:
        // 'Alice.AlicePrivateKey' is inaccessible due to its protection level
        Bob.Create().FriendRecieveMessageFromAlice<Alice.AlicePrivateKey>(42);
    }
}

工作原理

诀窍在于方法Bob.FriendRecieveMessageFromAlice 需要一个(虚拟)泛型类型参数作为标记。该泛型类型必须继承自 Alice 和虚拟接口 IKey

由于Alice 本身没有实现IKey,调用者需要提供Alice 的一些子类,它确实实现了IKey。但是Alice只有私有构造函数,所以它只能被嵌套类子类化,不能被其他地方声明的类子类化。

这意味着只有嵌套在Alice 中的类才能对其进行子类化以实现IKey。这就是AlicePrivateKey 所做的,因为它被声明为私有,所以只有Alice 可以将它作为通用参数传递给Bob.FriendRecieveMessageFromAlice,所以只有Alice 可以调用该方法。

然后我们反过来做同样的事情,这样只有Bob可以调用Alice.FriendRecieveMessageFromBob

密钥泄露

值得注意的是,Bob.FriendRecieveMessageFromAlice 在调用时可以访问TKey 泛型类型参数,并且可以使用它来欺骗来自Alice 的另一个方法OtherClass.OtherMethod&lt;OtherTkey&gt; 接受OtherTKey : Alice, IKey 的调用。因此,让密钥从不同的接口继承会更安全:Alice, IBobKey 用于第一个接口,Alice, IOtherKey 用于第二个接口。

比 C++ 好朋友

  • 即使Bob本身也不能调用自己的方法Bob.FriendRecieveMessageFromAlice
  • Bob 可以通过不同的好友方法拥有多个好友:

    // Can only be called by Alice, not by Carol or Bob itself
    Bob.FriendRecieveMessageFromAlice <TKey>(int message) where TKey : Alice, IKey { }
    // Can only be called by Carol, not by Alice or Bob itself
    Bob.FriendRecieveMessageFromCarol <TKey>(int message) where TKey : Carol, IKey { }
    

我很想知道是否有某种方法可以比蛮力试错更有效地找到此类技巧。某种“C# 类型系统的代数”,它告诉我们哪些限制可以强制执行,哪些不能强制执行,但我还没有看到任何关于此类主题的讨论。

【讨论】:

  • 这是对类型系统的一个非常巧妙的使用,但我认为你不能让一个给定的方法有两个朋友,因为约束的 where 子句不能有 ||中定义的运算符。 msdn.microsoft.com/en-us/library/d5x73970.aspx
  • @Mike 我认为你是对的。您总是可以为该方法声明几个包装器,每个“朋友”一个,尽管这会导致更多的“聪明膨胀”:)。如果您有许多方法或许多朋友,则可以只使用一个“工厂”方法,该方法返回一个包含所有朋友方法的内部类,请参见此处的示例:dotnetfiddle.net/n30ZtX。这应该被称为丑陋的设计模式:)。
  • 一个私有的单例(当然是一个暴露的类)可以让你以更少的混淆来调用函数(完全避免泛型和类型约束),并且仍然实现你的“比 C++ 朋友更好”的目标。看我的回答。 :)
  • 这很聪明。唯一的问题是,任何人都可以通过创建一个继承类和接口的类来制作密钥,这是对单一继承使用的浪费。解决这个问题的一种方法是使课程密封,这样就万无一失了。
  • 比 C++ 版本更好的另一件事是,这是基于每个方法的,所以 Bob 现在只需要查看 Alice 的方法,而不是看到 all 的方法,他只看到他需要的东西。我更喜欢这样。
【解决方案2】:

内部

您可以使用internal 关键字。然后,您的类型(或类型成员)将仅对同一程序集中的其他类型可见;还有:

如果您需要您的内部类型对其他程序集可见,您可以使用InternalsVisibleToAttribute。此属性针对您的整个程序集,通常写入AssemblyInfo.cs 文件中。


PS:C#中不存在Friend关键字,但存在友谊的概念(与C++中的不完全相同),在MSDN的Friend Assemblies文章中有描述。另请注意,friend keyword exists in VB.NET 与 C# internal 关键字的行为完全相同。

【讨论】:

    【解决方案3】:

    您只能使用 5 个可访问性修饰符:

    公开访问不受限制。

    受保护访问仅限于包含类或从包含类派生的类型。

    内部访问仅限于当前程序集。

    受保护的内部 访问权限仅限于当前程序集或派生自包含类的类型。

    私人 访问仅限于包含类型。

    【讨论】:

      【解决方案4】:

      我修改了您发布的代码,所以它应该可以完全按照您的要求工作:

      using System.Reflection;
      using System.Diagnostics;
      
      public class A 
      {
          public string Info { get; set; }
          /* much more data */
      }
      
      public class B
      {
          private A m_instanceOfA;
          public string Info { get; set; }
      
          public B(A a) => Info = a;
      
          private readonly ConstructorInfo friend = typeof(C).GetConstructor(new Type[] { typeof(B) });
          public A InstanceOfA
          {
              get
              {
                  if (new StackFrame(1).GetMethod() != friend)
                     throw new Exception("Call this property only inside the constructor of C");
                  return this.m_instanceOfA;
              }
          }
      }
      
      public class C
      {
          private A m_instanceOfA;
      
          // Only the constructor of C can set his m_instanceOfA
          // to the same value as b.m_instanceOfA.
          public C(B b)
          {
              Info = b.InstanceOfA; // Call the public property, not the private field. Now it is allowed and it will work too, because you call it inside the constructor of C. In Main method, for example, an exception will be thrown, if you try to get InstanceOfA there.
          }
      }
      

      【讨论】:

      • 为什么在使用多个命名空间时总是要解释?我们可以清楚地看到你是……
      【解决方案5】:

      我认为您正在寻找“内部”关键字 - 基本上只对同一程序集中的类可见

      或者你可以这样(原谅方法名称!):

      public interface IAmAFriendOfB {
         void DoSomethingWithA(A instanceOfA);
      }
      
      public class B {
          private A m_instanceOfA;
      
          public B(A a) { m_instanceOfA = a; }
      
          public void BeFriendlyWith(IAmAFriendOfB friend) {
             friend.DoSomethingWithA(m_instanceOfA);
          }
      
          // the rest of your class
      }
      
      public class C : IAmAFriendOfB {
      
          private A m_instanceOfA;
      
          public C(B b) {
              b.BeFriendlyWith(this);
          }
      
          void DoSomethingWithA(A instanceOfA) {
              m_instanceOfA = b.m_instanceOfA;
          }   
      }
      

      【讨论】:

      • 这确实是一个很好的建议,在我目前的情况下很有用。但是让我们假设它们可能不在同一个程序集中,还有其他选择吗?
      • 我用界面更新了我的答案
      • 哈,这是一个非常棒的选择!但我是否正确地说 class C 具有 A m_instanceOfAB b 作为类数据?这对我来说并不完全清楚,为了清楚起见,最好补充一下。
      【解决方案6】:

      这是另一种使用 internal 类和 private 单例实例的替代方法,它允许您微调哪些方法暴露给伪friend 类。

      using System;
      
      namespace Test
      {
          public class A 
          {
              public string Info { get; set; }
              /* much more data */
          }
      
          public class B
          {
              private A m_instanceOfA;
      
              public B(A a) { m_instanceOfA = a; }
      
              public string Info
              {
                  get { return m_instanceOfA.Info; }
                  set { m_instanceOfA.Info = value; }
              }
      
              // requires an instance of a private object, this establishes our pseudo-friendship
              internal A GetInstanceOfA(C.AGetter getter) { return getter.Get(m_instanceOfA); }
      
              /* And some more data of its own*/
          }
      
          public class C
          {
              private A m_instanceOfA;
      
              private static AGetter m_AGetter; // initialized before first use; not visible outside of C
      
              // class needs to be visible to B, actual instance does not (we call b.GetInstanceOfA from C)
              internal class AGetter
              {
                  static AGetter() { m_AGetter = new AGetter(); } // initialize singleton
      
                  private AGetter() { } // disallow instantiation except our private singleton in C
      
                  public A Get(A a) { return a; } // force a NullReferenceException if calling b.GetInstanceOfA(null)
              }
      
              static C()
              {
                  // ensure that m_AGetter is initialized
                  System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(AGetter).TypeHandle);
              }
      
              public C(B b)
              {
                  m_instanceOfA = b.GetInstanceOfA(m_AGetter);
              }
      
              public string Info
              {
                  get { return m_instanceOfA.Info; }
                  set { m_instanceOfA.Info = value; }
              }
      
              /* And some more data of its own*/
          }
      
          public class Test
          {
              public static void Main()
              {
                  A a = new A();
                  B b = new B(a);
                  C c = new C(b);
                  c.Info = "Hello World!";
                  Console.WriteLine(a.Info);
              }
          }
      }
      

      Live Demo

      C.AGetter 类无法在其自身之外实例化,因此C.m_AGetter(既是private 又是static)表示只能从C 内部访问的单例实例。由于B.GetInstanceOfA 需要C.AGetter 的实例,这使得函数在C 之外无用。该函数被标记为internal 以最大限度地减少其暴露,但该参数也应作为一种自我文档形式,它不适合常用。

      接口替代方法可能会暴露超出其预期范围的方法(例如,实现接口的类不应访问暴露的方法),而这种方法可以防止这种情况发生。 friend 访问的反对者可能仍然反对它,但这使事情更接近预期范围。

      【讨论】:

        猜你喜欢
        • 2010-10-01
        • 2011-05-08
        • 2010-12-10
        • 1970-01-01
        • 2013-03-14
        • 1970-01-01
        • 2010-09-19
        • 1970-01-01
        • 2016-06-21
        相关资源
        最近更新 更多